github.com/redhat-appstudio/e2e-tests@v0.0.0-20240520140907-9709f6f59323/tests/build/tkn-bundle.go (about)

     1  package build
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"os"
     7  	"strings"
     8  
     9  	"github.com/google/go-containerregistry/pkg/authn"
    10  	"github.com/google/go-containerregistry/pkg/crane"
    11  	. "github.com/onsi/ginkgo/v2"
    12  	. "github.com/onsi/gomega"
    13  	kubeapi "github.com/redhat-appstudio/e2e-tests/pkg/clients/kubernetes"
    14  	"github.com/redhat-appstudio/e2e-tests/pkg/constants"
    15  	"github.com/redhat-appstudio/e2e-tests/pkg/framework"
    16  	"github.com/redhat-appstudio/e2e-tests/pkg/utils"
    17  	"github.com/redhat-appstudio/e2e-tests/pkg/utils/build"
    18  	"github.com/tektoncd/cli/pkg/bundle"
    19  	pipeline "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
    20  	corev1 "k8s.io/api/core/v1"
    21  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    22  	"k8s.io/apimachinery/pkg/runtime"
    23  )
    24  
    25  /* to run locally on a kind cluster
    26  1. set environment variables with examples
    27    - E2E_APPLICATIONS_NAMESPACE=konflux-tasks
    28    - TKN_BUNDLE_REPO=quay.io/my-user-org/tkn-bundle:latest
    29  2. AFTER the namespace is created, create a docker secret and patch the sa
    30    - kubectl create secret generic docker-config --from-file=.dockerconfigjson="$HOME/.docker/config.json" --type=kubernetes.io/dockerconfigjson --dry-run=client -o yaml | kubectl apply -f
    31    - kubectl patch serviceaccount appstudio-pipeline -p '{"imagePullSecrets": [{"name": "docker-config"}], "secrets": [{"name": "docker-config"}]}'
    32  */
    33  
    34  var _ = framework.TknBundleSuiteDescribe("tkn bundle task", Label("tasks", "HACBS"), func() {
    35  
    36  	defer GinkgoRecover()
    37  
    38  	var namespace string
    39  	var kubeClient *framework.ControllerHub
    40  	var fwk *framework.Framework
    41  	var taskName string = "tkn-bundle"
    42  	var pathInRepo string = fmt.Sprintf("task/%s/0.1/%s.yaml", taskName, taskName)
    43  	var pvcName string = "source-pvc"
    44  	var pvcAccessMode corev1.PersistentVolumeAccessMode = "ReadWriteOnce"
    45  	var baseTaskRun *pipeline.TaskRun
    46  	var qeBundleRepo string = fmt.Sprintf("quay.io/%s/test-images:%s", utils.GetQuayIOOrganization(), taskName)
    47  
    48  	var gitRevision, gitURL, bundleImg string
    49  
    50  	AfterEach(framework.ReportFailure(&fwk))
    51  
    52  	BeforeAll(func() {
    53  		namespace = os.Getenv(constants.E2E_APPLICATIONS_NAMESPACE_ENV)
    54  		if len(namespace) > 0 {
    55  			adminClient, err := kubeapi.NewAdminKubernetesClient()
    56  			Expect(err).ShouldNot(HaveOccurred())
    57  			kubeClient, err = framework.InitControllerHub(adminClient)
    58  			Expect(err).ShouldNot(HaveOccurred())
    59  			_, err = kubeClient.CommonController.CreateTestNamespace(namespace)
    60  			Expect(err).ShouldNot(HaveOccurred())
    61  
    62  			// set a custom bundle repo for the task
    63  			bundleImg = utils.GetEnv("TKN_BUNDLE_REPO", qeBundleRepo)
    64  		} else {
    65  			var err error
    66  			fwk, err = framework.NewFramework(utils.GetGeneratedNamespace("konflux-task-runner"))
    67  			Expect(err).NotTo(HaveOccurred())
    68  			Expect(fwk.UserNamespace).NotTo(BeNil(), "failed to create sandbox user")
    69  			namespace = fwk.UserNamespace
    70  			kubeClient = fwk.AsKubeAdmin
    71  
    72  			err = kubeClient.CommonController.CreateQuayRegistrySecret(namespace)
    73  			Expect(err).NotTo(HaveOccurred())
    74  			bundleImg = qeBundleRepo
    75  		}
    76  
    77  		// resolve the gitURL and gitRevision
    78  		var err error
    79  		gitURL, gitRevision, err = build.ResolveGitDetails(constants.TASK_REPO_URL_ENV, constants.TASK_REPO_REVISION_ENV)
    80  		Expect(err).NotTo(HaveOccurred())
    81  
    82  		// if pvc does not exist create it
    83  		if _, err := kubeClient.TektonController.GetPVC(pvcName, namespace); err != nil {
    84  			_, err = kubeClient.TektonController.CreatePVCInAccessMode(pvcName, namespace, pvcAccessMode)
    85  			Expect(err).NotTo(HaveOccurred())
    86  		}
    87  		// use a pod to copy test data to the pvc
    88  		testData, err := setupTestData(pvcName)
    89  		Expect(err).NotTo(HaveOccurred())
    90  		pod, err := kubeClient.CommonController.CreatePod(testData, namespace)
    91  		Expect(err).NotTo(HaveOccurred())
    92  		// wait for setup pod. make sure it's successful
    93  		err = kubeClient.CommonController.WaitForPod(kubeClient.CommonController.IsPodSuccessful(pod.Name, namespace), 300)
    94  		Expect(err).NotTo(HaveOccurred())
    95  	})
    96  
    97  	BeforeEach(func() {
    98  		resolverRef := pipeline.ResolverRef{
    99  			Resolver: "git",
   100  			Params: []pipeline.Param{
   101  				{Name: "url", Value: *pipeline.NewStructuredValues(gitURL)},
   102  				{Name: "revision", Value: *pipeline.NewStructuredValues(gitRevision)},
   103  				{Name: "pathInRepo", Value: *pipeline.NewStructuredValues(pathInRepo)},
   104  			},
   105  		}
   106  		// get a new taskRun on each Entry
   107  		baseTaskRun = taskRunTemplate(taskName, pvcName, bundleImg, resolverRef)
   108  	})
   109  
   110  	DescribeTable("creates Tekton bundles with different params",
   111  		func(params map[string]string, expectedOutput, notExpectedOutput []string, expectedHomeVar, stepImage string) {
   112  			for key, val := range params {
   113  				baseTaskRun.Spec.Params = append(baseTaskRun.Spec.Params, pipeline.Param{
   114  					Name: key,
   115  					Value: pipeline.ParamValue{
   116  						Type:      "string",
   117  						StringVal: val,
   118  					},
   119  				})
   120  			}
   121  			tr, err := kubeClient.TektonController.RunTaskAndWait(baseTaskRun, namespace)
   122  			Expect(err).NotTo(HaveOccurred())
   123  
   124  			// check for a success of the taskRun
   125  			status, err := kubeClient.TektonController.CheckTaskRunSucceeded(tr.Name, namespace)()
   126  			Expect(err).NotTo(HaveOccurred())
   127  			Expect(status).To(BeTrue(), fmt.Sprintf("taskRun %q failed", tr.Name))
   128  
   129  			// verify taskRun results
   130  			imgUrl, err := kubeClient.TektonController.GetResultFromTaskRun(tr, "IMAGE_URL")
   131  			Expect(err).NotTo(HaveOccurred())
   132  			Expect(imgUrl).To(Equal(bundleImg))
   133  
   134  			imgDigest, err := kubeClient.TektonController.GetResultFromTaskRun(tr, "IMAGE_DIGEST")
   135  			Expect(err).NotTo(HaveOccurred())
   136  			Expect(imgDigest).To(MatchRegexp(`^sha256:[a-fA-F0-9]{64}$`))
   137  
   138  			// verify taskRun log output
   139  			podLogs, err := kubeClient.CommonController.GetPodLogsByName(tr.Status.PodName, namespace)
   140  			Expect(err).NotTo(HaveOccurred())
   141  			podLog := fmt.Sprintf("pod-%s-step-build.log", tr.Status.PodName)
   142  			matchOutput(podLogs[podLog], expectedOutput)
   143  			notMatchOutput(podLogs[podLog], notExpectedOutput)
   144  
   145  			// verify environment variables
   146  			envVar, err := kubeClient.TektonController.GetEnvVariable(tr, "HOME")
   147  			Expect(err).NotTo(HaveOccurred())
   148  			Expect(envVar).To(Equal(expectedHomeVar))
   149  
   150  			// verify the step images
   151  			visitor := func(apiVersion, kind, name string, obj runtime.Object, raw []byte) {
   152  				task := &pipeline.Task{}
   153  				err := json.Unmarshal(raw, task)
   154  				Expect(err).ToNot(HaveOccurred())
   155  				for _, step := range task.Spec.Steps {
   156  					Expect(step.Image).To(Equal(stepImage))
   157  				}
   158  			}
   159  			bundle := fmt.Sprintf("%s@%s", imgUrl, imgDigest)
   160  			GinkgoWriter.Print(bundle)
   161  			fetchImage(fmt.Sprintf("%s@%s", imgUrl, imgDigest), visitor)
   162  
   163  		},
   164  		Entry("when context points to a file", map[string]string{"CONTEXT": "task2.yaml"},
   165  			[]string{
   166  				"\t- Added Task: task2 to image",
   167  			},
   168  			[]string{
   169  				"\t- Added Task: task1 to image",
   170  				"\t- Added Task: task3 to image",
   171  			},
   172  			"/tekton/home",
   173  			"ubuntu",
   174  		),
   175  		Entry("creates Tekton bundles from specific context", map[string]string{"CONTEXT": "sub"}, []string{
   176  			"\t- Added Task: task3 to image",
   177  		},
   178  			[]string{
   179  				"\t- Added Task: task1 to image",
   180  				"\t- Added Task: task2 to image",
   181  			},
   182  			"/tekton/home",
   183  			"ubuntu",
   184  		),
   185  		Entry("when context is the root directory", map[string]string{}, []string{
   186  			"\t- Added Task: task1 to image",
   187  			"\t- Added Task: task2 to image",
   188  			"\t- Added Task: task3 to image",
   189  		},
   190  			[]string{},
   191  			"/tekton/home",
   192  			"ubuntu",
   193  		),
   194  		Entry("creates Tekton bundles when context points to a file and a directory", map[string]string{"CONTEXT": "task2.yaml,sub"}, []string{
   195  			"\t- Added Task: task2 to image",
   196  			"\t- Added Task: task3 to image",
   197  		},
   198  			[]string{
   199  				"\t- Added Task: task1 to image",
   200  			},
   201  			"/tekton/home",
   202  			"ubuntu",
   203  		),
   204  		Entry("creates Tekton bundles when using negation", map[string]string{"CONTEXT": "!sub"}, []string{
   205  			"\t- Added Task: task1 to image",
   206  			"\t- Added Task: task2 to image",
   207  		},
   208  			[]string{
   209  				"\t- Added Task: task3 to image",
   210  			},
   211  			"/tekton/home",
   212  			"ubuntu",
   213  		),
   214  		Entry("allows overriding HOME environment variable", map[string]string{"CONTEXT": ".", "HOME": "/tekton/summer-home"}, []string{
   215  			"\t- Added Task: task1 to image",
   216  			"\t- Added Task: task2 to image",
   217  			"\t- Added Task: task3 to image",
   218  		},
   219  			[]string{},
   220  			"/tekton/summer-home",
   221  			"ubuntu",
   222  		),
   223  		Entry("allows overriding STEP image", map[string]string{"STEPS_IMAGE": "quay.io/enterprise-contract/contract:latest"}, []string{
   224  			"\t- Added Task: task1 to image",
   225  			"\t- Added Task: task2 to image",
   226  			"\t- Added Task: task3 to image",
   227  		},
   228  			[]string{},
   229  			"/tekton/home",
   230  			"quay.io/enterprise-contract/contract:latest",
   231  		),
   232  	)
   233  })
   234  
   235  // check output that should exist
   236  func matchOutput(logs []byte, expectedOutput []string) {
   237  	for _, out := range expectedOutput {
   238  		Expect(strings.Split(string(logs), "\n")).To(ContainElement(out))
   239  	}
   240  }
   241  
   242  // check that output does not exist
   243  func notMatchOutput(logs []byte, expectedOutput []string) {
   244  	for _, out := range expectedOutput {
   245  		Expect(strings.Split(string(logs), "\n")).NotTo(ContainElement(out))
   246  	}
   247  }
   248  
   249  // fetch the image
   250  func fetchImage(image string, visitor func(version, kind, name string, element runtime.Object, raw []byte)) {
   251  	img, err := crane.Pull(image, crane.WithAuthFromKeychain(authn.DefaultKeychain))
   252  	Expect(err).ToNot(HaveOccurred())
   253  
   254  	err = bundle.List(img, visitor)
   255  	Expect(err).ToNot(HaveOccurred())
   256  }
   257  
   258  // sets the task files on a pvc for use by the task
   259  func setupTestData(pvcName string) (*corev1.Pod, error) {
   260  	// setup test data
   261  	testTasks, err := testData([]string{"task1", "task2", "task3"})
   262  	if err != nil {
   263  		return nil, err
   264  	}
   265  
   266  	return &corev1.Pod{
   267  		TypeMeta: metav1.TypeMeta{
   268  			Kind:       "Pod",
   269  			APIVersion: "v1",
   270  		},
   271  		ObjectMeta: metav1.ObjectMeta{
   272  			GenerateName: "setup-pod-",
   273  		},
   274  		Spec: corev1.PodSpec{
   275  			RestartPolicy: "Never",
   276  			Containers: []corev1.Container{
   277  				{
   278  					Command: []string{
   279  						"bash",
   280  						"-c",
   281  						"mkdir -p /source/sub; echo $TASK1_JSON > /source/task1.yaml; echo $TASK2_JSON > /source/task2.yaml; echo $TASK3_JSON > /source/sub/task3.yaml",
   282  					},
   283  					Image: "registry.access.redhat.com/ubi9/ubi-minimal:latest",
   284  					Name:  "setup-pod",
   285  					VolumeMounts: []corev1.VolumeMount{
   286  						{
   287  							MountPath: "/source",
   288  							Name:      "source",
   289  						},
   290  					},
   291  					Env: []corev1.EnvVar{
   292  						{
   293  							Name:  "TASK1_JSON",
   294  							Value: testTasks["task1"],
   295  						},
   296  						{
   297  							Name:  "TASK2_JSON",
   298  							Value: testTasks["task2"],
   299  						},
   300  						{
   301  							Name:  "TASK3_JSON",
   302  							Value: testTasks["task3"],
   303  						},
   304  					},
   305  				},
   306  			},
   307  			Volumes: []corev1.Volume{
   308  				{
   309  					Name: "source",
   310  					VolumeSource: corev1.VolumeSource{
   311  						PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
   312  							ClaimName: pvcName,
   313  						},
   314  					},
   315  				},
   316  			},
   317  		},
   318  	}, nil
   319  }
   320  
   321  // the test tasks
   322  func testData(tasks []string) (map[string]string, error) {
   323  	apiVersion := "tekton.dev/v1"
   324  	allTasks := make(map[string]string)
   325  	for idx, task := range tasks {
   326  		taskJson, err := serializeTask(&pipeline.Task{
   327  			TypeMeta: metav1.TypeMeta{
   328  				Kind:       "Task",
   329  				APIVersion: apiVersion,
   330  			},
   331  			ObjectMeta: metav1.ObjectMeta{
   332  				Name: task,
   333  			},
   334  			Spec: pipeline.TaskSpec{
   335  				Steps: []pipeline.Step{
   336  					{
   337  						Name:  fmt.Sprintf("test%d-step", idx),
   338  						Image: "ubuntu",
   339  					},
   340  				},
   341  			},
   342  		})
   343  		if err != nil {
   344  			return nil, err
   345  		}
   346  		allTasks[task] = taskJson
   347  	}
   348  	return allTasks, nil
   349  }
   350  
   351  // the taskRun that runs tkn-bundle
   352  func taskRunTemplate(taskName, pvcName, bundleImg string, resolverRef pipeline.ResolverRef) *pipeline.TaskRun {
   353  	return &pipeline.TaskRun{
   354  		TypeMeta: metav1.TypeMeta{
   355  			Kind:       "Task",
   356  			APIVersion: "tekton.dev/v1",
   357  		},
   358  		ObjectMeta: metav1.ObjectMeta{
   359  			GenerateName: fmt.Sprintf("%s-", taskName),
   360  		},
   361  		Spec: pipeline.TaskRunSpec{
   362  			ServiceAccountName: constants.DefaultPipelineServiceAccount,
   363  			TaskRef: &pipeline.TaskRef{
   364  				ResolverRef: resolverRef,
   365  			},
   366  			Params: pipeline.Params{
   367  				{
   368  					Name: "IMAGE",
   369  					Value: pipeline.ParamValue{
   370  						Type:      "string",
   371  						StringVal: bundleImg,
   372  					},
   373  				},
   374  			},
   375  			Workspaces: []pipeline.WorkspaceBinding{
   376  				{
   377  					Name: "source",
   378  					PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
   379  						ClaimName: pvcName,
   380  					},
   381  				},
   382  			},
   383  		},
   384  	}
   385  }
   386  
   387  func serializeTask(task *pipeline.Task) (string, error) {
   388  	taskJson, err := json.Marshal(task)
   389  	if err != nil {
   390  		return "", err
   391  	}
   392  	return string(taskJson), nil
   393  }