package providerctl import ( "context" "strings" "time" goext "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" "github.com/fluxcd/pkg/ssa" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" logger "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/client" unstructuredutil "edge-infra.dev/pkg/k8s/unstructured" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" "edge-infra.dev/pkg/edge/constants" api "edge-infra.dev/pkg/edge/iam/api/v1alpha1" "edge-infra.dev/pkg/edge/iam/config" ) func (r *ProviderReconciler) reconcileEncryptionKeyRotation(ctx context.Context, provider api.Provider) (api.Provider, error) { log := logger.FromContext(ctx) // if we do not have an encryption version, do not encrypt if provider.Spec.Encryption.Version == "" { //nolint // try to get any encryption-key-[] external secrets in the edge-iam ns var externalSecrets = &goext.ExternalSecretList{} err := r.Client.List(ctx, externalSecrets, &client.ListOptions{Namespace: "edge-iam"}) if err != nil { return provider, err } if len(externalSecrets.Items) > 0 { // do they start with the encryption prefix for _, s := range externalSecrets.Items { if strings.Contains(s.Name, EncryptionKeySecretPrefix) { log.Info("Found encryption external secrets that should be deleted as encryption is no longer enabled") // grab the external secret extSec, err := r.checkExternalSecrets(ctx, s.Name) if err != nil { log.Error(err, "Unable to check if external secret exists") return provider, err } _, err = r.ResourceManager.Delete(ctx, extSec, ssa.DefaultDeleteOptions()) if err != nil { log.Error(err, "Unable to delete old external secret") return provider, err } log.Info("Deleted old external secret", "secret", s.Name) } } // remove the encryption status from the provider provider = RemoveEncryptionStatus(provider) } log.Info("Encryption is not enabled") return provider, nil } else { // nolint log.Info("Encryption is enabled", "version", provider.Spec.Encryption.Version) } // create the k8s secret name secretName := EncryptionKeySecretPrefix + provider.Spec.Encryption.Version // check if we've already created the external secret before extSec, err := r.checkExternalSecrets(ctx, secretName) if err != nil { log.Error(err, "Unable to check if external secret exists") return provider, err } // if it doesn't exist, create if extSec == nil { // generate name of encryption key keyName := "id-encryption-key-" + config.ClusterID() // create external secret as unstructured extSec, err := CreateEncryptionExternalSecret(provider.Spec.Encryption.Version, keyName, secretName) if err != nil { return provider, err } // apply external secret _, err = r.ResourceManager.Apply(ctx, extSec, ssa.ApplyOptions{Force: true}) if err != nil { return provider, err } log.Info("Created external secret for encryption", "secret", secretName) } // if provider's db status version = provider's encryption.version // we can delete the old external secret (if found) _, matches := DoesStatusMatchSpecVersion(provider) if matches { err = r.removeOldExternalSecret(ctx, secretName) if err != nil { log.Error(err, "Error trying to remove an old external secret") return provider, err } } return provider, nil } // check if we need to create a new external secret object by trying to grab it func (r *ProviderReconciler) checkExternalSecrets(ctx context.Context, secretName string) (*unstructured.Unstructured, error) { var externalSecret = &goext.ExternalSecret{} err := r.Get(ctx, types.NamespacedName{Namespace: "edge-iam", Name: secretName}, externalSecret) if err != nil { if errors.IsNotFound(err) { return nil, nil } return nil, err } // convert to unstructured uobj, err := unstructuredutil.ToUnstructured(externalSecret) if err != nil { return nil, err } return uobj, nil } // Creating the following as an unstructured obj: // // apiVersion: external-secrets.io/v1beta1 // kind: ExternalSecret // metadata: // name: id-encryption-key-[version] // namespace: edge-iam // labels: // platform.edge.ncr.com/component: edge-iam // spec: // dataFrom: // - extract: // key: [key] // typically id-encryption-key-[clusterid] // version: version // refreshInterval: 1m // secretStoreRef: // name: gcp-provider // kind: ClusterSecretStore // target: // name: id-encryption-key-[version] // creationPolicy: Owner func CreateEncryptionExternalSecret(version string, keyName string, secretName string) (*unstructured.Unstructured, error) { extSec := &goext.ExternalSecret{ TypeMeta: metav1.TypeMeta{ APIVersion: goext.ExtSecretGroupVersionKind.GroupVersion().String(), Kind: goext.ExtSecretGroupVersionKind.Kind, }, ObjectMeta: metav1.ObjectMeta{ Name: secretName, Namespace: "edge-iam", Labels: map[string]string{ constants.PlatformComponent: "edge-iam", }, }, Spec: goext.ExternalSecretSpec{ DataFrom: []goext.ExternalSecretDataFromRemoteRef{ { Extract: &goext.ExternalSecretDataRemoteRef{ Key: keyName, Version: version, }, }, }, RefreshInterval: &metav1.Duration{ Duration: time.Minute, }, SecretStoreRef: goext.SecretStoreRef{ Name: "gcp-provider", Kind: "ClusterSecretStore", }, Target: goext.ExternalSecretTarget{ Name: secretName, CreationPolicy: goext.CreatePolicyOwner, }, }, } uobj, err := unstructuredutil.ToUnstructured(extSec) if err != nil { return uobj, err } return uobj, nil } // DoesStatusMatchSpecVersion checks if the provider status version (aka, the databases) matches Spec.Encryption.Version func DoesStatusMatchSpecVersion(provider api.Provider) (string, bool) { dbVersion := "" prefix := "successfully updated databases to version: " for i := range provider.Status.Conditions { if provider.Status.Conditions[i].Reason == "EncryptionRotationSucceeded" { // grab the version from the end of the message dbVersion = strings.TrimSpace(provider.Status.Conditions[i].Message[len(prefix):]) } } // does the version in the EncryptionRotationSucceeded status match the provider's latest version return dbVersion, dbVersion == provider.Spec.Encryption.Version } // removeOldExternalSecret removes the old external secret from the ns func (r *ProviderReconciler) removeOldExternalSecret(ctx context.Context, newSecretName string) error { log := logger.FromContext(ctx) // get external secrets in the namespace secretList := &goext.ExternalSecretList{} if err := r.Client.List(ctx, secretList, &client.ListOptions{Namespace: "edge-iam"}); err != nil { log.Error(err, "Couldn't list external secrets in the edge-iam namespace") return err } // search for external secret starting with 'id-encryption-key' that is NOT the new secret for _, secret := range secretList.Items { if strings.HasPrefix(secret.Name, "id-encryption-key") && secret.Name != newSecretName { // grab the old external secret extSec, err := r.checkExternalSecrets(ctx, secret.Name) if err != nil { log.Error(err, "Unable to check if external secret exists") return err } _, err = r.ResourceManager.Delete(ctx, extSec, ssa.DefaultDeleteOptions()) if err != nil { log.Error(err, "Unable to delete old external secret") return err } log.Info("Deleted old external secret", "secret", secret.Name) } } return nil } func RemoveEncryptionStatus(provider api.Provider) api.Provider { providerStatus := provider.Status.Conditions for i, condition := range providerStatus { if condition.Reason == "EncryptionRotationSucceeded" { // remove the condition from the list provider.Status.Conditions = append(providerStatus[:i], providerStatus[i+1:]...) } } return provider }