mirror of
				https://github.com/1Password/onepassword-operator.git
				synced 2025-10-31 11:49:40 +00:00 
			
		
		
		
	 cabc020cc6
			
		
	
	cabc020cc6
	
	
	
		
			
			* Add missing improvements from Operator SDK 1.34.1 These were not mentioned in the upgrade documentation for version 1.34.x (https://sdk.operatorframework.io/docs/upgrading-sdk-version/v1.34.0/), but I've found them by compating the release with the previous one (https://github.com/operator-framework/operator-sdk/compare/v1.33.0...v1.34.1). * Upgrade to Operator SDK 1.36.0 Source of upgrade steps: https://sdk.operatorframework.io/docs/upgrading-sdk-version/v1.36.0/ Key differences: - Go packages `k8s.io/*` are already at a version higher than the one in the upgrade. - `ENVTEST_K8S_VERSION` is at a version higher than the one in the upgrade - We didn't have the golangci-lint make command before, thus we only needed to add things. * Upgrade to Operator SDK 1.38.0 Source of upgrade steps: https://sdk.operatorframework.io/docs/upgrading-sdk-version/v1.38.0/ * Upgrade to Operator SDK 1.39.0 Source of upgrade steps: https://sdk.operatorframework.io/docs/upgrading-sdk-version/v1.39.0/ * Upgrade to Operator SDK 1.40.0 Source of upgrade steps: https://sdk.operatorframework.io/docs/upgrading-sdk-version/v1.40.0/ I didn't do the "Add app.kubernetes.io/name label to your manifests" since it seems that we have it already, and it's customized. * Address lint errors * Update golangci-lint version used to support Go 1.24 * Improve workflows - Make workflow targets more specific. - Make build workflow only build (i.e. remove test part of it). - Rearrange steps and improve naming for build workflow. * Add back deleted test Initially the test has been removed due to lint saying that it was duplicate code, but it falsely errored since the values are different. * Improve code and add missing upgrade pieces * Upgrade to Operator SDK 1.41.1 Source of upgrade steps: https://sdk.operatorframework.io/docs/upgrading-sdk-version/v1.41.0/ Upgrading to 1.41.1 from 1.40.0 doesn't have any migration steps. Key elements: - Upgrade to golangci-lint v2 - Made the manifests using the updated controller tools * Address linter errors golanci-lint v2 seems to be more robust than the previous one, which is beneficial. Thus, we address the linter errors thrown by v2 and improve our code even further. * Add Makefile improvements These were brought in by comparing the Makefile of a freshly created operator using the latest operator-sdk with ours. * Add missing default kustomization for 1.40.0 upgrade * Bring default kustomization to latest version This is done by putting the file's content from a newly-generated operator. * Switch metrics-bind-address default value back to 8080 This ensures that the upgrade is backwards-compatible. * Add webhook-related scaffolding This enables us to easily add support for webhooks by running `operator-sdk create webhook` whenever we want to add them. * Fix typo
		
			
				
	
	
		
			285 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			285 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package onepassword
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"time"
 | |
| 
 | |
| 	onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
 | |
| 	kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
 | |
| 	"github.com/1Password/onepassword-operator/pkg/logs"
 | |
| 	opclient "github.com/1Password/onepassword-operator/pkg/onepassword/client"
 | |
| 	"github.com/1Password/onepassword-operator/pkg/onepassword/model"
 | |
| 	"github.com/1Password/onepassword-operator/pkg/utils"
 | |
| 
 | |
| 	appsv1 "k8s.io/api/apps/v1"
 | |
| 	corev1 "k8s.io/api/core/v1"
 | |
| 	"sigs.k8s.io/controller-runtime/pkg/client"
 | |
| 	logf "sigs.k8s.io/controller-runtime/pkg/log"
 | |
| )
 | |
| 
 | |
| // const envHostVariable = "OP_HOST"
 | |
| const lockTag = "operator.1password.io:ignore-secret"
 | |
| 
 | |
| var log = logf.Log.WithName("update_op_kubernetes_secrets_task")
 | |
| 
 | |
