From 7d2596a4aa56760672511c142c10cc10935b9bd7 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 15 Aug 2025 13:27:25 -0500 Subject: [PATCH 01/64] Create e2e tests package --- test/e2e/e2e_suite_test.go | 14 ++++++++++++++ test/e2e/e2e_test.go | 1 + 2 files changed, 15 insertions(+) create mode 100644 test/e2e/e2e_suite_test.go create mode 100644 test/e2e/e2e_test.go diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go new file mode 100644 index 0000000..c45bc90 --- /dev/null +++ b/test/e2e/e2e_suite_test.go @@ -0,0 +1,14 @@ +package e2e + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// Run e2e tests using the Ginkgo runner. +func TestE2E(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "onepassword-operator e2e suite") +} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go new file mode 100644 index 0000000..df8caf7 --- /dev/null +++ b/test/e2e/e2e_test.go @@ -0,0 +1 @@ +package e2e From d504e5ef354e9d08f8694bb97a4b4ee6fd73390a Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Tue, 19 Aug 2025 09:51:19 -0500 Subject: [PATCH 02/64] Add e2e tests using Service Accounts --- test/cmd/cmd.go | 29 +++++++++++++++ test/e2e/e2e_test.go | 66 ++++++++++++++++++++++++++++++++++ test/e2e/manifests/secret.yaml | 6 ++++ test/kind/kind.go | 16 +++++++++ 4 files changed, 117 insertions(+) create mode 100644 test/cmd/cmd.go create mode 100644 test/e2e/manifests/secret.yaml create mode 100644 test/kind/kind.go diff --git a/test/cmd/cmd.go b/test/cmd/cmd.go new file mode 100644 index 0000000..8303934 --- /dev/null +++ b/test/cmd/cmd.go @@ -0,0 +1,29 @@ +package cmd + +import ( + "fmt" + "os" + "os/exec" + "strings" +) + +// Run executes the provided command within this context +func Run(name string, args ...string) (string, error) { + cmd := exec.Command(name, args...) + + wd, err := os.Getwd() + if err != nil { + return wd, err + } + wd = strings.Replace(wd, "/test/e2e", "", -1) + // Command will run from project root + cmd.Dir = wd + + command := strings.Join(cmd.Args, " ") + output, err := cmd.CombinedOutput() + if err != nil { + return string(output), fmt.Errorf("%s failed with error: (%v) %s", command, err, string(output)) + } + + return string(output), nil +} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index df8caf7..9eb87ef 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -1 +1,67 @@ package e2e + +import ( + "os" + "path/filepath" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/1Password/onepassword-operator/test/cmd" + "github.com/1Password/onepassword-operator/test/kind" +) + +const ( + operatorImage = "1password/onepassword-operator:latest" + e2eInterval = 500 * time.Millisecond +) + +var _ = Describe("Onepassword Operator e2e", Ordered, func() { + BeforeAll(func() { + By("building the operator image") + _, err := cmd.Run("make", "docker-build") + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("loading the operator image on Kind") + err = kind.LoadImageToKind(operatorImage) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("create onepassword-service-account-token secret") + serviceAccountTokenToken, _ := os.LookupEnv("OP_SERVICE_ACCOUNT_TOKEN") + Expect(serviceAccountTokenToken).NotTo(BeEmpty()) + _, err = cmd.Run("kubectl", "create", "secret", "generic", "onepassword-service-account-token", "--from-literal=token="+serviceAccountTokenToken) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("deploying the operator") + _, err = cmd.Run("make", "deploy") + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("waiting for the operator pod to be 'Running'") + Eventually(func(g Gomega) { + output, err := cmd.Run("kubectl", "get", "pods", + "-l", "name=onepassword-connect-operator", + "-o", "jsonpath={.items[0].status.phase}") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(output).To(ContainSubstring("Running")) + }, 30*time.Second, 1*time.Second).Should(Succeed()) + }) + + Describe("Deployment annotations", func() { + It("Should create secret from manifest file", func() { + By("creating secret") + wd, err := os.Getwd() + Expect(err).NotTo(HaveOccurred()) + yamlPath := filepath.Join(wd, "manifests", "secret.yaml") + _, err = cmd.Run("kubectl", "apply", "-f", yamlPath) + Expect(err).NotTo(HaveOccurred()) + + By("waiting for secret to be created") + Eventually(func(g Gomega) { + output, err := cmd.Run("kubectl", "get", "secret", "login", "-o", "jsonpath={.metadata.name}") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(output).To(Equal("login")) + }, 5*time.Second, e2eInterval).Should(Succeed()) + }) + }) +}) diff --git a/test/e2e/manifests/secret.yaml b/test/e2e/manifests/secret.yaml new file mode 100644 index 0000000..c2b89df --- /dev/null +++ b/test/e2e/manifests/secret.yaml @@ -0,0 +1,6 @@ +apiVersion: onepassword.com/v1 +kind: OnePasswordItem +metadata: + name: login +spec: + itemPath: "vaults/h4l46uopmjps2cgmpeysnvscum/items/sg2gfcren47mzbx2bcgumc7ekm" diff --git a/test/kind/kind.go b/test/kind/kind.go new file mode 100644 index 0000000..55f382b --- /dev/null +++ b/test/kind/kind.go @@ -0,0 +1,16 @@ +package kind + +import ( + "github.com/1Password/onepassword-operator/test/cmd" + "os" +) + +// LoadImageToKind loads a local docker image to the Kind cluster +func LoadImageToKind(imageName string) error { + clusterName := "kind" + if value, ok := os.LookupEnv("KIND_CLUSTER"); ok { + clusterName = value + } + _, err := cmd.Run("kind", "load", "docker-image", imageName, "--name", clusterName) + return err +} From 5630d788a29c1ee0dd6c3ea338dd469333960043 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Tue, 19 Aug 2025 11:52:28 -0500 Subject: [PATCH 03/64] Create `kube` package that abstracts interactions with kubernetes cluster --- test/e2e/e2e_test.go | 30 +++++++------------ test/kube/deploy.go | 69 ++++++++++++++++++++++++++++++++++++++++++++ test/kube/kube.go | 21 ++++++++++++++ 3 files changed, 101 insertions(+), 19 deletions(-) create mode 100644 test/kube/deploy.go create mode 100644 test/kube/kube.go diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 9eb87ef..c0c734e 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -10,11 +10,13 @@ import ( "github.com/1Password/onepassword-operator/test/cmd" "github.com/1Password/onepassword-operator/test/kind" + "github.com/1Password/onepassword-operator/test/kube" ) const ( - operatorImage = "1password/onepassword-operator:latest" - e2eInterval = 500 * time.Millisecond + operatorImage = "1password/onepassword-operator:latest" + defaultInterval = 1 * time.Second + defaultTimeout = 30 * time.Second ) var _ = Describe("Onepassword Operator e2e", Ordered, func() { @@ -27,24 +29,14 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() { err = kind.LoadImageToKind(operatorImage) ExpectWithOffset(1, err).NotTo(HaveOccurred()) + By("create onepassword-token secret") + kube.CreateSecretFromEnvVar("OP_CONNECT_TOKEN", "onepassword-token") + By("create onepassword-service-account-token secret") - serviceAccountTokenToken, _ := os.LookupEnv("OP_SERVICE_ACCOUNT_TOKEN") - Expect(serviceAccountTokenToken).NotTo(BeEmpty()) - _, err = cmd.Run("kubectl", "create", "secret", "generic", "onepassword-service-account-token", "--from-literal=token="+serviceAccountTokenToken) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + kube.CreateSecretFromEnvVar("OP_SERVICE_ACCOUNT_TOKEN", "onepassword-service-account-token") - By("deploying the operator") - _, err = cmd.Run("make", "deploy") - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - - By("waiting for the operator pod to be 'Running'") - Eventually(func(g Gomega) { - output, err := cmd.Run("kubectl", "get", "pods", - "-l", "name=onepassword-connect-operator", - "-o", "jsonpath={.items[0].status.phase}") - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(output).To(ContainSubstring("Running")) - }, 30*time.Second, 1*time.Second).Should(Succeed()) + kube.DeployOperator() + kube.PathOperatorToUseServiceAccount() }) Describe("Deployment annotations", func() { @@ -61,7 +53,7 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() { output, err := cmd.Run("kubectl", "get", "secret", "login", "-o", "jsonpath={.metadata.name}") g.Expect(err).NotTo(HaveOccurred()) g.Expect(output).To(Equal("login")) - }, 5*time.Second, e2eInterval).Should(Succeed()) + }, defaultTimeout, defaultInterval).Should(Succeed()) }) }) }) diff --git a/test/kube/deploy.go b/test/kube/deploy.go new file mode 100644 index 0000000..ed8e308 --- /dev/null +++ b/test/kube/deploy.go @@ -0,0 +1,69 @@ +package kube + +import ( + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/1Password/onepassword-operator/test/cmd" +) + +// DeployOperator deploys the Onepassword Operator in the default namespace. +// It waits for the operator pod to be in 'Running' state. +// All the resources created using manifests in `config/` dir. +// To make the operator use Connect or Service Accounts, patch `config/manager/manager.yaml` +func DeployOperator() { + By("deploying the operator") + _, err := cmd.Run("make", "deploy") + Expect(err).NotTo(HaveOccurred()) + + By("waiting for the operator pod to be 'Running'") + Eventually(func(g Gomega) { + output, err := cmd.Run("kubectl", "get", "pods", + "-l", "name=onepassword-connect-operator", + "-o", "jsonpath={.items[0].status.phase}") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(output).To(ContainSubstring("Running")) + }, 30*time.Second, 1*time.Second).Should(Succeed()) +} + +func UndeployOperator() { + Delete("secret", "onepassword-connect-token") + Delete("secret", "onepassword-service-account-token") + + By("undeploying the operator") + _, err := cmd.Run("make", "undeploy", "ignore-not-found") + Expect(err).NotTo(HaveOccurred()) +} + +func PathOperatorToUseServiceAccount() { + By("patching the operator deployment with service account token") + _, err := cmd.Run( + "kubectl", "patch", "deployment", "onepassword-connect-operator", + "--type=json", + `-p=[{"op":"replace","path":"/spec/template/spec/containers/0/env","value":[ + {"name":"OPERATOR_NAME","value":"onepassword-connect-operator"}, + {"name":"POD_NAME","valueFrom":{"fieldRef":{"fieldPath":"metadata.name"}}}, + {"name":"WATCH_NAMESPACE","value":"default"}, + {"name":"POLLING_INTERVAL","value":"10"}, + {"name":"AUTO_RESTART","value":"false"}, + {"name":"OP_SERVICE_ACCOUNT_TOKEN","valueFrom":{"secretKeyRef":{"name":"onepassword-service-account-token","key":"token"}}}, + {"name":"MANAGE_CONNECT","value":"false"} + ]}]`, + ) + Expect(err).NotTo(HaveOccurred()) + + _, err = cmd.Run("kubectl", "rollout", "status", + "deployment/onepassword-connect-operator", "-n", "default", "--timeout=120s") + Expect(err).NotTo(HaveOccurred()) + + By("waiting for the operator pod to be 'Running'") + Eventually(func(g Gomega) { + output, err := cmd.Run("kubectl", "get", "pods", + "-l", "name=onepassword-connect-operator", + "-o", "jsonpath={.items[0].status.phase}") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(output).To(ContainSubstring("Running")) + }, 120*time.Second, 1*time.Second).Should(Succeed()) +} diff --git a/test/kube/kube.go b/test/kube/kube.go new file mode 100644 index 0000000..fa26c4b --- /dev/null +++ b/test/kube/kube.go @@ -0,0 +1,21 @@ +package kube + +import ( + "os" + + . "github.com/onsi/gomega" + + "github.com/1Password/onepassword-operator/test/cmd" +) + +func CreateSecretFromEnvVar(envVar, secretName string) { + serviceAccountTokenToken, _ := os.LookupEnv(envVar) + Expect(serviceAccountTokenToken).NotTo(BeEmpty()) + _, err := cmd.Run("kubectl", "create", "secret", "generic", secretName, "--from-literal=token="+serviceAccountTokenToken) + Expect(err).NotTo(HaveOccurred()) +} + +func Delete(kind, name string) { + _, err := cmd.Run("kubectl", "delete", kind, name, "--ignore-not-found=true") + Expect(err).NotTo(HaveOccurred()) +} From f8704223c8959e20605c35e188b1712801251614 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Tue, 19 Aug 2025 12:04:56 -0500 Subject: [PATCH 04/64] Move all helpers to `testhelper` package --- test/kube/kube.go | 21 ---------- test/{ => testhelper}/kind/kind.go | 3 +- .../deploy.go => testhelper/kube/kube.go} | 29 ++++---------- test/testhelper/operator/operator.go | 39 +++++++++++++++++++ 4 files changed, 48 insertions(+), 44 deletions(-) delete mode 100644 test/kube/kube.go rename test/{ => testhelper}/kind/kind.go (99%) rename test/{kube/deploy.go => testhelper/kube/kube.go} (61%) create mode 100644 test/testhelper/operator/operator.go diff --git a/test/kube/kube.go b/test/kube/kube.go deleted file mode 100644 index fa26c4b..0000000 --- a/test/kube/kube.go +++ /dev/null @@ -1,21 +0,0 @@ -package kube - -import ( - "os" - - . "github.com/onsi/gomega" - - "github.com/1Password/onepassword-operator/test/cmd" -) - -func CreateSecretFromEnvVar(envVar, secretName string) { - serviceAccountTokenToken, _ := os.LookupEnv(envVar) - Expect(serviceAccountTokenToken).NotTo(BeEmpty()) - _, err := cmd.Run("kubectl", "create", "secret", "generic", secretName, "--from-literal=token="+serviceAccountTokenToken) - Expect(err).NotTo(HaveOccurred()) -} - -func Delete(kind, name string) { - _, err := cmd.Run("kubectl", "delete", kind, name, "--ignore-not-found=true") - Expect(err).NotTo(HaveOccurred()) -} diff --git a/test/kind/kind.go b/test/testhelper/kind/kind.go similarity index 99% rename from test/kind/kind.go rename to test/testhelper/kind/kind.go index 55f382b..f3d3b48 100644 --- a/test/kind/kind.go +++ b/test/testhelper/kind/kind.go @@ -1,8 +1,9 @@ package kind import ( - "github.com/1Password/onepassword-operator/test/cmd" "os" + + "github.com/1Password/onepassword-operator/test/cmd" ) // LoadImageToKind loads a local docker image to the Kind cluster diff --git a/test/kube/deploy.go b/test/testhelper/kube/kube.go similarity index 61% rename from test/kube/deploy.go rename to test/testhelper/kube/kube.go index ed8e308..4579b86 100644 --- a/test/kube/deploy.go +++ b/test/testhelper/kube/kube.go @@ -1,6 +1,7 @@ package kube import ( + "os" "time" . "github.com/onsi/ginkgo/v2" @@ -9,31 +10,15 @@ import ( "github.com/1Password/onepassword-operator/test/cmd" ) -// DeployOperator deploys the Onepassword Operator in the default namespace. -// It waits for the operator pod to be in 'Running' state. -// All the resources created using manifests in `config/` dir. -// To make the operator use Connect or Service Accounts, patch `config/manager/manager.yaml` -func DeployOperator() { - By("deploying the operator") - _, err := cmd.Run("make", "deploy") +func CreateSecretFromEnvVar(envVar, secretName string) { + serviceAccountTokenToken, _ := os.LookupEnv(envVar) + Expect(serviceAccountTokenToken).NotTo(BeEmpty()) + _, err := cmd.Run("kubectl", "create", "secret", "generic", secretName, "--from-literal=token="+serviceAccountTokenToken) Expect(err).NotTo(HaveOccurred()) - - By("waiting for the operator pod to be 'Running'") - Eventually(func(g Gomega) { - output, err := cmd.Run("kubectl", "get", "pods", - "-l", "name=onepassword-connect-operator", - "-o", "jsonpath={.items[0].status.phase}") - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(output).To(ContainSubstring("Running")) - }, 30*time.Second, 1*time.Second).Should(Succeed()) } -func UndeployOperator() { - Delete("secret", "onepassword-connect-token") - Delete("secret", "onepassword-service-account-token") - - By("undeploying the operator") - _, err := cmd.Run("make", "undeploy", "ignore-not-found") +func Delete(kind, name string) { + _, err := cmd.Run("kubectl", "delete", kind, name, "--ignore-not-found=true") Expect(err).NotTo(HaveOccurred()) } diff --git a/test/testhelper/operator/operator.go b/test/testhelper/operator/operator.go new file mode 100644 index 0000000..794efea --- /dev/null +++ b/test/testhelper/operator/operator.go @@ -0,0 +1,39 @@ +package operator + +import ( + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/1Password/onepassword-operator/test/cmd" + "github.com/1Password/onepassword-operator/test/testhelper/kube" +) + +// DeployOperator deploys the Onepassword Operator in the default namespace. +// It waits for the operator pod to be in 'Running' state. +// All the resources created using manifests in `config/` dir. +// To make the operator use Connect or Service Accounts, patch `config/manager/manager.yaml` +func DeployOperator() { + By("deploying the operator") + _, err := cmd.Run("make", "deploy") + Expect(err).NotTo(HaveOccurred()) + + By("waiting for the operator pod to be 'Running'") + Eventually(func(g Gomega) { + output, err := cmd.Run("kubectl", "get", "pods", + "-l", "name=onepassword-connect-operator", + "-o", "jsonpath={.items[0].status.phase}") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(output).To(ContainSubstring("Running")) + }, 30*time.Second, 1*time.Second).Should(Succeed()) +} + +func UndeployOperator() { + kube.Delete("secret", "onepassword-connect-token") + kube.Delete("secret", "onepassword-service-account-token") + + By("undeploying the operator") + _, err := cmd.Run("make", "undeploy", "ignore-not-found") + Expect(err).NotTo(HaveOccurred()) +} From 174f9526912e730013fe8590fb95c55b67f6882e Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Tue, 19 Aug 2025 12:05:29 -0500 Subject: [PATCH 05/64] Split testing flow for Connect and Service Accounts --- test/e2e/e2e_test.go | 48 +++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index c0c734e..f55da5e 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -9,8 +9,9 @@ import ( . "github.com/onsi/gomega" "github.com/1Password/onepassword-operator/test/cmd" - "github.com/1Password/onepassword-operator/test/kind" - "github.com/1Password/onepassword-operator/test/kube" + "github.com/1Password/onepassword-operator/test/testhelper/kind" + "github.com/1Password/onepassword-operator/test/testhelper/kube" + "github.com/1Password/onepassword-operator/test/testhelper/operator" ) const ( @@ -35,25 +36,36 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() { By("create onepassword-service-account-token secret") kube.CreateSecretFromEnvVar("OP_SERVICE_ACCOUNT_TOKEN", "onepassword-service-account-token") - kube.DeployOperator() - kube.PathOperatorToUseServiceAccount() + operator.DeployOperator() }) - Describe("Deployment annotations", func() { - It("Should create secret from manifest file", func() { - By("creating secret") - wd, err := os.Getwd() - Expect(err).NotTo(HaveOccurred()) - yamlPath := filepath.Join(wd, "manifests", "secret.yaml") - _, err = cmd.Run("kubectl", "apply", "-f", yamlPath) - Expect(err).NotTo(HaveOccurred()) + //Context("Use the operator with Connect", func() { + // runCommonTestCases() + //}) - By("waiting for secret to be created") - Eventually(func(g Gomega) { - output, err := cmd.Run("kubectl", "get", "secret", "login", "-o", "jsonpath={.metadata.name}") - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(output).To(Equal("login")) - }, defaultTimeout, defaultInterval).Should(Succeed()) + Context("Use the operator with Service Account", func() { + BeforeAll(func() { + kube.PathOperatorToUseServiceAccount() }) + + runCommonTestCases() }) }) + +func runCommonTestCases() { + It("Should create secret from manifest file", func() { + By("creating secret") + wd, err := os.Getwd() + Expect(err).NotTo(HaveOccurred()) + yamlPath := filepath.Join(wd, "manifests", "secret.yaml") + _, err = cmd.Run("kubectl", "apply", "-f", yamlPath) + Expect(err).NotTo(HaveOccurred()) + + By("waiting for secret to be created") + Eventually(func(g Gomega) { + output, err := cmd.Run("kubectl", "get", "secret", "login", "-o", "jsonpath={.metadata.name}") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(output).To(Equal("login")) + }, defaultTimeout, defaultInterval).Should(Succeed()) + }) +} From 19b629f2ee4e718f60030061d44293771c489618 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Tue, 19 Aug 2025 14:50:39 -0500 Subject: [PATCH 06/64] Move `BuildOperatorImage` function to `testhelper.operator` package --- test/e2e/e2e_test.go | 15 +++++---------- test/testhelper/kind/kind.go | 8 ++++++-- test/testhelper/operator/operator.go | 6 ++++++ 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index f55da5e..2d7a9a8 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -15,20 +15,15 @@ import ( ) const ( - operatorImage = "1password/onepassword-operator:latest" - defaultInterval = 1 * time.Second - defaultTimeout = 30 * time.Second + operatorImageName = "1password/onepassword-operator:latest" + defaultInterval = 1 * time.Second + defaultTimeout = 30 * time.Second ) var _ = Describe("Onepassword Operator e2e", Ordered, func() { BeforeAll(func() { - By("building the operator image") - _, err := cmd.Run("make", "docker-build") - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - - By("loading the operator image on Kind") - err = kind.LoadImageToKind(operatorImage) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + operator.BuildOperatorImage() + kind.LoadImageToKind(operatorImageName) By("create onepassword-token secret") kube.CreateSecretFromEnvVar("OP_CONNECT_TOKEN", "onepassword-token") diff --git a/test/testhelper/kind/kind.go b/test/testhelper/kind/kind.go index f3d3b48..5ff8fbc 100644 --- a/test/testhelper/kind/kind.go +++ b/test/testhelper/kind/kind.go @@ -3,15 +3,19 @@ package kind import ( "os" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/1Password/onepassword-operator/test/cmd" ) // LoadImageToKind loads a local docker image to the Kind cluster -func LoadImageToKind(imageName string) error { +func LoadImageToKind(imageName string) { + By("loading the operator image on Kind") clusterName := "kind" if value, ok := os.LookupEnv("KIND_CLUSTER"); ok { clusterName = value } _, err := cmd.Run("kind", "load", "docker-image", imageName, "--name", clusterName) - return err + Expect(err).NotTo(HaveOccurred()) } diff --git a/test/testhelper/operator/operator.go b/test/testhelper/operator/operator.go index 794efea..c56f5e0 100644 --- a/test/testhelper/operator/operator.go +++ b/test/testhelper/operator/operator.go @@ -10,6 +10,12 @@ import ( "github.com/1Password/onepassword-operator/test/testhelper/kube" ) +func BuildOperatorImage() { + By("building the operator image") + _, err := cmd.Run("make", "docker-build") + ExpectWithOffset(1, err).NotTo(HaveOccurred()) +} + // DeployOperator deploys the Onepassword Operator in the default namespace. // It waits for the operator pod to be in 'Running' state. // All the resources created using manifests in `config/` dir. From c1e99340886278cea0a5c91d9636fd4b477f5938 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Tue, 19 Aug 2025 14:51:32 -0500 Subject: [PATCH 07/64] Fix typo --- test/e2e/e2e_test.go | 2 +- test/testhelper/kube/kube.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 2d7a9a8..326b65f 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -40,7 +40,7 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() { Context("Use the operator with Service Account", func() { BeforeAll(func() { - kube.PathOperatorToUseServiceAccount() + kube.PatchOperatorToUseServiceAccount() }) runCommonTestCases() diff --git a/test/testhelper/kube/kube.go b/test/testhelper/kube/kube.go index 4579b86..0c9688f 100644 --- a/test/testhelper/kube/kube.go +++ b/test/testhelper/kube/kube.go @@ -22,7 +22,7 @@ func Delete(kind, name string) { Expect(err).NotTo(HaveOccurred()) } -func PathOperatorToUseServiceAccount() { +func PatchOperatorToUseServiceAccount() { By("patching the operator deployment with service account token") _, err := cmd.Run( "kubectl", "patch", "deployment", "onepassword-connect-operator", From 1759055edd80b0785d7b95e834b99d2d4b051922 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Wed, 20 Aug 2025 09:10:32 -0500 Subject: [PATCH 08/64] Update `sqlite-permissions` to run as root, so it can start Connect in e2e tests --- config/connect/deployment.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/config/connect/deployment.yaml b/config/connect/deployment.yaml index 3f2dd38..d0e90e4 100644 --- a/config/connect/deployment.yaml +++ b/config/connect/deployment.yaml @@ -14,6 +14,8 @@ spec: spec: securityContext: runAsNonRoot: true + fsGroup: 999 + fsGroupChangePolicy: OnRootMismatch volumes: - name: shared-data emptyDir: {} @@ -31,10 +33,19 @@ spec: volumeMounts: - mountPath: /home/opuser/.op/data name: shared-data + securityContext: + runAsUser: 0 + runAsNonRoot: false + allowPrivilegeEscalation: false + capabilities: + drop: [ "ALL" ] containers: - name: connect-api image: 1password/connect-api:latest securityContext: + runAsNonRoot: true + runAsUser: 999 + runAsGroup: 999 allowPrivilegeEscalation: false resources: limits: @@ -55,6 +66,9 @@ spec: - name: connect-sync image: 1password/connect-sync:latest securityContext: + runAsNonRoot: true + runAsUser: 999 + runAsGroup: 999 allowPrivilegeEscalation: false resources: limits: From 4307e9d713099c913c174328b32f69a6eff37695 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Wed, 20 Aug 2025 09:33:52 -0500 Subject: [PATCH 09/64] Add 1password-credentials.json and op-session to git ignore Add 1password-credentials.json to git ignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index e32a86d..f36aa42 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ go.work *.swp *.swo *~ + +1password-credentials.json +op-session From 116c8c92a74c817ab90d7f19198c668ea1e402d6 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Wed, 20 Aug 2025 09:36:20 -0500 Subject: [PATCH 10/64] Update item path to point to test secret --- test/e2e/manifests/secret.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/manifests/secret.yaml b/test/e2e/manifests/secret.yaml index c2b89df..f6691b3 100644 --- a/test/e2e/manifests/secret.yaml +++ b/test/e2e/manifests/secret.yaml @@ -3,4 +3,4 @@ kind: OnePasswordItem metadata: name: login spec: - itemPath: "vaults/h4l46uopmjps2cgmpeysnvscum/items/sg2gfcren47mzbx2bcgumc7ekm" + itemPath: "vaults/acceptance-tests/items/test-secret" From 91a9bb6d63571d26e207cc4d2651ab085ad37935 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Wed, 20 Aug 2025 10:24:16 -0500 Subject: [PATCH 11/64] Create op-credentials secret to use operator with Connect --- test/e2e/e2e_test.go | 13 ++++-- test/testhelper/kube/kube.go | 78 +++++++++++++++++++++++++++++------- 2 files changed, 74 insertions(+), 17 deletions(-) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 326b65f..668bd76 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -25,6 +25,9 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() { operator.BuildOperatorImage() kind.LoadImageToKind(operatorImageName) + By("create Connect credentials secret") + kube.CreateOpCredentialsSecret() + By("create onepassword-token secret") kube.CreateSecretFromEnvVar("OP_CONNECT_TOKEN", "onepassword-token") @@ -34,9 +37,13 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() { operator.DeployOperator() }) - //Context("Use the operator with Connect", func() { - // runCommonTestCases() - //}) + Context("Use the operator with Connect", func() { + BeforeAll(func() { + kube.PatchOperatorManageConnect() + }) + + runCommonTestCases() + }) Context("Use the operator with Service Account", func() { BeforeAll(func() { diff --git a/test/testhelper/kube/kube.go b/test/testhelper/kube/kube.go index 0c9688f..e4d000f 100644 --- a/test/testhelper/kube/kube.go +++ b/test/testhelper/kube/kube.go @@ -1,7 +1,9 @@ package kube import ( + "encoding/base64" "os" + "path/filepath" "time" . "github.com/onsi/ginkgo/v2" @@ -11,18 +13,40 @@ import ( ) func CreateSecretFromEnvVar(envVar, secretName string) { - serviceAccountTokenToken, _ := os.LookupEnv(envVar) - Expect(serviceAccountTokenToken).NotTo(BeEmpty()) - _, err := cmd.Run("kubectl", "create", "secret", "generic", secretName, "--from-literal=token="+serviceAccountTokenToken) + value, _ := os.LookupEnv(envVar) + Expect(value).NotTo(BeEmpty()) + _, err := cmd.Run("kubectl", "create", "secret", "generic", secretName, "--from-literal=token="+value) Expect(err).NotTo(HaveOccurred()) } +func CreateSecretFromFile(fileName, secretName string) { + _, err := cmd.Run("kubectl", "create", "secret", "generic", secretName, "--from-file="+fileName) + Expect(err).NotTo(HaveOccurred()) +} + +func CreateOpCredentialsSecret() { + rootDir, err := cmd.GetProjectRoot() + credentialsFilePath := filepath.Join(rootDir, "1password-credentials.json") + data, err := os.ReadFile(credentialsFilePath) + Expect(err).NotTo(HaveOccurred()) + + encoded := base64.RawURLEncoding.EncodeToString(data) + + // create op-session file in project root + sessionFilePath := filepath.Join(rootDir, "op-session") + err = os.WriteFile(sessionFilePath, []byte(encoded), 0o600) + Expect(err).NotTo(HaveOccurred()) + + CreateSecretFromFile("op-session", "op-credentials") +} + func Delete(kind, name string) { _, err := cmd.Run("kubectl", "delete", kind, name, "--ignore-not-found=true") Expect(err).NotTo(HaveOccurred()) } -func PatchOperatorToUseServiceAccount() { +// PatchOperatorToUseServiceAccount sets `OP_SERVICE_ACCOUNT_TOKEN` env variable +var PatchOperatorToUseServiceAccount = WithOperatorRestart(func() { By("patching the operator deployment with service account token") _, err := cmd.Run( "kubectl", "patch", "deployment", "onepassword-connect-operator", @@ -38,17 +62,43 @@ func PatchOperatorToUseServiceAccount() { ]}]`, ) Expect(err).NotTo(HaveOccurred()) +}) - _, err = cmd.Run("kubectl", "rollout", "status", - "deployment/onepassword-connect-operator", "-n", "default", "--timeout=120s") +// PatchOperatorManageConnect sets env variable `MANAGE_CONNECT: true` and restarts the operator. +var PatchOperatorManageConnect = WithOperatorRestart(func() { + By("patching the operator deployment with to manage Connect") + _, err := cmd.Run( + "kubectl", "patch", "deployment", "onepassword-connect-operator", + "--type=json", + `-p=[{"op":"replace","path":"/spec/template/spec/containers/0/env","value":[ + {"name":"OPERATOR_NAME","value":"onepassword-connect-operator"}, + {"name":"POD_NAME","valueFrom":{"fieldRef":{"fieldPath":"metadata.name"}}}, + {"name":"WATCH_NAMESPACE","value":"default"}, + {"name":"POLLING_INTERVAL","value":"10"}, + {"name":"AUTO_RESTART","value":"false"}, + {"name":"OP_CONNECT_HOST","value":"http://onepassword-connect:8080"}, + {"name":"OP_CONNECT_TOKEN","valueFrom":{"secretKeyRef":{"name":"onepassword-token","key":"token"}}}, + {"name":"MANAGE_CONNECT","value":"true"}, + ]}]`, + ) Expect(err).NotTo(HaveOccurred()) +}) - By("waiting for the operator pod to be 'Running'") - Eventually(func(g Gomega) { - output, err := cmd.Run("kubectl", "get", "pods", - "-l", "name=onepassword-connect-operator", - "-o", "jsonpath={.items[0].status.phase}") - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(output).To(ContainSubstring("Running")) - }, 120*time.Second, 1*time.Second).Should(Succeed()) +func WithOperatorRestart(operation func()) func() { + return func() { + operation() + + _, err := cmd.Run("kubectl", "rollout", "status", + "deployment/onepassword-connect-operator", "-n", "default", "--timeout=120s") + Expect(err).NotTo(HaveOccurred()) + + By("waiting for the operator pod to be 'Running'") + Eventually(func(g Gomega) { + output, err := cmd.Run("kubectl", "get", "pods", + "-l", "name=onepassword-connect-operator", + "-o", "jsonpath={.items[0].status.phase}") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(output).To(ContainSubstring("Running")) + }, 120*time.Second, 1*time.Second).Should(Succeed()) + } } From e167db2357131623903af2c055d7cb6188ac0e7e Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Wed, 20 Aug 2025 10:28:18 -0500 Subject: [PATCH 12/64] Remove secret from previous step --- test/cmd/cmd.go | 24 ++++++++++++++++++++++++ test/e2e/e2e_test.go | 1 + test/testhelper/kube/kube.go | 4 ++-- test/testhelper/operator/operator.go | 10 ---------- 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/test/cmd/cmd.go b/test/cmd/cmd.go index 8303934..ea3f9e9 100644 --- a/test/cmd/cmd.go +++ b/test/cmd/cmd.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "os/exec" + "path/filepath" "strings" ) @@ -27,3 +28,26 @@ func Run(name string, args ...string) (string, error) { return string(output), nil } + +func GetProjectRoot() (string, error) { + dir, err := os.Getwd() + if err != nil { + return "", err + } + + for { + // check if go.mod exists in current dir + modFile := filepath.Join(dir, "go.mod") + if _, err := os.Stat(modFile); err == nil { + return dir, nil + } + + // move one level up + parent := filepath.Dir(dir) + if parent == dir { + // reached filesystem root + return "", fmt.Errorf("project root not found (no go.mod)") + } + dir = parent + } +} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 668bd76..ac69e82 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -48,6 +48,7 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() { Context("Use the operator with Service Account", func() { BeforeAll(func() { kube.PatchOperatorToUseServiceAccount() + kube.DeleteSecret("login") // remove secret crated in previous test }) runCommonTestCases() diff --git a/test/testhelper/kube/kube.go b/test/testhelper/kube/kube.go index e4d000f..bba018c 100644 --- a/test/testhelper/kube/kube.go +++ b/test/testhelper/kube/kube.go @@ -40,8 +40,8 @@ func CreateOpCredentialsSecret() { CreateSecretFromFile("op-session", "op-credentials") } -func Delete(kind, name string) { - _, err := cmd.Run("kubectl", "delete", kind, name, "--ignore-not-found=true") +func DeleteSecret(name string) { + _, err := cmd.Run("kubectl", "delete", "secret", name, "--ignore-not-found=true") Expect(err).NotTo(HaveOccurred()) } diff --git a/test/testhelper/operator/operator.go b/test/testhelper/operator/operator.go index c56f5e0..d368bf0 100644 --- a/test/testhelper/operator/operator.go +++ b/test/testhelper/operator/operator.go @@ -7,7 +7,6 @@ import ( . "github.com/onsi/gomega" "github.com/1Password/onepassword-operator/test/cmd" - "github.com/1Password/onepassword-operator/test/testhelper/kube" ) func BuildOperatorImage() { @@ -34,12 +33,3 @@ func DeployOperator() { g.Expect(output).To(ContainSubstring("Running")) }, 30*time.Second, 1*time.Second).Should(Succeed()) } - -func UndeployOperator() { - kube.Delete("secret", "onepassword-connect-token") - kube.Delete("secret", "onepassword-service-account-token") - - By("undeploying the operator") - _, err := cmd.Run("make", "undeploy", "ignore-not-found") - Expect(err).NotTo(HaveOccurred()) -} From 88b2dfbf67fe3f41c2f8de3a3c47283dfa67ebd4 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Wed, 20 Aug 2025 14:15:26 -0500 Subject: [PATCH 13/64] Use `GetProjectRoot` in `Run` --- test/cmd/cmd.go | 8 ++++---- test/testhelper/kube/kube.go | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/test/cmd/cmd.go b/test/cmd/cmd.go index ea3f9e9..f316283 100644 --- a/test/cmd/cmd.go +++ b/test/cmd/cmd.go @@ -12,13 +12,13 @@ import ( func Run(name string, args ...string) (string, error) { cmd := exec.Command(name, args...) - wd, err := os.Getwd() + rootDir, err := GetProjectRoot() if err != nil { - return wd, err + return "", err } - wd = strings.Replace(wd, "/test/e2e", "", -1) + // Command will run from project root - cmd.Dir = wd + cmd.Dir = rootDir command := strings.Join(cmd.Args, " ") output, err := cmd.CombinedOutput() diff --git a/test/testhelper/kube/kube.go b/test/testhelper/kube/kube.go index bba018c..6db4524 100644 --- a/test/testhelper/kube/kube.go +++ b/test/testhelper/kube/kube.go @@ -15,6 +15,7 @@ import ( func CreateSecretFromEnvVar(envVar, secretName string) { value, _ := os.LookupEnv(envVar) Expect(value).NotTo(BeEmpty()) + _, err := cmd.Run("kubectl", "create", "secret", "generic", secretName, "--from-literal=token="+value) Expect(err).NotTo(HaveOccurred()) } @@ -26,6 +27,8 @@ func CreateSecretFromFile(fileName, secretName string) { func CreateOpCredentialsSecret() { rootDir, err := cmd.GetProjectRoot() + Expect(err).NotTo(HaveOccurred()) + credentialsFilePath := filepath.Join(rootDir, "1password-credentials.json") data, err := os.ReadFile(credentialsFilePath) Expect(err).NotTo(HaveOccurred()) From 600adf26708b228959fa2546880a4802c5fb0d83 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Wed, 20 Aug 2025 14:27:12 -0500 Subject: [PATCH 14/64] Move `cmd` package to `testhelper` and rename to be `system` --- test/e2e/e2e_test.go | 6 +++--- test/testhelper/kind/kind.go | 4 ++-- test/testhelper/kube/kube.go | 18 +++++++++--------- test/testhelper/operator/operator.go | 8 ++++---- .../cmd.go => testhelper/system/system.go} | 2 +- 5 files changed, 19 insertions(+), 19 deletions(-) rename test/{cmd/cmd.go => testhelper/system/system.go} (98%) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index ac69e82..172bdb7 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -8,10 +8,10 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/1Password/onepassword-operator/test/cmd" "github.com/1Password/onepassword-operator/test/testhelper/kind" "github.com/1Password/onepassword-operator/test/testhelper/kube" "github.com/1Password/onepassword-operator/test/testhelper/operator" + "github.com/1Password/onepassword-operator/test/testhelper/system" ) const ( @@ -61,12 +61,12 @@ func runCommonTestCases() { wd, err := os.Getwd() Expect(err).NotTo(HaveOccurred()) yamlPath := filepath.Join(wd, "manifests", "secret.yaml") - _, err = cmd.Run("kubectl", "apply", "-f", yamlPath) + _, err = system.Run("kubectl", "apply", "-f", yamlPath) Expect(err).NotTo(HaveOccurred()) By("waiting for secret to be created") Eventually(func(g Gomega) { - output, err := cmd.Run("kubectl", "get", "secret", "login", "-o", "jsonpath={.metadata.name}") + output, err := system.Run("kubectl", "get", "secret", "login", "-o", "jsonpath={.metadata.name}") g.Expect(err).NotTo(HaveOccurred()) g.Expect(output).To(Equal("login")) }, defaultTimeout, defaultInterval).Should(Succeed()) diff --git a/test/testhelper/kind/kind.go b/test/testhelper/kind/kind.go index 5ff8fbc..83b5bb1 100644 --- a/test/testhelper/kind/kind.go +++ b/test/testhelper/kind/kind.go @@ -6,7 +6,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/1Password/onepassword-operator/test/cmd" + "github.com/1Password/onepassword-operator/test/testhelper/system" ) // LoadImageToKind loads a local docker image to the Kind cluster @@ -16,6 +16,6 @@ func LoadImageToKind(imageName string) { if value, ok := os.LookupEnv("KIND_CLUSTER"); ok { clusterName = value } - _, err := cmd.Run("kind", "load", "docker-image", imageName, "--name", clusterName) + _, err := system.Run("kind", "load", "docker-image", imageName, "--name", clusterName) Expect(err).NotTo(HaveOccurred()) } diff --git a/test/testhelper/kube/kube.go b/test/testhelper/kube/kube.go index 6db4524..acb68c7 100644 --- a/test/testhelper/kube/kube.go +++ b/test/testhelper/kube/kube.go @@ -9,24 +9,24 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/1Password/onepassword-operator/test/cmd" + "github.com/1Password/onepassword-operator/test/testhelper/system" ) func CreateSecretFromEnvVar(envVar, secretName string) { value, _ := os.LookupEnv(envVar) Expect(value).NotTo(BeEmpty()) - _, err := cmd.Run("kubectl", "create", "secret", "generic", secretName, "--from-literal=token="+value) + _, err := system.Run("kubectl", "create", "secret", "generic", secretName, "--from-literal=token="+value) Expect(err).NotTo(HaveOccurred()) } func CreateSecretFromFile(fileName, secretName string) { - _, err := cmd.Run("kubectl", "create", "secret", "generic", secretName, "--from-file="+fileName) + _, err := system.Run("kubectl", "create", "secret", "generic", secretName, "--from-file="+fileName) Expect(err).NotTo(HaveOccurred()) } func CreateOpCredentialsSecret() { - rootDir, err := cmd.GetProjectRoot() + rootDir, err := system.GetProjectRoot() Expect(err).NotTo(HaveOccurred()) credentialsFilePath := filepath.Join(rootDir, "1password-credentials.json") @@ -44,14 +44,14 @@ func CreateOpCredentialsSecret() { } func DeleteSecret(name string) { - _, err := cmd.Run("kubectl", "delete", "secret", name, "--ignore-not-found=true") + _, err := system.Run("kubectl", "delete", "secret", name, "--ignore-not-found=true") Expect(err).NotTo(HaveOccurred()) } // PatchOperatorToUseServiceAccount sets `OP_SERVICE_ACCOUNT_TOKEN` env variable var PatchOperatorToUseServiceAccount = WithOperatorRestart(func() { By("patching the operator deployment with service account token") - _, err := cmd.Run( + _, err := system.Run( "kubectl", "patch", "deployment", "onepassword-connect-operator", "--type=json", `-p=[{"op":"replace","path":"/spec/template/spec/containers/0/env","value":[ @@ -70,7 +70,7 @@ var PatchOperatorToUseServiceAccount = WithOperatorRestart(func() { // PatchOperatorManageConnect sets env variable `MANAGE_CONNECT: true` and restarts the operator. var PatchOperatorManageConnect = WithOperatorRestart(func() { By("patching the operator deployment with to manage Connect") - _, err := cmd.Run( + _, err := system.Run( "kubectl", "patch", "deployment", "onepassword-connect-operator", "--type=json", `-p=[{"op":"replace","path":"/spec/template/spec/containers/0/env","value":[ @@ -91,13 +91,13 @@ func WithOperatorRestart(operation func()) func() { return func() { operation() - _, err := cmd.Run("kubectl", "rollout", "status", + _, err := system.Run("kubectl", "rollout", "status", "deployment/onepassword-connect-operator", "-n", "default", "--timeout=120s") Expect(err).NotTo(HaveOccurred()) By("waiting for the operator pod to be 'Running'") Eventually(func(g Gomega) { - output, err := cmd.Run("kubectl", "get", "pods", + output, err := system.Run("kubectl", "get", "pods", "-l", "name=onepassword-connect-operator", "-o", "jsonpath={.items[0].status.phase}") g.Expect(err).NotTo(HaveOccurred()) diff --git a/test/testhelper/operator/operator.go b/test/testhelper/operator/operator.go index d368bf0..cd2e2e6 100644 --- a/test/testhelper/operator/operator.go +++ b/test/testhelper/operator/operator.go @@ -6,12 +6,12 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/1Password/onepassword-operator/test/cmd" + "github.com/1Password/onepassword-operator/test/testhelper/system" ) func BuildOperatorImage() { By("building the operator image") - _, err := cmd.Run("make", "docker-build") + _, err := system.Run("make", "docker-build") ExpectWithOffset(1, err).NotTo(HaveOccurred()) } @@ -21,12 +21,12 @@ func BuildOperatorImage() { // To make the operator use Connect or Service Accounts, patch `config/manager/manager.yaml` func DeployOperator() { By("deploying the operator") - _, err := cmd.Run("make", "deploy") + _, err := system.Run("make", "deploy") Expect(err).NotTo(HaveOccurred()) By("waiting for the operator pod to be 'Running'") Eventually(func(g Gomega) { - output, err := cmd.Run("kubectl", "get", "pods", + output, err := system.Run("kubectl", "get", "pods", "-l", "name=onepassword-connect-operator", "-o", "jsonpath={.items[0].status.phase}") g.Expect(err).NotTo(HaveOccurred()) diff --git a/test/cmd/cmd.go b/test/testhelper/system/system.go similarity index 98% rename from test/cmd/cmd.go rename to test/testhelper/system/system.go index f316283..bded147 100644 --- a/test/cmd/cmd.go +++ b/test/testhelper/system/system.go @@ -1,4 +1,4 @@ -package cmd +package system import ( "fmt" From 7885ba649b57e39539a2662de1d685e1f3d3a1b6 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Wed, 20 Aug 2025 14:42:08 -0500 Subject: [PATCH 15/64] Set to namespace to default --- test/e2e/e2e_test.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 172bdb7..a7e59ce 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -22,16 +22,20 @@ const ( var _ = Describe("Onepassword Operator e2e", Ordered, func() { BeforeAll(func() { + By("Set namespace to default") + _, _ = system.Run("kubectl", "config", "set-context", "--current", "--namespace=default") + + By("Build the operator image") operator.BuildOperatorImage() kind.LoadImageToKind(operatorImageName) - By("create Connect credentials secret") + By("Create Connect credentials secret") kube.CreateOpCredentialsSecret() - By("create onepassword-token secret") + By("Create onepassword-token secret") kube.CreateSecretFromEnvVar("OP_CONNECT_TOKEN", "onepassword-token") - By("create onepassword-service-account-token secret") + By("Create onepassword-service-account-token secret") kube.CreateSecretFromEnvVar("OP_SERVICE_ACCOUNT_TOKEN", "onepassword-service-account-token") operator.DeployOperator() @@ -57,14 +61,14 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() { func runCommonTestCases() { It("Should create secret from manifest file", func() { - By("creating secret") + By("Creating secret") wd, err := os.Getwd() Expect(err).NotTo(HaveOccurred()) yamlPath := filepath.Join(wd, "manifests", "secret.yaml") _, err = system.Run("kubectl", "apply", "-f", yamlPath) Expect(err).NotTo(HaveOccurred()) - By("waiting for secret to be created") + By("Waiting for secret to be created") Eventually(func(g Gomega) { output, err := system.Run("kubectl", "get", "secret", "login", "-o", "jsonpath={.metadata.name}") g.Expect(err).NotTo(HaveOccurred()) From 882d8e951d0df9a0a1675e5589f69a5d2a0e3f18 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Wed, 20 Aug 2025 14:56:32 -0500 Subject: [PATCH 16/64] Never pull the image, but use local when deploying the operator. Deploy along with Connect --- config/manager/manager.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index e9602da..c381817 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -74,6 +74,7 @@ spec: - --leader-elect - --health-probe-bind-address=:8081 image: 1password/onepassword-operator:latest + imagePullPolicy: Never name: manager env: - name: OPERATOR_NAME @@ -96,7 +97,7 @@ spec: name: onepassword-token key: token - name: MANAGE_CONNECT - value: "false" + value: "true" # Uncomment the following lines to enable service account token and comment out the OP_CONNECT_TOKEN, OP_CONNECT_HOST and MANAGE_CONNECT env vars. # - name: OP_SERVICE_ACCOUNT_TOKEN # valueFrom: From 299689fe13bee7d54d812dc4186b2d81a64ed6c8 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Wed, 20 Aug 2025 14:57:47 -0500 Subject: [PATCH 17/64] Extract setting context namespace to standalone function `SetContextNamespace` --- config/default/kustomization.yaml | 205 ++---------------------------- test/e2e/e2e_test.go | 4 +- test/testhelper/kube/kube.go | 6 + 3 files changed, 15 insertions(+), 200 deletions(-) diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index f18595d..e914f4b 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -14,10 +14,6 @@ # pairs: # someName: someValue -resources: -- ../crd -- ../rbac -- ../manager # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml #- ../webhook @@ -26,6 +22,10 @@ resources: # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. #- ../prometheus # [METRICS] Expose the controller manager metrics service. +resources: +- ../crd +- ../rbac +- ../manager - metrics_service.yaml # [NETWORK POLICY] Protect the /metrics endpoint and Webhook Server with NetworkPolicy. # Only Pod(s) running a namespace labeled with 'metrics: enabled' will be able to gather the metrics. @@ -34,201 +34,12 @@ resources: #- ../network-policy # Uncomment the patches line if you enable Metrics -patches: # [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443. # More info: https://book.kubebuilder.io/reference/metrics +patches: - path: manager_metrics_patch.yaml target: kind: Deployment - -# Uncomment the patches line if you enable Metrics and CertManager -# [METRICS-WITH-CERTS] To enable metrics protected with certManager, uncomment the following line. -# This patch will protect the metrics with certManager self-signed certs. -#- path: cert_metrics_manager_patch.yaml -# target: -# kind: Deployment - -# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in -# crd/kustomization.yaml -#- path: manager_webhook_patch.yaml -# target: -# kind: Deployment - -# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. -# Uncomment the following replacements to add the cert-manager CA injection annotations -#replacements: -# - source: # Uncomment the following block to enable certificates for metrics -# kind: Service -# version: v1 -# name: controller-manager-metrics-service -# fieldPath: metadata.name -# targets: -# - select: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: metrics-certs -# fieldPaths: -# - spec.dnsNames.0 -# - spec.dnsNames.1 -# options: -# delimiter: '.' -# index: 0 -# create: true -# - select: # Uncomment the following to set the Service name for TLS config in Prometheus ServiceMonitor -# kind: ServiceMonitor -# group: monitoring.coreos.com -# version: v1 -# name: controller-manager-metrics-monitor -# fieldPaths: -# - spec.endpoints.0.tlsConfig.serverName -# options: -# delimiter: '.' -# index: 0 -# create: true -# -# - source: -# kind: Service -# version: v1 -# name: controller-manager-metrics-service -# fieldPath: metadata.namespace -# targets: -# - select: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: metrics-certs -# fieldPaths: -# - spec.dnsNames.0 -# - spec.dnsNames.1 -# options: -# delimiter: '.' -# index: 1 -# create: true -# - select: # Uncomment the following to set the Service namespace for TLS in Prometheus ServiceMonitor -# kind: ServiceMonitor -# group: monitoring.coreos.com -# version: v1 -# name: controller-manager-metrics-monitor -# fieldPaths: -# - spec.endpoints.0.tlsConfig.serverName -# options: -# delimiter: '.' -# index: 1 -# create: true -# -# - source: # Uncomment the following block if you have any webhook -# kind: Service -# version: v1 -# name: webhook-service -# fieldPath: .metadata.name # Name of the service -# targets: -# - select: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPaths: -# - .spec.dnsNames.0 -# - .spec.dnsNames.1 -# options: -# delimiter: '.' -# index: 0 -# create: true -# - source: -# kind: Service -# version: v1 -# name: webhook-service -# fieldPath: .metadata.namespace # Namespace of the service -# targets: -# - select: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPaths: -# - .spec.dnsNames.0 -# - .spec.dnsNames.1 -# options: -# delimiter: '.' -# index: 1 -# create: true -# -# - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation) -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert # This name should match the one in certificate.yaml -# fieldPath: .metadata.namespace # Namespace of the certificate CR -# targets: -# - select: -# kind: ValidatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 0 -# create: true -# - source: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPath: .metadata.name -# targets: -# - select: -# kind: ValidatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 1 -# create: true -# -# - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting ) -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPath: .metadata.namespace # Namespace of the certificate CR -# targets: -# - select: -# kind: MutatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 0 -# create: true -# - source: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPath: .metadata.name -# targets: -# - select: -# kind: MutatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 1 -# create: true -# -# - source: # Uncomment the following block if you have a ConversionWebhook (--conversion) -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPath: .metadata.namespace # Namespace of the certificate CR -# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. -# +kubebuilder:scaffold:crdkustomizecainjectionns -# - source: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPath: .metadata.name -# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. -# +kubebuilder:scaffold:crdkustomizecainjectionname +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: default diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index a7e59ce..dc35592 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -22,10 +22,8 @@ const ( var _ = Describe("Onepassword Operator e2e", Ordered, func() { BeforeAll(func() { - By("Set namespace to default") - _, _ = system.Run("kubectl", "config", "set-context", "--current", "--namespace=default") + kube.SetContextNamespace("default") - By("Build the operator image") operator.BuildOperatorImage() kind.LoadImageToKind(operatorImageName) diff --git a/test/testhelper/kube/kube.go b/test/testhelper/kube/kube.go index acb68c7..10f437b 100644 --- a/test/testhelper/kube/kube.go +++ b/test/testhelper/kube/kube.go @@ -48,6 +48,12 @@ func DeleteSecret(name string) { Expect(err).NotTo(HaveOccurred()) } +func SetContextNamespace(namespace string) { + By("Set namespace to " + namespace) + _, err := system.Run("kubectl", "config", "set-context", "--current", "--namespace="+namespace) + Expect(err).NotTo(HaveOccurred()) +} + // PatchOperatorToUseServiceAccount sets `OP_SERVICE_ACCOUNT_TOKEN` env variable var PatchOperatorToUseServiceAccount = WithOperatorRestart(func() { By("patching the operator deployment with service account token") From c144bd3d016b526238357de7efba0501a5ece16e Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Wed, 20 Aug 2025 15:02:21 -0500 Subject: [PATCH 18/64] Remove PatchOperatorManageConnect as manifest has `MANAGE_CONNECT: true` set already --- test/e2e/e2e_test.go | 4 ---- test/testhelper/kube/kube.go | 20 -------------------- 2 files changed, 24 deletions(-) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index dc35592..8b47186 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -40,10 +40,6 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() { }) Context("Use the operator with Connect", func() { - BeforeAll(func() { - kube.PatchOperatorManageConnect() - }) - runCommonTestCases() }) diff --git a/test/testhelper/kube/kube.go b/test/testhelper/kube/kube.go index 10f437b..9f9ac78 100644 --- a/test/testhelper/kube/kube.go +++ b/test/testhelper/kube/kube.go @@ -73,26 +73,6 @@ var PatchOperatorToUseServiceAccount = WithOperatorRestart(func() { Expect(err).NotTo(HaveOccurred()) }) -// PatchOperatorManageConnect sets env variable `MANAGE_CONNECT: true` and restarts the operator. -var PatchOperatorManageConnect = WithOperatorRestart(func() { - By("patching the operator deployment with to manage Connect") - _, err := system.Run( - "kubectl", "patch", "deployment", "onepassword-connect-operator", - "--type=json", - `-p=[{"op":"replace","path":"/spec/template/spec/containers/0/env","value":[ - {"name":"OPERATOR_NAME","value":"onepassword-connect-operator"}, - {"name":"POD_NAME","valueFrom":{"fieldRef":{"fieldPath":"metadata.name"}}}, - {"name":"WATCH_NAMESPACE","value":"default"}, - {"name":"POLLING_INTERVAL","value":"10"}, - {"name":"AUTO_RESTART","value":"false"}, - {"name":"OP_CONNECT_HOST","value":"http://onepassword-connect:8080"}, - {"name":"OP_CONNECT_TOKEN","valueFrom":{"secretKeyRef":{"name":"onepassword-token","key":"token"}}}, - {"name":"MANAGE_CONNECT","value":"true"}, - ]}]`, - ) - Expect(err).NotTo(HaveOccurred()) -}) - func WithOperatorRestart(operation func()) func() { return func() { operation() From 331e8d7bfb68a0ed7da796f7571e575662dd18e7 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Wed, 20 Aug 2025 15:29:58 -0500 Subject: [PATCH 19/64] Add e2e tests workflow --- .github/workflows/test-e2e.yml | 43 ++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/test-e2e.yml diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml new file mode 100644 index 0000000..75ec01d --- /dev/null +++ b/.github/workflows/test-e2e.yml @@ -0,0 +1,43 @@ +name: Test E2E + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] + branches: ['**'] # run for PRs targeting any branch (main and others) + +concurrency: + group: e2e-${{ github.event.pull_request.head.ref }} + cancel-in-progress: true # cancel previous job runs for the same branch + +jobs: + e2e-test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Install dependencies + run: go mod tidy + + - name: Create kind cluster + uses: helm/kind-action@v1 + with: + cluster_name: onepassword-operator-test-e2e + + - name: Install kubectl + uses: azure/setup-kubectl@v4 + + - name: Create '1password-credentials.json' file + run: | + echo ${{ secrets.OP_CONNECT_CREDENTIALS }} > 1password-credentials.json + + - name: Run E2E tests + run: make e2e-test + env: + OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }} + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} From 29b7ed7899712eccaa76c0e26a7a46efdb3d0a7c Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Wed, 20 Aug 2025 15:33:08 -0500 Subject: [PATCH 20/64] Run correct make command that starts e2e tests --- .github/workflows/test-e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 75ec01d..75c9548 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -37,7 +37,7 @@ jobs: echo ${{ secrets.OP_CONNECT_CREDENTIALS }} > 1password-credentials.json - name: Run E2E tests - run: make e2e-test + run: make test-e2e env: OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }} OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} From dcd7eefac0f547e9e69e664f9dfbf8ad59913db4 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Wed, 20 Aug 2025 15:39:44 -0500 Subject: [PATCH 21/64] Increase timeout to 1 minute --- test/e2e/e2e_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 8b47186..52a0c5a 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -17,7 +17,7 @@ import ( const ( operatorImageName = "1password/onepassword-operator:latest" defaultInterval = 1 * time.Second - defaultTimeout = 30 * time.Second + defaultTimeout = 1 * time.Minute ) var _ = Describe("Onepassword Operator e2e", Ordered, func() { From 5a56fd333080441b034f9b76f22d21226d85017c Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Wed, 20 Aug 2025 15:51:50 -0500 Subject: [PATCH 22/64] Wait for Connect pod is running --- test/e2e/e2e_test.go | 5 +++++ test/testhelper/operator/operator.go | 19 ++++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 52a0c5a..94973e3 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -37,9 +37,14 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() { kube.CreateSecretFromEnvVar("OP_SERVICE_ACCOUNT_TOKEN", "onepassword-service-account-token") operator.DeployOperator() + operator.WaitingForOperatorPod() }) Context("Use the operator with Connect", func() { + BeforeAll(func() { + operator.WaitingForConnectPod() + }) + runCommonTestCases() }) diff --git a/test/testhelper/operator/operator.go b/test/testhelper/operator/operator.go index cd2e2e6..af5bdae 100644 --- a/test/testhelper/operator/operator.go +++ b/test/testhelper/operator/operator.go @@ -10,7 +10,7 @@ import ( ) func BuildOperatorImage() { - By("building the operator image") + By("building the Operator image") _, err := system.Run("make", "docker-build") ExpectWithOffset(1, err).NotTo(HaveOccurred()) } @@ -20,11 +20,13 @@ func BuildOperatorImage() { // All the resources created using manifests in `config/` dir. // To make the operator use Connect or Service Accounts, patch `config/manager/manager.yaml` func DeployOperator() { - By("deploying the operator") + By("deploying the Operator") _, err := system.Run("make", "deploy") Expect(err).NotTo(HaveOccurred()) +} - By("waiting for the operator pod to be 'Running'") +func WaitingForOperatorPod() { + By("Waiting for the Operator pod to be 'Running'") Eventually(func(g Gomega) { output, err := system.Run("kubectl", "get", "pods", "-l", "name=onepassword-connect-operator", @@ -33,3 +35,14 @@ func DeployOperator() { g.Expect(output).To(ContainSubstring("Running")) }, 30*time.Second, 1*time.Second).Should(Succeed()) } + +func WaitingForConnectPod() { + By("Waiting for the Connect pod to be 'Running'") + Eventually(func(g Gomega) { + output, err := system.Run("kubectl", "get", "pods", + "-l", "app=onepassword-connect", + "-o", "jsonpath={.items[0].status.phase}") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(output).To(ContainSubstring("Running")) + }, 30*time.Second, 1*time.Second).Should(Succeed()) +} From 6bb6088353fe1bad331fefb4b08ff59be6355602 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Thu, 21 Aug 2025 09:56:37 -0500 Subject: [PATCH 23/64] Use `GetProjectRoot` to create secret --- test/e2e/e2e_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 94973e3..13ae96a 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -1,7 +1,6 @@ package e2e import ( - "os" "path/filepath" "time" @@ -61,9 +60,10 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() { func runCommonTestCases() { It("Should create secret from manifest file", func() { By("Creating secret") - wd, err := os.Getwd() + root, err := system.GetProjectRoot() Expect(err).NotTo(HaveOccurred()) - yamlPath := filepath.Join(wd, "manifests", "secret.yaml") + + yamlPath := filepath.Join(root, "test", "e2e", "manifests", "secret.yaml") _, err = system.Run("kubectl", "apply", "-f", yamlPath) Expect(err).NotTo(HaveOccurred()) From 9825cb57c9a794ae7fbd746c206498072038118b Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Thu, 21 Aug 2025 10:02:13 -0500 Subject: [PATCH 24/64] Test with service account --- test/e2e/e2e_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 13ae96a..8c95f6a 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -39,13 +39,13 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() { operator.WaitingForOperatorPod() }) - Context("Use the operator with Connect", func() { - BeforeAll(func() { - operator.WaitingForConnectPod() - }) - - runCommonTestCases() - }) + //Context("Use the operator with Connect", func() { + // BeforeAll(func() { + // operator.WaitingForConnectPod() + // }) + // + // runCommonTestCases() + //}) Context("Use the operator with Service Account", func() { BeforeAll(func() { From d0b11c70f0abc11bef503636112c36e451647e0b Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Thu, 21 Aug 2025 10:11:06 -0500 Subject: [PATCH 25/64] Roll back Connect test --- test/e2e/e2e_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 8c95f6a..13ae96a 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -39,13 +39,13 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() { operator.WaitingForOperatorPod() }) - //Context("Use the operator with Connect", func() { - // BeforeAll(func() { - // operator.WaitingForConnectPod() - // }) - // - // runCommonTestCases() - //}) + Context("Use the operator with Connect", func() { + BeforeAll(func() { + operator.WaitingForConnectPod() + }) + + runCommonTestCases() + }) Context("Use the operator with Service Account", func() { BeforeAll(func() { From 7187f41ef17cef50582a3ce88069389edfc666a6 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Thu, 21 Aug 2025 10:17:19 -0500 Subject: [PATCH 26/64] Checking that all secrets are created before running tests --- test/e2e/e2e_test.go | 30 +++++++++++++++++++++--------- test/testhelper/kube/kube.go | 11 +++++++++++ 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 13ae96a..bb89cf8 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -26,15 +26,30 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() { operator.BuildOperatorImage() kind.LoadImageToKind(operatorImageName) - By("Create Connect credentials secret") + By("Create Connect 'op-credentials' credentials secret") kube.CreateOpCredentialsSecret() - By("Create onepassword-token secret") + By("Checking Connect 'op-credentials' secret is created") + Eventually(func() { + kube.CheckSecretExists("op-credentials") + }, defaultTimeout, defaultInterval).Should(Succeed()) + + By("Create 'onepassword-token' secret") kube.CreateSecretFromEnvVar("OP_CONNECT_TOKEN", "onepassword-token") - By("Create onepassword-service-account-token secret") + By("Checking 'onepassword-token' secret is created") + Eventually(func() { + kube.CheckSecretExists("onepassword-token") + }, defaultTimeout, defaultInterval).Should(Succeed()) + + By("Create 'onepassword-service-account-token' secret") kube.CreateSecretFromEnvVar("OP_SERVICE_ACCOUNT_TOKEN", "onepassword-service-account-token") + By("Checking 'onepassword-service-account-token' secret is created") + Eventually(func() { + kube.CheckSecretExists("onepassword-service-account-token") + }, defaultTimeout, defaultInterval).Should(Succeed()) + operator.DeployOperator() operator.WaitingForOperatorPod() }) @@ -64,14 +79,11 @@ func runCommonTestCases() { Expect(err).NotTo(HaveOccurred()) yamlPath := filepath.Join(root, "test", "e2e", "manifests", "secret.yaml") - _, err = system.Run("kubectl", "apply", "-f", yamlPath) - Expect(err).NotTo(HaveOccurred()) + kube.Apply(yamlPath) By("Waiting for secret to be created") - Eventually(func(g Gomega) { - output, err := system.Run("kubectl", "get", "secret", "login", "-o", "jsonpath={.metadata.name}") - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(output).To(Equal("login")) + Eventually(func() { + kube.CheckSecretExists("login") }, defaultTimeout, defaultInterval).Should(Succeed()) }) } diff --git a/test/testhelper/kube/kube.go b/test/testhelper/kube/kube.go index 9f9ac78..5ebcc99 100644 --- a/test/testhelper/kube/kube.go +++ b/test/testhelper/kube/kube.go @@ -48,6 +48,17 @@ func DeleteSecret(name string) { Expect(err).NotTo(HaveOccurred()) } +func CheckSecretExists(name string) { + output, err := system.Run("kubectl", "get", "secret", name, "-o", "jsonpath={.metadata.name}") + Expect(err).NotTo(HaveOccurred()) + Expect(output).To(Equal(name)) +} + +func Apply(yamlPath string) { + _, err := system.Run("kubectl", "apply", "-f", yamlPath) + Expect(err).NotTo(HaveOccurred()) +} + func SetContextNamespace(namespace string) { By("Set namespace to " + namespace) _, err := system.Run("kubectl", "config", "set-context", "--current", "--namespace="+namespace) From 2003d13788a352f6ec9501dc0d5aedc02c025b86 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Thu, 21 Aug 2025 10:38:19 -0500 Subject: [PATCH 27/64] Fix lint issues and `CheckSecretExists` function --- test/e2e/e2e_test.go | 21 ++++------------- test/testhelper/defaults/defaults.go | 8 +++++++ test/testhelper/kind/kind.go | 2 ++ test/testhelper/kube/kube.go | 35 +++++++++++++++++++--------- test/testhelper/operator/operator.go | 2 ++ 5 files changed, 41 insertions(+), 27 deletions(-) create mode 100644 test/testhelper/defaults/defaults.go diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index bb89cf8..0e6db5e 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -2,7 +2,6 @@ package e2e import ( "path/filepath" - "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -15,8 +14,6 @@ import ( const ( operatorImageName = "1password/onepassword-operator:latest" - defaultInterval = 1 * time.Second - defaultTimeout = 1 * time.Minute ) var _ = Describe("Onepassword Operator e2e", Ordered, func() { @@ -30,25 +27,19 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() { kube.CreateOpCredentialsSecret() By("Checking Connect 'op-credentials' secret is created") - Eventually(func() { - kube.CheckSecretExists("op-credentials") - }, defaultTimeout, defaultInterval).Should(Succeed()) + kube.CheckSecretExists("op-credentials") By("Create 'onepassword-token' secret") kube.CreateSecretFromEnvVar("OP_CONNECT_TOKEN", "onepassword-token") By("Checking 'onepassword-token' secret is created") - Eventually(func() { - kube.CheckSecretExists("onepassword-token") - }, defaultTimeout, defaultInterval).Should(Succeed()) + kube.CheckSecretExists("onepassword-token") By("Create 'onepassword-service-account-token' secret") kube.CreateSecretFromEnvVar("OP_SERVICE_ACCOUNT_TOKEN", "onepassword-service-account-token") By("Checking 'onepassword-service-account-token' secret is created") - Eventually(func() { - kube.CheckSecretExists("onepassword-service-account-token") - }, defaultTimeout, defaultInterval).Should(Succeed()) + kube.CheckSecretExists("onepassword-service-account-token") operator.DeployOperator() operator.WaitingForOperatorPod() @@ -81,9 +72,7 @@ func runCommonTestCases() { yamlPath := filepath.Join(root, "test", "e2e", "manifests", "secret.yaml") kube.Apply(yamlPath) - By("Waiting for secret to be created") - Eventually(func() { - kube.CheckSecretExists("login") - }, defaultTimeout, defaultInterval).Should(Succeed()) + By("Checking for secret to be created") + kube.CheckSecretExists("login") }) } diff --git a/test/testhelper/defaults/defaults.go b/test/testhelper/defaults/defaults.go new file mode 100644 index 0000000..68532de --- /dev/null +++ b/test/testhelper/defaults/defaults.go @@ -0,0 +1,8 @@ +package defaults + +import "time" + +const ( + E2EInterval = 1 * time.Second + E2ETimeout = 1 * time.Minute +) diff --git a/test/testhelper/kind/kind.go b/test/testhelper/kind/kind.go index 83b5bb1..4f20160 100644 --- a/test/testhelper/kind/kind.go +++ b/test/testhelper/kind/kind.go @@ -3,7 +3,9 @@ package kind import ( "os" + //nolint:staticcheck // ST1001 . "github.com/onsi/ginkgo/v2" + //nolint:staticcheck // ST1001 . "github.com/onsi/gomega" "github.com/1Password/onepassword-operator/test/testhelper/system" diff --git a/test/testhelper/kube/kube.go b/test/testhelper/kube/kube.go index 5ebcc99..4684f34 100644 --- a/test/testhelper/kube/kube.go +++ b/test/testhelper/kube/kube.go @@ -6,9 +6,12 @@ import ( "path/filepath" "time" + //nolint:staticcheck // ST1001 . "github.com/onsi/ginkgo/v2" + //nolint:staticcheck // ST1001 . "github.com/onsi/gomega" + "github.com/1Password/onepassword-operator/test/testhelper/defaults" "github.com/1Password/onepassword-operator/test/testhelper/system" ) @@ -49,9 +52,11 @@ func DeleteSecret(name string) { } func CheckSecretExists(name string) { - output, err := system.Run("kubectl", "get", "secret", name, "-o", "jsonpath={.metadata.name}") - Expect(err).NotTo(HaveOccurred()) - Expect(output).To(Equal(name)) + Eventually(func(g Gomega) { + output, err := system.Run("kubectl", "get", "secret", name, "-o", "jsonpath={.metadata.name}") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(output).To(Equal(name)) + }, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed()) } func Apply(yamlPath string) { @@ -72,14 +77,22 @@ var PatchOperatorToUseServiceAccount = WithOperatorRestart(func() { "kubectl", "patch", "deployment", "onepassword-connect-operator", "--type=json", `-p=[{"op":"replace","path":"/spec/template/spec/containers/0/env","value":[ - {"name":"OPERATOR_NAME","value":"onepassword-connect-operator"}, - {"name":"POD_NAME","valueFrom":{"fieldRef":{"fieldPath":"metadata.name"}}}, - {"name":"WATCH_NAMESPACE","value":"default"}, - {"name":"POLLING_INTERVAL","value":"10"}, - {"name":"AUTO_RESTART","value":"false"}, - {"name":"OP_SERVICE_ACCOUNT_TOKEN","valueFrom":{"secretKeyRef":{"name":"onepassword-service-account-token","key":"token"}}}, - {"name":"MANAGE_CONNECT","value":"false"} - ]}]`, + {"name":"OPERATOR_NAME","value":"onepassword-connect-operator"}, + {"name":"POD_NAME","valueFrom":{"fieldRef":{"fieldPath":"metadata.name"}}}, + {"name":"WATCH_NAMESPACE","value":"default"}, + {"name":"POLLING_INTERVAL","value":"10"}, + {"name":"AUTO_RESTART","value":"false"}, + { + "name":"OP_SERVICE_ACCOUNT_TOKEN", + "valueFrom":{ + "secretKeyRef":{ + "name":"onepassword-service-account-token", + "key":"token", + }, + }, + }, + {"name":"MANAGE_CONNECT","value":"false"} + ]}]`, ) Expect(err).NotTo(HaveOccurred()) }) diff --git a/test/testhelper/operator/operator.go b/test/testhelper/operator/operator.go index af5bdae..69785b0 100644 --- a/test/testhelper/operator/operator.go +++ b/test/testhelper/operator/operator.go @@ -3,7 +3,9 @@ package operator import ( "time" + //nolint:staticcheck // ST1001 . "github.com/onsi/ginkgo/v2" + //nolint:staticcheck // ST1001 . "github.com/onsi/gomega" "github.com/1Password/onepassword-operator/test/testhelper/system" From 22a7c8f586ee787fc9a9ae21e98ddc2a311bff1b Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Thu, 21 Aug 2025 14:57:24 -0500 Subject: [PATCH 28/64] Create 1password-credentials.json from env var --- .github/workflows/test-e2e.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 75c9548..55c0313 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -33,8 +33,10 @@ jobs: uses: azure/setup-kubectl@v4 - name: Create '1password-credentials.json' file + env: + OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }} run: | - echo ${{ secrets.OP_CONNECT_CREDENTIALS }} > 1password-credentials.json + echo "$OP_CONNECT_CREDENTIALS" > 1password-credentials.json - name: Run E2E tests run: make test-e2e From ca051a08cf29311359cc7fb272589d03d8d83851 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Thu, 21 Aug 2025 15:22:41 -0500 Subject: [PATCH 29/64] Move `testhelper` package to `pkg` so it can be installed as dependency in secrets injector repo --- {test => pkg}/testhelper/defaults/defaults.go | 0 {test => pkg}/testhelper/kind/kind.go | 2 +- {test => pkg}/testhelper/kube/kube.go | 4 ++-- {test => pkg}/testhelper/operator/operator.go | 2 +- {test => pkg}/testhelper/system/system.go | 0 test/e2e/e2e_suite_test.go | 19 +++++++++++++++++++ test/e2e/e2e_test.go | 8 ++++---- 7 files changed, 27 insertions(+), 8 deletions(-) rename {test => pkg}/testhelper/defaults/defaults.go (100%) rename {test => pkg}/testhelper/kind/kind.go (88%) rename {test => pkg}/testhelper/kube/kube.go (96%) rename {test => pkg}/testhelper/operator/operator.go (95%) rename {test => pkg}/testhelper/system/system.go (100%) diff --git a/test/testhelper/defaults/defaults.go b/pkg/testhelper/defaults/defaults.go similarity index 100% rename from test/testhelper/defaults/defaults.go rename to pkg/testhelper/defaults/defaults.go diff --git a/test/testhelper/kind/kind.go b/pkg/testhelper/kind/kind.go similarity index 88% rename from test/testhelper/kind/kind.go rename to pkg/testhelper/kind/kind.go index 4f20160..a561f31 100644 --- a/test/testhelper/kind/kind.go +++ b/pkg/testhelper/kind/kind.go @@ -8,7 +8,7 @@ import ( //nolint:staticcheck // ST1001 . "github.com/onsi/gomega" - "github.com/1Password/onepassword-operator/test/testhelper/system" + "github.com/1Password/onepassword-operator/pkg/testhelper/system" ) // LoadImageToKind loads a local docker image to the Kind cluster diff --git a/test/testhelper/kube/kube.go b/pkg/testhelper/kube/kube.go similarity index 96% rename from test/testhelper/kube/kube.go rename to pkg/testhelper/kube/kube.go index 4684f34..6ee441b 100644 --- a/test/testhelper/kube/kube.go +++ b/pkg/testhelper/kube/kube.go @@ -11,8 +11,8 @@ import ( //nolint:staticcheck // ST1001 . "github.com/onsi/gomega" - "github.com/1Password/onepassword-operator/test/testhelper/defaults" - "github.com/1Password/onepassword-operator/test/testhelper/system" + "github.com/1Password/onepassword-operator/pkg/testhelper/defaults" + "github.com/1Password/onepassword-operator/pkg/testhelper/system" ) func CreateSecretFromEnvVar(envVar, secretName string) { diff --git a/test/testhelper/operator/operator.go b/pkg/testhelper/operator/operator.go similarity index 95% rename from test/testhelper/operator/operator.go rename to pkg/testhelper/operator/operator.go index 69785b0..41bd8cf 100644 --- a/test/testhelper/operator/operator.go +++ b/pkg/testhelper/operator/operator.go @@ -8,7 +8,7 @@ import ( //nolint:staticcheck // ST1001 . "github.com/onsi/gomega" - "github.com/1Password/onepassword-operator/test/testhelper/system" + "github.com/1Password/onepassword-operator/pkg/testhelper/system" ) func BuildOperatorImage() { diff --git a/test/testhelper/system/system.go b/pkg/testhelper/system/system.go similarity index 100% rename from test/testhelper/system/system.go rename to pkg/testhelper/system/system.go diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index c45bc90..04ed1b5 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -12,3 +12,22 @@ func TestE2E(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "onepassword-operator e2e suite") } + +//By("create onepassword-token secret") +//connectToken, _ := os.LookupEnv("OP_CONNECT_TOKEN") +//Expect(connectToken).NotTo(BeEmpty()) +//output := exec.Command("kubectl", "-n", namespace, "create", "secret", "generic", "onepassword-token", "--from-literal=token="+connectToken) +//_, err = utils.Run(output) +//ExpectWithOffset(1, err).NotTo(HaveOccurred()) + +//It("Secret is updated after POOLING_INTERVAL", func() { +// // TODO: implement +//}) +// +//It("Secret with `ignore-secret` annotation is not updated", func() { +// // TODO: implement +//}) +// +//It("Deployment not auto restarts when ", func() { +// // TODO: implement +//}) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 0e6db5e..d2e57ef 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -6,10 +6,10 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/1Password/onepassword-operator/test/testhelper/kind" - "github.com/1Password/onepassword-operator/test/testhelper/kube" - "github.com/1Password/onepassword-operator/test/testhelper/operator" - "github.com/1Password/onepassword-operator/test/testhelper/system" + "github.com/1Password/onepassword-operator/pkg/testhelper/kind" + "github.com/1Password/onepassword-operator/pkg/testhelper/kube" + "github.com/1Password/onepassword-operator/pkg/testhelper/operator" + "github.com/1Password/onepassword-operator/pkg/testhelper/system" ) const ( From 4d64beab86874c6ed5319d2e70d5f781d92e371d Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Thu, 21 Aug 2025 15:52:38 -0500 Subject: [PATCH 30/64] Exclude e2e tests from `make test` command --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 8b5d9b9..79ed7b7 100644 --- a/Makefile +++ b/Makefile @@ -117,7 +117,7 @@ vet: ## Run go vet against code. .PHONY: test test: manifests generate fmt vet setup-envtest ## Run tests. - KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./... -coverprofile cover.out + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $(shell go list ./... | grep -v /test/e2e) -coverprofile cover.out # TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'. # The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally. From cf9b267eafda45748d8cd4ab5b2fdf5acd45b5fc Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Thu, 21 Aug 2025 16:02:04 -0500 Subject: [PATCH 31/64] Remove commented code --- test/e2e/e2e_suite_test.go | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index 04ed1b5..c45bc90 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -12,22 +12,3 @@ func TestE2E(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "onepassword-operator e2e suite") } - -//By("create onepassword-token secret") -//connectToken, _ := os.LookupEnv("OP_CONNECT_TOKEN") -//Expect(connectToken).NotTo(BeEmpty()) -//output := exec.Command("kubectl", "-n", namespace, "create", "secret", "generic", "onepassword-token", "--from-literal=token="+connectToken) -//_, err = utils.Run(output) -//ExpectWithOffset(1, err).NotTo(HaveOccurred()) - -//It("Secret is updated after POOLING_INTERVAL", func() { -// // TODO: implement -//}) -// -//It("Secret with `ignore-secret` annotation is not updated", func() { -// // TODO: implement -//}) -// -//It("Deployment not auto restarts when ", func() { -// // TODO: implement -//}) From 904d269e7b14418a1aa25ffa46d365e8e1d8d786 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Thu, 21 Aug 2025 17:01:10 -0500 Subject: [PATCH 32/64] Roll back changes to customization yaml --- config/default/kustomization.yaml | 205 ++++++++++++++++++++++++++++-- 1 file changed, 197 insertions(+), 8 deletions(-) diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index e914f4b..f18595d 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -14,6 +14,10 @@ # pairs: # someName: someValue +resources: +- ../crd +- ../rbac +- ../manager # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml #- ../webhook @@ -22,10 +26,6 @@ # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. #- ../prometheus # [METRICS] Expose the controller manager metrics service. -resources: -- ../crd -- ../rbac -- ../manager - metrics_service.yaml # [NETWORK POLICY] Protect the /metrics endpoint and Webhook Server with NetworkPolicy. # Only Pod(s) running a namespace labeled with 'metrics: enabled' will be able to gather the metrics. @@ -34,12 +34,201 @@ resources: #- ../network-policy # Uncomment the patches line if you enable Metrics +patches: # [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443. # More info: https://book.kubebuilder.io/reference/metrics -patches: - path: manager_metrics_patch.yaml target: kind: Deployment -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -namespace: default + +# Uncomment the patches line if you enable Metrics and CertManager +# [METRICS-WITH-CERTS] To enable metrics protected with certManager, uncomment the following line. +# This patch will protect the metrics with certManager self-signed certs. +#- path: cert_metrics_manager_patch.yaml +# target: +# kind: Deployment + +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- path: manager_webhook_patch.yaml +# target: +# kind: Deployment + +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. +# Uncomment the following replacements to add the cert-manager CA injection annotations +#replacements: +# - source: # Uncomment the following block to enable certificates for metrics +# kind: Service +# version: v1 +# name: controller-manager-metrics-service +# fieldPath: metadata.name +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: metrics-certs +# fieldPaths: +# - spec.dnsNames.0 +# - spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 0 +# create: true +# - select: # Uncomment the following to set the Service name for TLS config in Prometheus ServiceMonitor +# kind: ServiceMonitor +# group: monitoring.coreos.com +# version: v1 +# name: controller-manager-metrics-monitor +# fieldPaths: +# - spec.endpoints.0.tlsConfig.serverName +# options: +# delimiter: '.' +# index: 0 +# create: true +# +# - source: +# kind: Service +# version: v1 +# name: controller-manager-metrics-service +# fieldPath: metadata.namespace +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: metrics-certs +# fieldPaths: +# - spec.dnsNames.0 +# - spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 1 +# create: true +# - select: # Uncomment the following to set the Service namespace for TLS in Prometheus ServiceMonitor +# kind: ServiceMonitor +# group: monitoring.coreos.com +# version: v1 +# name: controller-manager-metrics-monitor +# fieldPaths: +# - spec.endpoints.0.tlsConfig.serverName +# options: +# delimiter: '.' +# index: 1 +# create: true +# +# - source: # Uncomment the following block if you have any webhook +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.name # Name of the service +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 0 +# create: true +# - source: +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.namespace # Namespace of the service +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 1 +# create: true +# +# - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation) +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # This name should match the one in certificate.yaml +# fieldPath: .metadata.namespace # Namespace of the certificate CR +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - source: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert +# fieldPath: .metadata.name +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# +# - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting ) +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert +# fieldPath: .metadata.namespace # Namespace of the certificate CR +# targets: +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - source: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert +# fieldPath: .metadata.name +# targets: +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# +# - source: # Uncomment the following block if you have a ConversionWebhook (--conversion) +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert +# fieldPath: .metadata.namespace # Namespace of the certificate CR +# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. +# +kubebuilder:scaffold:crdkustomizecainjectionns +# - source: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert +# fieldPath: .metadata.name +# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. +# +kubebuilder:scaffold:crdkustomizecainjectionname From bb97134e10d41e748b3407e98f4f8a0591d95006 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 22 Aug 2025 08:30:35 -0500 Subject: [PATCH 33/64] Add comments on each test helper function --- pkg/testhelper/kind/kind.go | 2 +- pkg/testhelper/kube/kube.go | 20 +++++++++++++++++--- pkg/testhelper/operator/operator.go | 9 ++++++--- test/e2e/e2e_test.go | 13 +------------ 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/pkg/testhelper/kind/kind.go b/pkg/testhelper/kind/kind.go index a561f31..8b03879 100644 --- a/pkg/testhelper/kind/kind.go +++ b/pkg/testhelper/kind/kind.go @@ -13,7 +13,7 @@ import ( // LoadImageToKind loads a local docker image to the Kind cluster func LoadImageToKind(imageName string) { - By("loading the operator image on Kind") + By("Loading the operator image on Kind") clusterName := "kind" if value, ok := os.LookupEnv("KIND_CLUSTER"); ok { clusterName = value diff --git a/pkg/testhelper/kube/kube.go b/pkg/testhelper/kube/kube.go index 6ee441b..73339af 100644 --- a/pkg/testhelper/kube/kube.go +++ b/pkg/testhelper/kube/kube.go @@ -15,7 +15,10 @@ import ( "github.com/1Password/onepassword-operator/pkg/testhelper/system" ) +// CreateSecretFromEnvVar creates a kubernetes secret from an environment variable func CreateSecretFromEnvVar(envVar, secretName string) { + By("Creating '" + secretName + "' secret") + value, _ := os.LookupEnv(envVar) Expect(value).NotTo(BeEmpty()) @@ -23,11 +26,15 @@ func CreateSecretFromEnvVar(envVar, secretName string) { Expect(err).NotTo(HaveOccurred()) } +// CreateSecretFromFile creates a kubernetes secret from a file func CreateSecretFromFile(fileName, secretName string) { + By("Creating '" + secretName + "' secret from file") _, err := system.Run("kubectl", "create", "secret", "generic", secretName, "--from-file="+fileName) Expect(err).NotTo(HaveOccurred()) } +// CreateOpCredentialsSecret creates a kubernetes secret from 1password-credentials.json file +// encodes it in base64 and saves it to op-session file func CreateOpCredentialsSecret() { rootDir, err := system.GetProjectRoot() Expect(err).NotTo(HaveOccurred()) @@ -46,12 +53,16 @@ func CreateOpCredentialsSecret() { CreateSecretFromFile("op-session", "op-credentials") } +// DeleteSecret deletes a kubernetes secret func DeleteSecret(name string) { + By("Deleting '" + name + "' secret") _, err := system.Run("kubectl", "delete", "secret", name, "--ignore-not-found=true") Expect(err).NotTo(HaveOccurred()) } +// CheckSecretExists checks if a kubernetes secret exists func CheckSecretExists(name string) { + By("Checking '" + name + "' secret exists") Eventually(func(g Gomega) { output, err := system.Run("kubectl", "get", "secret", name, "-o", "jsonpath={.metadata.name}") g.Expect(err).NotTo(HaveOccurred()) @@ -59,11 +70,13 @@ func CheckSecretExists(name string) { }, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed()) } +// Apply applies a kubernetes manifest file func Apply(yamlPath string) { _, err := system.Run("kubectl", "apply", "-f", yamlPath) Expect(err).NotTo(HaveOccurred()) } +// SetContextNamespace sets the current kubernetes context namespace func SetContextNamespace(namespace string) { By("Set namespace to " + namespace) _, err := system.Run("kubectl", "config", "set-context", "--current", "--namespace="+namespace) @@ -71,7 +84,7 @@ func SetContextNamespace(namespace string) { } // PatchOperatorToUseServiceAccount sets `OP_SERVICE_ACCOUNT_TOKEN` env variable -var PatchOperatorToUseServiceAccount = WithOperatorRestart(func() { +var PatchOperatorToUseServiceAccount = withOperatorRestart(func() { By("patching the operator deployment with service account token") _, err := system.Run( "kubectl", "patch", "deployment", "onepassword-connect-operator", @@ -97,7 +110,8 @@ var PatchOperatorToUseServiceAccount = WithOperatorRestart(func() { Expect(err).NotTo(HaveOccurred()) }) -func WithOperatorRestart(operation func()) func() { +// withOperatorRestart is a helper function that restarts the operator deployment +func withOperatorRestart(operation func()) func() { return func() { operation() @@ -105,7 +119,7 @@ func WithOperatorRestart(operation func()) func() { "deployment/onepassword-connect-operator", "-n", "default", "--timeout=120s") Expect(err).NotTo(HaveOccurred()) - By("waiting for the operator pod to be 'Running'") + By("Waiting for the operator pod to be 'Running'") Eventually(func(g Gomega) { output, err := system.Run("kubectl", "get", "pods", "-l", "name=onepassword-connect-operator", diff --git a/pkg/testhelper/operator/operator.go b/pkg/testhelper/operator/operator.go index 41bd8cf..f3ff4d5 100644 --- a/pkg/testhelper/operator/operator.go +++ b/pkg/testhelper/operator/operator.go @@ -11,22 +11,24 @@ import ( "github.com/1Password/onepassword-operator/pkg/testhelper/system" ) +// BuildOperatorImage builds the Operator image using `make docker-build` func BuildOperatorImage() { - By("building the Operator image") + By("Building the Operator image") _, err := system.Run("make", "docker-build") ExpectWithOffset(1, err).NotTo(HaveOccurred()) } -// DeployOperator deploys the Onepassword Operator in the default namespace. +// DeployOperator deploys the Operator in the default namespace. // It waits for the operator pod to be in 'Running' state. // All the resources created using manifests in `config/` dir. // To make the operator use Connect or Service Accounts, patch `config/manager/manager.yaml` func DeployOperator() { - By("deploying the Operator") + By("Deploying the Operator") _, err := system.Run("make", "deploy") Expect(err).NotTo(HaveOccurred()) } +// WaitingForOperatorPod waits for the Operator pod to be in 'Running' state func WaitingForOperatorPod() { By("Waiting for the Operator pod to be 'Running'") Eventually(func(g Gomega) { @@ -38,6 +40,7 @@ func WaitingForOperatorPod() { }, 30*time.Second, 1*time.Second).Should(Succeed()) } +// WaitingForConnectPod waits for the Connect pod to be in 'Running' state func WaitingForConnectPod() { By("Waiting for the Connect pod to be 'Running'") Eventually(func(g Gomega) { diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index d2e57ef..11b54b9 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -19,26 +19,16 @@ const ( var _ = Describe("Onepassword Operator e2e", Ordered, func() { BeforeAll(func() { kube.SetContextNamespace("default") - operator.BuildOperatorImage() kind.LoadImageToKind(operatorImageName) - By("Create Connect 'op-credentials' credentials secret") kube.CreateOpCredentialsSecret() - - By("Checking Connect 'op-credentials' secret is created") kube.CheckSecretExists("op-credentials") - By("Create 'onepassword-token' secret") kube.CreateSecretFromEnvVar("OP_CONNECT_TOKEN", "onepassword-token") - - By("Checking 'onepassword-token' secret is created") kube.CheckSecretExists("onepassword-token") - By("Create 'onepassword-service-account-token' secret") kube.CreateSecretFromEnvVar("OP_SERVICE_ACCOUNT_TOKEN", "onepassword-service-account-token") - - By("Checking 'onepassword-service-account-token' secret is created") kube.CheckSecretExists("onepassword-service-account-token") operator.DeployOperator() @@ -63,6 +53,7 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() { }) }) +// runCommonTestCases contains test cases that are common to both Connect and Service Account authentication methods. func runCommonTestCases() { It("Should create secret from manifest file", func() { By("Creating secret") @@ -71,8 +62,6 @@ func runCommonTestCases() { yamlPath := filepath.Join(root, "test", "e2e", "manifests", "secret.yaml") kube.Apply(yamlPath) - - By("Checking for secret to be created") kube.CheckSecretExists("login") }) } From 2b36f16940ef6fa6e6389b82121a8ea2264a74bd Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 22 Aug 2025 09:38:21 -0500 Subject: [PATCH 34/64] Introduce `op` package to handle op-cli commands --- pkg/testhelper/op/op.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 pkg/testhelper/op/op.go diff --git a/pkg/testhelper/op/op.go b/pkg/testhelper/op/op.go new file mode 100644 index 0000000..3f5f997 --- /dev/null +++ b/pkg/testhelper/op/op.go @@ -0,0 +1,24 @@ +package op + +import ( + "fmt" + "github.com/1Password/onepassword-operator/pkg/testhelper/system" +) + +// UpdateItemPassword updates the password of an item in 1Password +func UpdateItemPassword(item string) error { + _, err := system.Run("op", "item", "edit", item, "--generate-password=letters,digits,symbols,32") + if err != nil { + return err + } + return nil +} + +// ReadItemPassword reads the password of an item in 1Password +func ReadItemPassword(item, vault string) (string, error) { + output, err := system.Run("op", "read", fmt.Sprintf("op://%s/%s/password", vault, item)) + if err != nil { + return "", err + } + return output, nil +} From 4836140f66ed7e2315e09e94a729d0ba328746d4 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 22 Aug 2025 09:56:38 -0500 Subject: [PATCH 35/64] Add `CheckSecretPasswordWasUpdated` function to the `kube` package --- pkg/testhelper/kube/kube.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pkg/testhelper/kube/kube.go b/pkg/testhelper/kube/kube.go index 73339af..7a3c5ad 100644 --- a/pkg/testhelper/kube/kube.go +++ b/pkg/testhelper/kube/kube.go @@ -70,6 +70,19 @@ func CheckSecretExists(name string) { }, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed()) } +func ReadingSecretData(name, key string) (string, error) { + return system.Run("kubectl", "get", "secret", name, "-o", "jsonpath={.data."+key+"}") +} + +func CheckSecretPasswordWasUpdated(name, oldPassword string) { + By("Checking '" + name + "' secret password was updated") + Eventually(func(g Gomega) { + newPassword, err := ReadingSecretData(name, "password") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(newPassword).NotTo(Equal(oldPassword)) + }, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed()) +} + // Apply applies a kubernetes manifest file func Apply(yamlPath string) { _, err := system.Run("kubectl", "apply", "-f", yamlPath) From 57478247cfb40321ecd27054f8368bae27be743e Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 22 Aug 2025 09:57:20 -0500 Subject: [PATCH 36/64] Update secret to point to `operator-acceptance-tests` vault --- test/e2e/manifests/secret.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/manifests/secret.yaml b/test/e2e/manifests/secret.yaml index f6691b3..8e9baa8 100644 --- a/test/e2e/manifests/secret.yaml +++ b/test/e2e/manifests/secret.yaml @@ -3,4 +3,4 @@ kind: OnePasswordItem metadata: name: login spec: - itemPath: "vaults/acceptance-tests/items/test-secret" + itemPath: "vaults/operator-acceptance-tests/items/test-login" From c082f9562e8ba4501e0cc7b25be0080341a8859c Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 22 Aug 2025 10:14:49 -0500 Subject: [PATCH 37/64] Add tests case to check that kubernetes secret is updated after item is updated in 1Password --- test/e2e/e2e_test.go | 26 ++++++++++++++++++++++- test/e2e/manifests/secret-for-update.yaml | 6 ++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 test/e2e/manifests/secret-for-update.yaml diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 11b54b9..fc4685a 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -8,6 +8,7 @@ import ( "github.com/1Password/onepassword-operator/pkg/testhelper/kind" "github.com/1Password/onepassword-operator/pkg/testhelper/kube" + "github.com/1Password/onepassword-operator/pkg/testhelper/op" "github.com/1Password/onepassword-operator/pkg/testhelper/operator" "github.com/1Password/onepassword-operator/pkg/testhelper/system" ) @@ -56,7 +57,7 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() { // runCommonTestCases contains test cases that are common to both Connect and Service Account authentication methods. func runCommonTestCases() { It("Should create secret from manifest file", func() { - By("Creating secret") + By("Creating secret `login` from 1Password item") root, err := system.GetProjectRoot() Expect(err).NotTo(HaveOccurred()) @@ -64,4 +65,27 @@ func runCommonTestCases() { kube.Apply(yamlPath) kube.CheckSecretExists("login") }) + + It("Secret is updated after POOLING_INTERVAL", func() { + itemName := "secret-for-update" + secretName := itemName + + By("Creating secret `" + secretName + "` from 1Password item") + root, err := system.GetProjectRoot() + Expect(err).NotTo(HaveOccurred()) + + yamlPath := filepath.Join(root, "test", "e2e", "manifests", secretName+".yaml") + kube.Apply(yamlPath) + kube.CheckSecretExists(secretName) + + By("Reading old password") + oldPassword, err := kube.ReadingSecretData(secretName, "password") + Expect(err).NotTo(HaveOccurred()) + + By("Updating `" + secretName + "` 1Password item") + err = op.UpdateItemPassword(itemName) + Expect(err).NotTo(HaveOccurred()) + + kube.CheckSecretPasswordWasUpdated(secretName, oldPassword) + }) } diff --git a/test/e2e/manifests/secret-for-update.yaml b/test/e2e/manifests/secret-for-update.yaml new file mode 100644 index 0000000..75f66d3 --- /dev/null +++ b/test/e2e/manifests/secret-for-update.yaml @@ -0,0 +1,6 @@ +apiVersion: onepassword.com/v1 +kind: OnePasswordItem +metadata: + name: secret-for-update +spec: + itemPath: "vaults/operator-acceptance-tests/items/secret-for-update" From 71b29d5fe668d5757c7be0e09cb68a87b817df16 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 22 Aug 2025 10:25:04 -0500 Subject: [PATCH 38/64] Install op-cli into github action job --- .github/workflows/test-e2e.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 55c0313..f8925ba 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -32,6 +32,11 @@ jobs: - name: Install kubectl uses: azure/setup-kubectl@v4 + - name: Install 1Password CLI + uses: 1password/install-cli-action@v2 + with: + version: 2.32.0 + - name: Create '1password-credentials.json' file env: OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }} From 05ad484bd67b2a9289aae8f63253e4c668903fc4 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 22 Aug 2025 10:25:45 -0500 Subject: [PATCH 39/64] Fix lint error --- pkg/testhelper/op/op.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pkg/testhelper/op/op.go b/pkg/testhelper/op/op.go index 3f5f997..94a8b8f 100644 --- a/pkg/testhelper/op/op.go +++ b/pkg/testhelper/op/op.go @@ -1,7 +1,6 @@ package op import ( - "fmt" "github.com/1Password/onepassword-operator/pkg/testhelper/system" ) @@ -13,12 +12,3 @@ func UpdateItemPassword(item string) error { } return nil } - -// ReadItemPassword reads the password of an item in 1Password -func ReadItemPassword(item, vault string) (string, error) { - output, err := system.Run("op", "read", fmt.Sprintf("op://%s/%s/password", vault, item)) - if err != nil { - return "", err - } - return output, nil -} From 9aac824066cea65a41631857917afbe4a099603b Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 22 Aug 2025 11:22:39 -0500 Subject: [PATCH 40/64] Add test case for `ignore-secret` tag --- pkg/testhelper/kube/kube.go | 33 ++++++++++++++++++++++++++ pkg/testhelper/op/op.go | 11 +++++++++ test/e2e/e2e_test.go | 25 +++++++++++++++++++ test/e2e/manifests/secret-ignored.yaml | 6 +++++ 4 files changed, 75 insertions(+) create mode 100644 test/e2e/manifests/secret-ignored.yaml diff --git a/pkg/testhelper/kube/kube.go b/pkg/testhelper/kube/kube.go index 7a3c5ad..0321acf 100644 --- a/pkg/testhelper/kube/kube.go +++ b/pkg/testhelper/kube/kube.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "os" "path/filepath" + "strconv" "time" //nolint:staticcheck // ST1001 @@ -83,6 +84,26 @@ func CheckSecretPasswordWasUpdated(name, oldPassword string) { }, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed()) } +func CheckSecretPasswordNotUpdated(name, newPassword, oldPassword string) { + By("Ensuring '" + name + "' secret password has NOT been updated") + + intervalStr := readPullingInterval() + Expect(intervalStr).NotTo(BeEmpty()) + + i, err := strconv.Atoi(intervalStr) + Expect(err).NotTo(HaveOccurred()) + + interval := time.Duration(i) * time.Second // convert to duration in seconds + time.Sleep(interval + 2*time.Second) // wait for one polling interval + 2 seconds to make sure updated secret is pulled + + // read password again + currentPassword, err := ReadingSecretData(name, "password") + Expect(err).NotTo(HaveOccurred()) + + Expect(currentPassword).To(Equal(oldPassword)) + Expect(currentPassword).NotTo(Equal(newPassword)) +} + // Apply applies a kubernetes manifest file func Apply(yamlPath string) { _, err := system.Run("kubectl", "apply", "-f", yamlPath) @@ -142,3 +163,15 @@ func withOperatorRestart(operation func()) func() { }, 120*time.Second, 1*time.Second).Should(Succeed()) } } + +// readPullingInterval reads the POLLING_INTERVAL env variable from the operator deployment +// returns pulling interval in seconds as string +func readPullingInterval() string { + output, err := system.Run( + "kubectl", "get", "deployment", "onepassword-connect-operator", + "-o", "jsonpath={.spec.template.spec.containers[0].env[?(@.name==\"POLLING_INTERVAL\")].value}", + ) + Expect(err).NotTo(HaveOccurred()) + + return output +} diff --git a/pkg/testhelper/op/op.go b/pkg/testhelper/op/op.go index 94a8b8f..658cf21 100644 --- a/pkg/testhelper/op/op.go +++ b/pkg/testhelper/op/op.go @@ -1,6 +1,8 @@ package op import ( + "fmt" + "github.com/1Password/onepassword-operator/pkg/testhelper/system" ) @@ -12,3 +14,12 @@ func UpdateItemPassword(item string) error { } return nil } + +// ReadItemPassword reads the password of an item in 1Password +func ReadItemPassword(item, vault string) (string, error) { + output, err := system.Run("op", "read", fmt.Sprintf("op://%s/%s/password", vault, item)) + if err != nil { + return "", err + } + return output, nil +} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index fc4685a..5436bb3 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -15,6 +15,7 @@ import ( const ( operatorImageName = "1password/onepassword-operator:latest" + vaultName = "operator-acceptance-tests" ) var _ = Describe("Onepassword Operator e2e", Ordered, func() { @@ -88,4 +89,28 @@ func runCommonTestCases() { kube.CheckSecretPasswordWasUpdated(secretName, oldPassword) }) + + It("1Password item with `ignore-secret` doesn't pull updates to kubernetes secret", func() { + itemName := "secret-ignored" + secretName := itemName + + By("Creating secret `" + secretName + "` from 1Password item") + root, err := system.GetProjectRoot() + Expect(err).NotTo(HaveOccurred()) + + yamlPath := filepath.Join(root, "test", "e2e", "manifests", secretName+".yaml") + kube.Apply(yamlPath) + kube.CheckSecretExists(secretName) + + By("Reading old password") + oldPassword, err := kube.ReadingSecretData(secretName, "password") + Expect(err).NotTo(HaveOccurred()) + + By("Updating `" + secretName + "` 1Password item") + err = op.UpdateItemPassword(itemName) + Expect(err).NotTo(HaveOccurred()) + + newPassword, err := op.ReadItemPassword(itemName, vaultName) + kube.CheckSecretPasswordNotUpdated(secretName, newPassword, oldPassword) + }) } diff --git a/test/e2e/manifests/secret-ignored.yaml b/test/e2e/manifests/secret-ignored.yaml new file mode 100644 index 0000000..847d18f --- /dev/null +++ b/test/e2e/manifests/secret-ignored.yaml @@ -0,0 +1,6 @@ +apiVersion: onepassword.com/v1 +kind: OnePasswordItem +metadata: + name: secret-ignored +spec: + itemPath: "vaults/operator-acceptance-tests/items/secret-ignored" From cd03a651ad05aa4acf1d2c25d97327a6a5880e44 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Mon, 25 Aug 2025 22:52:17 -0500 Subject: [PATCH 41/64] Refactor kube package to use controller-runtime client instead of using kubectl CLI. This allows to runt the tests faster --- pkg/testhelper/kube/deployment.go | 45 +++++ pkg/testhelper/kube/kube.go | 269 +++++++++++++++++++----------- pkg/testhelper/kube/secret.go | 142 ++++++++++++++++ test/e2e/e2e_test.go | 126 +++++++++----- 4 files changed, 444 insertions(+), 138 deletions(-) create mode 100644 pkg/testhelper/kube/deployment.go create mode 100644 pkg/testhelper/kube/secret.go diff --git a/pkg/testhelper/kube/deployment.go b/pkg/testhelper/kube/deployment.go new file mode 100644 index 0000000..2ee1064 --- /dev/null +++ b/pkg/testhelper/kube/deployment.go @@ -0,0 +1,45 @@ +package kube + +import ( + "context" + "time" + + "sigs.k8s.io/controller-runtime/pkg/client" + //nolint:staticcheck // ST1001 + . "github.com/onsi/ginkgo/v2" + //nolint:staticcheck // ST1001 + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" +) + +type Deployment struct { + client client.Client + config *ClusterConfig + name string +} + +func (d *Deployment) ReadEnvVar(ctx context.Context, envVarName string) string { + By("Reading " + envVarName + " value from deployment/" + d.name) + + // Derive a short-lived context so this API call won't hang indefinitely. + c, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + deployment := &appsv1.Deployment{} + err := d.client.Get(c, client.ObjectKey{Name: d.name, Namespace: d.config.Namespace}, deployment) + Expect(err).ToNot(HaveOccurred()) + + // Search env across all containers + found := "" + for _, container := range deployment.Spec.Template.Spec.Containers { + for _, env := range container.Env { + if env.Name == envVarName && env.Value != "" { + found = env.Value + break + } + } + } + + Expect(found).NotTo(BeEmpty()) + return found +} diff --git a/pkg/testhelper/kube/kube.go b/pkg/testhelper/kube/kube.go index 0321acf..fab90b2 100644 --- a/pkg/testhelper/kube/kube.go +++ b/pkg/testhelper/kube/kube.go @@ -1,114 +1,174 @@ package kube import ( - "encoding/base64" + "context" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + + //"encoding/base64" + "fmt" + "k8s.io/apimachinery/pkg/api/errors" + //"k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/clientcmd" "os" "path/filepath" + "sigs.k8s.io/yaml" "strconv" + "strings" "time" + //metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + //"k8s.io/client-go/kubernetes" + //"k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" //nolint:staticcheck // ST1001 . "github.com/onsi/ginkgo/v2" //nolint:staticcheck // ST1001 . "github.com/onsi/gomega" - "github.com/1Password/onepassword-operator/pkg/testhelper/defaults" + //"github.com/1Password/onepassword-operator/pkg/testhelper/defaults" + apiv1 "github.com/1Password/onepassword-operator/api/v1" "github.com/1Password/onepassword-operator/pkg/testhelper/system" ) -// CreateSecretFromEnvVar creates a kubernetes secret from an environment variable -func CreateSecretFromEnvVar(envVar, secretName string) { - By("Creating '" + secretName + "' secret") +type ClusterConfig struct { + Namespace string + ManifestsDir string +} - value, _ := os.LookupEnv(envVar) - Expect(value).NotTo(BeEmpty()) +type Kube struct { + Config *ClusterConfig + Client client.Client +} - _, err := system.Run("kubectl", "create", "secret", "generic", secretName, "--from-literal=token="+value) +func NewKubeClient(clusterConfig *ClusterConfig) *Kube { + By("Creating a kubernetes client") + kubeconfig := os.Getenv("KUBECONFIG") + if kubeconfig == "" { + home, _ := os.UserHomeDir() + kubeconfig = filepath.Join(home, ".kube", "config") + } + restConfig, err := clientcmd.BuildConfigFromFlags("", kubeconfig) + Expect(err).NotTo(HaveOccurred()) + + scheme := runtime.NewScheme() + utilruntime.Must(corev1.AddToScheme(scheme)) + utilruntime.Must(appsv1.AddToScheme(scheme)) + utilruntime.Must(apiv1.AddToScheme(scheme)) + + kubernetesClient, err := client.New(restConfig, client.Options{Scheme: scheme}) + Expect(err).NotTo(HaveOccurred()) + + return &Kube{ + Config: clusterConfig, + Client: kubernetesClient, + } +} + +func (k *Kube) Secret(name string) *Secret { + return &Secret{ + client: k.Client, + config: k.Config, + name: name, + } +} + +func (k *Kube) Deployment(name string) *Deployment { + return &Deployment{ + client: k.Client, + config: k.Config, + name: name, + } +} + +// ApplyOnePasswordItem applies a OnePasswordItem manifest. +func (k *Kube) ApplyOnePasswordItem(ctx context.Context, fileName string) { + By("Applying " + fileName) + + // Derive a short-lived context so this API call won't hang indefinitely. + c, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + data, err := os.ReadFile(k.Config.ManifestsDir + "/" + fileName) + Expect(err).NotTo(HaveOccurred()) + + item := &apiv1.OnePasswordItem{} + err = yaml.Unmarshal(data, item) + Expect(err).NotTo(HaveOccurred()) + + if item.Namespace == "" { + item.Namespace = k.Config.Namespace + } + + err = k.Client.Get(c, client.ObjectKey{Name: item.Name, Namespace: k.Config.Namespace}, item) + if errors.IsNotFound(err) { + err = k.Client.Create(c, item) + } else { + err = k.Client.Update(c, item) + } Expect(err).NotTo(HaveOccurred()) } -// CreateSecretFromFile creates a kubernetes secret from a file -func CreateSecretFromFile(fileName, secretName string) { - By("Creating '" + secretName + "' secret from file") - _, err := system.Run("kubectl", "create", "secret", "generic", secretName, "--from-file="+fileName) - Expect(err).NotTo(HaveOccurred()) +func RestartDeployment(name string) (string, error) { + return system.Run("kubectl", "rollout", "status", name, "--timeout=120s") } -// CreateOpCredentialsSecret creates a kubernetes secret from 1password-credentials.json file -// encodes it in base64 and saves it to op-session file -func CreateOpCredentialsSecret() { - rootDir, err := system.GetProjectRoot() - Expect(err).NotTo(HaveOccurred()) - - credentialsFilePath := filepath.Join(rootDir, "1password-credentials.json") - data, err := os.ReadFile(credentialsFilePath) - Expect(err).NotTo(HaveOccurred()) - - encoded := base64.RawURLEncoding.EncodeToString(data) - - // create op-session file in project root - sessionFilePath := filepath.Join(rootDir, "op-session") - err = os.WriteFile(sessionFilePath, []byte(encoded), 0o600) - Expect(err).NotTo(HaveOccurred()) - - CreateSecretFromFile("op-session", "op-credentials") +func GetPodNameBySelector(selector string) (string, error) { + return system.Run("kubectl", "get", "pods", "-l", selector, "-o", "jsonpath={.items[0].metadata.name}") } -// DeleteSecret deletes a kubernetes secret -func DeleteSecret(name string) { - By("Deleting '" + name + "' secret") - _, err := system.Run("kubectl", "delete", "secret", name, "--ignore-not-found=true") - Expect(err).NotTo(HaveOccurred()) -} - -// CheckSecretExists checks if a kubernetes secret exists -func CheckSecretExists(name string) { - By("Checking '" + name + "' secret exists") - Eventually(func(g Gomega) { - output, err := system.Run("kubectl", "get", "secret", name, "-o", "jsonpath={.metadata.name}") - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(output).To(Equal(name)) - }, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed()) -} - -func ReadingSecretData(name, key string) (string, error) { - return system.Run("kubectl", "get", "secret", name, "-o", "jsonpath={.data."+key+"}") -} - -func CheckSecretPasswordWasUpdated(name, oldPassword string) { - By("Checking '" + name + "' secret password was updated") - Eventually(func(g Gomega) { - newPassword, err := ReadingSecretData(name, "password") - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(newPassword).NotTo(Equal(oldPassword)) - }, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed()) -} - -func CheckSecretPasswordNotUpdated(name, newPassword, oldPassword string) { - By("Ensuring '" + name + "' secret password has NOT been updated") - - intervalStr := readPullingInterval() - Expect(intervalStr).NotTo(BeEmpty()) - - i, err := strconv.Atoi(intervalStr) +func CountOperatorReplicaSets() int { + By("Counting operator replicasets") + countStr, err := system.Run( + "kubectl", "get", "rs", + "-l", "name=onepassword-connect-operator", + "-o", "jsonpath={.items[*].metadata.name}", + ) Expect(err).NotTo(HaveOccurred()) - interval := time.Duration(i) * time.Second // convert to duration in seconds - time.Sleep(interval + 2*time.Second) // wait for one polling interval + 2 seconds to make sure updated secret is pulled + fields := strings.Fields(countStr) + replicaSetCount := len(fields) - // read password again - currentPassword, err := ReadingSecretData(name, "password") - Expect(err).NotTo(HaveOccurred()) - - Expect(currentPassword).To(Equal(oldPassword)) - Expect(currentPassword).NotTo(Equal(newPassword)) + return replicaSetCount } -// Apply applies a kubernetes manifest file -func Apply(yamlPath string) { - _, err := system.Run("kubectl", "apply", "-f", yamlPath) - Expect(err).NotTo(HaveOccurred()) -} +// PatchOperatorToUseServiceAccount sets `OP_SERVICE_ACCOUNT_TOKEN` env variable +//func (s *Kube) PatchOperatorToUseServiceAccount(ctx context.Context) { +// By("Patching the operator deployment with service account token") +// +// // Derive a short-lived context so this API call won't hang indefinitely. +// c, cancel := context.WithTimeout(ctx, 10*time.Second) +// defer cancel() +// +// secret, err := s.ClientSet.CoreV1().Secrets(s.Namespace).Get(c, "onepassword-service-account-token", metav1.GetOptions{}) +// Expect(err).NotTo(HaveOccurred()) +// +// rawServiceAccountToken, ok := secret.Data["token"] +// Expect(ok).To(BeTrue()) +// +// serviceAccountToken, err := base64.StdEncoding.DecodeString(string(rawServiceAccountToken)) +// Expect(err).NotTo(HaveOccurred()) +// +// deployment, err := s.ClientSet.AppsV1(). +// Deployments(s.Namespace). +// Get(c, "onepassword-connect-operator", metav1.GetOptions{}) +// Expect(err).NotTo(HaveOccurred()) +// +// container := &deployment.Spec.Template.Spec.Containers[0] +// +// withOperatorRestart[struct{}](func(_ struct{}) { +// _, err = system.Run( +// "kubectl", "set", "env", "deployment/onepassword-connect-operator", +// "OP_SERVICE_ACCOUNT_TOKEN="+string(serviceAccountToken), +// "OP_CONNECT_HOST-", // remove +// "OP_CONNECT_TOKEN-", // remove +// "MANAGE_CONNECT=false", // ensure operator doesn't try to manage Connect +// ) +// Expect(err).NotTo(HaveOccurred()) +// }) +//} // SetContextNamespace sets the current kubernetes context namespace func SetContextNamespace(namespace string) { @@ -117,40 +177,59 @@ func SetContextNamespace(namespace string) { Expect(err).NotTo(HaveOccurred()) } -// PatchOperatorToUseServiceAccount sets `OP_SERVICE_ACCOUNT_TOKEN` env variable -var PatchOperatorToUseServiceAccount = withOperatorRestart(func() { - By("patching the operator deployment with service account token") +// PatchOperatorToAutoRestart sets `OP_SERVICE_ACCOUNT_TOKEN` env variable +var PatchOperatorToAutoRestart = withOperatorRestart[bool](func(value bool) { + By("patching the operator to enable AUTO_RESTART") + _, err := system.Run( + "kubectl", "set", "env", "deployment/onepassword-connect-operator", + "AUTO_RESTART="+strconv.FormatBool(value), + ) + Expect(err).NotTo(HaveOccurred()) +}) + +// PatchOperatorWithCustomSecret sets new env variable CUSTOM_SECRET +var PatchOperatorWithCustomSecret = withOperatorRestart[map[string]string](func(secret map[string]string) { + By("patching the operator with custom secret and AUTO_RESTART=true") _, err := system.Run( "kubectl", "patch", "deployment", "onepassword-connect-operator", "--type=json", - `-p=[{"op":"replace","path":"/spec/template/spec/containers/0/env","value":[ + fmt.Sprintf(`-p=[{"op":"replace","path":"/spec/template/spec/containers/0/env","value":[ {"name":"OPERATOR_NAME","value":"onepassword-connect-operator"}, {"name":"POD_NAME","valueFrom":{"fieldRef":{"fieldPath":"metadata.name"}}}, {"name":"WATCH_NAMESPACE","value":"default"}, {"name":"POLLING_INTERVAL","value":"10"}, - {"name":"AUTO_RESTART","value":"false"}, + {"name":"MANAGE_CONNECT","value":"true"}, + {"name":"AUTO_RESTART","value":"true"}, + {"name":"OP_CONNECT_HOST","value":"http://onepassword-connect:8080"}, { - "name":"OP_SERVICE_ACCOUNT_TOKEN", + "name":"OP_CONNECT_TOKEN", "valueFrom":{ "secretKeyRef":{ - "name":"onepassword-service-account-token", + "name":"onepassword-token", "key":"token", }, }, }, - {"name":"MANAGE_CONNECT","value":"false"} - ]}]`, + { + "name":"CUSTOM_SECRET", + "valueFrom":{ + "secretKeyRef":{ + "name":"%s", + "key":"%s", + }, + }, + } + ]}]`, secret["name"], secret["key"]), ) Expect(err).NotTo(HaveOccurred()) }) // withOperatorRestart is a helper function that restarts the operator deployment -func withOperatorRestart(operation func()) func() { - return func() { - operation() +func withOperatorRestart[T any](operation func(arg T)) func(arg T) { + return func(arg T) { + operation(arg) - _, err := system.Run("kubectl", "rollout", "status", - "deployment/onepassword-connect-operator", "-n", "default", "--timeout=120s") + _, err := RestartDeployment("deployment/onepassword-connect-operator") Expect(err).NotTo(HaveOccurred()) By("Waiting for the operator pod to be 'Running'") diff --git a/pkg/testhelper/kube/secret.go b/pkg/testhelper/kube/secret.go new file mode 100644 index 0000000..4d4100a --- /dev/null +++ b/pkg/testhelper/kube/secret.go @@ -0,0 +1,142 @@ +package kube + +import ( + "context" + "encoding/base64" + "os" + "path/filepath" + "time" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + //nolint:staticcheck // ST1001 + . "github.com/onsi/ginkgo/v2" + //nolint:staticcheck // ST1001 + . "github.com/onsi/gomega" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/1Password/onepassword-operator/pkg/testhelper/defaults" + "github.com/1Password/onepassword-operator/pkg/testhelper/system" +) + +type Secret struct { + client client.Client + config *ClusterConfig + name string +} + +// CreateFromEnvVar creates a kubernetes secret from an environment variable +func (s *Secret) CreateFromEnvVar(ctx context.Context, envVar string) *corev1.Secret { + By("Creating '" + s.name + "' secret from environment variable") + + // Derive a short-lived context so this API call won't hang indefinitely. + c, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + value, ok := os.LookupEnv(envVar) + Expect(ok).To(BeTrue()) + Expect(value).NotTo(BeEmpty()) + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: s.name, + Namespace: s.config.Namespace, + }, + StringData: map[string]string{ + "token": value, + }, + } + + err := s.client.Create(c, secret) + Expect(err).NotTo(HaveOccurred()) + + return secret +} + +// CreateFromFile creates a kubernetes secret from a file +func (s *Secret) CreateFromFile(ctx context.Context, fileName string, content []byte) *corev1.Secret { + By("Creating '" + s.name + "' secret from file " + fileName) + + // Derive a short-lived context so this API call won't hang indefinitely. + c, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: s.name, + Namespace: s.config.Namespace, + }, + Data: map[string][]byte{ + filepath.Base(fileName): content, + }, + } + + err := s.client.Create(c, secret) + Expect(err).NotTo(HaveOccurred()) + + return secret +} + +// CreateOpCredentials creates a kubernetes secret from 1password-credentials.json file in the project root +// encodes it in base64 and saves it to op-session file +func (s *Secret) CreateOpCredentials(ctx context.Context) *corev1.Secret { + rootDir, err := system.GetProjectRoot() + Expect(err).NotTo(HaveOccurred()) + + credentialsFilePath := filepath.Join(rootDir, "1password-credentials.json") + data, err := os.ReadFile(credentialsFilePath) + Expect(err).NotTo(HaveOccurred()) + + encoded := base64.RawURLEncoding.EncodeToString(data) + + return s.CreateFromFile(ctx, "op-session", []byte(encoded)) +} + +// Get retrieves a kubernetes secret +func (s *Secret) Get(ctx context.Context) *corev1.Secret { + By("Getting '" + s.name + "' secret") + + // Derive a short-lived context so this API call won't hang indefinitely. + c, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + secret := &corev1.Secret{} + err := s.client.Get(c, client.ObjectKey{Name: s.name, Namespace: s.config.Namespace}, secret) + Expect(err).NotTo(HaveOccurred()) + + return secret +} + +// Delete deletes a kubernetes secret +func (s *Secret) Delete(ctx context.Context) { + By("Deleting '" + s.name + "' secret") + + // Derive a short-lived context so this API call won't hang indefinitely. + c, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: s.name, + Namespace: s.config.Namespace, + }, + } + err := s.client.Delete(c, secret) + Expect(err).NotTo(HaveOccurred()) +} + +// CheckIfExists repeatedly attempts to retrieve the given Secret +// from the cluster until it is found or the test's timeout expires. +func (s *Secret) CheckIfExists(ctx context.Context) { + By("Checking '" + s.name + "' secret") + + Eventually(func(g Gomega) { + // Derive a short-lived context so this API call won't hang indefinitely. + attemptCtx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + secret := &corev1.Secret{} + err := s.client.Get(attemptCtx, client.ObjectKey{Name: s.name, Namespace: s.config.Namespace}, secret) + g.Expect(err).NotTo(HaveOccurred()) + }, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed()) +} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 5436bb3..d33f65c 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -1,16 +1,21 @@ package e2e import ( + "context" "path/filepath" + "strconv" + "time" + //nolint:staticcheck // ST1001 . "github.com/onsi/ginkgo/v2" + //nolint:staticcheck // ST1001 . "github.com/onsi/gomega" + "github.com/1Password/onepassword-operator/pkg/testhelper/defaults" "github.com/1Password/onepassword-operator/pkg/testhelper/kind" "github.com/1Password/onepassword-operator/pkg/testhelper/kube" "github.com/1Password/onepassword-operator/pkg/testhelper/op" "github.com/1Password/onepassword-operator/pkg/testhelper/operator" - "github.com/1Password/onepassword-operator/pkg/testhelper/system" ) const ( @@ -18,20 +23,29 @@ const ( vaultName = "operator-acceptance-tests" ) +var kubeClient *kube.Kube + var _ = Describe("Onepassword Operator e2e", Ordered, func() { + ctx := context.Background() + BeforeAll(func() { + kubeClient = kube.NewKubeClient(&kube.ClusterConfig{ + Namespace: "default", + ManifestsDir: filepath.Join("manifests"), + }) kube.SetContextNamespace("default") + operator.BuildOperatorImage() kind.LoadImageToKind(operatorImageName) - kube.CreateOpCredentialsSecret() - kube.CheckSecretExists("op-credentials") + kubeClient.Secret("op-credentials").CreateOpCredentials(ctx) + kubeClient.Secret("op-credentials").CheckIfExists(ctx) - kube.CreateSecretFromEnvVar("OP_CONNECT_TOKEN", "onepassword-token") - kube.CheckSecretExists("onepassword-token") + kubeClient.Secret("onepassword-token").CreateFromEnvVar(ctx, "OP_CONNECT_TOKEN") + kubeClient.Secret("onepassword-token").CheckIfExists(ctx) - kube.CreateSecretFromEnvVar("OP_SERVICE_ACCOUNT_TOKEN", "onepassword-service-account-token") - kube.CheckSecretExists("onepassword-service-account-token") + kubeClient.Secret("onepassword-service-account-token").CreateFromEnvVar(ctx, "OP_SERVICE_ACCOUNT_TOKEN") + kubeClient.Secret("onepassword-service-account-token").CheckIfExists(ctx) operator.DeployOperator() operator.WaitingForOperatorPod() @@ -42,29 +56,25 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() { operator.WaitingForConnectPod() }) - runCommonTestCases() + runCommonTestCases(ctx) }) - Context("Use the operator with Service Account", func() { - BeforeAll(func() { - kube.PatchOperatorToUseServiceAccount() - kube.DeleteSecret("login") // remove secret crated in previous test - }) - - runCommonTestCases() - }) + //Context("Use the operator with Service Account", func() { + // BeforeAll(func() { + // kube.PatchOperatorToUseServiceAccount(struct{}{}) + // kubeClient.DeleteSecret(ctx, "login") // remove secret crated in previous test + // }) + // + // runCommonTestCases(ctx) + //}) }) // runCommonTestCases contains test cases that are common to both Connect and Service Account authentication methods. -func runCommonTestCases() { +func runCommonTestCases(ctx context.Context) { It("Should create secret from manifest file", func() { By("Creating secret `login` from 1Password item") - root, err := system.GetProjectRoot() - Expect(err).NotTo(HaveOccurred()) - - yamlPath := filepath.Join(root, "test", "e2e", "manifests", "secret.yaml") - kube.Apply(yamlPath) - kube.CheckSecretExists("login") + kubeClient.ApplyOnePasswordItem(ctx, "secret.yaml") + kubeClient.Secret("login").CheckIfExists(ctx) }) It("Secret is updated after POOLING_INTERVAL", func() { @@ -72,22 +82,31 @@ func runCommonTestCases() { secretName := itemName By("Creating secret `" + secretName + "` from 1Password item") - root, err := system.GetProjectRoot() - Expect(err).NotTo(HaveOccurred()) - - yamlPath := filepath.Join(root, "test", "e2e", "manifests", secretName+".yaml") - kube.Apply(yamlPath) - kube.CheckSecretExists(secretName) + kubeClient.ApplyOnePasswordItem(ctx, secretName+".yaml") + kubeClient.Secret(secretName).CheckIfExists(ctx) By("Reading old password") - oldPassword, err := kube.ReadingSecretData(secretName, "password") - Expect(err).NotTo(HaveOccurred()) + secret := kubeClient.Secret(secretName).Get(ctx) + oldPassword, ok := secret.Data["password"] + Expect(ok).To(BeTrue()) By("Updating `" + secretName + "` 1Password item") - err = op.UpdateItemPassword(itemName) + err := op.UpdateItemPassword(itemName) Expect(err).NotTo(HaveOccurred()) - kube.CheckSecretPasswordWasUpdated(secretName, oldPassword) + // checking that password was updated + Eventually(func(g Gomega) { + // Derive a short-lived context so this API call won't hang indefinitely. + attemptCtx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + secret = kubeClient.Secret(secretName).Get(attemptCtx) + g.Expect(err).NotTo(HaveOccurred()) + + newPassword, ok := secret.Data["password"] + g.Expect(ok).To(BeTrue()) + g.Expect(newPassword).NotTo(Equal(oldPassword)) + }, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed()) }) It("1Password item with `ignore-secret` doesn't pull updates to kubernetes secret", func() { @@ -95,22 +114,43 @@ func runCommonTestCases() { secretName := itemName By("Creating secret `" + secretName + "` from 1Password item") - root, err := system.GetProjectRoot() - Expect(err).NotTo(HaveOccurred()) - - yamlPath := filepath.Join(root, "test", "e2e", "manifests", secretName+".yaml") - kube.Apply(yamlPath) - kube.CheckSecretExists(secretName) + kubeClient.ApplyOnePasswordItem(ctx, secretName+".yaml") + kubeClient.Secret(secretName).CheckIfExists(ctx) By("Reading old password") - oldPassword, err := kube.ReadingSecretData(secretName, "password") - Expect(err).NotTo(HaveOccurred()) + secret := kubeClient.Secret(secretName).Get(ctx) + oldPassword, ok := secret.Data["password"] + Expect(ok).To(BeTrue()) By("Updating `" + secretName + "` 1Password item") - err = op.UpdateItemPassword(itemName) + err := op.UpdateItemPassword(itemName) Expect(err).NotTo(HaveOccurred()) newPassword, err := op.ReadItemPassword(itemName, vaultName) - kube.CheckSecretPasswordNotUpdated(secretName, newPassword, oldPassword) + Expect(newPassword).NotTo(Equal(oldPassword)) + + // checking that password was NOT updated + Eventually(func(g Gomega) { + // Derive a short-lived context so this API call won't hang indefinitely. + attemptCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + intervalStr := kubeClient.Deployment("onepassword-connect-operator").ReadEnvVar(attemptCtx, "POLLING_INTERVAL") + Expect(intervalStr).NotTo(BeEmpty()) + + i, err := strconv.Atoi(intervalStr) + Expect(err).NotTo(HaveOccurred()) + + interval := time.Duration(i) * time.Second // convert to duration in seconds + time.Sleep(interval + 2*time.Second) // wait for one polling interval + 2 seconds to make sure updated secret is pulled + + secret = kubeClient.Secret(secretName).Get(attemptCtx) + g.Expect(err).NotTo(HaveOccurred()) + + currentPassword, ok := secret.Data["password"] + Expect(ok).To(BeTrue()) + Expect(currentPassword).To(Equal(oldPassword)) + Expect(currentPassword).NotTo(Equal(newPassword)) + }, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed()) }) } From b40f27b052271bb7a4d214f7589a09814ee78a19 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Tue, 26 Aug 2025 15:11:04 -0500 Subject: [PATCH 42/64] Refactor kube package to use `controller-runtime` golang client to interact with cluster --- pkg/testhelper/kube/deployment.go | 95 +++++++++++++- pkg/testhelper/kube/kube.go | 193 +++++----------------------- pkg/testhelper/kube/pod.go | 49 +++++++ pkg/testhelper/kube/secret.go | 4 +- pkg/testhelper/operator/operator.go | 26 ---- test/e2e/e2e_test.go | 92 ++++++++++--- 6 files changed, 249 insertions(+), 210 deletions(-) create mode 100644 pkg/testhelper/kube/pod.go diff --git a/pkg/testhelper/kube/deployment.go b/pkg/testhelper/kube/deployment.go index 2ee1064..fc8f94b 100644 --- a/pkg/testhelper/kube/deployment.go +++ b/pkg/testhelper/kube/deployment.go @@ -2,14 +2,18 @@ package kube import ( "context" + "fmt" "time" - "sigs.k8s.io/controller-runtime/pkg/client" //nolint:staticcheck // ST1001 . "github.com/onsi/ginkgo/v2" //nolint:staticcheck // ST1001 . "github.com/onsi/gomega" appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/1Password/onepassword-operator/pkg/testhelper/defaults" ) type Deployment struct { @@ -18,9 +22,7 @@ type Deployment struct { name string } -func (d *Deployment) ReadEnvVar(ctx context.Context, envVarName string) string { - By("Reading " + envVarName + " value from deployment/" + d.name) - +func (d *Deployment) Get(ctx context.Context) *appsv1.Deployment { // Derive a short-lived context so this API call won't hang indefinitely. c, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() @@ -29,6 +31,13 @@ func (d *Deployment) ReadEnvVar(ctx context.Context, envVarName string) string { err := d.client.Get(c, client.ObjectKey{Name: d.name, Namespace: d.config.Namespace}, deployment) Expect(err).ToNot(HaveOccurred()) + return deployment +} + +func (d *Deployment) ReadEnvVar(ctx context.Context, envVarName string) string { + By("Reading " + envVarName + " value from deployment/" + d.name) + deployment := d.Get(ctx) + // Search env across all containers found := "" for _, container := range deployment.Spec.Template.Spec.Containers { @@ -43,3 +52,81 @@ func (d *Deployment) ReadEnvVar(ctx context.Context, envVarName string) string { Expect(found).NotTo(BeEmpty()) return found } + +func (d *Deployment) PatchEnvVars(ctx context.Context, upsert []corev1.EnvVar, remove []string) { + By("Patching env variables for deployment/" + d.name) + deployment := d.Get(ctx) + deploymentCopy := deployment.DeepCopy() + container := &deployment.Spec.Template.Spec.Containers[0] + + // Build removal set for quick lookup + toRemove := make(map[string]struct{}, len(remove)) + for _, n := range remove { + toRemove[n] = struct{}{} + } + + // Build upsert map for quick lookup + upserts := make(map[string]corev1.EnvVar, len(upsert)) + for _, e := range upsert { + upserts[e.Name] = e + } + + // Filter existing envs: keep if not in remove and not being upserted + filtered := make([]corev1.EnvVar, 0, len(container.Env)) + for _, e := range container.Env { + if _, ok := toRemove[e.Name]; ok { + continue + } + if newE, ok := upserts[e.Name]; ok { + filtered = append(filtered, newE) // replace existing + delete(upserts, e.Name) // delete from map to not use once again + } else { + filtered = append(filtered, e) + } + } + + // Append any new envs that weren’t already in the container + for _, e := range upserts { + filtered = append(filtered, e) + } + + container.Env = filtered + + // Derive a short-lived context so this API call won't hang indefinitely. + c, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + err := d.client.Patch(c, deployment, client.MergeFrom(deploymentCopy)) + Expect(err).ToNot(HaveOccurred()) + + // wait for new deployment to roll out + d.WaitDeploymentRolledOut(ctx) +} + +// WaitDeploymentRolledOut waits for deployment to finish a rollout. +func (d *Deployment) WaitDeploymentRolledOut(ctx context.Context) { + By("Waiting for deployment/" + d.name + " to roll out") + + deployment := d.Get(ctx) + targetGen := deployment.Generation + + Eventually(func(g Gomega) error { + newDeployment := d.Get(ctx) + // Has controller observed the new spec? + if newDeployment.Status.ObservedGeneration < targetGen { + return fmt.Errorf("observedGeneration %d < desired %d", newDeployment.Status.ObservedGeneration, targetGen) + } + g.Expect(newDeployment.Status.ObservedGeneration).To(BeNumerically(">=", targetGen)) + + desired := int32(1) + if newDeployment.Spec.Replicas != nil { + desired = *newDeployment.Spec.Replicas + } + + g.Expect(newDeployment.Status.UpdatedReplicas).To(Equal(desired)) + g.Expect(newDeployment.Status.AvailableReplicas).To(Equal(desired)) + g.Expect(newDeployment.Status.Replicas).To(Equal(desired)) + + return nil + }, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed()) +} diff --git a/pkg/testhelper/kube/kube.go b/pkg/testhelper/kube/kube.go index fab90b2..4d5f9f5 100644 --- a/pkg/testhelper/kube/kube.go +++ b/pkg/testhelper/kube/kube.go @@ -2,35 +2,25 @@ package kube import ( "context" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - - //"encoding/base64" "fmt" - "k8s.io/apimachinery/pkg/api/errors" - //"k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/tools/clientcmd" "os" "path/filepath" - "sigs.k8s.io/yaml" - "strconv" - "strings" "time" - //metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - //"k8s.io/client-go/kubernetes" - //"k8s.io/client-go/rest" - "sigs.k8s.io/controller-runtime/pkg/client" //nolint:staticcheck // ST1001 . "github.com/onsi/ginkgo/v2" //nolint:staticcheck // ST1001 . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" - //"github.com/1Password/onepassword-operator/pkg/testhelper/defaults" apiv1 "github.com/1Password/onepassword-operator/api/v1" - "github.com/1Password/onepassword-operator/pkg/testhelper/system" ) type ClusterConfig struct { @@ -61,6 +51,21 @@ func NewKubeClient(clusterConfig *ClusterConfig) *Kube { kubernetesClient, err := client.New(restConfig, client.Options{Scheme: scheme}) Expect(err).NotTo(HaveOccurred()) + // update the current context’s namespace in kubeconfig + pathOpts := clientcmd.NewDefaultPathOptions() + cfg, err := pathOpts.GetStartingConfig() + Expect(err).NotTo(HaveOccurred()) + + currentContext := cfg.CurrentContext + Expect(currentContext).NotTo(BeEmpty(), "no current kube context is set in kubeconfig") + + ctx, ok := cfg.Contexts[currentContext] + Expect(ok).To(BeTrue(), fmt.Sprintf("current context %q not found in kubeconfig", currentContext)) + + ctx.Namespace = clusterConfig.Namespace + err = clientcmd.ModifyConfig(pathOpts, *cfg, true) + Expect(err).NotTo(HaveOccurred()) + return &Kube{ Config: clusterConfig, Client: kubernetesClient, @@ -83,6 +88,14 @@ func (k *Kube) Deployment(name string) *Deployment { } } +func (k *Kube) Pod(selector map[string]string) *Pod { + return &Pod{ + client: k.Client, + config: k.Config, + selector: selector, + } +} + // ApplyOnePasswordItem applies a OnePasswordItem manifest. func (k *Kube) ApplyOnePasswordItem(ctx context.Context, fileName string) { By("Applying " + fileName) @@ -110,147 +123,3 @@ func (k *Kube) ApplyOnePasswordItem(ctx context.Context, fileName string) { } Expect(err).NotTo(HaveOccurred()) } - -func RestartDeployment(name string) (string, error) { - return system.Run("kubectl", "rollout", "status", name, "--timeout=120s") -} - -func GetPodNameBySelector(selector string) (string, error) { - return system.Run("kubectl", "get", "pods", "-l", selector, "-o", "jsonpath={.items[0].metadata.name}") -} - -func CountOperatorReplicaSets() int { - By("Counting operator replicasets") - countStr, err := system.Run( - "kubectl", "get", "rs", - "-l", "name=onepassword-connect-operator", - "-o", "jsonpath={.items[*].metadata.name}", - ) - Expect(err).NotTo(HaveOccurred()) - - fields := strings.Fields(countStr) - replicaSetCount := len(fields) - - return replicaSetCount -} - -// PatchOperatorToUseServiceAccount sets `OP_SERVICE_ACCOUNT_TOKEN` env variable -//func (s *Kube) PatchOperatorToUseServiceAccount(ctx context.Context) { -// By("Patching the operator deployment with service account token") -// -// // Derive a short-lived context so this API call won't hang indefinitely. -// c, cancel := context.WithTimeout(ctx, 10*time.Second) -// defer cancel() -// -// secret, err := s.ClientSet.CoreV1().Secrets(s.Namespace).Get(c, "onepassword-service-account-token", metav1.GetOptions{}) -// Expect(err).NotTo(HaveOccurred()) -// -// rawServiceAccountToken, ok := secret.Data["token"] -// Expect(ok).To(BeTrue()) -// -// serviceAccountToken, err := base64.StdEncoding.DecodeString(string(rawServiceAccountToken)) -// Expect(err).NotTo(HaveOccurred()) -// -// deployment, err := s.ClientSet.AppsV1(). -// Deployments(s.Namespace). -// Get(c, "onepassword-connect-operator", metav1.GetOptions{}) -// Expect(err).NotTo(HaveOccurred()) -// -// container := &deployment.Spec.Template.Spec.Containers[0] -// -// withOperatorRestart[struct{}](func(_ struct{}) { -// _, err = system.Run( -// "kubectl", "set", "env", "deployment/onepassword-connect-operator", -// "OP_SERVICE_ACCOUNT_TOKEN="+string(serviceAccountToken), -// "OP_CONNECT_HOST-", // remove -// "OP_CONNECT_TOKEN-", // remove -// "MANAGE_CONNECT=false", // ensure operator doesn't try to manage Connect -// ) -// Expect(err).NotTo(HaveOccurred()) -// }) -//} - -// SetContextNamespace sets the current kubernetes context namespace -func SetContextNamespace(namespace string) { - By("Set namespace to " + namespace) - _, err := system.Run("kubectl", "config", "set-context", "--current", "--namespace="+namespace) - Expect(err).NotTo(HaveOccurred()) -} - -// PatchOperatorToAutoRestart sets `OP_SERVICE_ACCOUNT_TOKEN` env variable -var PatchOperatorToAutoRestart = withOperatorRestart[bool](func(value bool) { - By("patching the operator to enable AUTO_RESTART") - _, err := system.Run( - "kubectl", "set", "env", "deployment/onepassword-connect-operator", - "AUTO_RESTART="+strconv.FormatBool(value), - ) - Expect(err).NotTo(HaveOccurred()) -}) - -// PatchOperatorWithCustomSecret sets new env variable CUSTOM_SECRET -var PatchOperatorWithCustomSecret = withOperatorRestart[map[string]string](func(secret map[string]string) { - By("patching the operator with custom secret and AUTO_RESTART=true") - _, err := system.Run( - "kubectl", "patch", "deployment", "onepassword-connect-operator", - "--type=json", - fmt.Sprintf(`-p=[{"op":"replace","path":"/spec/template/spec/containers/0/env","value":[ - {"name":"OPERATOR_NAME","value":"onepassword-connect-operator"}, - {"name":"POD_NAME","valueFrom":{"fieldRef":{"fieldPath":"metadata.name"}}}, - {"name":"WATCH_NAMESPACE","value":"default"}, - {"name":"POLLING_INTERVAL","value":"10"}, - {"name":"MANAGE_CONNECT","value":"true"}, - {"name":"AUTO_RESTART","value":"true"}, - {"name":"OP_CONNECT_HOST","value":"http://onepassword-connect:8080"}, - { - "name":"OP_CONNECT_TOKEN", - "valueFrom":{ - "secretKeyRef":{ - "name":"onepassword-token", - "key":"token", - }, - }, - }, - { - "name":"CUSTOM_SECRET", - "valueFrom":{ - "secretKeyRef":{ - "name":"%s", - "key":"%s", - }, - }, - } - ]}]`, secret["name"], secret["key"]), - ) - Expect(err).NotTo(HaveOccurred()) -}) - -// withOperatorRestart is a helper function that restarts the operator deployment -func withOperatorRestart[T any](operation func(arg T)) func(arg T) { - return func(arg T) { - operation(arg) - - _, err := RestartDeployment("deployment/onepassword-connect-operator") - Expect(err).NotTo(HaveOccurred()) - - By("Waiting for the operator pod to be 'Running'") - Eventually(func(g Gomega) { - output, err := system.Run("kubectl", "get", "pods", - "-l", "name=onepassword-connect-operator", - "-o", "jsonpath={.items[0].status.phase}") - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(output).To(ContainSubstring("Running")) - }, 120*time.Second, 1*time.Second).Should(Succeed()) - } -} - -// readPullingInterval reads the POLLING_INTERVAL env variable from the operator deployment -// returns pulling interval in seconds as string -func readPullingInterval() string { - output, err := system.Run( - "kubectl", "get", "deployment", "onepassword-connect-operator", - "-o", "jsonpath={.spec.template.spec.containers[0].env[?(@.name==\"POLLING_INTERVAL\")].value}", - ) - Expect(err).NotTo(HaveOccurred()) - - return output -} diff --git a/pkg/testhelper/kube/pod.go b/pkg/testhelper/kube/pod.go new file mode 100644 index 0000000..8b7f2d3 --- /dev/null +++ b/pkg/testhelper/kube/pod.go @@ -0,0 +1,49 @@ +package kube + +import ( + "context" + "time" + + //nolint:staticcheck // ST1001 + . "github.com/onsi/ginkgo/v2" + //nolint:staticcheck // ST1001 + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/labels" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/1Password/onepassword-operator/pkg/testhelper/defaults" +) + +type Pod struct { + client client.Client + config *ClusterConfig + selector map[string]string +} + +func (p *Pod) WaitingForRunningPod(ctx context.Context) { + By("Waiting for the pod " + labels.Set(p.selector).String() + " to be 'Running'") + + Eventually(func(g Gomega) { + // short per-attempt timeout to avoid hanging calls while Eventually polls + attemptCtx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + var pods corev1.PodList + listOpts := []client.ListOption{ + client.InNamespace(p.config.Namespace), + client.MatchingLabels(p.selector), + } + g.Expect(p.client.List(attemptCtx, &pods, listOpts...)).To(Succeed()) + g.Expect(pods.Items).NotTo(BeEmpty(), "no pods found with selector %q", labels.Set(p.selector).String()) + + foundRunning := false + for _, p := range pods.Items { + if p.Status.Phase == corev1.PodRunning { + foundRunning = true + break + } + } + g.Expect(foundRunning).To(BeTrue(), "pod not Running yet") + }, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed()) +} diff --git a/pkg/testhelper/kube/secret.go b/pkg/testhelper/kube/secret.go index 4d4100a..5770611 100644 --- a/pkg/testhelper/kube/secret.go +++ b/pkg/testhelper/kube/secret.go @@ -7,12 +7,12 @@ import ( "path/filepath" "time" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" //nolint:staticcheck // ST1001 . "github.com/onsi/ginkgo/v2" //nolint:staticcheck // ST1001 . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/1Password/onepassword-operator/pkg/testhelper/defaults" diff --git a/pkg/testhelper/operator/operator.go b/pkg/testhelper/operator/operator.go index f3ff4d5..a965e7b 100644 --- a/pkg/testhelper/operator/operator.go +++ b/pkg/testhelper/operator/operator.go @@ -1,8 +1,6 @@ package operator import ( - "time" - //nolint:staticcheck // ST1001 . "github.com/onsi/ginkgo/v2" //nolint:staticcheck // ST1001 @@ -27,27 +25,3 @@ func DeployOperator() { _, err := system.Run("make", "deploy") Expect(err).NotTo(HaveOccurred()) } - -// WaitingForOperatorPod waits for the Operator pod to be in 'Running' state -func WaitingForOperatorPod() { - By("Waiting for the Operator pod to be 'Running'") - Eventually(func(g Gomega) { - output, err := system.Run("kubectl", "get", "pods", - "-l", "name=onepassword-connect-operator", - "-o", "jsonpath={.items[0].status.phase}") - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(output).To(ContainSubstring("Running")) - }, 30*time.Second, 1*time.Second).Should(Succeed()) -} - -// WaitingForConnectPod waits for the Connect pod to be in 'Running' state -func WaitingForConnectPod() { - By("Waiting for the Connect pod to be 'Running'") - Eventually(func(g Gomega) { - output, err := system.Run("kubectl", "get", "pods", - "-l", "app=onepassword-connect", - "-o", "jsonpath={.items[0].status.phase}") - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(output).To(ContainSubstring("Running")) - }, 30*time.Second, 1*time.Second).Should(Succeed()) -} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index d33f65c..7712970 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -10,6 +10,7 @@ import ( . "github.com/onsi/ginkgo/v2" //nolint:staticcheck // ST1001 . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" "github.com/1Password/onepassword-operator/pkg/testhelper/defaults" "github.com/1Password/onepassword-operator/pkg/testhelper/kind" @@ -33,7 +34,6 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() { Namespace: "default", ManifestsDir: filepath.Join("manifests"), }) - kube.SetContextNamespace("default") operator.BuildOperatorImage() kind.LoadImageToKind(operatorImageName) @@ -48,25 +48,41 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() { kubeClient.Secret("onepassword-service-account-token").CheckIfExists(ctx) operator.DeployOperator() - operator.WaitingForOperatorPod() + kubeClient.Pod(map[string]string{"name": "onepassword-connect-operator"}).WaitingForRunningPod(ctx) }) Context("Use the operator with Connect", func() { BeforeAll(func() { - operator.WaitingForConnectPod() + kubeClient.Pod(map[string]string{"app": "onepassword-connect"}).WaitingForRunningPod(ctx) }) runCommonTestCases(ctx) }) - //Context("Use the operator with Service Account", func() { - // BeforeAll(func() { - // kube.PatchOperatorToUseServiceAccount(struct{}{}) - // kubeClient.DeleteSecret(ctx, "login") // remove secret crated in previous test - // }) - // - // runCommonTestCases(ctx) - //}) + Context("Use the operator with Service Account", func() { + BeforeAll(func() { + kubeClient.Deployment("onepassword-connect-operator").PatchEnvVars(ctx, []corev1.EnvVar{ + {Name: "MANAGE_CONNECT", Value: "false"}, + { + Name: "OP_SERVICE_ACCOUNT_TOKEN", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "onepassword-service-account-token", + }, + Key: "token", + }, + }, + }, + }, []string{"OP_CONNECT_HOST", "OP_CONNECT_TOKEN"}) + + kubeClient.Secret("login").Delete(ctx) // remove secret crated in previous test + kubeClient.Secret("secret-ignored").Delete(ctx) // remove secret crated in previous test + kubeClient.Secret("secret-for-update").Delete(ctx) // remove secret crated in previous test + }) + + runCommonTestCases(ctx) + }) }) // runCommonTestCases contains test cases that are common to both Connect and Service Account authentication methods. @@ -127,7 +143,8 @@ func runCommonTestCases(ctx context.Context) { Expect(err).NotTo(HaveOccurred()) newPassword, err := op.ReadItemPassword(itemName, vaultName) - Expect(newPassword).NotTo(Equal(oldPassword)) + Expect(err).NotTo(HaveOccurred()) + Expect(newPassword).NotTo(BeEquivalentTo(oldPassword)) // checking that password was NOT updated Eventually(func(g Gomega) { @@ -141,16 +158,59 @@ func runCommonTestCases(ctx context.Context) { i, err := strconv.Atoi(intervalStr) Expect(err).NotTo(HaveOccurred()) - interval := time.Duration(i) * time.Second // convert to duration in seconds - time.Sleep(interval + 2*time.Second) // wait for one polling interval + 2 seconds to make sure updated secret is pulled + // convert to duration in seconds + interval := time.Duration(i) * time.Second + // wait for one polling interval + 2 seconds to make sure updated secret is pulled + time.Sleep(interval + 2*time.Second) secret = kubeClient.Secret(secretName).Get(attemptCtx) g.Expect(err).NotTo(HaveOccurred()) currentPassword, ok := secret.Data["password"] Expect(ok).To(BeTrue()) - Expect(currentPassword).To(Equal(oldPassword)) - Expect(currentPassword).NotTo(Equal(newPassword)) + Expect(currentPassword).To(BeEquivalentTo(oldPassword)) + Expect(currentPassword).NotTo(BeEquivalentTo(newPassword)) }, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed()) }) + + It("AUTO_RESTART restarts deployments using 1Password secrets after item update", func() { + By("Enabling AUTO_RESTART") + kubeClient.Deployment("onepassword-connect-operator").PatchEnvVars(ctx, []corev1.EnvVar{ + {Name: "AUTO_RESTART", Value: "true"}, + }, nil) + + DeferCleanup(func() { + By("Disabling AUTO_RESTART") + // disable AUTO_RESTART after test + kubeClient.Deployment("onepassword-connect-operator").PatchEnvVars(ctx, []corev1.EnvVar{ + {Name: "AUTO_RESTART", Value: "false"}, + }, nil) + }) + + // Ensure the secret exists (created in earlier test), but apply again safely just in case + kubeClient.ApplyOnePasswordItem(ctx, "secret-for-update.yaml") + kubeClient.Secret("secret-for-update").CheckIfExists(ctx) + + // add custom secret to the operator + kubeClient.Deployment("onepassword-connect-operator").PatchEnvVars(ctx, []corev1.EnvVar{ + { + Name: "CUSTOM_SECRET", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "secret-for-update", + }, + Key: "password", + }, + }, + }, + }, nil) + + By("Updating `secret-for-update` 1Password item") + err := op.UpdateItemPassword("secret-for-update") + Expect(err).NotTo(HaveOccurred()) + + By("Checking the operator is restarted") + kubeClient.Deployment("onepassword-connect-operator").WaitDeploymentRolledOut(ctx) + }) } From d5f10445714eabd730c2c299a8c164775cb33030 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Tue, 26 Aug 2025 15:12:16 -0500 Subject: [PATCH 43/64] Do not install `kubectl` cli in pipeline as we use golang library to interact with cluster --- .github/workflows/test-e2e.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index f8925ba..b57879c 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -29,9 +29,6 @@ jobs: with: cluster_name: onepassword-operator-test-e2e - - name: Install kubectl - uses: azure/setup-kubectl@v4 - - name: Install 1Password CLI uses: 1password/install-cli-action@v2 with: From 8c893270f4b25e33cf8f3cec4e137288c507f00a Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Tue, 26 Aug 2025 15:51:59 -0500 Subject: [PATCH 44/64] Update CONTRIBUTING.md with instructions on how to run e2e tests locally --- CONTRIBUTING.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 753c22d..80fc075 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,18 @@ Thank you for your interest in contributing to the 1Password Kubernetes Operator ## Testing -- For functional testing, run the local version of the operator. From the project root: +To run e2e tests: + +1. [Install `kind`](https://kind.sigs.k8s.io/docs/user/quick-start/#installing-with-a-package-manager) to spin up local Kubernetes cluster. +2. `export OP_CONNECT_TOKEN=` +3. `export OP_SERVICE_ACCOUNT_TOKEN=` +4. `make test-e2e` + +In case tests are failed, before running them again, reset kind using `make cleanup-test-e2e` + +---- + +For functional testing, run the local version of the operator. From the project root: ```sh # Go to the K8s environment (e.g. minikube) @@ -24,6 +35,8 @@ Thank you for your interest in contributing to the 1Password Kubernetes Operator 1. Rebuild the Docker image by running `make docker-build` 2. Restart deployment `make restart` +---- + - For testing the changes made to the `OnePasswordItem` Custom Resource Definition (CRD), you need to re-generate the object: ```sh make manifests From 24edff22d468db63e56e270bda6a5bdcf74e87fb Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Tue, 26 Aug 2025 16:25:23 -0500 Subject: [PATCH 45/64] Do not run e2e tests when moving from draft to ready and vise versa --- .github/workflows/test-e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index b57879c..c2fe3d7 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -2,7 +2,7 @@ name: Test E2E on: pull_request: - types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] + types: [opened, synchronize, reopened] branches: ['**'] # run for PRs targeting any branch (main and others) concurrency: From 0c3caf88b6be21008494a381b12b83ea8b945e4d Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Tue, 26 Aug 2025 16:31:07 -0500 Subject: [PATCH 46/64] Provide default inerval and timeout via config --- pkg/testhelper/kube/deployment.go | 6 ++---- pkg/testhelper/kube/kube.go | 16 +++++++++++----- pkg/testhelper/kube/pod.go | 6 ++---- pkg/testhelper/kube/secret.go | 5 ++--- test/e2e/e2e_test.go | 6 +++++- 5 files changed, 22 insertions(+), 17 deletions(-) diff --git a/pkg/testhelper/kube/deployment.go b/pkg/testhelper/kube/deployment.go index fc8f94b..411d785 100644 --- a/pkg/testhelper/kube/deployment.go +++ b/pkg/testhelper/kube/deployment.go @@ -12,13 +12,11 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/1Password/onepassword-operator/pkg/testhelper/defaults" ) type Deployment struct { client client.Client - config *ClusterConfig + config *Config name string } @@ -128,5 +126,5 @@ func (d *Deployment) WaitDeploymentRolledOut(ctx context.Context) { g.Expect(newDeployment.Status.Replicas).To(Equal(desired)) return nil - }, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed()) + }, d.config.TestConfig.Timeout, d.config.TestConfig.Interval).Should(Succeed()) } diff --git a/pkg/testhelper/kube/kube.go b/pkg/testhelper/kube/kube.go index 4d5f9f5..6c613b9 100644 --- a/pkg/testhelper/kube/kube.go +++ b/pkg/testhelper/kube/kube.go @@ -23,17 +23,23 @@ import ( apiv1 "github.com/1Password/onepassword-operator/api/v1" ) -type ClusterConfig struct { +type TestConfig struct { + Timeout time.Duration + Interval time.Duration +} + +type Config struct { Namespace string ManifestsDir string + TestConfig *TestConfig } type Kube struct { - Config *ClusterConfig + Config *Config Client client.Client } -func NewKubeClient(clusterConfig *ClusterConfig) *Kube { +func NewKubeClient(config *Config) *Kube { By("Creating a kubernetes client") kubeconfig := os.Getenv("KUBECONFIG") if kubeconfig == "" { @@ -62,12 +68,12 @@ func NewKubeClient(clusterConfig *ClusterConfig) *Kube { ctx, ok := cfg.Contexts[currentContext] Expect(ok).To(BeTrue(), fmt.Sprintf("current context %q not found in kubeconfig", currentContext)) - ctx.Namespace = clusterConfig.Namespace + ctx.Namespace = config.Namespace err = clientcmd.ModifyConfig(pathOpts, *cfg, true) Expect(err).NotTo(HaveOccurred()) return &Kube{ - Config: clusterConfig, + Config: config, Client: kubernetesClient, } } diff --git a/pkg/testhelper/kube/pod.go b/pkg/testhelper/kube/pod.go index 8b7f2d3..b26dd98 100644 --- a/pkg/testhelper/kube/pod.go +++ b/pkg/testhelper/kube/pod.go @@ -11,13 +11,11 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/labels" "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/1Password/onepassword-operator/pkg/testhelper/defaults" ) type Pod struct { client client.Client - config *ClusterConfig + config *Config selector map[string]string } @@ -45,5 +43,5 @@ func (p *Pod) WaitingForRunningPod(ctx context.Context) { } } g.Expect(foundRunning).To(BeTrue(), "pod not Running yet") - }, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed()) + }, p.config.TestConfig.Timeout, p.config.TestConfig.Interval).Should(Succeed()) } diff --git a/pkg/testhelper/kube/secret.go b/pkg/testhelper/kube/secret.go index 5770611..bbed2f4 100644 --- a/pkg/testhelper/kube/secret.go +++ b/pkg/testhelper/kube/secret.go @@ -15,13 +15,12 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/1Password/onepassword-operator/pkg/testhelper/defaults" "github.com/1Password/onepassword-operator/pkg/testhelper/system" ) type Secret struct { client client.Client - config *ClusterConfig + config *Config name string } @@ -138,5 +137,5 @@ func (s *Secret) CheckIfExists(ctx context.Context) { secret := &corev1.Secret{} err := s.client.Get(attemptCtx, client.ObjectKey{Name: s.name, Namespace: s.config.Namespace}, secret) g.Expect(err).NotTo(HaveOccurred()) - }, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed()) + }, s.config.TestConfig.Timeout, s.config.TestConfig.Interval).Should(Succeed()) } diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 7712970..dbe4a5a 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -30,9 +30,13 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() { ctx := context.Background() BeforeAll(func() { - kubeClient = kube.NewKubeClient(&kube.ClusterConfig{ + kubeClient = kube.NewKubeClient(&kube.Config{ Namespace: "default", ManifestsDir: filepath.Join("manifests"), + TestConfig: &kube.TestConfig{ + Timeout: defaults.E2ETimeout, + Interval: defaults.E2EInterval, + }, }) operator.BuildOperatorImage() From b1b6c97a88f68875f5b5b3d8ad03659131a95afe Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Tue, 26 Aug 2025 16:33:49 -0500 Subject: [PATCH 47/64] Remove redundant if statement --- pkg/testhelper/kube/deployment.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pkg/testhelper/kube/deployment.go b/pkg/testhelper/kube/deployment.go index 411d785..79a4a51 100644 --- a/pkg/testhelper/kube/deployment.go +++ b/pkg/testhelper/kube/deployment.go @@ -2,7 +2,6 @@ package kube import ( "context" - "fmt" "time" //nolint:staticcheck // ST1001 @@ -110,10 +109,6 @@ func (d *Deployment) WaitDeploymentRolledOut(ctx context.Context) { Eventually(func(g Gomega) error { newDeployment := d.Get(ctx) - // Has controller observed the new spec? - if newDeployment.Status.ObservedGeneration < targetGen { - return fmt.Errorf("observedGeneration %d < desired %d", newDeployment.Status.ObservedGeneration, targetGen) - } g.Expect(newDeployment.Status.ObservedGeneration).To(BeNumerically(">=", targetGen)) desired := int32(1) From d75a33d524f6c2ad82a6c240a8597bdecf429839 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Tue, 26 Aug 2025 16:50:06 -0500 Subject: [PATCH 48/64] Add testing.md doc to describe where specific tests should be added --- docs/testing.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 docs/testing.md diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..da077be --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,26 @@ +# Testing + +## Unit tests +**When**: Pure Go logic, no Kubernetes apiserver or network. +**Where**: `internal/...`, `pkg/...` +**Add files in**: `*_test.go` next to the code. +**Run**: `make test` + +## Integration tests (envtest) +**When**: Controller/reconciler behavior against a mocked kubernetes cluster. +**Where**: `internal/controller/...` +**Framework**: controller-runtime’s `envtest`. +**Run**: `make test` + +## E2E tests (kind) +**When**: Full cluster behavior (CRDs, operator image, Connect/SA flows). +**Where**: `test/e2e/...` +**Framework**: Ginkgo + `pkg/testhelper`. + +**Local prep**: +1. [Install `kind`](https://kind.sigs.k8s.io/docs/user/quick-start/#installing-with-a-package-manager) to spin up local Kubernetes cluster. +2. `export OP_CONNECT_TOKEN=` +3. `export OP_SERVICE_ACCOUNT_TOKEN=` +4. `make test-e2e` + +**Run**: `make test-e2e` \ No newline at end of file From a1cbd40f9e1fd18540bbafd032edbb52fcc78af8 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Tue, 26 Aug 2025 16:56:50 -0500 Subject: [PATCH 49/64] Refer to testing.md from contributing.md --- CONTRIBUTING.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 80fc075..bd86261 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,14 +4,13 @@ Thank you for your interest in contributing to the 1Password Kubernetes Operator ## Testing -To run e2e tests: +All contributions must include tests where applicable. -1. [Install `kind`](https://kind.sigs.k8s.io/docs/user/quick-start/#installing-with-a-package-manager) to spin up local Kubernetes cluster. -2. `export OP_CONNECT_TOKEN=` -3. `export OP_SERVICE_ACCOUNT_TOKEN=` -4. `make test-e2e` +- **Unit tests** for pure Go logic. +- **Integration tests** for controller/reconciler logic using envtest. +- **E2E tests** for full cluster behavior with kind. -In case tests are failed, before running them again, reset kind using `make cleanup-test-e2e` +👉 See the [Testing Guide](docs/testing.md) for details on when to use each, how to run them locally, and how they are run in CI. ---- From f7f5462133e20eb8ba21e460b16c1edc9db9273b Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Wed, 27 Aug 2025 11:19:17 -0500 Subject: [PATCH 50/64] Pass CRDs on createion of kube instance --- pkg/testhelper/kube/kube.go | 113 +++++++++++++++++++++++++++++++----- test/e2e/e2e_test.go | 7 +++ 2 files changed, 106 insertions(+), 14 deletions(-) diff --git a/pkg/testhelper/kube/kube.go b/pkg/testhelper/kube/kube.go index 6c613b9..1af2046 100644 --- a/pkg/testhelper/kube/kube.go +++ b/pkg/testhelper/kube/kube.go @@ -13,14 +13,22 @@ import ( . "github.com/onsi/gomega" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" + apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apix "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/yaml" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" apiv1 "github.com/1Password/onepassword-operator/api/v1" + "github.com/1Password/onepassword-operator/pkg/testhelper/defaults" ) type TestConfig struct { @@ -32,11 +40,13 @@ type Config struct { Namespace string ManifestsDir string TestConfig *TestConfig + CRDs []string } type Kube struct { Config *Config Client client.Client + Mapper meta.RESTMapper } func NewKubeClient(config *Config) *Kube { @@ -49,12 +59,26 @@ func NewKubeClient(config *Config) *Kube { restConfig, err := clientcmd.BuildConfigFromFlags("", kubeconfig) Expect(err).NotTo(HaveOccurred()) + // Install CRDs first (so discovery sees them) + installCRDs(context.Background(), restConfig, config.CRDs) + + // Build an http.Client from restConfig + httpClient, err := rest.HTTPClientFor(restConfig) + Expect(err).NotTo(HaveOccurred()) + + // Create a Dynamic RESTMapper that uses restConfig + rm, err := apiutil.NewDynamicRESTMapper(restConfig, httpClient) + Expect(err).NotTo(HaveOccurred()) + scheme := runtime.NewScheme() utilruntime.Must(corev1.AddToScheme(scheme)) utilruntime.Must(appsv1.AddToScheme(scheme)) - utilruntime.Must(apiv1.AddToScheme(scheme)) + utilruntime.Must(apiv1.AddToScheme(scheme)) // add OnePasswordItem to scheme - kubernetesClient, err := client.New(restConfig, client.Options{Scheme: scheme}) + kubernetesClient, err := client.New(restConfig, client.Options{ + Scheme: scheme, + Mapper: rm, + }) Expect(err).NotTo(HaveOccurred()) // update the current context’s namespace in kubeconfig @@ -75,6 +99,7 @@ func NewKubeClient(config *Config) *Kube { return &Kube{ Config: config, Client: kubernetesClient, + Mapper: rm, } } @@ -113,19 +138,79 @@ func (k *Kube) ApplyOnePasswordItem(ctx context.Context, fileName string) { data, err := os.ReadFile(k.Config.ManifestsDir + "/" + fileName) Expect(err).NotTo(HaveOccurred()) - item := &apiv1.OnePasswordItem{} - err = yaml.Unmarshal(data, item) + // Decode YAML -> JSON -> unstructured.Unstructured + jsonBytes, err := yaml.ToJSON(data) Expect(err).NotTo(HaveOccurred()) - if item.Namespace == "" { - item.Namespace = k.Config.Namespace + var obj unstructured.Unstructured + Expect(obj.UnmarshalJSON(jsonBytes)).To(Succeed()) + + // Default namespace for namespaced resources if not set in YAML + if obj.GetNamespace() == "" && k.Config.Namespace != "" { + gvk := obj.GroupVersionKind() + mapping, mapErr := k.Mapper.RESTMapping(gvk.GroupKind(), gvk.Version) + if mapErr == nil && mapping.Scope.Name() == meta.RESTScopeNameNamespace { + obj.SetNamespace(k.Config.Namespace) + } } - err = k.Client.Get(c, client.ObjectKey{Name: item.Name, Namespace: k.Config.Namespace}, item) - if errors.IsNotFound(err) { - err = k.Client.Create(c, item) - } else { - err = k.Client.Update(c, item) + // Server-Side Apply (create or update) + patchOpts := []client.PatchOption{ + client.FieldOwner("onepassword-e2e"), + client.ForceOwnership, // to force-take conflicting fields } - Expect(err).NotTo(HaveOccurred()) + Expect(k.Client.Patch(c, &obj, client.Apply, patchOpts...)).To(Succeed()) +} + +func installCRDs(ctx context.Context, restConfig *rest.Config, crdFiles []string) { + apixClient, err := apix.NewForConfig(restConfig) + Expect(err).NotTo(HaveOccurred()) + + for _, f := range crdFiles { + By("Installing CRD " + f) + b, err := os.ReadFile(filepath.Clean(f)) + Expect(err).NotTo(HaveOccurred()) + + var crd apiextv1.CustomResourceDefinition + err = yaml.Unmarshal(b, &crd) + Expect(err).NotTo(HaveOccurred()) + + // Create or Update + _, err = apixClient.ApiextensionsV1().CustomResourceDefinitions().Create(ctx, &crd, metav1.CreateOptions{}) + if apierrors.IsAlreadyExists(err) { + existing, getErr := apixClient.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, crd.Name, metav1.GetOptions{}) + Expect(getErr).NotTo(HaveOccurred()) + + crd.ResourceVersion = existing.ResourceVersion + _, err = apixClient.ApiextensionsV1().CustomResourceDefinitions().Update(ctx, &crd, metav1.UpdateOptions{}) + } + Expect(err).NotTo(HaveOccurred()) + + waitCRDEstablished(ctx, apixClient, crd.Name) + } +} + +// waitCRDEstablished Wait until the CRD reaches Established=True, retrying until the suite timeout. +func waitCRDEstablished(ctx context.Context, apixClient *apix.Clientset, name string) { + By("Waiting for CRD " + name + " to be Established") + + Eventually(func(g Gomega) { + // Short per-attempt timeout so a single Get can't hang the whole Eventually loop. + attemptCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + crd, err := apixClient.ApiextensionsV1(). + CustomResourceDefinitions(). + Get(attemptCtx, name, metav1.GetOptions{}) + g.Expect(err).NotTo(HaveOccurred()) + + established := false + for _, c := range crd.Status.Conditions { + if c.Type == apiextv1.Established && c.Status == apiextv1.ConditionTrue { + established = true + break + } + } + g.Expect(established).To(BeTrue(), "CRD %q is not Established yet", name) + }, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed()) } diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index dbe4a5a..8208353 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -17,6 +17,7 @@ import ( "github.com/1Password/onepassword-operator/pkg/testhelper/kube" "github.com/1Password/onepassword-operator/pkg/testhelper/op" "github.com/1Password/onepassword-operator/pkg/testhelper/operator" + "github.com/1Password/onepassword-operator/pkg/testhelper/system" ) const ( @@ -30,6 +31,9 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() { ctx := context.Background() BeforeAll(func() { + rootDir, err := system.GetProjectRoot() + Expect(err).NotTo(HaveOccurred()) + kubeClient = kube.NewKubeClient(&kube.Config{ Namespace: "default", ManifestsDir: filepath.Join("manifests"), @@ -37,6 +41,9 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() { Timeout: defaults.E2ETimeout, Interval: defaults.E2EInterval, }, + CRDs: []string{ + filepath.Join(rootDir, "config", "crd", "bases", "onepassword.com_onepassworditems.yaml"), + }, }) operator.BuildOperatorImage() From 9d08bcc8643616e20f94d42643633ed2d2c7b399 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Thu, 28 Aug 2025 11:25:57 -0500 Subject: [PATCH 51/64] Update e2e local testing steps --- docs/testing.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/testing.md b/docs/testing.md index da077be..e4fec11 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -22,5 +22,6 @@ 2. `export OP_CONNECT_TOKEN=` 3. `export OP_SERVICE_ACCOUNT_TOKEN=` 4. `make test-e2e` +5. Put `1password-credentials.json` into project root. **Run**: `make test-e2e` \ No newline at end of file From 6492b3cf34efc5f013d25953e7829c30edbd910c Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Thu, 28 Aug 2025 13:12:48 -0500 Subject: [PATCH 52/64] Remove `operator` package, as make commands can be run directly using `system.Run` --- pkg/testhelper/operator/operator.go | 27 --------------------------- test/e2e/e2e_test.go | 9 ++++++--- 2 files changed, 6 insertions(+), 30 deletions(-) delete mode 100644 pkg/testhelper/operator/operator.go diff --git a/pkg/testhelper/operator/operator.go b/pkg/testhelper/operator/operator.go deleted file mode 100644 index a965e7b..0000000 --- a/pkg/testhelper/operator/operator.go +++ /dev/null @@ -1,27 +0,0 @@ -package operator - -import ( - //nolint:staticcheck // ST1001 - . "github.com/onsi/ginkgo/v2" - //nolint:staticcheck // ST1001 - . "github.com/onsi/gomega" - - "github.com/1Password/onepassword-operator/pkg/testhelper/system" -) - -// BuildOperatorImage builds the Operator image using `make docker-build` -func BuildOperatorImage() { - By("Building the Operator image") - _, err := system.Run("make", "docker-build") - ExpectWithOffset(1, err).NotTo(HaveOccurred()) -} - -// DeployOperator deploys the Operator in the default namespace. -// It waits for the operator pod to be in 'Running' state. -// All the resources created using manifests in `config/` dir. -// To make the operator use Connect or Service Accounts, patch `config/manager/manager.yaml` -func DeployOperator() { - By("Deploying the Operator") - _, err := system.Run("make", "deploy") - Expect(err).NotTo(HaveOccurred()) -} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 8208353..a2ed2ec 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -16,7 +16,6 @@ import ( "github.com/1Password/onepassword-operator/pkg/testhelper/kind" "github.com/1Password/onepassword-operator/pkg/testhelper/kube" "github.com/1Password/onepassword-operator/pkg/testhelper/op" - "github.com/1Password/onepassword-operator/pkg/testhelper/operator" "github.com/1Password/onepassword-operator/pkg/testhelper/system" ) @@ -46,7 +45,10 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() { }, }) - operator.BuildOperatorImage() + By("Building the Operator image") + _, err = system.Run("make", "docker-build") + Expect(err).NotTo(HaveOccurred()) + kind.LoadImageToKind(operatorImageName) kubeClient.Secret("op-credentials").CreateOpCredentials(ctx) @@ -58,7 +60,8 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() { kubeClient.Secret("onepassword-service-account-token").CreateFromEnvVar(ctx, "OP_SERVICE_ACCOUNT_TOKEN") kubeClient.Secret("onepassword-service-account-token").CheckIfExists(ctx) - operator.DeployOperator() + _, err = system.Run("make", "deploy") + Expect(err).NotTo(HaveOccurred()) kubeClient.Pod(map[string]string{"name": "onepassword-connect-operator"}).WaitingForRunningPod(ctx) }) From e61ba4901865298a03c75a50cdcb2373f700370f Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Thu, 28 Aug 2025 13:17:27 -0500 Subject: [PATCH 53/64] Add `namespace` package --- go.mod | 2 +- pkg/testhelper/kube/kube.go | 8 +++++++ pkg/testhelper/kube/namespace.go | 41 ++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 pkg/testhelper/kube/namespace.go diff --git a/go.mod b/go.mod index 941b1a8..bcb22b7 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/onsi/gomega v1.36.1 github.com/stretchr/testify v1.10.0 k8s.io/api v0.33.0 + k8s.io/apiextensions-apiserver v0.33.0 k8s.io/apimachinery v0.33.0 k8s.io/client-go v0.33.0 k8s.io/kubectl v0.29.0 @@ -102,7 +103,6 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.33.0 // indirect k8s.io/apiserver v0.33.0 // indirect k8s.io/component-base v0.33.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect diff --git a/pkg/testhelper/kube/kube.go b/pkg/testhelper/kube/kube.go index 1af2046..77bbe68 100644 --- a/pkg/testhelper/kube/kube.go +++ b/pkg/testhelper/kube/kube.go @@ -127,6 +127,14 @@ func (k *Kube) Pod(selector map[string]string) *Pod { } } +func (k *Kube) Namespace(name string) *Namespace { + return &Namespace{ + client: k.Client, + config: k.Config, + name: name, + } +} + // ApplyOnePasswordItem applies a OnePasswordItem manifest. func (k *Kube) ApplyOnePasswordItem(ctx context.Context, fileName string) { By("Applying " + fileName) diff --git a/pkg/testhelper/kube/namespace.go b/pkg/testhelper/kube/namespace.go new file mode 100644 index 0000000..08aec1f --- /dev/null +++ b/pkg/testhelper/kube/namespace.go @@ -0,0 +1,41 @@ +package kube + +import ( + "context" + //nolint:staticcheck // ST1001 + . "github.com/onsi/ginkgo/v2" + //nolint:staticcheck // ST1001 + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/labels" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type Namespace struct { + client client.Client + config *Config + name string +} + +// LabelNamespace applies the given labels to the specified namespace +func (n *Namespace) LabelNamespace(ctx context.Context, labelsMap map[string]string) { + if labelsMap == nil || len(labelsMap) == 0 { + return + } + + By("Setting labelsMap " + labels.Set(labelsMap).String() + " to namespace/" + n.name) + ns := &corev1.Namespace{} + err := n.client.Get(ctx, client.ObjectKey{Name: n.name}, ns) + Expect(err).NotTo(HaveOccurred()) + + if ns.Labels == nil { + ns.Labels = map[string]string{} + } + + for k, v := range labelsMap { + ns.Labels[k] = v + } + + err = n.client.Update(ctx, ns) + Expect(err).NotTo(HaveOccurred()) +} From 976909c4384ba63ed40023b5a5a2c97914ec6b99 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Thu, 28 Aug 2025 13:20:45 -0500 Subject: [PATCH 54/64] Update OpReadField method to be able to read different item fields --- pkg/testhelper/op/op.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pkg/testhelper/op/op.go b/pkg/testhelper/op/op.go index 658cf21..cb25fb1 100644 --- a/pkg/testhelper/op/op.go +++ b/pkg/testhelper/op/op.go @@ -6,6 +6,13 @@ import ( "github.com/1Password/onepassword-operator/pkg/testhelper/system" ) +type Field string + +const ( + FieldUsername = "username" + FieldPassword = "password" +) + // UpdateItemPassword updates the password of an item in 1Password func UpdateItemPassword(item string) error { _, err := system.Run("op", "item", "edit", item, "--generate-password=letters,digits,symbols,32") @@ -15,9 +22,9 @@ func UpdateItemPassword(item string) error { return nil } -// ReadItemPassword reads the password of an item in 1Password -func ReadItemPassword(item, vault string) (string, error) { - output, err := system.Run("op", "read", fmt.Sprintf("op://%s/%s/password", vault, item)) +// ReadItemField reads the password of an item in 1Password +func ReadItemField(item, vault string, field Field) (string, error) { + output, err := system.Run("op", "read", fmt.Sprintf("op://%s/%s/%s", vault, item, field)) if err != nil { return "", err } From 7e08158d2fce12603f7a5d80525b297336fdbc68 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Thu, 28 Aug 2025 13:22:11 -0500 Subject: [PATCH 55/64] Use `op.ReadItemField` command --- test/e2e/e2e_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index a2ed2ec..94208e4 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -156,7 +156,7 @@ func runCommonTestCases(ctx context.Context) { err := op.UpdateItemPassword(itemName) Expect(err).NotTo(HaveOccurred()) - newPassword, err := op.ReadItemPassword(itemName, vaultName) + newPassword, err := op.ReadItemField(itemName, vaultName, op.FieldPassword) Expect(err).NotTo(HaveOccurred()) Expect(newPassword).NotTo(BeEquivalentTo(oldPassword)) From 6baef1b9cf18538313904b6a0813c3a697cad516 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Thu, 28 Aug 2025 13:24:55 -0500 Subject: [PATCH 56/64] Fix lint error --- pkg/testhelper/kube/namespace.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/testhelper/kube/namespace.go b/pkg/testhelper/kube/namespace.go index 08aec1f..9969d60 100644 --- a/pkg/testhelper/kube/namespace.go +++ b/pkg/testhelper/kube/namespace.go @@ -19,7 +19,7 @@ type Namespace struct { // LabelNamespace applies the given labels to the specified namespace func (n *Namespace) LabelNamespace(ctx context.Context, labelsMap map[string]string) { - if labelsMap == nil || len(labelsMap) == 0 { + if len(labelsMap) == 0 { return } From c2788770fd0131a26631faaeb7286d5b160c4297 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Thu, 4 Sep 2025 10:41:25 -0500 Subject: [PATCH 57/64] Add comment about installing 1p cli in test workflow --- .github/workflows/test-e2e.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index c2fe3d7..65f3a3e 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -29,6 +29,7 @@ jobs: with: cluster_name: onepassword-operator-test-e2e + # install cli to interact with item in 1Password to update/read using `testhelper/op` package - name: Install 1Password CLI uses: 1password/install-cli-action@v2 with: From 9c4849ec2e6068e5a93e72fc19bb21ca75f5ee1a Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Thu, 4 Sep 2025 10:43:55 -0500 Subject: [PATCH 58/64] Ignore these files across entire project --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index f36aa42..ec7068a 100644 --- a/.gitignore +++ b/.gitignore @@ -26,5 +26,5 @@ go.work *.swo *~ -1password-credentials.json -op-session +**/1password-credentials.json +**/op-session From bf6cac81cb1bb37ba68fedf7886c1ea73409e83a Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Thu, 4 Sep 2025 11:17:38 -0500 Subject: [PATCH 59/64] Add capabilities for ["CHOWN", "FOWNER"] to make it more striker --- config/connect/deployment.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/connect/deployment.yaml b/config/connect/deployment.yaml index d0e90e4..cee5e75 100644 --- a/config/connect/deployment.yaml +++ b/config/connect/deployment.yaml @@ -39,6 +39,7 @@ spec: allowPrivilegeEscalation: false capabilities: drop: [ "ALL" ] + add: ["CHOWN", "FOWNER"] containers: - name: connect-api image: 1password/connect-api:latest From bd963bcd1db564b822597cc5085758095676812d Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 5 Sep 2025 09:34:49 -0500 Subject: [PATCH 60/64] Revert config/manager.yaml --- config/manager/manager.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index c381817..e9602da 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -74,7 +74,6 @@ spec: - --leader-elect - --health-probe-bind-address=:8081 image: 1password/onepassword-operator:latest - imagePullPolicy: Never name: manager env: - name: OPERATOR_NAME @@ -97,7 +96,7 @@ spec: name: onepassword-token key: token - name: MANAGE_CONNECT - value: "true" + value: "false" # Uncomment the following lines to enable service account token and comment out the OP_CONNECT_TOKEN, OP_CONNECT_HOST and MANAGE_CONNECT env vars. # - name: OP_SERVICE_ACCOUNT_TOKEN # valueFrom: From 706ebdd8b876695ef6c76d57177aaf86b5456f8d Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 5 Sep 2025 10:13:23 -0500 Subject: [PATCH 61/64] Copy manager.yaml from test/e2e when starting e2e tests --- pkg/testhelper/system/system.go | 30 ++++++++++ test/e2e/e2e_test.go | 29 +++++----- test/e2e/manifests/manager.yaml | 99 +++++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 13 deletions(-) create mode 100644 test/e2e/manifests/manager.yaml diff --git a/pkg/testhelper/system/system.go b/pkg/testhelper/system/system.go index bded147..afc0a59 100644 --- a/pkg/testhelper/system/system.go +++ b/pkg/testhelper/system/system.go @@ -2,6 +2,7 @@ package system import ( "fmt" + "io" "os" "os/exec" "path/filepath" @@ -51,3 +52,32 @@ func GetProjectRoot() (string, error) { dir = parent } } + +func ReplaceFile(src, dst string) error { + rootDir, err := GetProjectRoot() + if err != nil { + return err + } + + // Open the source file + sourceFile, err := os.Open(filepath.Join(rootDir, src)) + if err != nil { + return err + } + defer sourceFile.Close() + + // Create (or overwrite) the destination file + destFile, err := os.Create(filepath.Join(rootDir, dst)) + if err != nil { + return err + } + defer destFile.Close() + + // Copy contents + if _, err = io.Copy(destFile, sourceFile); err != nil { + return err + } + + // Ensure data is written to disk + return destFile.Sync() +} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 94208e4..4aa5664 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -60,39 +60,42 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() { kubeClient.Secret("onepassword-service-account-token").CreateFromEnvVar(ctx, "OP_SERVICE_ACCOUNT_TOKEN") kubeClient.Secret("onepassword-service-account-token").CheckIfExists(ctx) + By("Replace manager.yaml") + err = system.ReplaceFile("test/e2e/manifests/manager.yaml", "config/manager/manager.yaml") + Expect(err).NotTo(HaveOccurred()) + _, err = system.Run("make", "deploy") Expect(err).NotTo(HaveOccurred()) kubeClient.Pod(map[string]string{"name": "onepassword-connect-operator"}).WaitingForRunningPod(ctx) }) - Context("Use the operator with Connect", func() { - BeforeAll(func() { - kubeClient.Pod(map[string]string{"app": "onepassword-connect"}).WaitingForRunningPod(ctx) - }) - + Context("Use the operator with Service Account", func() { runCommonTestCases(ctx) }) - Context("Use the operator with Service Account", func() { + Context("Use the operator with Connect", func() { BeforeAll(func() { kubeClient.Deployment("onepassword-connect-operator").PatchEnvVars(ctx, []corev1.EnvVar{ - {Name: "MANAGE_CONNECT", Value: "false"}, + {Name: "MANAGE_CONNECT", Value: "true"}, + {Name: "OP_CONNECT_HOST", Value: "http://onepassword-connect:8080"}, { - Name: "OP_SERVICE_ACCOUNT_TOKEN", + Name: "OP_CONNECT_TOKEN", ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ - Name: "onepassword-service-account-token", + Name: "onepassword-token", }, Key: "token", }, }, }, - }, []string{"OP_CONNECT_HOST", "OP_CONNECT_TOKEN"}) + }, []string{"OP_SERVICE_ACCOUNT_TOKEN"}) kubeClient.Secret("login").Delete(ctx) // remove secret crated in previous test kubeClient.Secret("secret-ignored").Delete(ctx) // remove secret crated in previous test kubeClient.Secret("secret-for-update").Delete(ctx) // remove secret crated in previous test + + kubeClient.Pod(map[string]string{"app": "onepassword-connect"}).WaitingForRunningPod(ctx) }) runCommonTestCases(ctx) @@ -101,13 +104,13 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() { // runCommonTestCases contains test cases that are common to both Connect and Service Account authentication methods. func runCommonTestCases(ctx context.Context) { - It("Should create secret from manifest file", func() { + It("Should create kubernetes secret from manifest file", func() { By("Creating secret `login` from 1Password item") kubeClient.ApplyOnePasswordItem(ctx, "secret.yaml") kubeClient.Secret("login").CheckIfExists(ctx) }) - It("Secret is updated after POOLING_INTERVAL", func() { + It("Kubernetes secret is updated after POOLING_INTERVAL, when updating item in 1Password", func() { itemName := "secret-for-update" secretName := itemName @@ -139,7 +142,7 @@ func runCommonTestCases(ctx context.Context) { }, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed()) }) - It("1Password item with `ignore-secret` doesn't pull updates to kubernetes secret", func() { + It("1Password item with `ignore-secret` tag doesn't pull updates to kubernetes secret", func() { itemName := "secret-ignored" secretName := itemName diff --git a/test/e2e/manifests/manager.yaml b/test/e2e/manifests/manager.yaml new file mode 100644 index 0000000..d76b0d7 --- /dev/null +++ b/test/e2e/manifests/manager.yaml @@ -0,0 +1,99 @@ +# This manager file is used for e2e tests. +# It will be copied to `config/manager` and be used when deploying the operator in e2e tests +# The purpose of it is to increase e2e tests speed and do not introduce additional changes in original `manager.yaml` + +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: onepassword-connect-operator + app.kubernetes.io/name: namespace + app.kubernetes.io/instance: system + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: onepassword-connect-operator + app.kubernetes.io/part-of: onepassword-connect-operator + app.kubernetes.io/managed-by: kustomize + name: system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: onepassword-connect-operator + namespace: system + labels: + control-plane: controller-manager + app.kubernetes.io/name: deployment + app.kubernetes.io/instance: controller-manager + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: onepassword-connect-operator + app.kubernetes.io/part-of: onepassword-connect-operator + app.kubernetes.io/managed-by: kustomize +spec: + selector: + matchLabels: + name: onepassword-connect-operator + control-plane: onepassword-connect-operator + replicas: 1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + name: onepassword-connect-operator + control-plane: onepassword-connect-operator + spec: + securityContext: + runAsNonRoot: true + containers: + - command: + - /manager + args: + - --leader-elect + - --health-probe-bind-address=:8081 + image: 1password/onepassword-operator:latest + imagePullPolicy: Never + name: manager + env: + - name: OPERATOR_NAME + value: "onepassword-connect-operator" + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: WATCH_NAMESPACE + value: "default" + - name: POLLING_INTERVAL + value: "10" + - name: AUTO_RESTART + value: "false" + - name: OP_SERVICE_ACCOUNT_TOKEN + valueFrom: + secretKeyRef: + name: onepassword-service-account-token + key: token + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 128Mi + serviceAccountName: onepassword-connect-operator + terminationGracePeriodSeconds: 10 From 0f1293ca9590422e42954f1999b61df464b37534 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 5 Sep 2025 11:20:08 -0500 Subject: [PATCH 62/64] Update testing doc to merge integration and unit tests under single command --- docs/testing.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/docs/testing.md b/docs/testing.md index e4fec11..9b7f0a1 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1,17 +1,11 @@ # Testing -## Unit tests -**When**: Pure Go logic, no Kubernetes apiserver or network. +## Unit/integration tests +**When**: Pure Go logic. Controller/reconciler behavior against a mocked kubernetes cluster. **Where**: `internal/...`, `pkg/...` **Add files in**: `*_test.go` next to the code. **Run**: `make test` -## Integration tests (envtest) -**When**: Controller/reconciler behavior against a mocked kubernetes cluster. -**Where**: `internal/controller/...` -**Framework**: controller-runtime’s `envtest`. -**Run**: `make test` - ## E2E tests (kind) **When**: Full cluster behavior (CRDs, operator image, Connect/SA flows). **Where**: `test/e2e/...` From 292c6f0e9328b506a77192f7a057ecba45a13c6c Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 5 Sep 2025 11:24:02 -0500 Subject: [PATCH 63/64] Update testing doc to mention integration and unit tests under single command --- docs/testing.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/testing.md b/docs/testing.md index 9b7f0a1..9380407 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1,14 +1,15 @@ # Testing -## Unit/integration tests -**When**: Pure Go logic. Controller/reconciler behavior against a mocked kubernetes cluster. -**Where**: `internal/...`, `pkg/...` -**Add files in**: `*_test.go` next to the code. +## Unit & Integration tests +**When**: Unit (pure Go) and integration (controller-runtime envtest). +**Where**: `internal/...`, `pkg/...` +**Add files in**: `*_test.go` next to the code. **Run**: `make test` ## E2E tests (kind) -**When**: Full cluster behavior (CRDs, operator image, Connect/SA flows). -**Where**: `test/e2e/...` +**When**: Full cluster behavior (CRDs, operator image, Connect/SA flows). +**Where**: `test/e2e/...` +**Add files in**: `*_test.go` next to the code. **Framework**: Ginkgo + `pkg/testhelper`. **Local prep**: @@ -18,4 +19,4 @@ 4. `make test-e2e` 5. Put `1password-credentials.json` into project root. -**Run**: `make test-e2e` \ No newline at end of file +**Run**: `make test-e2e` From 94602ddd72eb4aa443247bbf5e534cf85288df38 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 5 Sep 2025 11:34:18 -0500 Subject: [PATCH 64/64] Fix lint errors --- pkg/testhelper/system/system.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pkg/testhelper/system/system.go b/pkg/testhelper/system/system.go index afc0a59..defe461 100644 --- a/pkg/testhelper/system/system.go +++ b/pkg/testhelper/system/system.go @@ -1,6 +1,7 @@ package system import ( + "errors" "fmt" "io" "os" @@ -64,14 +65,24 @@ func ReplaceFile(src, dst string) error { if err != nil { return err } - defer sourceFile.Close() + defer func(sourceFile *os.File) { + cerr := sourceFile.Close() + if err != nil { + err = errors.Join(err, cerr) + } + }(sourceFile) // Create (or overwrite) the destination file destFile, err := os.Create(filepath.Join(rootDir, dst)) if err != nil { return err } - defer destFile.Close() + defer func(destFile *os.File) { + cerr := destFile.Close() + if err != nil { + err = errors.Join(err, cerr) + } + }(destFile) // Copy contents if _, err = io.Copy(destFile, sourceFile); err != nil {