diff --git a/cmd/imagedigestexporter/main.go b/cmd/imagedigestexporter/main.go index 64937da777a..d31eefe03bc 100644 --- a/cmd/imagedigestexporter/main.go +++ b/cmd/imagedigestexporter/main.go @@ -19,8 +19,8 @@ package main import ( "encoding/json" "flag" - "fmt" "log" + "os" "github.com/google/go-containerregistry/pkg/v1/layout" v1alpha1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" @@ -41,8 +41,7 @@ func main() { flag.Parse() imageResources := []*v1alpha1.ImageResource{} - err := json.Unmarshal([]byte(*images), &imageResources) - if err != nil { + if err := json.Unmarshal([]byte(*images), &imageResources); err != nil { log.Fatalf("Error reading images array: %v", err) } @@ -52,6 +51,7 @@ func main() { if err != nil { // if this image doesn't have a builder that supports index.json file, // then it will be skipped + log.Printf("ImageResource %s doesn't have an index.json file: %s", imageResource.Name, err) continue } digest, err := ii.Digest() @@ -65,5 +65,18 @@ func main() { if err != nil { log.Fatalf("Unexpected error converting images to json %v: %v", output, err) } - fmt.Println(string(imagesJSON)) + log.Printf("Image digest exporter output: %s ", string(imagesJSON)) + f, err := os.OpenFile("/workspace/builder/termination-log", os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + log.Fatalf("Unexpected error converting images to json %v: %v", output, err) + } + defer f.Close() + + _, err = f.Write(imagesJSON) + if err != nil { + log.Fatalf("Unexpected error converting images to json %v: %v", output, err) + } + if err := f.Sync(); err != nil { + log.Fatalf("Unexpected error converting images to json %v: %v", output, err) + } } diff --git a/docs/resources.md b/docs/resources.md index 67c469448bc..1c4656d3a19 100644 --- a/docs/resources.md +++ b/docs/resources.md @@ -291,7 +291,7 @@ spec: ``` If no value is specified for `outputImageDir`, it will default to -`/builder/image-outputs/{resource-name}`. +`/builder/home/image-outputs/{resource-name}`. _Please check the builder tool used on how to pass this path to create the output file._ diff --git a/examples/taskruns/task-multiple-output-image.yaml b/examples/taskruns/task-multiple-output-image.yaml new file mode 100644 index 00000000000..3fb9f0c5ff6 --- /dev/null +++ b/examples/taskruns/task-multiple-output-image.yaml @@ -0,0 +1,128 @@ +apiVersion: tekton.dev/v1alpha1 +kind: PipelineResource +metadata: + name: skaffold-image-leeroy-web-1 +spec: + type: image + params: + - name: url + value: gcr.io/christiewilson-catfactory/leeroy-web # Replace this URL with ${KO_DOCKER_REPO} +--- +apiVersion: tekton.dev/v1alpha1 +kind: PipelineResource +metadata: + name: skaffold-image-leeroy-web-2 +spec: + type: image + params: + - name: url + value: gcr.io/christiewilson-catfactory/leeroy-web # Replace this URL with ${KO_DOCKER_REPO} +--- +# This demo modifies the cluster (deploys to it) you must use a service +# account with permission to admin the cluster (or make your default user an admin +# of the `default` namespace with default-cluster-admin). +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: default-cluster-admin +subjects: + - kind: ServiceAccount + name: default + namespace: default +roleRef: + kind: ClusterRole + name: cluster-admin + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: tekton.dev/v1alpha1 +kind: PipelineResource +metadata: + name: skaffold-git +spec: + type: git + params: + - name: revision + value: master + - name: url + value: https://github.com/GoogleContainerTools/skaffold +--- +#Builds an image via kaniko and pushes it to registry. +apiVersion: tekton.dev/v1alpha1 +kind: Task +metadata: + name: multiple-build-push-kaniko +spec: + inputs: + resources: + - name: sourcerepo + type: git + outputs: + resources: + - name: builtImage1 + type: image + - name: builtImage2 + type: image + steps: + - name: build-and-push-1 + image: busybox + command: + - /bin/sh + args: + - -ce + - | + set -ex + cat < /builder/home/image-outputs/builtImage1/index.json + { + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.index.v1+json", + "size": 314, + "digest": "sha256:05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b5" + } + ] + } + EOF + - name: build-and-push-2 + image: busybox + command: + - /bin/sh + args: + - -ce + - | + set -e + cat < /builder/home/image-outputs/builtImage2/index.json + { + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.index.v1+json", + "size": 314, + "digest": "sha256:05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b5" + } + ] + } + EOF +--- +apiVersion: tekton.dev/v1alpha1 +kind: TaskRun +metadata: + name: multiple-build-push-kaniko-run +spec: + taskRef: + name: multiple-build-push-kaniko + trigger: + type: manual + inputs: + resources: + - name: sourcerepo + resourceRef: + name: skaffold-git + outputs: + resources: + - name: builtImage1 + resourceRef: + name: skaffold-image-leeroy-web-1 + - name: builtImage2 + resourceRef: + name: skaffold-image-leeroy-web-2 diff --git a/pkg/apis/pipeline/v1alpha1/task_defaults.go b/pkg/apis/pipeline/v1alpha1/task_defaults.go index 9d074b70efe..f8b2a3e9942 100644 --- a/pkg/apis/pipeline/v1alpha1/task_defaults.go +++ b/pkg/apis/pipeline/v1alpha1/task_defaults.go @@ -31,7 +31,7 @@ func (ts *TaskSpec) SetDefaults(ctx context.Context) { for i, o := range ts.Outputs.Resources { if o.Type == PipelineResourceTypeImage { if o.OutputImageDir == "" { - ts.Outputs.Resources[i].OutputImageDir = fmt.Sprintf("/builder/image-outputs/%s", o.Name) + ts.Outputs.Resources[i].OutputImageDir = fmt.Sprintf("%s/%s", TaskOutputImageDefaultDir, o.Name) } } } diff --git a/pkg/apis/pipeline/v1alpha1/task_types.go b/pkg/apis/pipeline/v1alpha1/task_types.go index 9dfbaa05566..f2662879b28 100644 --- a/pkg/apis/pipeline/v1alpha1/task_types.go +++ b/pkg/apis/pipeline/v1alpha1/task_types.go @@ -67,6 +67,11 @@ type TaskSpec struct { var _ apis.Validatable = (*Task)(nil) var _ apis.Defaultable = (*Task)(nil) +const ( + // TaskOutputImageDefaultDir is the default directory for output image resource, + TaskOutputImageDefaultDir = "/builder/home/image-outputs" +) + // +genclient // +genclient:noStatus // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/reconciler/v1alpha1/taskrun/resources/image_exporter.go b/pkg/reconciler/v1alpha1/taskrun/resources/image_exporter.go index 9a82dabf27f..c7488ad006c 100644 --- a/pkg/reconciler/v1alpha1/taskrun/resources/image_exporter.go +++ b/pkg/reconciler/v1alpha1/taskrun/resources/image_exporter.go @@ -80,12 +80,13 @@ func AddOutputImageDigestExporter( taskSpec.Steps = augmentedSteps } + } return nil } -// UpdateTaskRunStatusWithResourceResult if there an update to the outout image resource, add to taskrun status result +// UpdateTaskRunStatusWithResourceResult if there is an update to the outout image resource, add to taskrun status result func UpdateTaskRunStatusWithResourceResult(taskRun *v1alpha1.TaskRun, logContent []byte) error { err := json.Unmarshal(logContent, &taskRun.Status.ResourcesResult) if err != nil { @@ -102,6 +103,8 @@ func imageDigestExporterContainer(stepName string, imagesJSON []byte) corev1.Con Args: []string{ "-images", string(imagesJSON), }, + TerminationMessagePath: "/workspace/builder/termination-log", + TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, } } diff --git a/pkg/reconciler/v1alpha1/taskrun/resources/image_exporter_test.go b/pkg/reconciler/v1alpha1/taskrun/resources/image_exporter_test.go index ece9569f114..e7a66495183 100644 --- a/pkg/reconciler/v1alpha1/taskrun/resources/image_exporter_test.go +++ b/pkg/reconciler/v1alpha1/taskrun/resources/image_exporter_test.go @@ -93,6 +93,8 @@ func TestAddOutputImageDigestExporter(t *testing.T) { Image: "override-with-imagedigest-exporter-image:latest", Command: []string{"/ko-app/imagedigestexporter"}, Args: []string{"-images", fmt.Sprintf("[{\"name\":\"source-image-1\",\"type\":\"image\",\"url\":\"gcr.io/some-image-1\",\"digest\":\"\",\"OutputImageDir\":\"%s\"}]", currentDir)}, + TerminationMessagePath: "/workspace/builder/termination-log", + TerminationMessagePolicy: "FallbackToLogsOnError", }}, }, { desc: "image resource in task with multiple steps", @@ -155,6 +157,8 @@ func TestAddOutputImageDigestExporter(t *testing.T) { Image: "override-with-imagedigest-exporter-image:latest", Command: []string{"/ko-app/imagedigestexporter"}, Args: []string{"-images", fmt.Sprintf("[{\"name\":\"source-image-1\",\"type\":\"image\",\"url\":\"gcr.io/some-image-1\",\"digest\":\"\",\"OutputImageDir\":\"%s\"}]", currentDir)}, + TerminationMessagePath: "/workspace/builder/termination-log", + TerminationMessagePolicy: "FallbackToLogsOnError", }, { Name: "step2", }, { @@ -162,6 +166,8 @@ func TestAddOutputImageDigestExporter(t *testing.T) { Image: "override-with-imagedigest-exporter-image:latest", Command: []string{"/ko-app/imagedigestexporter"}, Args: []string{"-images", fmt.Sprintf("[{\"name\":\"source-image-1\",\"type\":\"image\",\"url\":\"gcr.io/some-image-1\",\"digest\":\"\",\"OutputImageDir\":\"%s\"}]", currentDir)}, + TerminationMessagePath: "/workspace/builder/termination-log", + TerminationMessagePolicy: "FallbackToLogsOnError", }, }, }} { diff --git a/pkg/reconciler/v1alpha1/taskrun/resources/pod.go b/pkg/reconciler/v1alpha1/taskrun/resources/pod.go index a565a3b80a0..2c73516c48b 100644 --- a/pkg/reconciler/v1alpha1/taskrun/resources/pod.go +++ b/pkg/reconciler/v1alpha1/taskrun/resources/pod.go @@ -208,6 +208,26 @@ func makeWorkingDirInitializer(steps []corev1.Container) *corev1.Container { return nil } +// initOutputResourcesDefaultDir checks if there are any output image resources expecting a default path +// and creates an init container to create that folder +func initOutputResourcesDefaultDir(taskRun *v1alpha1.TaskRun, taskSpec v1alpha1.TaskSpec) []corev1.Container { + makeDirSteps := []corev1.Container{} + if len(taskRun.Spec.Outputs.Resources) > 0 { + for _, r := range taskRun.Spec.Outputs.Resources { + for _, o := range taskSpec.Outputs.Resources { + if o.Name == r.Name { + if strings.HasPrefix(o.OutputImageDir, v1alpha1.TaskOutputImageDefaultDir) { + cn := v1alpha1.CreateDirContainer("default-image-output", fmt.Sprintf("%s/%s", v1alpha1.TaskOutputImageDefaultDir, r.Name)) + cn.VolumeMounts = append(cn.VolumeMounts, implicitVolumeMounts...) + makeDirSteps = append(makeDirSteps, cn) + } + } + } + } + } + return makeDirSteps +} + // GetPod returns the Pod for the given pod name type GetPod func(string, metav1.GetOptions) (*corev1.Pod, error) @@ -239,6 +259,8 @@ func MakePod(taskRun *v1alpha1.TaskRun, taskSpec v1alpha1.TaskSpec, kubeclient k initContainers = append(initContainers, *workingDir) } + initContainers = append(initContainers, initOutputResourcesDefaultDir(taskRun, taskSpec)...) + maxIndicesByResource := findMaxResourceRequest(taskSpec.Steps, corev1.ResourceCPU, corev1.ResourceMemory, corev1.ResourceEphemeralStorage) for i := range taskSpec.Steps { diff --git a/pkg/reconciler/v1alpha1/taskrun/resources/pod_test.go b/pkg/reconciler/v1alpha1/taskrun/resources/pod_test.go index 3d7b0337966..d03e88eb441 100644 --- a/pkg/reconciler/v1alpha1/taskrun/resources/pod_test.go +++ b/pkg/reconciler/v1alpha1/taskrun/resources/pod_test.go @@ -510,3 +510,139 @@ func TestMakeAnnotations(t *testing.T) { }) } } + +func TestInitOutputResourcesDefaultDir(t *testing.T) { + names.TestingSeed() + + randReader = strings.NewReader(strings.Repeat("a", 10000)) + defer func() { randReader = rand.Reader }() + + for _, c := range []struct { + desc string + trs v1alpha1.TaskRunSpec + ts v1alpha1.TaskSpec + bAnnotations map[string]string + want *corev1.PodSpec + wantErr error + }{{ + desc: "task-with-output-image-resource", + trs: v1alpha1.TaskRunSpec{ + Outputs: v1alpha1.TaskRunOutputs{ + Resources: []v1alpha1.TaskResourceBinding{{ + Name: "outputimage", + ResourceRef: v1alpha1.PipelineResourceRef{ + Name: "outputimage", + }, + }}, + Params: []v1alpha1.Param{}, + }, + }, + ts: v1alpha1.TaskSpec{ + Steps: []corev1.Container{{ + Name: "task-with-output-image", + Image: "image", + }}, + Outputs: &v1alpha1.Outputs{ + Resources: []v1alpha1.TaskResource{{ + Name: "outputimage", + OutputImageDir: fmt.Sprintf("%s/outputimage", v1alpha1.TaskOutputImageDefaultDir), + }}, + }, + }, + bAnnotations: map[string]string{ + "simple-annotation-key": "simple-annotation-val", + }, + want: &corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyNever, + InitContainers: []corev1.Container{{ + Name: containerPrefix + credsInit + "-9l9zj", + Image: *credsImage, + Command: []string{"/ko-app/creds-init"}, + Args: []string{}, + Env: implicitEnvVars, + VolumeMounts: implicitVolumeMounts, + WorkingDir: workspaceDir, + }, { + Name: "create-dir-default-image-output-mz4c7", + Image: "override-with-bash-noop:latest", + Command: []string{"/ko-app/bash"}, + Args: []string{"-args", "mkdir -p /builder/home/image-outputs/outputimage"}, + VolumeMounts: implicitVolumeMounts, + }}, + Containers: []corev1.Container{{ + Name: "step-task-with-output-image", + Image: "image", + Env: implicitEnvVars, + VolumeMounts: implicitVolumeMounts, + WorkingDir: workspaceDir, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("0"), + corev1.ResourceMemory: resource.MustParse("0"), + corev1.ResourceEphemeralStorage: resource.MustParse("0"), + }, + }, + }, + }, + Volumes: implicitVolumes, + }, + }} { + t.Run(c.desc, func(t *testing.T) { + names.TestingSeed() + cs := fakek8s.NewSimpleClientset( + &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "default"}}, + &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "service-account"}, + Secrets: []corev1.ObjectReference{{ + Name: "multi-creds", + }}, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "multi-creds", + Annotations: map[string]string{ + "tekton.dev/docker-0": "https://us.gcr.io", + "tekton.dev/docker-1": "https://docker.io", + "tekton.dev/git-0": "github.com", + "tekton.dev/git-1": "gitlab.com", + }}, + Type: "kubernetes.io/basic-auth", + Data: map[string][]byte{ + "username": []byte("foo"), + "password": []byte("BestEver"), + }, + }, + ) + tr := &v1alpha1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "taskrun-name", + Annotations: c.bAnnotations, + }, + Spec: c.trs, + } + cache, _ := entrypoint.NewCache() + got, err := MakePod(tr, c.ts, cs, cache, logger) + if err != c.wantErr { + t.Fatalf("MakePod: %v", err) + } + + // Generated name from hexlifying a stream of 'a's. + wantName := "taskrun-name-pod-616161" + if got.Name != wantName { + t.Errorf("Pod name got %q, want %q", got.Name, wantName) + } + + if d := cmp.Diff(&got.Spec, c.want, resourceQuantityCmp); d != "" { + t.Errorf("Diff spec:\n%s", d) + } + + wantAnnotations := map[string]string{"simple-annotation-key": "simple-annotation-val", ReadyAnnotation: ""} + if c.bAnnotations != nil { + for key, val := range c.bAnnotations { + wantAnnotations[key] = val + } + } + if d := cmp.Diff(got.Annotations, wantAnnotations); d != "" { + t.Errorf("Diff annotations:\n%s", d) + } + }) + } +} diff --git a/pkg/reconciler/v1alpha1/taskrun/taskrun.go b/pkg/reconciler/v1alpha1/taskrun/taskrun.go index 4a89dfe13f5..1b889f0a1c2 100644 --- a/pkg/reconciler/v1alpha1/taskrun/taskrun.go +++ b/pkg/reconciler/v1alpha1/taskrun/taskrun.go @@ -368,14 +368,9 @@ func (c *Reconciler) handlePodCreationError(tr *v1alpha1.TaskRun, err error) { func updateTaskRunResourceResult(taskRun *v1alpha1.TaskRun, pod *corev1.Pod, resourceLister listers.PipelineResourceLister, kubeclient kubernetes.Interface, logger *zap.SugaredLogger) { if resources.TaskRunHasOutputImageResource(resourceLister.PipelineResources(taskRun.Namespace).Get, taskRun) && taskRun.IsSuccessful() { - for _, container := range pod.Spec.Containers { - if strings.HasPrefix(container.Name, imageDigestExporterContainerName) { - req := kubeclient.CoreV1().Pods(taskRun.Namespace).GetLogs(pod.Name, &corev1.PodLogOptions{Container: container.Name}) - logContent, err := req.Do().Raw() - if err != nil { - logger.Errorf("Error getting output from image-digest-exporter for %s/%s: %s", taskRun.Name, taskRun.Namespace, err) - } - err = resources.UpdateTaskRunStatusWithResourceResult(taskRun, logContent) + for _, cs := range pod.Status.ContainerStatuses { + if strings.HasPrefix(cs.Name, imageDigestExporterContainerName) { + err := resources.UpdateTaskRunStatusWithResourceResult(taskRun, []byte(cs.State.Terminated.Message)) if err != nil { logger.Errorf("Error getting output from image-digest-exporter for %s/%s: %s", taskRun.Name, taskRun.Namespace, err) } diff --git a/pkg/reconciler/v1alpha1/taskrun/taskrun_test.go b/pkg/reconciler/v1alpha1/taskrun/taskrun_test.go index e32d6e87926..41afaba2cf4 100644 --- a/pkg/reconciler/v1alpha1/taskrun/taskrun_test.go +++ b/pkg/reconciler/v1alpha1/taskrun/taskrun_test.go @@ -530,6 +530,8 @@ func TestReconcile(t *testing.T) { tb.Memory("0"), tb.EphemeralStorage("0"), )), + tb.TerminationMessagePath("/workspace/builder/termination-log"), + tb.TerminationMessagePolicy(corev1.TerminationMessageFallbackToLogsOnError), ), tb.PodContainer("step-myothercontainer", "myotherimage", tb.Command(entrypointLocation), @@ -560,6 +562,8 @@ func TestReconcile(t *testing.T) { tb.Memory("0"), tb.EphemeralStorage("0"), )), + tb.TerminationMessagePath("/workspace/builder/termination-log"), + tb.TerminationMessagePolicy(corev1.TerminationMessageFallbackToLogsOnError), ), ), ), diff --git a/test/builder/container.go b/test/builder/container.go index 72422803625..2d6ce7e0afe 100644 --- a/test/builder/container.go +++ b/test/builder/container.go @@ -128,3 +128,17 @@ func EphemeralStorage(val string) ResourceListOp { r[corev1.ResourceEphemeralStorage] = resource.MustParse(val) } } + +// TerminationMessagePath sets the source of the termination message. +func TerminationMessagePath(terminationMessagePath string) ContainerOp { + return func(c *corev1.Container) { + c.TerminationMessagePath = terminationMessagePath + } +} + +// TerminationMessagePolicy sets the policy of the termination message. +func TerminationMessagePolicy(terminationMessagePolicy corev1.TerminationMessagePolicy) ContainerOp { + return func(c *corev1.Container) { + c.TerminationMessagePolicy = terminationMessagePolicy + } +}