Merge pull request #6 from 1Password/configure-restarts

Adding configuration for auto rolling restart on deployments
This commit is contained in:
Jillian W
2021-01-25 16:50:18 -04:00
committed by GitHub
10 changed files with 494 additions and 49 deletions

View File

@@ -4,7 +4,7 @@ The 1Password Connect Kubernetes Operator provides the ability to integrate Kube
The 1Password Connect Kubernetes Operator also allows for Kubernetes Secrets to be composed from a 1Password Item through annotation of an Item Path on a deployment. The 1Password Connect Kubernetes Operator also allows for Kubernetes Secrets to be composed from a 1Password Item through annotation of an Item Path on a deployment.
The 1Password Connect Kubernetes Operator will continually check for updates from 1Password for any Kubernetes Secret that it has generated. If a Kubernetes Secret is updated, any Deployment using that secret will be automatically restarted. The 1Password Connect Kubernetes Operator will continually check for updates from 1Password for any Kubernetes Secret that it has generated. If a Kubernetes Secret is updated, any Deployment using that secret can be automatically restarted.
## Setup ## Setup
@@ -85,6 +85,7 @@ To further configure the 1Password Kubernetes Operator the Following Environment
- **OP_CONNECT_HOST** (required): Specifies the host name within Kubernetes in which to access the 1Password Connect. - **OP_CONNECT_HOST** (required): Specifies the host name within Kubernetes in which to access the 1Password Connect.
- **POLLING_INTERVAL** (default: 600)**:** The number of seconds the 1Password Kubernetes Operator will wait before checking for updates from 1Password Connect. - **POLLING_INTERVAL** (default: 600)**:** The number of seconds the 1Password Kubernetes Operator will wait before checking for updates from 1Password Connect.
- **MANAGE_CONNECT** (default: false): If set to true, on deployment of the operator, a default configuration of the OnePassword Connect Service will be deployed to the `default` namespace. - **MANAGE_CONNECT** (default: false): If set to true, on deployment of the operator, a default configuration of the OnePassword Connect Service will be deployed to the `default` namespace.
- **AUTO_RESTART** (default: false): If set to true, the operator will restart any deployment using a secret from 1Password Connect. This can be overwritten by namespace, deployment, or individual secret. More details on AUTO_RESTART can be found in the ["Configuring Automatic Rolling Restarts of Deployments"](#configuring-automatic-rolling-restarts-of-deployments) section.
Apply the deployment file: Apply the deployment file:
@@ -135,8 +136,7 @@ Applying this yaml file will create a Kubernetes Secret with the name `{secret_n
Note: Deleting the Deployment that you've created will automatically delete the created Kubernetes Secret only if the deployment is still annotated with `onepasswordoperator./item-path` and `onepasswordoperator/item-name` and no other deployment is using the secret. Note: Deleting the Deployment that you've created will automatically delete the created Kubernetes Secret only if the deployment is still annotated with `onepasswordoperator./item-path` and `onepasswordoperator/item-name` and no other deployment is using the secret.
If a 1Password Item that is linked to a Kubernetes Secret is updated within the `POLLING_INTERVAL` the associated Kubernetes Secret will be updated. Furthermore, any deployments using that secret will be given a rolling restart. If a 1Password Item that is linked to a Kubernetes Secret is updated within the POLLING_INTERVAL the associated Kubernetes Secret will be updated. However, if you do not want a specific secret to be updated you can add the tag `onepasswordconnectoperator:ignore_secret` to the item stored in 1Password. While this tag is in place, any updates made to an item will not trigger an update to the associated secret in Kubernetes.
--- ---
**NOTE** **NOTE**
@@ -144,6 +144,38 @@ If a 1Password Item that is linked to a Kubernetes Secret is updated within the
If multiple 1Password vaults/items have the same `title` when using a title in the access path, the desired action will be performed on the oldest vault/item. Furthermore, titles that include white space characters cannot be used. If multiple 1Password vaults/items have the same `title` when using a title in the access path, the desired action will be performed on the oldest vault/item. Furthermore, titles that include white space characters cannot be used.
--- ---
### Configuring Automatic Rolling Restarts of Deployments
If a 1Password Item that is linked to a Kubernetes Secret is updated, any deployments configured to `auto_restart` AND are using that secret will be given a rolling restart the next time 1Password Connect is polled for updates.
There are many levels of granularity on which to configure auto restarts on deployments: at the operator level, per-namespace, or per-deployment.
**On the operator**: This method allows for managing auto restarts on all deployments within the namespaces watched by operator. Auto restarts can be enabled by setting the environemnt variable `AUTO_RESTART` to true. If the value is not set, the operator will default this value to false.
**Per Namespace**: This method allows for managing auto restarts on all deployments within a namespace. Auto restarts can by managed by setting the annotation `onepasswordoperator/auto_restart` to either `true` or `false` on the desired namespace. An example of this is shown below:
```yaml
# enabled auto restarts for all deployments within a namespace unless overwritten within a deployment
apiVersion: v1
kind: Namespace
metadata:
name: "example-namespace"
onepasswordoperator/auto_restart: "true"
```
If the value is not set, the auto reset settings on the operator will be used. This value can be overwritten by deployment.
**Per Deployment**
This method allows for managing auto restarts on a given deployment. Auto restarts can by managed by setting the annotation `onepasswordoperator/auto_restart` to either `true` or `false` on the desired deployment. An example of this is shown below:
```yaml
# enabled auto restarts for the deployment
apiVersion: v1
kind: Deployment
metadata:
name: "example-deployment"
onepasswordoperator/auto_restart: "true"
```
If the value is not set, the auto reset settings on the namespace will be used.
## Development ## Development
### Creating a Docker image ### Creating a Docker image

View File

@@ -42,6 +42,7 @@ import (
const envPollingIntervalVariable = "POLLING_INTERVAL" const envPollingIntervalVariable = "POLLING_INTERVAL"
const manageConnect = "MANAGE_CONNECT" const manageConnect = "MANAGE_CONNECT"
const restartDeploymentsEnvVariable = "AUTO_RESTART"
const defaultPollingInterval = 600 const defaultPollingInterval = 600
// Change below variables to serve metrics on different host or port. // Change below variables to serve metrics on different host or port.
@@ -165,7 +166,7 @@ func main() {
addMetrics(ctx, cfg) addMetrics(ctx, cfg)
// Setup update secrets task // Setup update secrets task
updatedSecretsPoller := op.NewManager(mgr.GetClient(), opConnectClient) updatedSecretsPoller := op.NewManager(mgr.GetClient(), opConnectClient, shouldAutoRestartDeployments())
done := make(chan bool) done := make(chan bool)
ticker := time.NewTicker(getPollingIntervalForUpdatingSecrets()) ticker := time.NewTicker(getPollingIntervalForUpdatingSecrets())
go func() { go func() {
@@ -284,3 +285,16 @@ func shouldManageConnect() bool {
} }
return false return false
} }
func shouldAutoRestartDeployments() bool {
shouldAutoRestartDeployments, found := os.LookupEnv(restartDeploymentsEnvVariable)
if found {
shouldAutoRestartDeploymentsBool, err := strconv.ParseBool(strings.ToLower(shouldAutoRestartDeployments))
if err != nil {
log.Error(err, "")
os.Exit(1)
}
return shouldAutoRestartDeploymentsBool
}
return false
}

View File

@@ -35,3 +35,5 @@ spec:
secretKeyRef: secretKeyRef:
name: onepassword-token name: onepassword-token
key: token key: token
- name: AUTO_RESTART
value: "false"

View File

@@ -16,9 +16,7 @@ spec:
containers: containers:
- name: onepassword-connect-operator - name: onepassword-connect-operator
image: 1password/onepassword-operator image: 1password/onepassword-operator
command: command: ["/manager"]
- onepassword-connect-operator
imagePullPolicy: Never
env: env:
- name: WATCH_NAMESPACE - name: WATCH_NAMESPACE
value: "default,development" value: "default,development"
@@ -29,7 +27,7 @@ spec:
- name: OPERATOR_NAME - name: OPERATOR_NAME
value: "onepassword-connect-operator" value: "onepassword-connect-operator"
- name: OP_CONNECT_HOST - name: OP_CONNECT_HOST
value: "http://secret-service:8080" value: "http://onepassword-connect:8080"
- name: POLLING_INTERVAL - name: POLLING_INTERVAL
value: "10" value: "10"
- name: OP_CONNECT_TOKEN - name: OP_CONNECT_TOKEN
@@ -37,3 +35,5 @@ spec:
secretKeyRef: secretKeyRef:
name: onepassword-token name: onepassword-token
key: token key: token
- name: AUTO_RESTART
value: "false"

View File

@@ -3,7 +3,7 @@ kind: ServiceAccount
metadata: metadata:
name: onepassword-connect-operator name: onepassword-connect-operator
--- ---
kind: RoleBinding kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: rbac.authorization.k8s.io/v1
metadata: metadata:
name: onepassword-connect-operator-default name: onepassword-connect-operator-default
@@ -13,12 +13,12 @@ subjects:
name: onepassword-connect-operator name: onepassword-connect-operator
namespace: default namespace: default
roleRef: roleRef:
kind: Role kind: ClusterRole
name: onepassword-connect-operator name: onepassword-connect-operator
apiGroup: rbac.authorization.k8s.io apiGroup: rbac.authorization.k8s.io
--- ---
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: rbac.authorization.k8s.io/v1
kind: Role kind: ClusterRole
metadata: metadata:
creationTimestamp: null creationTimestamp: null
name: onepassword-connect-operator name: onepassword-connect-operator
@@ -34,6 +34,7 @@ rules:
- events - events
- configmaps - configmaps
- secrets - secrets
- namespaces
verbs: verbs:
- create - create
- delete - delete

View File

@@ -3,7 +3,7 @@ kind: ServiceAccount
metadata: metadata:
name: onepassword-connect-operator name: onepassword-connect-operator
--- ---
kind: RoleBinding kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: rbac.authorization.k8s.io/v1
metadata: metadata:
name: onepassword-connect-operator-default name: onepassword-connect-operator-default
@@ -17,7 +17,7 @@ roleRef:
name: onepassword-connect-operator name: onepassword-connect-operator
apiGroup: rbac.authorization.k8s.io apiGroup: rbac.authorization.k8s.io
--- ---
kind: RoleBinding kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: rbac.authorization.k8s.io/v1
metadata: metadata:
name: onepassword-connect-operator-development name: onepassword-connect-operator-development
@@ -48,6 +48,7 @@ rules:
- events - events
- configmaps - configmaps
- secrets - secrets
- namespaces
verbs: verbs:
- create - create
- delete - delete

1
go.sum
View File

@@ -1447,6 +1447,7 @@ k8s.io/apimachinery v0.18.2/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftc
k8s.io/apimachinery v0.19.2 h1:5Gy9vQpAGTKHPVOh5c4plE274X8D/6cuEiTO2zve7tc= k8s.io/apimachinery v0.19.2 h1:5Gy9vQpAGTKHPVOh5c4plE274X8D/6cuEiTO2zve7tc=
k8s.io/apimachinery v0.19.3 h1:bpIQXlKjB4cB/oNpnNnV+BybGPR7iP5oYpsOTEJ4hgc= k8s.io/apimachinery v0.19.3 h1:bpIQXlKjB4cB/oNpnNnV+BybGPR7iP5oYpsOTEJ4hgc=
k8s.io/apimachinery v0.20.1 h1:LAhz8pKbgR8tUwn7boK+b2HZdt7MiTu2mkYtFMUjTRQ= k8s.io/apimachinery v0.20.1 h1:LAhz8pKbgR8tUwn7boK+b2HZdt7MiTu2mkYtFMUjTRQ=
k8s.io/apimachinery v0.20.2 h1:hFx6Sbt1oG0n6DZ+g4bFt5f6BoMkOjKWsQFu077M3Vg=
k8s.io/apiserver v0.0.0-20190918160949-bfa5e2e684ad/go.mod h1:XPCXEwhjaFN29a8NldXA901ElnKeKLrLtREO9ZhFyhg= k8s.io/apiserver v0.0.0-20190918160949-bfa5e2e684ad/go.mod h1:XPCXEwhjaFN29a8NldXA901ElnKeKLrLtREO9ZhFyhg=
k8s.io/apiserver v0.0.0-20191122221311-9d521947b1e1/go.mod h1:RbsZY5zzBIWnz4KbctZsTVjwIuOpTp4Z8oCgFHN4kZQ= k8s.io/apiserver v0.0.0-20191122221311-9d521947b1e1/go.mod h1:RbsZY5zzBIWnz4KbctZsTVjwIuOpTp4Z8oCgFHN4kZQ=
k8s.io/apiserver v0.16.7/go.mod h1:/5zSatF30/L9zYfMTl55jzzOnx7r/gGv5a5wtRp8yAw= k8s.io/apiserver v0.16.7/go.mod h1:/5zSatF30/L9zYfMTl55jzzOnx7r/gGv5a5wtRp8yAw=

View File

@@ -7,11 +7,12 @@ import (
) )
const ( const (
OnepasswordPrefix = "onepasswordoperator" OnepasswordPrefix = "onepasswordoperator"
ItemPathAnnotation = OnepasswordPrefix + "/item-path" ItemPathAnnotation = OnepasswordPrefix + "/item-path"
NameAnnotation = OnepasswordPrefix + "/item-name" NameAnnotation = OnepasswordPrefix + "/item-name"
VersionAnnotation = OnepasswordPrefix + "/item-version" VersionAnnotation = OnepasswordPrefix + "/item-version"
RestartAnnotation = OnepasswordPrefix + "/lastRestarted" RestartAnnotation = OnepasswordPrefix + "/lastRestarted"
RestartDeploymentsAnnotation = OnepasswordPrefix + "/auto_restart"
) )
func GetAnnotationsForDeployment(deployment *appsv1.Deployment, regex *regexp.Regexp) (map[string]string, bool) { func GetAnnotationsForDeployment(deployment *appsv1.Deployment, regex *regexp.Regexp) (map[string]string, bool) {
@@ -34,7 +35,7 @@ func GetAnnotationsForDeployment(deployment *appsv1.Deployment, regex *regexp.Re
func FilterAnnotations(annotations map[string]string, regex *regexp.Regexp) map[string]string { func FilterAnnotations(annotations map[string]string, regex *regexp.Regexp) map[string]string {
filteredAnnotations := make(map[string]string) filteredAnnotations := make(map[string]string)
for key, value := range annotations { for key, value := range annotations {
if regex.MatchString(key) && key != RestartAnnotation { if regex.MatchString(key) && key != RestartAnnotation && key != RestartDeploymentsAnnotation {
filteredAnnotations[key] = value filteredAnnotations[key] = value
} }
} }

View File

@@ -3,11 +3,14 @@ package onepassword
import ( import (
"context" "context"
"fmt" "fmt"
"strconv"
"strings"
"time" "time"
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets" kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
"github.com/1Password/connect-sdk-go/connect" "github.com/1Password/connect-sdk-go/connect"
"github.com/1Password/connect-sdk-go/onepassword"
appsv1 "k8s.io/api/apps/v1" appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
@@ -15,19 +18,22 @@ import (
) )
const envHostVariable = "OP_HOST" const envHostVariable = "OP_HOST"
const lockTag = "onepasswordconnectoperator:ignore_secret"
var log = logf.Log.WithName("update_op_kubernetes_secrets_task") var log = logf.Log.WithName("update_op_kubernetes_secrets_task")
func NewManager(kubernetesClient client.Client, opConnectClient connect.Client) *SecretUpdateHandler { func NewManager(kubernetesClient client.Client, opConnectClient connect.Client, shouldAutoRestartDeploymentsGlobal bool) *SecretUpdateHandler {
return &SecretUpdateHandler{ return &SecretUpdateHandler{
client: kubernetesClient, client: kubernetesClient,
opConnectClient: opConnectClient, opConnectClient: opConnectClient,
shouldAutoRestartDeploymentsGlobal: shouldAutoRestartDeploymentsGlobal,
} }
} }
type SecretUpdateHandler struct { type SecretUpdateHandler struct {
client client.Client client client.Client
opConnectClient connect.Client opConnectClient connect.Client
shouldAutoRestartDeploymentsGlobal bool
} }
func (h *SecretUpdateHandler) UpdateKubernetesSecretsTask() error { func (h *SecretUpdateHandler) UpdateKubernetesSecretsTask() error {
@@ -52,22 +58,33 @@ func (h *SecretUpdateHandler) restartDeploymentsWithUpdatedSecrets(updatedSecret
return err return err
} }
if len(deployments.Items) == 0 {
return nil
}
setForAutoRestartByNamespaceMap, err := h.getIsSetForAutoRestartByNamespaceMap()
if err != nil {
return err
}
for i := 0; i < len(deployments.Items); i++ { for i := 0; i < len(deployments.Items); i++ {
deployment := &deployments.Items[i] deployment := &deployments.Items[i]
if !isDeploymentSetForAutoRestart(deployment, setForAutoRestartByNamespaceMap) {
continue
}
updatedSecrets := updatedSecretsByNamespace[deployment.Namespace] updatedSecrets := updatedSecretsByNamespace[deployment.Namespace]
secretName := deployment.Annotations[NameAnnotation] secretName := deployment.Annotations[NameAnnotation]
log.Info(fmt.Sprintf("Looking at secret %v for deployment %v", secretName, deployment.Name))
if isUpdatedSecret(secretName, updatedSecrets) || IsDeploymentUsingSecrets(deployment, updatedSecrets) { if isUpdatedSecret(secretName, updatedSecrets) || IsDeploymentUsingSecrets(deployment, updatedSecrets) {
h.restartDeployment(deployment) h.restartDeployment(deployment)
} else { } else {
log.Info(fmt.Sprintf("Deployment '%v' is up to date", deployment.GetName())) log.Info(fmt.Sprintf("Deployment %q at namespace %q is up to date", deployment.GetName(), deployment.Namespace))
} }
} }
return nil return nil
} }
func (h *SecretUpdateHandler) restartDeployment(deployment *appsv1.Deployment) { func (h *SecretUpdateHandler) restartDeployment(deployment *appsv1.Deployment) {
log.Info(fmt.Sprintf("Deployment '%v' references an updated secret. Restarting", deployment.GetName())) log.Info(fmt.Sprintf("Deployment %q at namespace %q references an updated secret. Restarting", deployment.GetName(), deployment.Namespace))
deployment.Spec.Template.Annotations = map[string]string{ deployment.Spec.Template.Annotations = map[string]string{
RestartAnnotation: time.Now().String(), RestartAnnotation: time.Now().String(),
} }
@@ -102,6 +119,12 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]b
itemVersion := fmt.Sprint(item.Version) itemVersion := fmt.Sprint(item.Version)
if currentVersion != itemVersion { if currentVersion != itemVersion {
if isItemLockedForForcedRestarts(item) {
log.Info(fmt.Sprintf("Secret '%v' has been updated in 1Password but is set to be ignored. Updates to an ignored secret will not trigger an update to a kubernetes secret or a rolling restart.", secret.GetName()))
secret.Annotations[VersionAnnotation] = itemVersion
h.client.Update(context.Background(), &secret)
continue
}
log.Info(fmt.Sprintf("Updating kubernetes secret '%v'", secret.GetName())) log.Info(fmt.Sprintf("Updating kubernetes secret '%v'", secret.GetName()))
secret.Annotations[VersionAnnotation] = itemVersion secret.Annotations[VersionAnnotation] = itemVersion
updatedSecret := kubeSecrets.BuildKubernetesSecretFromOnePasswordItem(secret.Name, secret.Namespace, secret.Annotations, *item) updatedSecret := kubeSecrets.BuildKubernetesSecretFromOnePasswordItem(secret.Name, secret.Namespace, secret.Annotations, *item)
@@ -115,6 +138,16 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]b
return updatedSecrets, nil return updatedSecrets, nil
} }
func isItemLockedForForcedRestarts(item *onepassword.Item) bool {
tags := item.Tags
for i := 0; i < len(tags); i++ {
if tags[i] == lockTag {
return true
}
}
return false
}
func isUpdatedSecret(secretName string, updatedSecrets map[string]bool) bool { func isUpdatedSecret(secretName string, updatedSecrets map[string]bool) bool {
_, ok := updatedSecrets[secretName] _, ok := updatedSecrets[secretName]
if ok { if ok {
@@ -122,3 +155,57 @@ func isUpdatedSecret(secretName string, updatedSecrets map[string]bool) bool {
} }
return false return false
} }
func (h *SecretUpdateHandler) getIsSetForAutoRestartByNamespaceMap() (map[string]bool, error) {
namespaces := &corev1.NamespaceList{}
err := h.client.List(context.Background(), namespaces)
if err != nil {
log.Error(err, "Failed to list kubernetes namespaces")
return nil, err
}
namespacesMap := map[string]bool{}
for _, namespace := range namespaces.Items {
namespacesMap[namespace.Name] = h.isNamespaceSetToAutoRestart(&namespace)
}
return namespacesMap, nil
}
func isDeploymentSetForAutoRestart(deployment *appsv1.Deployment, setForAutoRestartByNamespace map[string]bool) bool {
restartDeployment := deployment.Annotations[RestartDeploymentsAnnotation]
//If annotation for auto restarts for deployment is not set. Check for the annotation on its namepsace
if restartDeployment == "" {
return setForAutoRestartByNamespace[deployment.Namespace]
}
restartDeploymentBool, err := stringToBool(restartDeployment)
if err != nil {
log.Error(err, "Error parsing %v annotation on Deployment %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, deployment.Name)
return false
}
return restartDeploymentBool
}
func (h *SecretUpdateHandler) isNamespaceSetToAutoRestart(namespace *corev1.Namespace) bool {
restartDeployment := namespace.Annotations[RestartDeploymentsAnnotation]
//If annotation for auto restarts for deployment is not set. Check environment variable set on the operator
if restartDeployment == "" {
return h.shouldAutoRestartDeploymentsGlobal
}
restartDeploymentBool, err := stringToBool(restartDeployment)
if err != nil {
log.Error(err, "Error parsing %v annotation on Namespace %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, namespace.Name)
return false
}
return restartDeploymentBool
}
func stringToBool(str string) (bool, error) {
restartDeploymentBool, err := strconv.ParseBool(strings.ToLower(str))
if err != nil {
return false, err
}
return restartDeploymentBool, nil
}

View File

@@ -34,14 +34,16 @@ const (
) )
type testUpdateSecretTask struct { type testUpdateSecretTask struct {
testName string testName string
existingDeployment *appsv1.Deployment existingDeployment *appsv1.Deployment
existingSecret *corev1.Secret existingNamespace *corev1.Namespace
expectedError error existingSecret *corev1.Secret
expectedResultSecret *corev1.Secret expectedError error
expectedEvents []string expectedResultSecret *corev1.Secret
opItem map[string]string expectedEvents []string
expectedRestart bool opItem map[string]string
expectedRestart bool
globalAutoRestartEnabled bool
} }
var ( var (
@@ -52,9 +54,16 @@ var (
itemPath = fmt.Sprintf("vaults/%v/items/%v", vaultId, itemId) itemPath = fmt.Sprintf("vaults/%v/items/%v", vaultId, itemId)
) )
var defaultNamespace = &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: namespace,
},
}
var tests = []testUpdateSecretTask{ var tests = []testUpdateSecretTask{
{ {
testName: "Test unrelated deployment is not restarted with an updated secret", testName: "Test unrelated deployment is not restarted with an updated secret",
existingNamespace: defaultNamespace,
existingDeployment: &appsv1.Deployment{ existingDeployment: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{ TypeMeta: metav1.TypeMeta{
Kind: deploymentKind, Kind: deploymentKind,
@@ -96,10 +105,12 @@ var tests = []testUpdateSecretTask{
userKey: username, userKey: username,
passKey: password, passKey: password,
}, },
expectedRestart: false, expectedRestart: false,
globalAutoRestartEnabled: true,
}, },
{ {
testName: "OP item has new version. Secret needs update. Deployment is restarted based on containers", testName: "OP item has new version. Secret needs update. Deployment is restarted based on containers",
existingNamespace: defaultNamespace,
existingDeployment: &appsv1.Deployment{ existingDeployment: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{ TypeMeta: metav1.TypeMeta{
Kind: deploymentKind, Kind: deploymentKind,
@@ -160,10 +171,12 @@ var tests = []testUpdateSecretTask{
userKey: username, userKey: username,
passKey: password, passKey: password,
}, },
expectedRestart: true, expectedRestart: true,
globalAutoRestartEnabled: true,
}, },
{ {
testName: "OP item has new version. Secret needs update. Deployment is restarted based on annotation", testName: "OP item has new version. Secret needs update. Deployment is restarted based on annotation",
existingNamespace: defaultNamespace,
existingDeployment: &appsv1.Deployment{ existingDeployment: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{ TypeMeta: metav1.TypeMeta{
Kind: deploymentKind, Kind: deploymentKind,
@@ -205,10 +218,12 @@ var tests = []testUpdateSecretTask{
userKey: username, userKey: username,
passKey: password, passKey: password,
}, },
expectedRestart: true, expectedRestart: true,
globalAutoRestartEnabled: true,
}, },
{ {
testName: "OP item has new version. Secret needs update. Deployment is restarted based on volume", testName: "OP item has new version. Secret needs update. Deployment is restarted based on volume",
existingNamespace: defaultNamespace,
existingDeployment: &appsv1.Deployment{ existingDeployment: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{ TypeMeta: metav1.TypeMeta{
Kind: deploymentKind, Kind: deploymentKind,
@@ -262,10 +277,12 @@ var tests = []testUpdateSecretTask{
userKey: username, userKey: username,
passKey: password, passKey: password,
}, },
expectedRestart: true, expectedRestart: true,
globalAutoRestartEnabled: true,
}, },
{ {
testName: "No secrets need update. No deployment is restarted", testName: "No secrets need update. No deployment is restarted",
existingNamespace: defaultNamespace,
existingDeployment: &appsv1.Deployment{ existingDeployment: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{ TypeMeta: metav1.TypeMeta{
Kind: deploymentKind, Kind: deploymentKind,
@@ -307,11 +324,298 @@ var tests = []testUpdateSecretTask{
userKey: username, userKey: username,
passKey: password, passKey: password,
}, },
expectedRestart: false, expectedRestart: false,
globalAutoRestartEnabled: true,
},
{
testName: `Deployment is not restarted when no auto restart is set to true for all
deployments and is not overwritten by by a namespace or deployment annotation`,
existingNamespace: defaultNamespace,
existingDeployment: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: deploymentKind,
APIVersion: deploymentAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Env: []corev1.EnvVar{
{
Name: name,
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: name,
},
Key: passKey,
},
},
},
},
},
},
},
},
},
},
existingSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: "old version",
ItemPathAnnotation: itemPath,
},
},
Data: expectedSecretData,
},
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: fmt.Sprint(itemVersion),
ItemPathAnnotation: itemPath,
},
},
Data: expectedSecretData,
},
opItem: map[string]string{
userKey: username,
passKey: password,
},
expectedRestart: false,
globalAutoRestartEnabled: false,
},
{
testName: `Deployment autostart true value takes precedence over false global auto restart value`,
existingNamespace: defaultNamespace,
existingDeployment: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: deploymentKind,
APIVersion: deploymentAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
RestartDeploymentsAnnotation: "true",
},
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Env: []corev1.EnvVar{
{
Name: name,
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: name,
},
Key: passKey,
},
},
},
},
},
},
},
},
},
},
existingSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: "old version",
ItemPathAnnotation: itemPath,
},
},
Data: expectedSecretData,
},
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: fmt.Sprint(itemVersion),
ItemPathAnnotation: itemPath,
},
},
Data: expectedSecretData,
},
opItem: map[string]string{
userKey: username,
passKey: password,
},
expectedRestart: true,
globalAutoRestartEnabled: true,
},
{
testName: `Deployment autostart false value takes precedence over false global auto restart value,
and true namespace value.`,
existingNamespace: &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: namespace,
Annotations: map[string]string{
RestartDeploymentsAnnotation: "true",
},
},
},
existingDeployment: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: deploymentKind,
APIVersion: deploymentAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
RestartDeploymentsAnnotation: "false",
},
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Env: []corev1.EnvVar{
{
Name: name,
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: name,
},
Key: passKey,
},
},
},
},
},
},
},
},
},
},
existingSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: "old version",
ItemPathAnnotation: itemPath,
},
},
Data: expectedSecretData,
},
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: fmt.Sprint(itemVersion),
ItemPathAnnotation: itemPath,
},
},
Data: expectedSecretData,
},
opItem: map[string]string{
userKey: username,
passKey: password,
},
expectedRestart: false,
globalAutoRestartEnabled: false,
},
{
testName: `Namespace autostart true value takes precedence over false global auto restart value`,
existingNamespace: &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: namespace,
Annotations: map[string]string{
RestartDeploymentsAnnotation: "true",
},
},
},
existingDeployment: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: deploymentKind,
APIVersion: deploymentAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Env: []corev1.EnvVar{
{
Name: name,
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: name,
},
Key: passKey,
},
},
},
},
},
},
},
},
},
},
existingSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: "old version",
ItemPathAnnotation: itemPath,
},
},
Data: expectedSecretData,
},
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: fmt.Sprint(itemVersion),
ItemPathAnnotation: itemPath,
},
},
Data: expectedSecretData,
},
opItem: map[string]string{
userKey: username,
passKey: password,
},
expectedRestart: true,
globalAutoRestartEnabled: false,
}, },
} }
func TestReconcileDepoyment(t *testing.T) { func TestUpdateSecretHandler(t *testing.T) {
for _, testData := range tests { for _, testData := range tests {
t.Run(testData.testName, func(t *testing.T) { t.Run(testData.testName, func(t *testing.T) {
@@ -322,6 +626,7 @@ func TestReconcileDepoyment(t *testing.T) {
// Objects to track in the fake client. // Objects to track in the fake client.
objs := []runtime.Object{ objs := []runtime.Object{
testData.existingDeployment, testData.existingDeployment,
testData.existingNamespace,
} }
if testData.existingSecret != nil { if testData.existingSecret != nil {
@@ -342,8 +647,9 @@ func TestReconcileDepoyment(t *testing.T) {
return &item, nil return &item, nil
} }
h := &SecretUpdateHandler{ h := &SecretUpdateHandler{
client: cl, client: cl,
opConnectClient: opConnectClient, opConnectClient: opConnectClient,
shouldAutoRestartDeploymentsGlobal: testData.globalAutoRestartEnabled,
} }
err := h.UpdateKubernetesSecretsTask() err := h.UpdateKubernetesSecretsTask()
@@ -377,7 +683,7 @@ func TestReconcileDepoyment(t *testing.T) {
_, ok := deployment.Spec.Template.Annotations[RestartAnnotation] _, ok := deployment.Spec.Template.Annotations[RestartAnnotation]
if ok { if ok {
assert.True(t, testData.expectedRestart) assert.True(t, testData.expectedRestart, "Expected deployment to restart but it did not")
} else { } else {
assert.False(t, testData.expectedRestart) assert.False(t, testData.expectedRestart)
} }