package infisicalpushsecret import ( "bytes" "context" "fmt" "strings" tpl "text/template" "github.com/Infisical/infisical/k8-operator/api/v1alpha1" "github.com/Infisical/infisical/k8-operator/internal/api" "github.com/Infisical/infisical/k8-operator/internal/constants" "github.com/Infisical/infisical/k8-operator/internal/model" "github.com/Infisical/infisical/k8-operator/internal/template" "github.com/Infisical/infisical/k8-operator/internal/util" "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" generatorUtil "github.com/Infisical/infisical/k8-operator/internal/generator" infisicalSdk "github.com/infisical/go-sdk" "k8s.io/apimachinery/pkg/runtime" ) type InfisicalPushSecretReconciler struct { client.Client Scheme *runtime.Scheme IsNamespaceScoped bool } func (r *InfisicalPushSecretReconciler) getResourceVariables(infisicalPushSecret v1alpha1.InfisicalPushSecret, resourceVariablesMap map[string]util.ResourceVariables) util.ResourceVariables { var resourceVariables util.ResourceVariables if _, ok := resourceVariablesMap[string(infisicalPushSecret.UID)]; !ok { ctx, cancel := context.WithCancel(context.Background()) client := infisicalSdk.NewInfisicalClient(ctx, infisicalSdk.Config{ SiteUrl: api.API_HOST_URL, CaCertificate: api.API_CA_CERTIFICATE, UserAgent: api.USER_AGENT_NAME, }) resourceVariablesMap[string(infisicalPushSecret.UID)] = util.ResourceVariables{ InfisicalClient: client, CancelCtx: cancel, AuthDetails: util.AuthenticationDetails{}, } resourceVariables = resourceVariablesMap[string(infisicalPushSecret.UID)] } else { resourceVariables = resourceVariablesMap[string(infisicalPushSecret.UID)] } return resourceVariables } func (r *InfisicalPushSecretReconciler) updateResourceVariables(infisicalPushSecret v1alpha1.InfisicalPushSecret, resourceVariables util.ResourceVariables, resourceVariablesMap map[string]util.ResourceVariables) { resourceVariablesMap[string(infisicalPushSecret.UID)] = resourceVariables } func (r *InfisicalPushSecretReconciler) processGenerators(ctx context.Context, infisicalPushSecret v1alpha1.InfisicalPushSecret) (map[string]string, error) { processedSecrets := make(map[string]string) if len(infisicalPushSecret.Spec.Push.Generators) == 0 { return processedSecrets, nil } for _, generator := range infisicalPushSecret.Spec.Push.Generators { generatorRef := generator.GeneratorRef clusterGenerator := &v1alpha1.ClusterGenerator{} err := r.Client.Get(ctx, types.NamespacedName{Name: generatorRef.Name}, clusterGenerator) if err != nil { return nil, fmt.Errorf("unable to get ClusterGenerator resource [err=%s]", err) } if generatorRef.Kind == v1alpha1.GeneratorKindPassword { // get the custom ClusterGenerator resource from the cluster if clusterGenerator.Spec.Generator.PasswordSpec == nil { return nil, fmt.Errorf("password spec is not defined in the ClusterGenerator resource") } password, err := generatorUtil.GeneratorPassword(*clusterGenerator.Spec.Generator.PasswordSpec) if err != nil { return nil, fmt.Errorf("unable to generate password [err=%s]", err) } processedSecrets[generator.DestinationSecretName] = password } if generatorRef.Kind == v1alpha1.GeneratorKindUUID { uuid, err := generatorUtil.GeneratorUUID() if err != nil { return nil, fmt.Errorf("unable to generate UUID [err=%s]", err) } processedSecrets[generator.DestinationSecretName] = uuid } } return processedSecrets, nil } func (r *InfisicalPushSecretReconciler) processTemplatedSecrets(infisicalPushSecret v1alpha1.InfisicalPushSecret, kubePushSecret *corev1.Secret, destination v1alpha1.InfisicalPushSecretDestination) (map[string]string, error) { processedSecrets := make(map[string]string) sourceSecrets := make(map[string]model.SecretTemplateOptions) for key, value := range kubePushSecret.Data { sourceSecrets[key] = model.SecretTemplateOptions{ Value: string(value), SecretPath: destination.SecretsPath, } } if infisicalPushSecret.Spec.Push.Secret.Template == nil || (infisicalPushSecret.Spec.Push.Secret.Template != nil && infisicalPushSecret.Spec.Push.Secret.Template.IncludeAllSecrets) { for key, value := range kubePushSecret.Data { processedSecrets[key] = string(value) } } if infisicalPushSecret.Spec.Push.Secret.Template != nil && len(infisicalPushSecret.Spec.Push.Secret.Template.Data) > 0 { for templateKey, userTemplate := range infisicalPushSecret.Spec.Push.Secret.Template.Data { tmpl, err := tpl.New("push-secret-templates").Funcs(template.GetTemplateFunctions()).Parse(userTemplate) if err != nil { return nil, fmt.Errorf("unable to compile template: %s [err=%v]", templateKey, err) } buf := bytes.NewBuffer(nil) err = tmpl.Execute(buf, sourceSecrets) if err != nil { return nil, fmt.Errorf("unable to execute template: %s [err=%v]", templateKey, err) } processedSecrets[templateKey] = buf.String() } } return processedSecrets, nil } func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context.Context, logger logr.Logger, infisicalPushSecret *v1alpha1.InfisicalPushSecret, resourceVariablesMap map[string]util.ResourceVariables) error { resourceVariables := r.getResourceVariables(*infisicalPushSecret, resourceVariablesMap) infisicalClient := resourceVariables.InfisicalClient cancelCtx := resourceVariables.CancelCtx authDetails := resourceVariables.AuthDetails var err error if authDetails.AuthStrategy == "" { logger.Info("No authentication strategy found. Attempting to authenticate") authDetails, err = util.HandleAuthentication(ctx, util.SecretAuthInput{ Secret: *infisicalPushSecret, Type: util.SecretCrd.INFISICAL_PUSH_SECRET, }, r.Client, infisicalClient, r.IsNamespaceScoped) r.SetAuthenticatedStatusCondition(ctx, infisicalPushSecret, err) if err != nil { return fmt.Errorf("unable to authenticate [err=%s]", err) } r.updateResourceVariables(*infisicalPushSecret, util.ResourceVariables{ InfisicalClient: infisicalClient, CancelCtx: cancelCtx, AuthDetails: authDetails, }, resourceVariablesMap) } processedSecrets := make(map[string]string) if infisicalPushSecret.Spec.Push.Secret != nil { kubePushSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{ Namespace: infisicalPushSecret.Spec.Push.Secret.SecretNamespace, Name: infisicalPushSecret.Spec.Push.Secret.SecretName, }) if err != nil { if util.IsNamespaceScopedError(err, r.IsNamespaceScoped) { return fmt.Errorf("unable to fetch Kubernetes destination secret. Your Operator installation is namespace scoped, and cannot read secrets outside of the namespace it is installed in. Please ensure the destination secret is in the same namespace as the operator. [err=%v]", err) } return fmt.Errorf("unable to fetch kube secret [err=%s]", err) } processedSecrets, err = r.processTemplatedSecrets(*infisicalPushSecret, kubePushSecret, infisicalPushSecret.Spec.Destination) if err != nil { return fmt.Errorf("unable to process templated secrets [err=%s]", err) } } generatorSecrets, err := r.processGenerators(ctx, *infisicalPushSecret) if err != nil { return fmt.Errorf("unable to process generators [err=%s]", err) } for key, value := range generatorSecrets { processedSecrets[key] = value } destination := infisicalPushSecret.Spec.Destination existingSecrets, err := infisicalClient.Secrets().List(infisicalSdk.ListSecretsOptions{ ProjectID: destination.ProjectID, Environment: destination.EnvironmentSlug, SecretPath: destination.SecretsPath, IncludeImports: false, }) getExistingSecretByKey := func(key string) *infisicalSdk.Secret { for _, secret := range existingSecrets { if secret.SecretKey == key { return &secret } } return nil } getExistingSecretById := func(id string) *infisicalSdk.Secret { for _, secret := range existingSecrets { if secret.ID == id { return &secret } } return nil } updateExistingSecretByKey := func(key string, newSecretValue string) { for i := range existingSecrets { if existingSecrets[i].SecretKey == key { existingSecrets[i].SecretValue = newSecretValue break } } } if err != nil { return fmt.Errorf("unable to list secrets [err=%s]", err) } updatePolicy := infisicalPushSecret.Spec.UpdatePolicy var secretsFailedToCreate []string var secretsFailedToUpdate []string var secretsFailedToDelete []string var secretsFailedToReplaceById []string // If the ManagedSecrets are nil, we know this is the first time the InfisicalPushSecret is being reconciled. if infisicalPushSecret.Status.ManagedSecrets == nil { infisicalPushSecret.Status.ManagedSecrets = make(map[string]string) // (string[id], string[key] ) for secretKey, secretValue := range processedSecrets { if exists := getExistingSecretByKey(secretKey); exists != nil { if updatePolicy == string(constants.PUSH_SECRET_REPLACE_POLICY_ENABLED) { updatedSecret, err := infisicalClient.Secrets().Update(infisicalSdk.UpdateSecretOptions{ SecretKey: secretKey, ProjectID: destination.ProjectID, Environment: destination.EnvironmentSlug, SecretPath: destination.SecretsPath, NewSecretValue: secretValue, }) if err != nil { secretsFailedToUpdate = append(secretsFailedToUpdate, secretKey) logger.Info(fmt.Sprintf("unable to update secret [key=%s] [err=%s]", secretKey, err)) continue } infisicalPushSecret.Status.ManagedSecrets[updatedSecret.ID] = secretKey } } else { createdSecret, err := infisicalClient.Secrets().Create(infisicalSdk.CreateSecretOptions{ SecretKey: secretKey, SecretValue: secretValue, ProjectID: destination.ProjectID, Environment: destination.EnvironmentSlug, SecretPath: destination.SecretsPath, }) if err != nil { secretsFailedToCreate = append(secretsFailedToCreate, secretKey) logger.Info(fmt.Sprintf("unable to create secret [key=%s] [err=%s]", secretKey, err)) continue } infisicalPushSecret.Status.ManagedSecrets[createdSecret.ID] = secretKey } } } else { // Loop over all the managed secrets, and find the corresponding existingSecret that has the same ID. If the key doesn't match, delete the secret, and re-create it with the correct key/value for managedSecretId, managedSecretKey := range infisicalPushSecret.Status.ManagedSecrets { existingSecret := getExistingSecretById(managedSecretId) if existingSecret != nil { if existingSecret.SecretKey != managedSecretKey { // Secret key has changed, lets delete the secret and re-create it with the correct key logger.Info(fmt.Sprintf("Secret with ID [id=%s] has changed key from [%s] to [%s]. Deleting and re-creating secret", managedSecretId, managedSecretKey, existingSecret.SecretKey)) deletedSecret, err := infisicalClient.Secrets().Delete(infisicalSdk.DeleteSecretOptions{ SecretKey: existingSecret.SecretKey, ProjectID: destination.ProjectID, Environment: destination.EnvironmentSlug, SecretPath: destination.SecretsPath, }) if err != nil { secretsFailedToReplaceById = append(secretsFailedToReplaceById, managedSecretKey) logger.Info(fmt.Sprintf("unable to delete secret [key=%s] [err=%s]", managedSecretKey, err)) continue } createdSecret, err := infisicalClient.Secrets().Create(infisicalSdk.CreateSecretOptions{ SecretKey: managedSecretKey, SecretValue: existingSecret.SecretValue, ProjectID: destination.ProjectID, Environment: destination.EnvironmentSlug, SecretPath: destination.SecretsPath, }) if err != nil { secretsFailedToReplaceById = append(secretsFailedToReplaceById, managedSecretKey) logger.Info(fmt.Sprintf("unable to create secret [key=%s] [err=%s]", managedSecretKey, err)) continue } delete(infisicalPushSecret.Status.ManagedSecrets, deletedSecret.ID) infisicalPushSecret.Status.ManagedSecrets[createdSecret.ID] = managedSecretKey } } } // We need to check if any of the secrets have been removed in the new kube secret for _, managedSecretKey := range infisicalPushSecret.Status.ManagedSecrets { if _, ok := processedSecrets[managedSecretKey]; !ok { // Secret has been removed, verify that the secret is managed by the operator if getExistingSecretByKey(managedSecretKey) != nil { logger.Info(fmt.Sprintf("Secret with key [key=%s] has been removed from the kube secret. Deleting secret from Infisical", managedSecretKey)) deletedSecret, err := infisicalClient.Secrets().Delete(infisicalSdk.DeleteSecretOptions{ SecretKey: managedSecretKey, ProjectID: destination.ProjectID, Environment: destination.EnvironmentSlug, SecretPath: destination.SecretsPath, }) if err != nil { secretsFailedToDelete = append(secretsFailedToDelete, managedSecretKey) logger.Info(fmt.Sprintf("unable to delete secret [key=%s] [err=%s]", managedSecretKey, err)) continue } delete(infisicalPushSecret.Status.ManagedSecrets, deletedSecret.ID) } } } // We need to check if any new secrets have been added in the kube secret for currentSecretKey := range processedSecrets { if exists := getExistingSecretByKey(currentSecretKey); exists == nil { // Some secrets has been added, verify that the secret that has been added is not already managed by the operator if _, ok := infisicalPushSecret.Status.ManagedSecrets[currentSecretKey]; !ok { // Secret was not managed by the operator, lets add it logger.Info(fmt.Sprintf("Secret with key [key=%s] has been added to the kube secret. Creating secret in Infisical", currentSecretKey)) createdSecret, err := infisicalClient.Secrets().Create(infisicalSdk.CreateSecretOptions{ SecretKey: currentSecretKey, SecretValue: processedSecrets[currentSecretKey], ProjectID: destination.ProjectID, Environment: destination.EnvironmentSlug, SecretPath: destination.SecretsPath, }) if err != nil { secretsFailedToCreate = append(secretsFailedToCreate, currentSecretKey) logger.Info(fmt.Sprintf("unable to create secret [key=%s] [err=%s]", currentSecretKey, err)) continue } infisicalPushSecret.Status.ManagedSecrets[createdSecret.ID] = currentSecretKey } } else { if updatePolicy == string(constants.PUSH_SECRET_REPLACE_POLICY_ENABLED) { existingSecret := getExistingSecretByKey(currentSecretKey) if existingSecret != nil && existingSecret.SecretValue != processedSecrets[currentSecretKey] { logger.Info(fmt.Sprintf("Secret with key [key=%s] has changed value. Updating secret in Infisical", currentSecretKey)) updatedSecret, err := infisicalClient.Secrets().Update(infisicalSdk.UpdateSecretOptions{ SecretKey: currentSecretKey, NewSecretValue: processedSecrets[currentSecretKey], ProjectID: destination.ProjectID, Environment: destination.EnvironmentSlug, SecretPath: destination.SecretsPath, }) if err != nil { secretsFailedToUpdate = append(secretsFailedToUpdate, currentSecretKey) logger.Info(fmt.Sprintf("unable to update secret [key=%s] [err=%s]", currentSecretKey, err)) continue } updateExistingSecretByKey(currentSecretKey, processedSecrets[currentSecretKey]) infisicalPushSecret.Status.ManagedSecrets[updatedSecret.ID] = currentSecretKey } } } } // Check if any of the existing secrets values have changed for secretKey, secretValue := range processedSecrets { existingSecret := getExistingSecretByKey(secretKey) if existingSecret != nil { _, managedByOperator := infisicalPushSecret.Status.ManagedSecrets[existingSecret.ID] if secretValue != existingSecret.SecretValue { if managedByOperator || updatePolicy == string(constants.PUSH_SECRET_REPLACE_POLICY_ENABLED) { logger.Info(fmt.Sprintf("Secret with key [key=%s] has changed value. Updating secret in Infisical", secretKey)) updatedSecret, err := infisicalClient.Secrets().Update(infisicalSdk.UpdateSecretOptions{ SecretKey: secretKey, NewSecretValue: secretValue, ProjectID: destination.ProjectID, Environment: destination.EnvironmentSlug, SecretPath: destination.SecretsPath, }) if err != nil { secretsFailedToUpdate = append(secretsFailedToUpdate, secretKey) logger.Info(fmt.Sprintf("unable to update secret [key=%s] [err=%s]", secretKey, err)) continue } infisicalPushSecret.Status.ManagedSecrets[updatedSecret.ID] = secretKey } } } } } var errorMessage string if len(secretsFailedToCreate) > 0 { errorMessage = fmt.Sprintf("Failed to create secrets: [%s]", strings.Join(secretsFailedToCreate, ", ")) } else { errorMessage = "" } r.SetFailedToCreateSecretsStatusCondition(ctx, infisicalPushSecret, fmt.Sprintf("Failed to create secrets: [%s]", errorMessage)) if len(secretsFailedToUpdate) > 0 { errorMessage = fmt.Sprintf("Failed to update secrets: [%s]", strings.Join(secretsFailedToUpdate, ", ")) } else { errorMessage = "" } r.SetFailedToUpdateSecretsStatusCondition(ctx, infisicalPushSecret, fmt.Sprintf("Failed to update secrets: [%s]", errorMessage)) if len(secretsFailedToDelete) > 0 { errorMessage = fmt.Sprintf("Failed to delete secrets: [%s]", strings.Join(secretsFailedToDelete, ", ")) } else { errorMessage = "" } r.SetFailedToDeleteSecretsStatusCondition(ctx, infisicalPushSecret, errorMessage) if len(secretsFailedToReplaceById) > 0 { errorMessage = fmt.Sprintf("Failed to replace secrets: [%s]", strings.Join(secretsFailedToReplaceById, ", ")) } else { errorMessage = "" } r.SetFailedToReplaceSecretsStatusCondition(ctx, infisicalPushSecret, errorMessage) // Update the status of the InfisicalPushSecret if err := r.Client.Status().Update(ctx, infisicalPushSecret); err != nil { return fmt.Errorf("unable to update status of InfisicalPushSecret [err=%s]", err) } return nil } func (r *InfisicalPushSecretReconciler) DeleteManagedSecrets(ctx context.Context, logger logr.Logger, infisicalPushSecret *v1alpha1.InfisicalPushSecret, resourceVariablesMap map[string]util.ResourceVariables) error { if infisicalPushSecret.Spec.DeletionPolicy != string(constants.PUSH_SECRET_DELETE_POLICY_ENABLED) { return nil } resourceVariables := r.getResourceVariables(*infisicalPushSecret, resourceVariablesMap) infisicalClient := resourceVariables.InfisicalClient cancelCtx := resourceVariables.CancelCtx authDetails := resourceVariables.AuthDetails var err error if authDetails.AuthStrategy == "" { logger.Info("No authentication strategy found. Attempting to authenticate") authDetails, err = util.HandleAuthentication(ctx, util.SecretAuthInput{ Secret: *infisicalPushSecret, Type: util.SecretCrd.INFISICAL_PUSH_SECRET, }, r.Client, infisicalClient, r.IsNamespaceScoped) r.SetAuthenticatedStatusCondition(ctx, infisicalPushSecret, err) if err != nil { return fmt.Errorf("unable to authenticate [err=%s]", err) } r.updateResourceVariables(*infisicalPushSecret, util.ResourceVariables{ InfisicalClient: infisicalClient, CancelCtx: cancelCtx, AuthDetails: authDetails, }, resourceVariablesMap) } destination := infisicalPushSecret.Spec.Destination existingSecrets, err := resourceVariables.InfisicalClient.Secrets().List(infisicalSdk.ListSecretsOptions{ ProjectID: destination.ProjectID, Environment: destination.EnvironmentSlug, SecretPath: destination.SecretsPath, IncludeImports: false, }) if err != nil { return fmt.Errorf("unable to list secrets [err=%s]", err) } existingSecretsMappedById := make(map[string]infisicalSdk.Secret) for _, secret := range existingSecrets { existingSecretsMappedById[secret.ID] = secret } for managedSecretId, managedSecretKey := range infisicalPushSecret.Status.ManagedSecrets { if _, ok := existingSecretsMappedById[managedSecretId]; ok { logger.Info(fmt.Sprintf("Deleting secret with key [key=%s]", managedSecretKey)) _, err := infisicalClient.Secrets().Delete(infisicalSdk.DeleteSecretOptions{ SecretKey: managedSecretKey, ProjectID: destination.ProjectID, Environment: destination.EnvironmentSlug, SecretPath: destination.SecretsPath, }) if err != nil { logger.Info(fmt.Sprintf("unable to delete secret [key=%s] [err=%s]", managedSecretKey, err)) continue } } } return nil }