Skip to content

Commit

Permalink
feat: happy path e2e tests
Browse files Browse the repository at this point in the history
  • Loading branch information
puffitos committed Sep 29, 2023
1 parent dcc3e84 commit 5f23750
Show file tree
Hide file tree
Showing 5 changed files with 669 additions and 161 deletions.
9 changes: 4 additions & 5 deletions .github/workflows/end2end.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
30 changes: 14 additions & 16 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,30 +1,18 @@
test-create-cluster:
e2e-cluster:
@echo "Creating registry..."
@k3d registry create registry.localhost --port 5000
@echo "Adding registry to cluster..."
@k3d cluster create cosign-tests --registry-use k3d-registry.localhost:5000
@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..."
Expand All @@ -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 \
Expand Down
243 changes: 243 additions & 0 deletions test/framework/client.go
Original file line number Diff line number Diff line change
@@ -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
}
7 changes: 6 additions & 1 deletion test/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 5f23750

Please sign in to comment.