| func NewManager(
 | |
| 	kubernetesClient client.Client,
 | |
| 	opClient opclient.Client,
 | |
| 	shouldAutoRestartDeploymentsGlobal bool,
 | |
| ) *SecretUpdateHandler {
 | |
| 	return &SecretUpdateHandler{
 | |
| 		client:                             kubernetesClient,
 | |
| 		opClient:                           opClient,
 | |
| 		shouldAutoRestartDeploymentsGlobal: shouldAutoRestartDeploymentsGlobal,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| type SecretUpdateHandler struct {
 | |
| 	client                             client.Client
 | |
| 	opClient                           opclient.Client
 | |
| 	shouldAutoRestartDeploymentsGlobal bool
 | |
| }
 | |
| 
 | |
| func (h *SecretUpdateHandler) UpdateKubernetesSecretsTask(ctx context.Context) error {
 | |
| 	updatedKubernetesSecrets, err := h.updateKubernetesSecrets(ctx)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return h.restartDeploymentsWithUpdatedSecrets(ctx, updatedKubernetesSecrets)
 | |
| }
 | |
| 
 | |
| func (h *SecretUpdateHandler) restartDeploymentsWithUpdatedSecrets(
 | |
| 	ctx context.Context,
 | |
| 	updatedSecretsByNamespace map[string]map[string]*corev1.Secret,
 | |
| ) error {
 | |
| 	// No secrets to update. Exit
 | |
| 	if len(updatedSecretsByNamespace) == 0 || updatedSecretsByNamespace == nil {
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	deployments := &appsv1.DeploymentList{}
 | |
| 	err := h.client.List(ctx, deployments)
 | |
| 	if err != nil {
 | |
| 		log.Error(err, "Failed to list kubernetes deployments")
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	if len(deployments.Items) == 0 {
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	setForAutoRestartByNamespaceMap, err := h.getIsSetForAutoRestartByNamespaceMap(ctx)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	for i := 0; i < len(deployments.Items); i++ {
 | |
| 		deployment := &deployments.Items[i]
 | |
| 		updatedSecrets := updatedSecretsByNamespace[deployment.Namespace]
 | |
| 
 | |
| 		updatedDeploymentSecrets := GetUpdatedSecretsForDeployment(deployment, updatedSecrets)
 | |
| 		if len(updatedDeploymentSecrets) == 0 {
 | |
| 			continue
 | |
| 		}
 | |
| 		for _, secret := range updatedDeploymentSecrets {
 | |
| 			if isSecretSetForAutoRestart(secret, deployment, setForAutoRestartByNamespaceMap) {
 | |
| 				h.restartDeployment(ctx, deployment)
 | |
| 				continue
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		log.V(logs.DebugLevel).Info(fmt.Sprintf("Deployment %q at namespace %q is up to date",
 | |
| 			deployment.GetName(), deployment.Namespace,
 | |
| 		))
 | |
| 
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (h *SecretUpdateHandler) restartDeployment(ctx context.Context, deployment *appsv1.Deployment) {
 | |
| 	log.Info(fmt.Sprintf("Deployment %q at namespace %q references an updated secret. Restarting",
 | |
| 		deployment.GetName(), deployment.Namespace,
 | |
| 	))
 | |
| 	if deployment.Spec.Template.Annotations == nil {
 | |
| 		deployment.Spec.Template.Annotations = map[string]string{}
 | |
| 	}
 | |
| 	deployment.Spec.Template.Annotations[RestartAnnotation] = time.Now().String()
 | |
| 	err := h.client.Update(ctx, deployment)
 | |
| 	if err != nil {
 | |
| 		log.Error(err, "Problem restarting deployment")
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (h *SecretUpdateHandler) updateKubernetesSecrets(ctx context.Context) (
 | |
| 	map[string]map[string]*corev1.Secret, error,
 | |
| ) {
 | |
| 	secrets := &corev1.SecretList{}
 | |
| 	err := h.client.List(ctx, secrets)
 | |
| 	if err != nil {
 | |
| 		log.Error(err, "Failed to list kubernetes secrets")
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	updatedSecrets := map[string]map[string]*corev1.Secret{}
 | |
| 	for i := 0; i < len(secrets.Items); i++ {
 | |
| 		secret := secrets.Items[i]
 | |
| 
 | |
| 		itemPath := secret.Annotations[ItemPathAnnotation]
 | |
| 		currentVersion := secret.Annotations[VersionAnnotation]
 | |
| 		if len(itemPath) == 0 || len(currentVersion) == 0 {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		OnePasswordItemPath := h.getPathFromOnePasswordItem(secret)
 | |
| 
 | |
| 		item, err := GetOnePasswordItemByPath(ctx, h.opClient, OnePasswordItemPath)
 | |
| 		if err != nil {
 | |
| 			log.Error(err, fmt.Sprintf("failed to retrieve 1Password item at path %s for secret %s",
 | |
| 				secret.Annotations[ItemPathAnnotation], secret.Name,
 | |
| 			))
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		itemVersion := fmt.Sprint(item.Version)
 | |
| 		itemPathString := fmt.Sprintf("vaults/%v/items/%v", item.VaultID, item.ID)
 | |
| 
 | |
| 		if currentVersion != itemVersion || secret.Annotations[ItemPathAnnotation] != itemPathString {
 | |
| 			if isItemLockedForForcedRestarts(item) {
 | |
| 				log.V(logs.DebugLevel).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
 | |
| 				secret.Annotations[ItemPathAnnotation] = itemPathString
 | |
| 				if err := h.client.Update(ctx, &secret); err != nil {
 | |
| 					log.Error(err, fmt.Sprintf("failed to update secret %s annotations to version %s", secret.Name, itemVersion))
 | |
| 					continue
 | |
| 				}
 | |
| 				continue
 | |
| 			}
 | |
| 			log.Info(fmt.Sprintf("Updating kubernetes secret '%v'", secret.GetName()))
 | |
| 			secret.Annotations[VersionAnnotation] = itemVersion
 | |
| 			secret.Annotations[ItemPathAnnotation] = itemPathString
 | |
| 			secret.Data = kubeSecrets.BuildKubernetesSecretData(item.Fields, item.Files)
 | |
| 			log.V(logs.DebugLevel).Info(fmt.Sprintf("New secret path: %v and version: %v",
 | |
| 				secret.Annotations[ItemPathAnnotation], secret.Annotations[VersionAnnotation],
 | |
| 			))
 | |
| 			if err := h.client.Update(ctx, &secret); err != nil {
 | |
| 				log.Error(err, fmt.Sprintf("failed to update secret %s to version %s", secret.Name, itemVersion))
 | |
| 				continue
 | |
| 			}
 | |
| 			if updatedSecrets[secret.Namespace] == nil {
 | |
| 				updatedSecrets[secret.Namespace] = make(map[string]*corev1.Secret)
 | |
| 			}
 | |
| 			updatedSecrets[secret.Namespace][secret.Name] = &secret
 | |
| 		}
 | |
| 	}
 | |
| 	return updatedSecrets, nil
 | |
| }
 | |
| 
 | |
| func isItemLockedForForcedRestarts(item *model.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]*corev1.Secret) bool {
 | |
| 	_, ok := updatedSecrets[secretName]
 | |
| 	return ok
 | |
| }
 | |
| 
 | |
| func (h *SecretUpdateHandler) getIsSetForAutoRestartByNamespaceMap(ctx context.Context) (map[string]bool, error) {
 | |
| 	namespaces := &corev1.NamespaceList{}
 | |
| 	err := h.client.List(ctx, 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 (h *SecretUpdateHandler) getPathFromOnePasswordItem(secret corev1.Secret) string {
 | |
| 	onePasswordItem := &onepasswordv1.OnePasswordItem{}
 | |
| 
 | |
| 	// Search for our original OnePasswordItem if it exists
 | |
| 	err := h.client.Get(context.TODO(), client.ObjectKey{
 | |
| 		Namespace: secret.Namespace,
 | |
| 		Name:      secret.Name}, onePasswordItem)
 | |
| 
 | |
| 	if err == nil {
 | |
| 		return onePasswordItem.Spec.ItemPath
 | |
| 	}
 | |
| 
 | |
| 	// If we can't find the OnePassword Item we'll just return the annotation from the secret item.
 | |
| 	return secret.Annotations[ItemPathAnnotation]
 | |
| }
 | |
| 
 | |
| func isSecretSetForAutoRestart(
 | |
| 	secret *corev1.Secret,
 | |
| 	deployment *appsv1.Deployment,
 | |
| 	setForAutoRestartByNamespace map[string]bool,
 | |
| ) bool {
 | |
| 	restartDeployment := secret.Annotations[RestartDeploymentsAnnotation]
 | |
| 	// If annotation for auto restarts for deployment is not set. Check for the annotation on its namepsace
 | |
| 	if restartDeployment == "" {
 | |
| 		return isDeploymentSetForAutoRestart(deployment, setForAutoRestartByNamespace)
 | |
| 	}
 | |
| 
 | |
| 	restartDeploymentBool, err := utils.StringToBool(restartDeployment)
 | |
| 	if err != nil {
 | |
| 		log.Error(err, fmt.Sprintf("Error parsing %s annotation on Secret %s. Must be true or false. Defaulting to false.",
 | |
| 			RestartDeploymentsAnnotation, secret.Name,
 | |
| 		))
 | |
| 		return false
 | |
| 	}
 | |
| 	return restartDeploymentBool
 | |
| }
 | |
| 
 | |
| 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 := utils.StringToBool(restartDeployment)
 | |
| 	if err != nil {
 | |
| 		log.Error(err, fmt.Sprintf(
 | |
| 			"Error parsing %s annotation on Deployment %s. 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 := utils.StringToBool(restartDeployment)
 | |
| 	if err != nil {
 | |
| 		log.Error(err, fmt.Sprintf("Error parsing %s annotation on Namespace %s. Must be true or false. Defaulting to false.",
 | |
| 			RestartDeploymentsAnnotation, namespace.Name,
 | |
| 		))
 | |
| 		return false
 | |
| 	}
 | |
| 	return restartDeploymentBool
 | |
| }
 |