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()) }) }