From 87ff93daaddf7698cb3d7da741cc11f171597963 Mon Sep 17 00:00:00 2001 From: Eddy Filip Date: Tue, 13 Sep 2022 16:34:36 +0300 Subject: [PATCH] Add some tests for both the API Resource Controller and the Deployment Controller --- config/rbac/role.yaml | 26 ++ controllers/deployment_controller_test.go | 131 ++++++++++ .../onepassworditem_controller_test.go | 237 ++++++++++++++++++ controllers/suite_test.go | 87 ++++++- 4 files changed, 478 insertions(+), 3 deletions(-) create mode 100644 controllers/deployment_controller_test.go create mode 100644 controllers/onepassworditem_controller_test.go diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index a9e947e..3083dee 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -46,6 +46,18 @@ rules: - patch - update - watch +- apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - apps resources: @@ -53,6 +65,12 @@ rules: - replicasets verbs: - get +- apiGroups: + - apps + resources: + - deployments/finalizers + verbs: + - update - apiGroups: - apps resourceNames: @@ -61,6 +79,14 @@ rules: - deployments/finalizers verbs: - update +- apiGroups: + - apps + resources: + - deployments/status + verbs: + - get + - patch + - update - apiGroups: - monitoring.coreos.com resources: diff --git a/controllers/deployment_controller_test.go b/controllers/deployment_controller_test.go new file mode 100644 index 0000000..ee16887 --- /dev/null +++ b/controllers/deployment_controller_test.go @@ -0,0 +1,131 @@ +package controllers + +import ( + "context" + + "github.com/1Password/connect-sdk-go/onepassword" + "github.com/1Password/onepassword-operator/pkg/mocks" + op "github.com/1Password/onepassword-operator/pkg/onepassword" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + onepasswordv1 "github.com/1Password/onepassword-operator/api/v1" +) + +var _ = Describe("Deployment controller", func() { + const ( + deploymentKind = "Deployment" + deploymentAPIVersion = "v1" + deploymentName = "test-deployment" + ) + + BeforeEach(func() { + // failed test runs that don't clean up leave resources behind. + k8sClient.DeleteAllOf(context.Background(), &onepasswordv1.OnePasswordItem{}, client.InNamespace(namespace)) + k8sClient.DeleteAllOf(context.Background(), &v1.Secret{}, client.InNamespace(namespace)) + k8sClient.DeleteAllOf(context.Background(), &appsv1.Deployment{}, client.InNamespace(namespace)) + + mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) { + + item := onepassword.Item{} + item.Fields = []*onepassword.ItemField{} + for k, v := range itemData { + item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v}) + } + item.Version = version + item.Vault.ID = vaultUUID + item.ID = uuid + return &item, nil + } + }) + + Context("Deployment with secrets from 1Password", func() { + It("Should Handle a deployment correctly", func() { + ctx := context.Background() + + deploymentKey := types.NamespacedName{ + Name: deploymentName, + Namespace: namespace, + } + + secretKey := types.NamespacedName{ + Name: ItemName, + Namespace: namespace, + } + + By("Deploying a pod with proper annotations successfully") + deploymentResource := &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: deploymentKind, + APIVersion: deploymentAPIVersion, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: deploymentKey.Name, + Namespace: deploymentKey.Namespace, + Annotations: map[string]string{ + op.ItemPathAnnotation: itemPath, + op.NameAnnotation: ItemName, + }, + }, + Spec: appsv1.DeploymentSpec{ + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app": deploymentName}, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: deploymentName, + Image: "eu.gcr.io/kyma-project/example/http-db-service:0.0.6", + ImagePullPolicy: "IfNotPresent", + }, + }, + }, + }, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": deploymentName}, + }, + }, + } + Expect(k8sClient.Create(ctx, deploymentResource)).Should(Succeed()) + + By("Creating the K8s secret successfully") + createdSecret := &v1.Secret{} + Eventually(func() bool { + err := k8sClient.Get(ctx, secretKey, createdSecret) + if err != nil { + return false + } + return true + }, timeout, interval).Should(BeTrue()) + Expect(createdSecret.Data).Should(Equal(expectedSecretData)) + + By("Deleting the pod") + Eventually(func() error { + f := &appsv1.Deployment{} + err := k8sClient.Get(ctx, deploymentKey, f) + if err != nil { + return err + } + return k8sClient.Delete(ctx, f) + }, timeout, interval).Should(Succeed()) + + Eventually(func() error { + f := &appsv1.Deployment{} + return k8sClient.Get(ctx, deploymentKey, f) + }, timeout, interval).ShouldNot(Succeed()) + + Eventually(func() error { + f := &v1.Secret{} + return k8sClient.Get(ctx, secretKey, f) + }, timeout, interval).ShouldNot(Succeed()) + }) + }) +}) diff --git a/controllers/onepassworditem_controller_test.go b/controllers/onepassworditem_controller_test.go new file mode 100644 index 0000000..0ec98fd --- /dev/null +++ b/controllers/onepassworditem_controller_test.go @@ -0,0 +1,237 @@ +package controllers + +import ( + "context" + + "github.com/1Password/connect-sdk-go/onepassword" + "github.com/1Password/onepassword-operator/pkg/mocks" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + onepasswordv1 "github.com/1Password/onepassword-operator/api/v1" +) + +const ( + firstHost = "http://localhost:8080" + awsKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" + iceCream = "freezing blue 20%" +) + +var _ = Describe("OnePasswordItem controller", func() { + BeforeEach(func() { + // failed test runs that don't clean up leave resources behind. + k8sClient.DeleteAllOf(context.Background(), &onepasswordv1.OnePasswordItem{}, client.InNamespace(namespace)) + k8sClient.DeleteAllOf(context.Background(), &v1.Secret{}, client.InNamespace(namespace)) + + mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) { + + item := onepassword.Item{} + item.Fields = []*onepassword.ItemField{} + for k, v := range itemData { + item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v}) + } + item.Version = version + item.Vault.ID = vaultUUID + item.ID = uuid + return &item, nil + } + }) + + Context("Happy path", func() { + It("Should handle 1Password Item and secret correctly", func() { + ctx := context.Background() + spec := onepasswordv1.OnePasswordItemSpec{ + ItemPath: itemPath, + } + + key := types.NamespacedName{ + Name: ItemName, + Namespace: namespace, + } + + toCreate := &onepasswordv1.OnePasswordItem{ + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Spec: spec, + } + + By("Creating a new OnePasswordItem successfully") + Expect(k8sClient.Create(ctx, toCreate)).Should(Succeed()) + + created := &onepasswordv1.OnePasswordItem{} + Eventually(func() bool { + err := k8sClient.Get(ctx, key, created) + if err != nil { + return false + } + return true + }, timeout, interval).Should(BeTrue()) + + By("Creating the K8s secret successfully") + createdSecret := &v1.Secret{} + Eventually(func() bool { + err := k8sClient.Get(ctx, key, createdSecret) + if err != nil { + return false + } + return true + }, timeout, interval).Should(BeTrue()) + Expect(createdSecret.Data).Should(Equal(expectedSecretData)) + + By("Updating existing secret successfully") + newData := map[string]string{ + "username": "newUser1234", + "password": "##newPassword##", + "extraField": "dev", + } + newDataByte := map[string][]byte{ + "username": []byte("newUser1234"), + "password": []byte("##newPassword##"), + "extraField": []byte("dev"), + } + mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) { + item := onepassword.Item{} + item.Fields = []*onepassword.ItemField{} + for k, v := range newData { + item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v}) + } + item.Version = version + 1 + item.Vault.ID = vaultUUID + item.ID = uuid + return &item, nil + } + _, err := onePasswordItemReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: key}) + Expect(err).ToNot(HaveOccurred()) + + updatedSecret := &v1.Secret{} + Eventually(func() bool { + err := k8sClient.Get(ctx, key, updatedSecret) + if err != nil { + return false + } + return true + }, timeout, interval).Should(BeTrue()) + Expect(updatedSecret.Data).Should(Equal(newDataByte)) + + By("Deleting the OnePasswordItem successfully") + Eventually(func() error { + f := &onepasswordv1.OnePasswordItem{} + err := k8sClient.Get(ctx, key, f) + if err != nil { + return err + } + return k8sClient.Delete(ctx, f) + }, timeout, interval).Should(Succeed()) + + Eventually(func() error { + f := &onepasswordv1.OnePasswordItem{} + return k8sClient.Get(ctx, key, f) + }, timeout, interval).ShouldNot(Succeed()) + + Eventually(func() error { + f := &v1.Secret{} + return k8sClient.Get(ctx, key, f) + }, timeout, interval).ShouldNot(Succeed()) + }) + + It("Should handle 1Password Item with fields and sections that have invalid K8s labels correctly", func() { + ctx := context.Background() + spec := onepasswordv1.OnePasswordItemSpec{ + ItemPath: itemPath, + } + + key := types.NamespacedName{ + Name: "my-secret-it3m", + Namespace: namespace, + } + + toCreate := &onepasswordv1.OnePasswordItem{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-secret-it3m", + Namespace: key.Namespace, + }, + Spec: spec, + } + + testData := map[string]string{ + "username": username, + "password": password, + "first host": firstHost, + "AWS Access Key": awsKey, + "😄 ice-cream type": iceCream, + } + expectedData := map[string][]byte{ + "username": []byte(username), + "password": []byte(password), + "first-host": []byte(firstHost), + "AWS-Access-Key": []byte(awsKey), + "ice-cream-type": []byte(iceCream), + } + + mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) { + item := onepassword.Item{} + item.Title = "!my sECReT it3m%" + item.Fields = []*onepassword.ItemField{} + for k, v := range testData { + item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v}) + } + item.Version = version + 1 + item.Vault.ID = vaultUUID + item.ID = uuid + return &item, nil + } + + By("Creating a new OnePasswordItem successfully") + Expect(k8sClient.Create(ctx, toCreate)).Should(Succeed()) + + created := &onepasswordv1.OnePasswordItem{} + Eventually(func() bool { + err := k8sClient.Get(ctx, key, created) + if err != nil { + return false + } + return true + }, timeout, interval).Should(BeTrue()) + + By("Creating the K8s secret successfully") + createdSecret := &v1.Secret{} + Eventually(func() bool { + err := k8sClient.Get(ctx, key, createdSecret) + if err != nil { + return false + } + return true + }, timeout, interval).Should(BeTrue()) + Expect(createdSecret.Data).Should(Equal(expectedData)) + + By("Deleting the OnePasswordItem successfully") + Eventually(func() error { + f := &onepasswordv1.OnePasswordItem{} + err := k8sClient.Get(ctx, key, f) + if err != nil { + return err + } + return k8sClient.Delete(ctx, f) + }, timeout, interval).Should(Succeed()) + + Eventually(func() error { + f := &onepasswordv1.OnePasswordItem{} + return k8sClient.Get(ctx, key, f) + }, timeout, interval).ShouldNot(Succeed()) + + Eventually(func() error { + f := &v1.Secret{} + return k8sClient.Get(ctx, key, f) + }, timeout, interval).ShouldNot(Succeed()) + }) + }) +}) diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 1fab47a..1d1a5c5 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -25,14 +25,21 @@ SOFTWARE. package controllers import ( + "context" + "fmt" "path/filepath" + "regexp" "testing" + "time" + + "github.com/1Password/onepassword-operator/pkg/mocks" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" logf "sigs.k8s.io/controller-runtime/pkg/log" @@ -45,9 +52,49 @@ import ( // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. -var cfg *rest.Config -var k8sClient client.Client -var testEnv *envtest.Environment +var ( + cfg *rest.Config + k8sClient client.Client + testEnv *envtest.Environment + ctx context.Context + cancel context.CancelFunc + + itemData = map[string]string{ + "username": username, + "password": password, + } +) + +const ( + vaultId = "hfnjvi6aymbsnfc2xeeoheizda" + itemId = "nwrhuano7bcwddcviubpp4mhfq" + username = "test-user" + password = "QmHumKc$mUeEem7caHtbaBaJ" + version = 123 + + annotationRegExpString = "^operator.1password.io\\/[a-zA-Z\\.]+" +) + +// Define utility constants for object names and testing timeouts/durations and intervals. +const ( + namespace = "default" + ItemName = "test-item" + + timeout = time.Second * 10 + duration = time.Second * 10 + interval = time.Millisecond * 250 +) + +var ( + onePasswordItemReconciler *OnePasswordItemReconciler + deploymentReconciler *DeploymentReconciler + + itemPath = fmt.Sprintf("vaults/%v/items/%v", vaultId, itemId) + expectedSecretData = map[string][]byte{ + "password": []byte(password), + "username": []byte(username), + } +) func TestAPIs(t *testing.T) { RegisterFailHandler(Fail) @@ -58,6 +105,8 @@ func TestAPIs(t *testing.T) { var _ = BeforeSuite(func() { logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + ctx, cancel = context.WithCancel(context.TODO()) + By("bootstrapping test environment") testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, @@ -79,9 +128,41 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) + k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + }) + Expect(err).ToNot(HaveOccurred()) + + opConnectClient := &mocks.TestClient{} + + onePasswordItemReconciler = &OnePasswordItemReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + OpConnectClient: opConnectClient, + } + err = (onePasswordItemReconciler).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + r, _ := regexp.Compile(annotationRegExpString) + deploymentReconciler = &DeploymentReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + OpConnectClient: opConnectClient, + OpAnnotationRegExp: r, + } + err = (deploymentReconciler).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + go func() { + defer GinkgoRecover() + err = k8sManager.Start(ctx) + Expect(err).ToNot(HaveOccurred(), "failed to run manager") + }() + }) var _ = AfterSuite(func() { + cancel() By("tearing down the test environment") err := testEnv.Stop() Expect(err).NotTo(HaveOccurred())