From 79ee171b7fa81ea3b8e32f7cb280c0bb49a35634 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Wed, 17 Sep 2025 11:08:11 -0500 Subject: [PATCH] Add functions to testhelper package --- pkg/testhelper/kube/kube.go | 38 ++++++++++---- pkg/testhelper/kube/pod.go | 96 ++++++++++++++++++++++++++++++++-- pkg/testhelper/kube/webhook.go | 33 ++++++++++++ test/e2e/e2e_test.go | 8 +-- 4 files changed, 157 insertions(+), 18 deletions(-) create mode 100644 pkg/testhelper/kube/webhook.go diff --git a/pkg/testhelper/kube/kube.go b/pkg/testhelper/kube/kube.go index e8ef275..e67032e 100644 --- a/pkg/testhelper/kube/kube.go +++ b/pkg/testhelper/kube/kube.go @@ -22,6 +22,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "sigs.k8s.io/controller-runtime/pkg/client" @@ -43,9 +44,10 @@ type Config struct { } type Kube struct { - Config *Config - Client client.Client - Mapper meta.RESTMapper + Config *Config + Client client.Client + Clientset kubernetes.Interface + Mapper meta.RESTMapper } func NewKubeClient(config *Config) *Kube { @@ -79,6 +81,10 @@ func NewKubeClient(config *Config) *Kube { }) Expect(err).NotTo(HaveOccurred()) + // Create Kubernetes clientset for logs and other operations + clientset, err := kubernetes.NewForConfig(restConfig) + Expect(err).NotTo(HaveOccurred()) + // update the current context’s namespace in kubeconfig pathOpts := clientcmd.NewDefaultPathOptions() cfg, err := pathOpts.GetStartingConfig() @@ -95,9 +101,10 @@ func NewKubeClient(config *Config) *Kube { Expect(err).NotTo(HaveOccurred()) return &Kube{ - Config: config, - Client: kubernetesClient, - Mapper: rm, + Config: config, + Client: kubernetesClient, + Clientset: clientset, + Mapper: rm, } } @@ -119,9 +126,10 @@ 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, + client: k.Client, + clientset: k.Clientset, + config: k.Config, + selector: selector, } } @@ -133,8 +141,16 @@ func (k *Kube) Namespace(name string) *Namespace { } } -// ApplyOnePasswordItem applies a OnePasswordItem manifest. -func (k *Kube) ApplyOnePasswordItem(ctx context.Context, fileName string) { +func (k *Kube) Webhook(name string) *Webhook { + return &Webhook{ + client: k.Client, + config: k.Config, + name: name, + } +} + +// Apply applies a Kubernetes manifest file using server-side apply. +func (k *Kube) Apply(ctx context.Context, fileName string) { By("Applying " + fileName) // Derive a short-lived context so this API call won't hang indefinitely. diff --git a/pkg/testhelper/kube/pod.go b/pkg/testhelper/kube/pod.go index b26dd98..72d4a68 100644 --- a/pkg/testhelper/kube/pod.go +++ b/pkg/testhelper/kube/pod.go @@ -2,6 +2,7 @@ package kube import ( "context" + "io" "time" //nolint:staticcheck // ST1001 @@ -10,13 +11,15 @@ import ( . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/kubernetes" "sigs.k8s.io/controller-runtime/pkg/client" ) type Pod struct { - client client.Client - config *Config - selector map[string]string + client client.Client + clientset kubernetes.Interface + config *Config + selector map[string]string } func (p *Pod) WaitingForRunningPod(ctx context.Context) { @@ -45,3 +48,90 @@ func (p *Pod) WaitingForRunningPod(ctx context.Context) { g.Expect(foundRunning).To(BeTrue(), "pod not Running yet") }, p.config.TestConfig.Timeout, p.config.TestConfig.Interval).Should(Succeed()) } + +func (p *Pod) GetPodLogs(ctx context.Context) string { + // First find the pod by label selector + var pods corev1.PodList + listOpts := []client.ListOption{ + client.InNamespace(p.config.Namespace), + client.MatchingLabels(p.selector), + } + err := p.client.List(ctx, &pods, listOpts...) + Expect(err).NotTo(HaveOccurred()) + Expect(pods.Items).NotTo(BeEmpty(), "no pods found with selector %q", labels.Set(p.selector).String()) + + // Use the first pod found + pod := pods.Items[0] + podName := pod.Name + + // Verify pod is running before getting logs + Expect(pod.Status.Phase).To(Equal(corev1.PodRunning), "pod %s is not running (status: %s)", podName, pod.Status.Phase) + + // Get logs using the Kubernetes clientset + req := p.clientset.CoreV1().Pods(p.config.Namespace).GetLogs(podName, &corev1.PodLogOptions{}) + stream, err := req.Stream(context.TODO()) + Expect(err).NotTo(HaveOccurred(), "failed to stream logs for pod %s", podName) + defer stream.Close() + + // Read all logs from the stream + logs, err := io.ReadAll(stream) + Expect(err).NotTo(HaveOccurred(), "failed to read logs for pod %s", podName) + + return string(logs) +} + +func (p *Pod) VerifyWebhookInjection(ctx context.Context) { + By("Verifying webhook injection for pod with selector " + labels.Set(p.selector).String()) + + 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() + + // First find the pod by label selector + 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()) + + // Use the first pod found + pod := pods.Items[0] + + // Check injection status annotation + g.Expect(pod.Annotations).To(HaveKey("operator.1password.io/status")) + g.Expect(pod.Annotations["operator.1password.io/status"]).To(Equal("injected")) + + // Check command was modified to use op run + if len(pod.Spec.Containers) > 0 { + container := pod.Spec.Containers[0] + g.Expect(container.Command).To(HaveLen(4)) + g.Expect(container.Command[0]).To(Equal("/op/bin/op")) + g.Expect(container.Command[1]).To(Equal("run")) + g.Expect(container.Command[2]).To(Equal("--")) + } + + // Check init container was added + g.Expect(pod.Spec.InitContainers).To(HaveLen(1)) + g.Expect(pod.Spec.InitContainers[0].Name).To(Equal("copy-op-bin")) + + // Check volume mount was added + g.Expect(pod.Spec.Containers[0].VolumeMounts).To(ContainElement(HaveField("Name", "op-bin"))) + }, p.config.TestConfig.Timeout, p.config.TestConfig.Interval).Should(Succeed()) +} + +func (p *Pod) VerifySecretsInjected(ctx context.Context) { + By("Verifying secrets are injected and concealed in pod with selector " + labels.Set(p.selector).String()) + + 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() + + logs := p.GetPodLogs(attemptCtx) + // Check that secrets are concealed in the application logs + g.Expect(logs).To(ContainSubstring("SECRET: ''")) + }, p.config.TestConfig.Timeout, p.config.TestConfig.Interval).Should(Succeed()) +} diff --git a/pkg/testhelper/kube/webhook.go b/pkg/testhelper/kube/webhook.go new file mode 100644 index 0000000..57382c1 --- /dev/null +++ b/pkg/testhelper/kube/webhook.go @@ -0,0 +1,33 @@ +package kube + +import ( + "context" + "time" + + //nolint:staticcheck // ST1001 + . "github.com/onsi/ginkgo/v2" + //nolint:staticcheck // ST1001 + . "github.com/onsi/gomega" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type Webhook struct { + client client.Client + config *Config + name string +} + +func (w *Webhook) WaitForWebhookToBeRegistered(ctx context.Context) { + By("Waiting for webhook " + w.name + " to be registered") + + 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() + + webhookConfig := &admissionregistrationv1.MutatingWebhookConfiguration{} + err := w.client.Get(attemptCtx, client.ObjectKey{Name: w.name}, webhookConfig) + g.Expect(err).ToNot(HaveOccurred()) + }, w.config.TestConfig.Timeout, w.config.TestConfig.Interval).Should(Succeed()) +} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 4aa5664..29c7d15 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -106,7 +106,7 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() { func runCommonTestCases(ctx context.Context) { It("Should create kubernetes secret from manifest file", func() { By("Creating secret `login` from 1Password item") - kubeClient.ApplyOnePasswordItem(ctx, "secret.yaml") + kubeClient.Apply(ctx, "secret.yaml") kubeClient.Secret("login").CheckIfExists(ctx) }) @@ -115,7 +115,7 @@ func runCommonTestCases(ctx context.Context) { secretName := itemName By("Creating secret `" + secretName + "` from 1Password item") - kubeClient.ApplyOnePasswordItem(ctx, secretName+".yaml") + kubeClient.Apply(ctx, secretName+".yaml") kubeClient.Secret(secretName).CheckIfExists(ctx) By("Reading old password") @@ -147,7 +147,7 @@ func runCommonTestCases(ctx context.Context) { secretName := itemName By("Creating secret `" + secretName + "` from 1Password item") - kubeClient.ApplyOnePasswordItem(ctx, secretName+".yaml") + kubeClient.Apply(ctx, secretName+".yaml") kubeClient.Secret(secretName).CheckIfExists(ctx) By("Reading old password") @@ -205,7 +205,7 @@ func runCommonTestCases(ctx context.Context) { }) // Ensure the secret exists (created in earlier test), but apply again safely just in case - kubeClient.ApplyOnePasswordItem(ctx, "secret-for-update.yaml") + kubeClient.Apply(ctx, "secret-for-update.yaml") kubeClient.Secret("secret-for-update").CheckIfExists(ctx) // add custom secret to the operator