diff --git a/.github/workflows/end2end.yaml b/.github/workflows/end2end.yaml index ee01a58..546bea6 100644 --- a/.github/workflows/end2end.yaml +++ b/.github/workflows/end2end.yaml @@ -13,7 +13,7 @@ jobs: k3d version - name: Create Cluster & Registry run: | - make test-create-cluster + make e2e-cluster - name: Check Cluster Nodes run: | kubectl get nodes @@ -51,14 +51,13 @@ jobs: cosign-release: 'v2.2.0' - name: Create ephemeral keys run: | - make test-generate-keys + make e2e-keys - name: Build test image run: | - make test-image + make e2e-images - name: Install Cosignwebhook run: | - make test-deploy + make e2e-deploy - name: Run End2End Tests run: | - make test-busybox-images make test-e2e diff --git a/Makefile b/Makefile index 3c323c1..bb94b51 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -test-create-cluster: +e2e-cluster: @echo "Creating registry..." @k3d registry create registry.localhost --port 5000 @echo "Adding registry to cluster..." @@ -6,25 +6,13 @@ test-create-cluster: @echo "Create test namespace..." @kubectl create namespace test-cases -test-generate-keys: +e2e-keys: @echo "Generating cosign keys..." @export COSIGN_PASSWORD="" && \ cosign generate-key-pair && \ cosign generate-key-pair --output-key-prefix second - -test-busybox-images: - @echo "Building busybox image..." - @docker pull busybox:latest - @echo "Tagging & pushing busybox images..." - @docker tag busybox:latest k3d-registry.localhost:5000/busybox:latest - @docker tag busybox:latest k3d-registry.localhost:5000/busybox:second - @docker push k3d-registry.localhost:5000/busybox --all-tags - @echo "Signing busybox images..." - @export COSIGN_PASSWORD="" && \ - cosign sign --tlog-upload=false --key cosign.key k3d-registry.localhost:5000/busybox:latest && \ - cosign sign --tlog-upload=false --key second.key k3d-registry.localhost:5000/busybox:second -test-image: +e2e-images: @echo "Checking for cosign.key..." @test -f cosign.key || (echo "cosign.key not found. Run 'make generate-key' to generate one." && exit 1) @echo "Building test image..." @@ -36,8 +24,18 @@ test-image: cosign sign --tlog-upload=false --key cosign.key k3d-registry.localhost:5000/cosignwebhook:dev @echo "Importing test image to cluster..." @k3d image import k3d-registry.localhost:5000/cosignwebhook:dev --cluster cosign-tests + @echo "Building busybox image..." + @docker pull busybox:latest + @echo "Tagging & pushing busybox images..." + @docker tag busybox:latest k3d-registry.localhost:5000/busybox:first + @docker tag busybox:latest k3d-registry.localhost:5000/busybox:second + @docker push k3d-registry.localhost:5000/busybox --all-tags + @echo "Signing busybox images..." + @export COSIGN_PASSWORD="" && \ + cosign sign --tlog-upload=false --key cosign.key k3d-registry.localhost:5000/busybox:first && \ + cosign sign --tlog-upload=false --key second.key k3d-registry.localhost:5000/busybox:second -test-deploy: +e2e-deploy: @echo "Deploying test image..." @helm upgrade -i cosignwebhook chart -n cosignwebhook --create-namespace \ --set image.repository=k3d-registry.localhost:5000/cosignwebhook \ diff --git a/test/framework/client.go b/test/framework/client.go new file mode 100644 index 0000000..446ff4c --- /dev/null +++ b/test/framework/client.go @@ -0,0 +1,243 @@ +package framework + +import ( + "context" + "fmt" + "github.com/sigstore/cosign/v2/cmd/cosign/cli" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" + "os" + "regexp" + "testing" + "time" +) + +// Framework is a helper struct for testing +// the cosignwebhook in a k8s cluster +type Framework struct { + k8s *kubernetes.Clientset +} + +func New() (*Framework, error) { + k8s, err := createClientSet() + if err != nil { + return nil, err + } + + return &Framework{ + k8s: k8s, + }, nil +} + +// CreateDeployment creates a deployment in the testing namespace +func (f *Framework) CreateDeployment(t testing.TB, d appsv1.Deployment) { + _, err := f.k8s.AppsV1().Deployments("test-cases").Create(context.Background(), &d, metav1.CreateOptions{}) + if err != nil { + f.Cleanup(t) + } +} + +// WaitForDeployment waits until the deployment is ready +func (f *Framework) WaitForDeployment(t *testing.T, ns, name string) { + + t.Logf("waiting for deployment %s to be ready", name) + // wait until the deployment is ready + w, err := f.k8s.AppsV1().Deployments(ns).Watch(context.Background(), metav1.ListOptions{ + FieldSelector: fmt.Sprintf("metadata.name=%s", name), + }) + + if err != nil { + t.Fatalf("failed watching deployment: %v", err) + f.Cleanup(t) + } + for event := range w.ResultChan() { + deployment, ok := event.Object.(*appsv1.Deployment) + if !ok { + continue + } + + if deployment.Status.ReadyReplicas == 1 { + t.Logf("deployment %s is ready", name) + return + } + } + + t.Fatalf("deployment %s is not ready", name) +} + +// Cleanup removes all resources created by the framework +// and cleans up the testing directory +func (f *Framework) Cleanup(t testing.TB) { + cleanupKeys(t) + f.cleanupDeployments(t) + f.cleanupSecrets(t) +} + +// cleanupKeys removes all keypair files from the testing directory +func cleanupKeys(t testing.TB) { + + t.Logf("cleaning up keypair files") + files, err := os.ReadDir(".") + if err != nil { + t.Fatalf("failed reading directory: %v", err) + } + for _, f := range files { + if f.IsDir() { + continue + } + reKey := regexp.MustCompile(".*.key") + rePub := regexp.MustCompile(".*.pub") + if reKey.MatchString(f.Name()) || rePub.MatchString(f.Name()) { + err = os.Remove(f.Name()) + if err != nil { + t.Fatalf("failed removing file %s: %v", f.Name(), err) + } + } + } + t.Logf("cleaned up keypair files for") +} + +// CreateKeys creates a signing keypair for cosing with the provided name +func (f *Framework) CreateKeys(t testing.TB, name string) (string, string) { + args := []string{fmt.Sprintf("--output-key-prefix=%s", name)} + err := os.Setenv("COSIGN_PASSWORD", "") + if err != nil { + t.Fatalf("failed setting COSIGN_PASSWORD: %v", err) + } + cmd := cli.GenerateKeyPair() + cmd.SetArgs(args) + err = cmd.Execute() + if err != nil { + f.Cleanup(t) + t.Fatalf("failed creating keypair: %v", err) + } + + // read private key and public key from the current directory + privateKey, err := os.ReadFile(fmt.Sprintf("%s.key", name)) + if err != nil { + f.Cleanup(t) + t.Fatalf("failed reading private key: %v", err) + } + pubKey, err := os.ReadFile(fmt.Sprintf("%s.pub", name)) + if err != nil { + f.Cleanup(t) + t.Fatalf("failed reading public key: %v", err) + } + + return string(privateKey), string(pubKey) +} + +// SignContainer signs the container with the provided private key +func (f *Framework) SignContainer(t *testing.T, priv, img string) { + // TODO: find a way to simplify this function - maybe use cosing CLI directly? + // get SHA of the container image + t.Setenv("COSIGN_PASSWORD", "") + args := []string{ + "sign", + img, + } + t.Setenv("COSIGN_PASSWORD", "") + cmd := cli.New() + _ = cmd.Flags().Set("timeout", "30s") + cmd.SetArgs(args) + + // find the sign subcommand in the commands slice + for _, c := range cmd.Commands() { + if c.Name() == "sign" { + cmd = c + break + } + } + _ = cmd.Flags().Set("key", fmt.Sprintf("%s.key", priv)) + _ = cmd.Flags().Set("tlog-upload", "false") + _ = cmd.Flags().Set("yes", "true") + _ = cmd.Flags().Set("allow-http-registry", "true") + err := cmd.Execute() + if err != nil { + t.Fatalf("failed signing container: %v", err) + } +} + +// cleanupDeployments removes all deployments from the testing namespace, +// if they exist +func (f *Framework) cleanupDeployments(t testing.TB) { + + t.Logf("cleaning up deployments") + deployments, err := f.k8s.AppsV1().Deployments("test-cases").List(context.Background(), metav1.ListOptions{}) + if err != nil { + t.Fatalf("failed listing deployments: %v", err) + } + for _, d := range deployments.Items { + err = f.k8s.AppsV1().Deployments("test-cases").Delete(context.Background(), d.Name, metav1.DeleteOptions{}) + if err != nil { + t.Fatalf("failed deleting deployment %s: %v", d.Name, err) + } + } + + timeout := time.After(30 * time.Second) + for { + select { + case <-timeout: + t.Fatalf("timeout reached while waiting for pods to be deleted") + default: + pods, err := f.k8s.CoreV1().Pods("test-cases").List(context.Background(), metav1.ListOptions{}) + if err != nil { + t.Fatalf("failed listing pods: %v", err) + } + + if len(pods.Items) == 0 { + t.Logf("All pods are deleted") + return + } + time.Sleep(5 * time.Second) + } + } +} + +// CreateSecret creates a secret in the testing namespace +func (f *Framework) CreateSecret(t *testing.T, secret corev1.Secret) { + t.Logf("creating secret %s", secret.Name) + s, err := f.k8s.CoreV1().Secrets("test-cases").Create(context.Background(), &secret, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("failed creating secret: %v", err) + } + t.Logf("created secret %s", s.Name) +} + +// cleanupSecrets removes all secrets from the testing namespace, +func (f *Framework) cleanupSecrets(t testing.TB) { + + t.Logf("cleaning up secrets") + secrets, err := f.k8s.CoreV1().Secrets("test-cases").List(context.Background(), metav1.ListOptions{}) + if err != nil { + t.Fatalf("failed listing secrets: %v", err) + } + for _, s := range secrets.Items { + err = f.k8s.CoreV1().Secrets("test-cases").Delete(context.Background(), s.Name, metav1.DeleteOptions{}) + if err != nil { + t.Fatalf("failed deleting secret %s: %v", s.Name, err) + } + } +} + +func createClientSet() (k8sClient *kubernetes.Clientset, err error) { + kubeconfig := os.Getenv("KUBECONFIG") + if kubeconfig == "" { + kubeconfig = os.Getenv("HOME") + "/.kube/config" + } + + // create restconfig from kubeconfig + config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) + if err != nil { + return nil, err + } + + cs, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, err + } + return cs, nil +} diff --git a/test/main_test.go b/test/main_test.go index d8d386b..9c5b0db 100644 --- a/test/main_test.go +++ b/test/main_test.go @@ -12,7 +12,12 @@ func TestDeployments(t *testing.T) { } testFuncs := map[string]func(t *testing.T){ - "OneContainerPubKeyEnvVar": testOneContainerPubKeyEnvVar, + "OneContainerSinglePubKeyEnvRef": testOneContainerSinglePubKeyEnvRef, + "TwoContainersSinglePubKeyEnvRef": testTwoContainersSinglePubKeyEnvRef, + "OneContainerSinglePubKeySecretRef": testOneContainerSinglePubKeySecretRef, + "TwoContainersSinglePubKeyMixedRef": testTwoContainersSinglePubKeyMixedRef, + "TwoContainersMixedPubKeyMixedRef": testTwoContainersMixedPubKeyMixedRef, + "TwoContainersSingleWithInitPubKeyMixedRef": testTwoContainersWithInitSinglePubKeyMixedRef, } for name, tf := range testFuncs { diff --git a/test/webhook_test.go b/test/webhook_test.go index cff36c6..80b2a4c 100644 --- a/test/webhook_test.go +++ b/test/webhook_test.go @@ -1,131 +1,185 @@ package test import ( - "context" - "fmt" - "os" + "github.com/eumel8/cosignwebhook/test/framework" "testing" "github.com/eumel8/cosignwebhook/webhook" - "github.com/sigstore/cosign/v2/cmd/cosign/cli" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/tools/clientcmd" ) -// createKeys creates a signing keypair for cosing with the provided name -func createKeys(t testing.TB, name string) (string, string) { - args := []string{fmt.Sprintf("--output-key-prefix=%s", name)} - err := os.Setenv("COSIGN_PASSWORD", "") - if err != nil { - t.Fatalf("failed setting COSIGN_PASSWORD: %v", err) - } - cmd := cli.GenerateKeyPair() - cmd.SetArgs(args) - err = cmd.Execute() - if err != nil { - cleanupKeys(t, name) - t.Fatalf("failed generating keypair: %v", err) - } +// terminationGracePeriodSeconds is the termination grace period for the test deployments +var terminationGracePeriodSeconds int64 = 3 - // read private key and public key from the current directory - privateKey, err := os.ReadFile(fmt.Sprintf("%s.key", name)) +// testOneContainerSinglePubKeyEnvRef tests that a deployment with a single signed container, +// with a public key provided via an environment variable, succeeds. +func testOneContainerSinglePubKeyEnvRef(t *testing.T) { + fw, err := framework.New() if err != nil { - t.Fatalf("failed reading private key: %v", err) + t.Fatal(err) } - pubKey, err := os.ReadFile(fmt.Sprintf("%s.pub", name)) - if err != nil { - t.Fatalf("failed reading public key: %v", err) + + _, pub := fw.CreateKeys(t, "test") + fw.SignContainer(t, "test", "k3d-registry.localhost:5000/busybox:first") + + // create a deployment with a single signed container and a public key provided via an environment variable + depl := appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-case-1", + Namespace: "test-cases", + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "test-case-1"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app": "test-case-1"}, + }, + Spec: corev1.PodSpec{ + TerminationGracePeriodSeconds: &terminationGracePeriodSeconds, + Containers: []corev1.Container{ + { + Name: "test-case-1", + Image: "k3d-registry.localhost:5000/busybox:latest", + Command: []string{ + "sh", + "-c", + "while true; do echo 'hello world, i am tired and will sleep now'; sleep 10; done", + }, + Env: []corev1.EnvVar{ + { + Name: webhook.CosignEnvVar, + Value: pub, + }, + }, + }, + }, + }, + }, + }, } - return string(privateKey), string(pubKey) + fw.CreateDeployment(t, depl) + fw.WaitForDeployment(t, "test-cases", "test-case-1") + fw.Cleanup(t) } -// cleanupKeys removes all keypair files with the passed name from the testing directory -func cleanupKeys(t testing.TB, name string) { +// testTwoContainersSinglePubKeyEnvRef tests that a deployment with two signed containers, +// with a public key provided via an environment variable, succeeds. +func testTwoContainersSinglePubKeyEnvRef(t *testing.T) { - t.Logf("cleaning up keypair files for %s", name) - // check if the keypair files exist - _, err := os.Stat(fmt.Sprintf("%s.key", name)) + fw, err := framework.New() if err != nil { - t.Fatalf("failed reading private key: %v", err) + t.Fatal(err) } - _, err = os.Stat(fmt.Sprintf("%s.pub", name)) - if err != nil { - t.Fatalf("failed reading public key: %v", err) - } + _, pub := fw.CreateKeys(t, "test") + fw.SignContainer(t, "test", "k3d-registry.localhost:5000/busybox:first") + fw.SignContainer(t, "test", "k3d-registry.localhost:5000/busybox:second") - err = os.Remove(fmt.Sprintf("%s.key", name)) - if err != nil { - t.Fatalf("failed removing private key: %v", err) - } - err = os.Remove(fmt.Sprintf("%s.pub", name)) - if err != nil { - t.Fatalf("failed removing public key: %v", err) + // create a deployment with two signed containers and a public key provided via an environment variable + depl := appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-case-2", + Namespace: "test-cases", + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "test-case-2"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app": "test-case-2"}, + }, + Spec: corev1.PodSpec{ + TerminationGracePeriodSeconds: &terminationGracePeriodSeconds, + Containers: []corev1.Container{ + { + Name: "test-case-2-first", + Image: "k3d-registry.localhost:5000/busybox:first", + Command: []string{ + "sh", + "-c", + "while true; do echo 'hello world, i am tired and will sleep now'; sleep 10; done", + }, + Env: []corev1.EnvVar{ + { + Name: webhook.CosignEnvVar, + Value: pub, + }, + }, + }, + { + Name: "test-case-2-second", + Image: "k3d-registry.localhost:5000/busybox:second", + Command: []string{ + "sh", + "-c", + "while true; do echo 'hello world, i am tired and will sleep now'; sleep 10; done", + }, + Env: []corev1.EnvVar{ + { + Name: webhook.CosignEnvVar, + Value: pub, + }, + }, + }, + }, + }, + }, + }, } - t.Logf("cleaned up keypair files for %s", name) -} -// signContainer signs the container with the provided private key -// TODO: find a way to simplify this function - maybe use cosing CLI directly? -func signContainer(t *testing.T, priv, img string) error { - args := []string{ - "sign", - img, - } - t.Setenv("COSIGN_PASSWORD", "") - cmd := cli.New() - _ = cmd.Flags().Set("timeout", "30s") - cmd.SetArgs(args) - - // find the sign subcommand in the commands slice - for _, c := range cmd.Commands() { - if c.Name() == "sign" { - cmd = c - break - } - } - _ = cmd.Flags().Set("key", fmt.Sprintf("%s.key", priv)) - _ = cmd.Flags().Set("tlog-upload", "false") - _ = cmd.Flags().Set("yes", "true") - _ = cmd.Flags().Set("allow-http-registry", "true") - return cmd.Execute() + fw.CreateDeployment(t, depl) + fw.WaitForDeployment(t, "test-cases", "test-case-2") + fw.Cleanup(t) } -// testOneContainerPubKeyEnvVar tests that a deployment with a single signed container, -// with a public key provided via an environment variable, succeeds. -func testOneContainerPubKeyEnvVar(t *testing.T) { - // create a keypair to sign the container - _, pub := createKeys(t, "test") - t.Setenv("COSIGN_PASSWORD", "") +// testOneContainerPubKeySecret tests that a deployment with a single signed container, +// with a public key provided via a secret, succeeds. +func testOneContainerSinglePubKeySecretRef(t *testing.T) { - // sign the container with the ephemeral keypair - err := signContainer(t, "test", "k3d-registry.localhost:5000/busybox:dev@sha256:023917ec6a886d0e8e15f28fb543515a5fcd8d938edb091e8147db4efed388ee") + fw, err := framework.New() if err != nil { - t.Fatalf("failed signing container: %v", err) + t.Fatal(err) } - // create a deployment with a single signed container and a public key provided via an environment variable + _, pub := fw.CreateKeys(t, "test") + fw.SignContainer(t, "test", "k3d-registry.localhost:5000/busybox:first") + + // create a secret with the public key + secret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-case-3", + Namespace: "test-cases", + }, + StringData: map[string]string{ + "cosign.pub": pub, + }, + } + + // create a deployment with a single signed container and a public key provided via a secret depl := appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ - Name: "test-case-1", + Name: "test-case-3", Namespace: "test-cases", }, Spec: appsv1.DeploymentSpec{ Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"app": "test-case-1"}, + MatchLabels: map[string]string{"app": "test-case-3"}, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{"app": "test-case-1"}, + Labels: map[string]string{"app": "test-case-3"}, }, Spec: corev1.PodSpec{ + TerminationGracePeriodSeconds: &terminationGracePeriodSeconds, Containers: []corev1.Container{ { - Name: "test-case-1", + Name: "test-case-3", Image: "k3d-registry.localhost:5000/busybox:latest", Command: []string{ "sh", @@ -134,8 +188,15 @@ func testOneContainerPubKeyEnvVar(t *testing.T) { }, Env: []corev1.EnvVar{ { - Name: webhook.CosignEnvVar, - Value: pub, + Name: webhook.CosignEnvVar, + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + Key: "cosign.pub", + LocalObjectReference: corev1.LocalObjectReference{ + Name: "test-case-3", + }, + }, + }, }, }, }, @@ -145,80 +206,282 @@ func testOneContainerPubKeyEnvVar(t *testing.T) { }, } - // create clientset - k8sClient, err := createClientSet() - if err != nil { - cleanupKeys(t, "test") - t.Fatalf("failed creating clientset: %v", err) - } + fw.CreateSecret(t, secret) + fw.CreateDeployment(t, depl) + fw.WaitForDeployment(t, "test-cases", "test-case-3") + fw.Cleanup(t) +} - // create the deployment - _, err = k8sClient.AppsV1().Deployments("test-cases").Create(context.Background(), &depl, metav1.CreateOptions{}) +// testTwoContainersMixedPubKeyMixedRef tests that a deployment with two signed containers with two different public keys, +// with the keys provided by a secret and an environment variable, succeeds. +func testTwoContainersMixedPubKeyMixedRef(t *testing.T) { + fw, err := framework.New() if err != nil { - cleanupKeys(t, "test") - t.Fatalf("failed creating deployment: %v", err) + t.Fatal(err) } - // wait for the deployment to be ready - err = waitForDeploymentReady(t, k8sClient, "test-cases", "test-case-1") - if err != nil { - cleanupKeys(t, "test") - t.Fatalf("failed waiting for deployment to be ready: %v", err) + _, pub1 := fw.CreateKeys(t, "test1") + _, pub2 := fw.CreateKeys(t, "test2") + fw.SignContainer(t, "test1", "k3d-registry.localhost:5000/busybox:first") + fw.SignContainer(t, "test2", "k3d-registry.localhost:5000/busybox:second") + + // create a secret with the public key + secret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-case-4", + Namespace: "test-cases", + }, + StringData: map[string]string{ + "cosign.pub": pub1, + }, } - // delete the deployment - err = k8sClient.AppsV1().Deployments("test-cases").Delete(context.Background(), "test-case-1", metav1.DeleteOptions{}) - if err != nil { - cleanupKeys(t, "test") - t.Fatalf("failed deleting deployment: %v", err) + // create a deployment with two signed containers and a public key provided via a secret + depl := appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-case-4", + Namespace: "test-cases", + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "test-case-4"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app": "test-case-4"}, + }, + Spec: corev1.PodSpec{ + TerminationGracePeriodSeconds: &terminationGracePeriodSeconds, + Containers: []corev1.Container{ + { + Name: "test-case-4-first", + Image: "k3d-registry.localhost:5000/busybox:first", + Command: []string{ + "sh", + "-c", + "while true; do echo 'hello world, i am tired and will sleep now'; sleep 10; done", + }, + Env: []corev1.EnvVar{ + { + Name: webhook.CosignEnvVar, + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + Key: "cosign.pub", + LocalObjectReference: corev1.LocalObjectReference{ + Name: "test-case-4", + }, + }, + }, + }, + }, + }, + { + Name: "test-case-4-second", + Image: "k3d-registry.localhost:5000/busybox:second", + Command: []string{ + "sh", + "-c", + "while true; do echo 'hello world, i am tired and will sleep now'; sleep 10; done", + }, + Env: []corev1.EnvVar{ + { + Name: webhook.CosignEnvVar, + Value: pub2, + }, + }, + }, + }, + }, + }, + }, } - // cleanup the keypair - cleanupKeys(t, "test") -} + fw.CreateSecret(t, secret) + fw.CreateDeployment(t, depl) + fw.WaitForDeployment(t, "test-cases", "test-case-4") + fw.Cleanup(t) -// waitForDeploymentReady waits for the deployment to be ready -func waitForDeploymentReady(t *testing.T, k8sClient *kubernetes.Clientset, ns, name string) error { +} - t.Logf("waiting for deployment %s to be ready", name) - // wait until the deployment is ready - w, err := k8sClient.AppsV1().Deployments(ns).Watch(context.Background(), metav1.ListOptions{ - FieldSelector: fmt.Sprintf("metadata.name=%s", name), - }) +// testTwoContainersSinglePubKeyMixedRef tests that a deployment with two signed containers, +// with a public key provided via a secret and an environment variable, succeeds. +func testTwoContainersSinglePubKeyMixedRef(t *testing.T) { + fw, err := framework.New() if err != nil { - return err + t.Fatal(err) + } + + _, pub := fw.CreateKeys(t, "test") + fw.SignContainer(t, "test", "k3d-registry.localhost:5000/busybox:first") + fw.SignContainer(t, "test", "k3d-registry.localhost:5000/busybox:second") + + // create a secret with the public key + secret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-case-5", + Namespace: "test-cases", + }, + StringData: map[string]string{ + "cosign.pub": pub, + }, } - for event := range w.ResultChan() { - deployment, ok := event.Object.(*appsv1.Deployment) - if !ok { - continue - } - if deployment.Status.ReadyReplicas == 1 { - t.Logf("deployment %s is ready", name) - return nil - } + // create a deployment with two signed containers and a public key provided via a secret + depl := appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-case-5", + Namespace: "test-cases", + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "test-case-5"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app": "test-case-5"}, + }, + Spec: corev1.PodSpec{ + TerminationGracePeriodSeconds: &terminationGracePeriodSeconds, + Containers: []corev1.Container{ + { + Name: "test-case-5-first", + Image: "k3d-registry.localhost:5000/busybox:first", + Command: []string{ + "sh", + "-c", + "while true; do echo 'hello world, i am tired and will sleep now'; sleep 10; done", + }, + Env: []corev1.EnvVar{ + { + Name: webhook.CosignEnvVar, + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + Key: "cosign.pub", + LocalObjectReference: corev1.LocalObjectReference{ + Name: "test-case-5", + }, + }, + }, + }, + }, + }, + { + Name: "test-case-5-second", + Image: "k3d-registry.localhost:5000/busybox:second", + Command: []string{ + "sh", + "-c", + "while true; do echo 'hello world, i am tired and will sleep now'; sleep 10; done", + }, + Env: []corev1.EnvVar{ + { + Name: webhook.CosignEnvVar, + Value: pub, + }, + }, + }, + }, + }, + }, + }, } - return nil + fw.CreateSecret(t, secret) + fw.CreateDeployment(t, depl) + fw.WaitForDeployment(t, "test-cases", "test-case-5") + fw.Cleanup(t) + } -func createClientSet() (k8sClient *kubernetes.Clientset, err error) { - kubeconfig := os.Getenv("KUBECONFIG") - if kubeconfig == "" { - kubeconfig = os.Getenv("HOME") + "/.kube/config" - } +// testTwoContainersSinglePubKeyMixedRef tests that a deployment with two signed containers, +// with a public key provided via a secret and an environment variable, succeeds. +func testTwoContainersWithInitSinglePubKeyMixedRef(t *testing.T) { - // create restconfig from kubeconfig - config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) + fw, err := framework.New() if err != nil { - return nil, err + t.Fatal(err) } - cs, err := kubernetes.NewForConfig(config) - if err != nil { - return nil, err + _, pub := fw.CreateKeys(t, "test") + fw.SignContainer(t, "test", "k3d-registry.localhost:5000/busybox:first") + fw.SignContainer(t, "test", "k3d-registry.localhost:5000/busybox:second") + + // create a secret with the public key + secret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-case-6", + Namespace: "test-cases", + }, + StringData: map[string]string{ + "cosign.pub": pub, + }, + } + + // create a deployment with two signed containers and a public key provided via a secret + depl := appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-case-6", + Namespace: "test-cases", + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "test-case-6"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app": "test-case-6"}, + }, + Spec: corev1.PodSpec{ + TerminationGracePeriodSeconds: &terminationGracePeriodSeconds, + InitContainers: []corev1.Container{ + { + Name: "test-case-6-first", + Image: "k3d-registry.localhost:5000/busybox:first", + Command: []string{ + "sh", + "-c", + "echo 'hello world, i am tired and will sleep now, for a bit...';", + }, + Env: []corev1.EnvVar{ + { + Name: webhook.CosignEnvVar, + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + Key: "cosign.pub", + LocalObjectReference: corev1.LocalObjectReference{ + Name: "test-case-6", + }, + }, + }, + }, + }, + }, + }, + Containers: []corev1.Container{ + { + Name: "test-case-6-second", + Image: "k3d-registry.localhost:5000/busybox:second", + Command: []string{ + "sh", + "-c", + "while true; do echo 'hello world, i am tired and will sleep now'; sleep 10; done", + }, + Env: []corev1.EnvVar{ + { + Name: webhook.CosignEnvVar, + Value: pub, + }, + }, + }, + }, + }, + }, + }, } - return cs, nil + + fw.CreateSecret(t, secret) + fw.CreateDeployment(t, depl) + fw.WaitForDeployment(t, "test-cases", "test-case-6") + fw.Cleanup(t) }