package providerctl_test import ( "context" "os" "path/filepath" "reflect" "testing" "time" goext "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" ctrl "sigs.k8s.io/controller-runtime" edgeConditions "edge-infra.dev/pkg/k8s/runtime/conditions" "edge-infra.dev/pkg/lib/fog" "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/client" "edge-infra.dev/test" "edge-infra.dev/test/framework" "edge-infra.dev/test/framework/k8s" "edge-infra.dev/pkg/edge/constants" api "edge-infra.dev/pkg/edge/iam/api/v1alpha1" "edge-infra.dev/pkg/edge/iam/ctl/providerctl" "edge-infra.dev/pkg/k8s/runtime/controller" unstructuredutil "edge-infra.dev/pkg/k8s/unstructured" "edge-infra.dev/test/framework/k8s/envtest" ) // bazel test pkg/edge/iam/ctl/providerctl:providerctl_test type Suite struct { *framework.Framework *k8s.K8s ctx context.Context timeout time.Duration tick time.Duration } func TestProviderEncryptionReconciler(t *testing.T) { // test setup testEnv := envtest.Setup() ctrl.SetLogger(fog.New()) cfg, opts := controller.ProcessOptions(controller.WithCfg(testEnv.Config), controller.WithMetricsAddress("0")) opts.Scheme = createScheme() mgr, err := ctrl.NewManager(cfg, opts) if err != nil { t.Errorf("unable to create manager %v", err) } // get manifest path cwd, err := os.Getwd() if err != nil { t.Errorf("error getting working directory: %v", err) } // go up 4 directories so we can go into /config parentDir := filepath.Dir(cwd) for i := 0; i < 4; i++ { parentDir = filepath.Dir(parentDir) } // set env variables os.Setenv("IAM_CLUSTER_ID", "123") os.Setenv("IAM_ENCRYPTION_ENABLED", "true") os.Setenv("IAM_ENV_TEST", "true") os.Setenv("IAM_ENCRYPTION_KEY", "my-32bit-super-extra-secret-key!") os.Setenv("IAM_ORGANIZATION_ID", "4efa46628b914711bce36939abcfd084") os.Setenv("IAM_ORGANIZATION_NAME", "dev-ex") os.Setenv("IAM_SITE_ID", "dev-site") f := framework.New("providerctl").Component("providerctl") s := &Suite{ Framework: f, ctx: context.Background(), timeout: 60 * time.Second, tick: 50 * time.Millisecond, } resmaps, err := providerctl.CreateResmaps([]string{"test/test_manifests.yaml"}) assert.NoError(t, err) assert.NotEmpty(t, resmaps) // match key to target in createProviderObj resmaps["kind"] = resmaps["test"] // create provider reconciler r := &providerctl.ProviderReconciler{ Name: "provider-controller", Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Resmaps: resmaps, } err = r.SetupWithManager(mgr) test.NoError(err) k := k8s.New(testEnv.Config, k8s.WithCtrlManager(mgr), k8s.WithKonfigKonnector()) s.K8s = k f.Register(k) suite.Run(t, s) t.Cleanup(func() { f.NoError(testEnv.Stop()) }) } // confirm external secret created func (s *Suite) TestReconcileExternalSecretCreation() { namespace := &corev1.Namespace{ TypeMeta: metav1.TypeMeta{ Kind: "Namespace", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "edge-iam", }, } s.Require().NoError(s.Client.Create(s.ctx, namespace)) // create private-key-secret secret := &corev1.Secret{} secret.ObjectMeta = metav1.ObjectMeta{ Name: "private-key-secret", Namespace: "edge-iam", } secret.Data = map[string][]byte{ "private_key": []byte("key"), "private_key_id": []byte("key-id"), } s.Require().NoError(s.Client.Create(s.ctx, secret)) // create challenge-secret challengeSecret := &corev1.Secret{} challengeSecret.ObjectMeta = metav1.ObjectMeta{ Name: "challenge-secret", Namespace: "edge-iam", } challengeSecret.Data = map[string][]byte{ "secret": []byte("secret"), } s.Require().NoError(s.Client.Create(s.ctx, challengeSecret)) // create initial provider obj w/ spec.encryption.version = 1 provider := createProviderObj("1", "1") s.Require().NoError(s.Client.Create(s.ctx, provider)) // initially provider's status is empty before reconciliation, so saving it for later emptyStatus := provider.Status // check the if our first encryption key exists var externalSecret = &goext.ExternalSecret{} s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: providerctl.EncryptionKeySecretPrefix + "1", Namespace: "edge-iam", }, externalSecret) return err == nil }, s.timeout, s.tick, "expected external secret was never found") // grab provider providerObj := &api.Provider{} s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: "provider", Namespace: "edge-iam", }, providerObj) // confirm provider exists & status not empty return !reflect.DeepEqual(providerObj.Status, emptyStatus) && err == nil }, s.timeout, s.tick, "expected provider object with non-empty status was never found") // update provider to version 2 providerObj.Spec.Encryption.Version = "2" s.Require().NoError(s.Client.Update(s.ctx, providerObj)) // check for the new v2 external secret var externalSecret2 = &goext.ExternalSecret{} s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: providerctl.EncryptionKeySecretPrefix + "2", Namespace: "edge-iam", }, externalSecret2) return err == nil }, s.timeout, s.tick, "expected second version of external secret was never found") // check provider to confirm version is updated providerObj2 := &api.Provider{} s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: "provider", Namespace: "edge-iam", }, providerObj2) return !reflect.DeepEqual(providerObj2.Status, emptyStatus) && err == nil }, s.timeout, s.tick, "expected second provider object with non-empty status was never found") s.Require().True(providerObj2.Spec.Encryption.Version == "2") // update status to pretend we successfully updated the databases with version 2 gen := providerObj2.GetGeneration() newCondition := metav1.Condition{ Type: "DatabaseUpdated", Status: metav1.ConditionTrue, Reason: "EncryptionRotationSucceeded", Message: "successfully updated databases to version: 2", LastTransitionTime: metav1.Now(), ObservedGeneration: gen, } patch := client.MergeFrom(providerObj2.DeepCopy()) edgeConditions.Set(providerObj2, &newCondition) s.Require().NoError(s.Client.Status().Patch(s.ctx, providerObj2, patch)) // check provider again to confirm status is updated providerObj3 := &api.Provider{} s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: "provider", Namespace: "edge-iam", }, providerObj3) return !reflect.DeepEqual(providerObj3.Status, emptyStatus) && err == nil }, s.timeout, s.tick, "expected second provider object with non-empty status was never found") // confirming new status was updated, should have 2 conditions now: // DatabaseUpdated and InstallationSucceeded s.Require().Equal(len(providerObj3.Status.Conditions), 2) // check that the v1 external secret was deleted var externalSecret3 = &goext.ExternalSecret{} s.Require().Eventually(func() bool { err := s.Client.Get(s.ctx, types.NamespacedName{ Name: providerctl.EncryptionKeySecretPrefix + "1", Namespace: "edge-iam", }, externalSecret3) // should have been deleted if spec & status version are the both version 2 if err != nil { if errors.IsNotFound(err) { return true } } return false }, s.timeout, s.tick, "external secret was found or errored, when it should have been deleted") } // test if the created external secret matches the expected func TestCreateEncryptionExternalSecret(t *testing.T) { externalSecret := &goext.ExternalSecret{ TypeMeta: metav1.TypeMeta{ APIVersion: goext.ExtSecretGroupVersionKind.GroupVersion().String(), Kind: goext.ExtSecretGroupVersionKind.Kind, }, ObjectMeta: metav1.ObjectMeta{ Name: providerctl.EncryptionKeySecretPrefix + "1", Namespace: "edge-iam", Labels: map[string]string{ constants.PlatformComponent: "edge-iam", }, }, Spec: goext.ExternalSecretSpec{ DataFrom: []goext.ExternalSecretDataFromRemoteRef{ { Extract: &goext.ExternalSecretDataRemoteRef{ Key: providerctl.EncryptionKeySecretPrefix + "123", Version: "1", }, }, }, RefreshInterval: &metav1.Duration{ Duration: time.Minute, }, SecretStoreRef: goext.SecretStoreRef{ Name: "gcp-provider", Kind: "ClusterSecretStore", }, Target: goext.ExternalSecretTarget{ Name: providerctl.EncryptionKeySecretPrefix + "1", CreationPolicy: goext.CreatePolicyOwner, }, }, } uobj, err := unstructuredutil.ToUnstructured(externalSecret) if err != nil { t.Error("Unable to create unstructured external secret") } extSec, err := providerctl.CreateEncryptionExternalSecret("1", providerctl.EncryptionKeySecretPrefix+"123", providerctl.EncryptionKeySecretPrefix+"1") if err != nil { t.Error("Unable to create encryption external secret") } assert.Equal(t, uobj, extSec) } func TestDoesStatusMatchSpecVersion(t *testing.T) { // create provider where db's version == provider.encryption.version provider := createProviderObj("1", "1") _, versionMatch := providerctl.DoesStatusMatchSpecVersion(*provider) if !versionMatch { t.Errorf("Error checking the status where the provider & db's versions match") } // create provider where db's version != the provider.encryption.version provider = createProviderObj("1", "2") _, versionMatch = providerctl.DoesStatusMatchSpecVersion(*provider) if versionMatch { t.Errorf("Error checking the status where the provider & db's versions do not match") } } func createProviderObj(specVersion string, statusVersion string) *api.Provider { statusMessage := "successfully updated databases to version: " + statusVersion providerObj := &api.Provider{ TypeMeta: metav1.TypeMeta{ APIVersion: "iam.edge-infra.dev/v1alpha1", Kind: "Provider", }, ObjectMeta: metav1.ObjectMeta{ Name: "provider", Namespace: "edge-iam", }, Spec: api.ProviderSpec{ Encryption: api.EncryptionFields{ Version: specVersion, }, Issuer: "http://localhost:8080", Target: "kind", Barcode: api.BarcodeSpec{ Expire: "4320h", Role: false, }, }, Status: api.ProviderStatus{ Conditions: []metav1.Condition{ { Type: "DatabaseUpdated", Status: metav1.ConditionTrue, Reason: "EncryptionRotationSucceeded", Message: statusMessage, }, }, }, } return providerObj } func createScheme() *runtime.Scheme { scheme := runtime.NewScheme() utilruntime.Must(api.AddToScheme(scheme)) utilruntime.Must(goext.AddToScheme(scheme)) utilruntime.Must(corev1.AddToScheme(scheme)) return scheme }