diff --git a/cmd/imagedigestexporter/main.go b/cmd/imagedigestexporter/main.go index 4e6b3dd4616..1422f8a5953 100644 --- a/cmd/imagedigestexporter/main.go +++ b/cmd/imagedigestexporter/main.go @@ -31,6 +31,13 @@ var ( images = flag.String("images", "", "List of images resources built by task in json format") ) +/* The input of this go program will be a JSON string with all the output PipelineResources of type +Image, which will include the path to where the index.json file will be located. The program will +read the related index.json file(s) and log another JSON string including the name of the image resource +and the digests. +The input is an array of ImageResource, ex: [{"name":"srcimg1","type":"image","url":"gcr.io/some-image-1","digest":"","OutputImagePath":"/path/image"}] +The output is an array of PipelineResourceResult, ex: [{"name":"image","digest":"sha256:eed29..660"}] +*/ func main() { flag.Parse() @@ -42,7 +49,7 @@ func main() { output := []v1alpha1.PipelineResourceResult{} for _, imageResource := range imageResources { - ii, err := layout.ImageIndexFromPath(imageResource.IndexPath) + ii, err := layout.ImageIndexFromPath(imageResource.OutputImagePath) if err != nil { // if this image doesn't have a builder that supports index.josn file, // then it will be skipped diff --git a/docs/resources.md b/docs/resources.md index c6241b2cc17..2a65ba755b6 100644 --- a/docs/resources.md +++ b/docs/resources.md @@ -164,10 +164,55 @@ spec: #### Surfacing the image digest built in a task -To surface the image digest in the output of the `taskRun` the builder tool should produce this information in a [oci-layout-image](https://github.com/opencontainers/image-spec/blob/master/image-layout.md) `index.json` file. This file should be placed on a location as specified in -the image resource `indexpath`. +To surface the image digest in the output of the `taskRun` the builder tool should produce this information in a [oci-layout-image](https://github.com/opencontainers/image-spec/blob/master/image-layout.md) `index.json` file. This file should be placed on a location as specified in the task definition under the resource `outputImagePath` -The `taskRun` will include the image digest in the `resourcesResult` field that is part of the taskRun.Status. +for example this build-push task defines the `outputImagePath` for the `buildImage` resource in `/worksapce/buildImage` +```yaml +apiVersion: tekton.dev/v1alpha1 +kind: Task +metadata: + name: build-push +spec: + inputs: + resources: + - name: workspace + type: git + outputs: + resources: + - name: builtImage + type: image + outputImagePath: /workspace/builtImage + steps: ... +``` +If no value is specified for `outputImagePath`, it will default to `/tools/image-outputs/{resource-name}`. + +*Please check the builder tool used on how to pass this path to create the output file.* + +The `taskRun` will include the image digest in the `resourcesResult` field that is part of the `taskRun.Status` + +for example: + +```yaml +status: + completionTime: 2019-04-29T14:20:41Z + conditions: + - lastTransitionTime: 2019-04-29T14:20:41Z + status: "True" + type: Succeeded + podName: build-push-run-pod-0a1777 + resourcesResult: + - digest: sha256:eed29cd0b6feeb1a92bc3c4f977fd203c63b376a638731c88cacefe3adb1c660 + name: skaffold-image-leeroy-web + startTime: 2019-04-29T14:20:30Z + steps: + - name: build-and-push + terminated: + containerID: docker://140fbe0d923bd0196e0ef1edbf088731ff1e8d37bfa9b04307701ccbdae77426 + exitCode: 0 + finishedAt: 2019-04-29T14:20:39Z + reason: Completed + startedAt: 2019-04-29T14:20:36Z +``` If the `index.json` file is not produced, the image digest will not be included in the `taskRun` output. diff --git a/examples/taskruns/task-output-image.yaml b/examples/taskruns/task-output-image.yaml new file mode 100644 index 00000000000..21c74cf27e2 --- /dev/null +++ b/examples/taskruns/task-output-image.yaml @@ -0,0 +1,103 @@ +apiVersion: tekton.dev/v1alpha1 +kind: PipelineResource +metadata: + name: skaffold-image-leeroy-web +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: build-push-kaniko +spec: + inputs: + resources: + - name: workspace + type: git + outputs: + resources: + - name: builtImage + type: image + outputImagePath: /workspace/workspace + steps: + - name: build-and-push + image: busybox + command: + - /bin/sh + args: + - -ce + - | + set -e + cat < /workspace/workspace/index.json + { + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.index.v1+json", + "size": 314, + "digest": "sha256:05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b5" + } + ] + } + EOF + - name: echo + image: busybox + command: + - /bin/sh + args: + - -ce + - | + set -e + cat /workspace/workspace/index.json +--- +apiVersion: tekton.dev/v1alpha1 +kind: TaskRun +metadata: + name: build-push-run +spec: + taskRef: + name: build-push-kaniko + trigger: + type: manual + inputs: + resources: + - name: workspace + resourceRef: + name: skaffold-git + outputs: + resources: + - name: builtImage + resourceRef: + name: skaffold-image-leeroy-web diff --git a/pkg/apis/pipeline/v1alpha1/image_resource.go b/pkg/apis/pipeline/v1alpha1/image_resource.go index c3469e24b75..72d3d5c0647 100644 --- a/pkg/apis/pipeline/v1alpha1/image_resource.go +++ b/pkg/apis/pipeline/v1alpha1/image_resource.go @@ -40,8 +40,6 @@ func NewImageResource(r *PipelineResource) (*ImageResource, error) { ir.URL = param.Value case strings.EqualFold(param.Name, "Digest"): ir.Digest = param.Value - case strings.EqualFold(param.Name, "IndexPath"): - ir.IndexPath = param.Value } } @@ -50,11 +48,11 @@ func NewImageResource(r *PipelineResource) (*ImageResource, error) { // ImageResource defines an endpoint where artifacts can be stored, such as images. type ImageResource struct { - Name string `json:"name"` - Type PipelineResourceType `json:"type"` - URL string `json:"url"` - Digest string `json:"digest"` - IndexPath string `json:"indexpath"` + Name string `json:"name"` + Type PipelineResourceType `json:"type"` + URL string `json:"url"` + Digest string `json:"digest"` + OutputImagePath string } // GetName returns the name of the resource @@ -73,11 +71,10 @@ func (s ImageResource) GetParams() []Param { return []Param{} } // Replacements is used for template replacement on an ImageResource inside of a Taskrun. func (s *ImageResource) Replacements() map[string]string { return map[string]string{ - "name": s.Name, - "type": string(s.Type), - "url": s.URL, - "digest": s.Digest, - "indexpath": s.IndexPath, + "name": s.Name, + "type": string(s.Type), + "url": s.URL, + "digest": s.Digest, } } @@ -95,9 +92,9 @@ func (s *ImageResource) GetDownloadContainerSpec() ([]corev1.Container, error) { func (s *ImageResource) SetDestinationDirectory(path string) { } -// GetIndexPath return the path to get the index.json file -func (s *ImageResource) GetIndexPath() string { - return s.IndexPath +// GetOutputImagePath return the path to get the index.json file +func (s *ImageResource) GetOutputImagePath() string { + return s.OutputImagePath } func (s ImageResource) String() string { diff --git a/pkg/apis/pipeline/v1alpha1/resource_types.go b/pkg/apis/pipeline/v1alpha1/resource_types.go index 73e07dfef03..64fa0b1be00 100644 --- a/pkg/apis/pipeline/v1alpha1/resource_types.go +++ b/pkg/apis/pipeline/v1alpha1/resource_types.go @@ -94,7 +94,9 @@ type TaskResource struct { // +optional // TargetPath is the path in workspace directory where the task resource will be copied. TargetPath string `json:"targetPath"` - // Resource Value stuff + // +optional + // Path to the index.json file for output container images + OutputImagePath string `json:"outputImagePath"` } // +genclient diff --git a/pkg/apis/pipeline/v1alpha1/task_defaults.go b/pkg/apis/pipeline/v1alpha1/task_defaults.go index 65b914a094a..bb7a82319b3 100644 --- a/pkg/apis/pipeline/v1alpha1/task_defaults.go +++ b/pkg/apis/pipeline/v1alpha1/task_defaults.go @@ -16,12 +16,25 @@ limitations under the License. package v1alpha1 -import "context" +import ( + "context" + "fmt" +) func (t *Task) SetDefaults(ctx context.Context) { t.Spec.SetDefaults(ctx) } +// SetDefaults set any defaults for the task spec func (ts *TaskSpec) SetDefaults(ctx context.Context) { + if ts.Outputs != nil && len(ts.Outputs.Resources) > 0 { + for i, o := range ts.Outputs.Resources { + if o.Type == PipelineResourceTypeImage { + if o.OutputImagePath == "" { + ts.Outputs.Resources[i].OutputImagePath = fmt.Sprintf("/tools/image-outputs/%s", o.Name) + } + } + } + } return } diff --git a/pkg/apis/pipeline/v1alpha1/task_validation.go b/pkg/apis/pipeline/v1alpha1/task_validation.go index e40d57fa740..7bba408c824 100644 --- a/pkg/apis/pipeline/v1alpha1/task_validation.go +++ b/pkg/apis/pipeline/v1alpha1/task_validation.go @@ -145,6 +145,11 @@ func validateResourceVariables(steps []corev1.Container, inputs *Inputs, outputs if outputs != nil { for _, r := range outputs.Resources { resourceNames[r.Name] = struct{}{} + if r.Type == PipelineResourceTypeImage { + if r.OutputImagePath == "" { + return apis.ErrMissingField("OutputImagePath") + } + } } } return validateVariables(steps, "resources", resourceNames) diff --git a/pkg/apis/pipeline/v1alpha1/task_validation_test.go b/pkg/apis/pipeline/v1alpha1/task_validation_test.go index 3dc631ce67d..ed2e8018b6c 100644 --- a/pkg/apis/pipeline/v1alpha1/task_validation_test.go +++ b/pkg/apis/pipeline/v1alpha1/task_validation_test.go @@ -31,6 +31,11 @@ var validResource = TaskResource{ Type: "git", } +var validImageResource = TaskResource{ + Name: "source", + Type: "image", +} + var validBuildSteps = []corev1.Container{{ Name: "mystep", Image: "myimage", @@ -84,6 +89,17 @@ func TestTaskSpecValidate(t *testing.T) { }, BuildSteps: validBuildSteps, }, + }, { + name: "output image resoure", + fields: fields{ + Inputs: &Inputs{ + Resources: []TaskResource{validImageResource}, + }, + Outputs: &Outputs{ + Resources: []TaskResource{validImageResource}, + }, + BuildSteps: validBuildSteps, + }, }, { name: "valid template variable", fields: fields{ @@ -116,7 +132,9 @@ func TestTaskSpecValidate(t *testing.T) { Outputs: tt.fields.Outputs, Steps: tt.fields.BuildSteps, } - if err := ts.Validate(context.Background()); err != nil { + ctx := context.Background() + ts.SetDefaults(ctx) + if err := ts.Validate(ctx); err != nil { t.Errorf("TaskSpec.Validate() = %v", err) } }) diff --git a/pkg/reconciler/v1alpha1/taskrun/resources/image_exporter.go b/pkg/reconciler/v1alpha1/taskrun/resources/image_exporter.go index 3d61584a2b2..1b836e7a86a 100644 --- a/pkg/reconciler/v1alpha1/taskrun/resources/image_exporter.go +++ b/pkg/reconciler/v1alpha1/taskrun/resources/image_exporter.go @@ -19,9 +19,10 @@ package resources import ( "encoding/json" "flag" + "os" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" - listers "github.com/tektoncd/pipeline/pkg/client/listers/pipeline/v1alpha1" + "github.com/tektoncd/pipeline/pkg/names" "go.uber.org/zap" corev1 "k8s.io/api/core/v1" ) @@ -34,7 +35,7 @@ var ( func AddOutputImageDigestExporter( tr *v1alpha1.TaskRun, taskSpec *v1alpha1.TaskSpec, - pipelineResourceLister listers.PipelineResourceLister, + gr GetResource, logger *zap.SugaredLogger, ) error { @@ -47,7 +48,7 @@ func AddOutputImageDigestExporter( return err } - resource, err := getResource(boundResource, pipelineResourceLister.PipelineResources(tr.Namespace).Get) + resource, err := getResource(boundResource, gr) if err != nil { logger.Errorf("Failed to get output pipeline Resource for taskRun %q resource %v; error: %s", tr.Name, boundResource, err.Error()) return err @@ -58,28 +59,74 @@ func AddOutputImageDigestExporter( logger.Errorf("Invalid Image Resource for taskRun %q resource %v; error: %s", tr.Name, boundResource, err.Error()) return err } + for _, o := range taskSpec.Outputs.Resources { + if o.Name == boundResource.Name { + if o.OutputImagePath != "" { + if _, err := os.Stat(o.OutputImagePath); os.IsNotExist(err) { + if err := os.MkdirAll(o.OutputImagePath, os.ModePerm); err != nil { + return err + } + } + imageResource.OutputImagePath = o.OutputImagePath + break + } + } + } output = append(output, imageResource) } } if len(output) > 0 { + augmentedSteps := []corev1.Container{} imagesJSON, err := json.Marshal(output) if err != nil { return err } - c := corev1.Container{ - Name: "image-digest-exporter", - Image: *imageDigestExporterImage, - Command: []string{"/ko-app/imagedigestexporter"}, - Args: []string{ - "-images", string(imagesJSON), - }, + for _, s := range taskSpec.Steps { + augmentedSteps = append(augmentedSteps, s) + augmentedSteps = append(augmentedSteps, imageDigestExporterContainer(s.Name, imagesJSON)) } - taskSpec.Steps = append(taskSpec.Steps, c) + taskSpec.Steps = augmentedSteps } } return nil } + +// UpdateTaskRunStatusWithResourceResult if there 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 { + return err + } + return nil +} + +func imageDigestExporterContainer(stepName string, imagesJSON []byte) corev1.Container { + return corev1.Container{ + Name: names.SimpleNameGenerator.RestrictLengthWithRandomSuffix("image-digest-exporter-" + stepName), + Image: *imageDigestExporterImage, + Command: []string{"/ko-app/imagedigestexporter"}, + Args: []string{ + "-images", string(imagesJSON), + }, + } +} + +// TaskRunHasOutputImageResource return true if the task has any output resources of type image +func TaskRunHasOutputImageResource(gr GetResource, taskRun *v1alpha1.TaskRun) bool { + if len(taskRun.Spec.Outputs.Resources) > 0 { + for _, r := range taskRun.Spec.Outputs.Resources { + resource, err := gr(r.ResourceRef.Name) + if err != nil { + return false + } + if resource.Spec.Type == v1alpha1.PipelineResourceTypeImage { + return true + } + } + } + return false +} diff --git a/pkg/reconciler/v1alpha1/taskrun/resources/image_exporter_test.go b/pkg/reconciler/v1alpha1/taskrun/resources/image_exporter_test.go index 9a25f3eeb9c..f361c38c322 100644 --- a/pkg/reconciler/v1alpha1/taskrun/resources/image_exporter_test.go +++ b/pkg/reconciler/v1alpha1/taskrun/resources/image_exporter_test.go @@ -14,80 +14,292 @@ limitations under the License. package resources import ( + "fmt" + "os" "testing" "github.com/google/go-cmp/cmp" + "github.com/knative/pkg/apis" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" - fakeclientset "github.com/tektoncd/pipeline/pkg/client/clientset/versioned/fake" - informers "github.com/tektoncd/pipeline/pkg/client/informers/externalversions" - listers "github.com/tektoncd/pipeline/pkg/client/listers/pipeline/v1alpha1" "github.com/tektoncd/pipeline/pkg/logging" + "github.com/tektoncd/pipeline/test/names" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -var ( - outputpipelineImageResourceLister listers.PipelineResourceLister -) - -func outputImageResourceSetup() { - logger, _ = logging.NewLogger("", "") - fakeClient := fakeclientset.NewSimpleClientset() - sharedInfomer := informers.NewSharedInformerFactory(fakeClient, 0) - pipelineResourceInformer := sharedInfomer.Tekton().V1alpha1().PipelineResources() - outputpipelineImageResourceLister = pipelineResourceInformer.Lister() - - rs := []*v1alpha1.PipelineResource{{ - ObjectMeta: metav1.ObjectMeta{ - Name: "source-image-1", - Namespace: "marshmallow", +func TestExportingOutputImageResource(t *testing.T) { + currentDir, _ := os.Getwd() + for _, c := range []struct { + desc string + task *v1alpha1.Task + taskRun *v1alpha1.TaskRun + wantSteps []corev1.Container + }{{ + desc: "image resource declared as both input and output", + task: &v1alpha1.Task{ + ObjectMeta: metav1.ObjectMeta{ + Name: "task1", + Namespace: "marshmallow", + }, + Spec: v1alpha1.TaskSpec{ + Inputs: &v1alpha1.Inputs{ + Resources: []v1alpha1.TaskResource{{ + Name: "source-image", + Type: "image", + }}, + }, + Outputs: &v1alpha1.Outputs{ + Resources: []v1alpha1.TaskResource{{ + Name: "source-image", + Type: "image", + OutputImagePath: currentDir, + }}, + }, + Steps: []corev1.Container{{ + Name: "step1", + }, + }, + }, }, - Spec: v1alpha1.PipelineResourceSpec{ - Type: "image", - Params: []v1alpha1.Param{{ - Name: "url", - Value: "gcr.io/some-image-1", - }, { - Name: "digest", - Value: "", - }, { - Name: "indexpath", - Value: "/workspace/source-image-1/index.json", - }}, + taskRun: &v1alpha1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-taskrun-run-output-steps", + Namespace: "marshmallow", + }, + Spec: v1alpha1.TaskRunSpec{ + Inputs: v1alpha1.TaskRunInputs{ + Resources: []v1alpha1.TaskResourceBinding{{ + Name: "source-image", + ResourceRef: v1alpha1.PipelineResourceRef{ + Name: "source-image-1", + }, + }}, + }, + Outputs: v1alpha1.TaskRunOutputs{ + Resources: []v1alpha1.TaskResourceBinding{{ + Name: "source-image", + ResourceRef: v1alpha1.PipelineResourceRef{ + Name: "source-image-1", + }, + }}, + }, + }, }, + wantSteps: []corev1.Container{ + { + Name: "step1", + }, + { + Name: "image-digest-exporter-step1-9l9zj", + 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\":\"\",\"OutputImagePath\":\"%s\"}]", currentDir)}, + }}, }, { - ObjectMeta: metav1.ObjectMeta{ - Name: "source-image-2", - Namespace: "marshmallow", + desc: "image resource in task with multiple steps", + task: &v1alpha1.Task{ + ObjectMeta: metav1.ObjectMeta{ + Name: "task1", + Namespace: "marshmallow", + }, + Spec: v1alpha1.TaskSpec{ + Inputs: &v1alpha1.Inputs{ + Resources: []v1alpha1.TaskResource{{ + Name: "source-image", + Type: "image", + }}, + }, + Outputs: &v1alpha1.Outputs{ + Resources: []v1alpha1.TaskResource{{ + Name: "source-image", + Type: "image", + OutputImagePath: currentDir, + }}, + }, + Steps: []corev1.Container{{ + Name: "step1", + }, { + Name: "step2", + }}, + }, + }, + taskRun: &v1alpha1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-taskrun-run-output-steps", + Namespace: "marshmallow", + }, + Spec: v1alpha1.TaskRunSpec{ + Inputs: v1alpha1.TaskRunInputs{ + Resources: []v1alpha1.TaskResourceBinding{{ + Name: "source-image", + ResourceRef: v1alpha1.PipelineResourceRef{ + Name: "source-image-1", + }, + }}, + }, + Outputs: v1alpha1.TaskRunOutputs{ + Resources: []v1alpha1.TaskResourceBinding{{ + Name: "source-image", + ResourceRef: v1alpha1.PipelineResourceRef{ + Name: "source-image-1", + }, + }}, + }, + }, }, - Spec: v1alpha1.PipelineResourceSpec{ - Type: "image", - Params: []v1alpha1.Param{{ - Name: "url", - Value: "gcr.io/some-image-2", + wantSteps: []corev1.Container{ + { + Name: "step1", + }, + { + Name: "image-digest-exporter-step1-9l9zj", + 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\":\"\",\"OutputImagePath\":\"%s\"}]", currentDir)}, }, { - Name: "digest", - Value: "", + Name: "step2", }, { - Name: "indexpath", - Value: "/workspace/source-image-2/index.json", - }}, + Name: "image-digest-exporter-step2-mz4c7", + 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\":\"\",\"OutputImagePath\":\"%s\"}]", currentDir)}, + }, }, - }} + }} { + t.Run(c.desc, func(t *testing.T) { + names.TestingSeed() + logger, _ := logging.NewLogger("", "") + gr := func(n string) (*v1alpha1.PipelineResource, error) { + return &v1alpha1.PipelineResource{ + ObjectMeta: metav1.ObjectMeta{ + Name: "source-image-1", + Namespace: "marshmallow", + }, + Spec: v1alpha1.PipelineResourceSpec{ + Type: "image", + Params: []v1alpha1.Param{{ + Name: "url", + Value: "gcr.io/some-image-1", + }, { + Name: "digest", + Value: "", + }, { + Name: "OutputImagePath", + Value: "/workspace/source-image-1/index.json", + }}, + }, + }, nil + } + err := AddOutputImageDigestExporter(c.taskRun, &c.task.Spec, gr, logger) + if err != nil { + t.Fatalf("Failed to declare output resources for test %q: error %v", c.desc, err) + } - for _, r := range rs { - pipelineResourceInformer.Informer().GetIndexer().Add(r) + if d := cmp.Diff(c.task.Spec.Steps, c.wantSteps); d != "" { + t.Fatalf("post build steps mismatch: %s", d) + } + }) } } -func TestExportingOutputImageResource(t *testing.T) { +func TestUpdateTaskRunStatus(t *testing.T) { for _, c := range []struct { - desc string - task *v1alpha1.Task - taskRun *v1alpha1.TaskRun - wantSteps []corev1.Container + desc string + podLog []byte + taskRun *v1alpha1.TaskRun + want []v1alpha1.PipelineResourceResult }{{ - desc: "image resource declared as both input and output", + desc: "image resource updated", + podLog: []byte("[{\"name\":\"source-image\",\"digest\":\"sha256:1234\"}]"), + taskRun: &v1alpha1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-taskrun-run-output-steps", + Namespace: "marshmallow", + }, + Spec: v1alpha1.TaskRunSpec{ + Inputs: v1alpha1.TaskRunInputs{ + Resources: []v1alpha1.TaskResourceBinding{{ + Name: "source-image", + ResourceRef: v1alpha1.PipelineResourceRef{ + Name: "source-image-1", + }, + }}, + }, + Outputs: v1alpha1.TaskRunOutputs{ + Resources: []v1alpha1.TaskResourceBinding{{ + Name: "source-image", + ResourceRef: v1alpha1.PipelineResourceRef{ + Name: "source-image-1", + }, + }}, + }, + }, + }, + want: []v1alpha1.PipelineResourceResult{{ + Name: "source-image", + Digest: "sha256:1234", + }}, + }, { + desc: "image resource exporter with malformed json output", + podLog: []byte("extralogscamehere[{\"name\":\"source-image\",\"digest\":\"sha256:1234\"}]"), + taskRun: &v1alpha1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-taskrun-run-output-steps", + Namespace: "marshmallow", + }, + Spec: v1alpha1.TaskRunSpec{ + Inputs: v1alpha1.TaskRunInputs{ + Resources: []v1alpha1.TaskResourceBinding{{ + Name: "source-image", + ResourceRef: v1alpha1.PipelineResourceRef{ + Name: "source-image-1", + }, + }}, + }, + Outputs: v1alpha1.TaskRunOutputs{ + Resources: []v1alpha1.TaskResourceBinding{{ + Name: "source-image", + ResourceRef: v1alpha1.PipelineResourceRef{ + Name: "source-image-1", + }, + }}, + }, + }, + }, + want: nil, + }, { + desc: "task with no image resource ", + podLog: []byte(""), + taskRun: &v1alpha1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-taskrun-run-output-steps", + Namespace: "marshmallow", + }, + }, + want: nil, + }} { + t.Run(c.desc, func(t *testing.T) { + names.TestingSeed() + c.taskRun.Status.SetCondition(&apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionTrue, + }) + UpdateTaskRunStatusWithResourceResult(c.taskRun, c.podLog) + if d := cmp.Diff(c.taskRun.Status.ResourcesResult, c.want); d != "" { + t.Fatalf("post build steps mismatch: %s", d) + } + }) + } +} + +func TestTaskRunHasOutputImageResource(t *testing.T) { + for _, c := range []struct { + desc string + task *v1alpha1.Task + taskRun *v1alpha1.TaskRun + wantResult bool + }{{ + desc: "image resource as output", task: &v1alpha1.Task{ ObjectMeta: metav1.ObjectMeta{ Name: "task1", @@ -106,6 +318,9 @@ func TestExportingOutputImageResource(t *testing.T) { Type: "image", }}, }, + Steps: []corev1.Container{{ + Name: "step1", + }}, }, }, taskRun: &v1alpha1.TaskRun{ @@ -132,21 +347,99 @@ func TestExportingOutputImageResource(t *testing.T) { }, }, }, - wantSteps: []corev1.Container{{ - Name: "image-digest-exporter", - Image: "override-with-imagedigest-exporter-image:latest", - Command: []string{"/ko-app/imagedigestexporter"}, - Args: []string{"-images", "[{\"name\":\"source-image-1\",\"type\":\"image\",\"url\":\"gcr.io/some-image-1\",\"digest\":\"\",\"indexpath\":\"/workspace/source-image-1/index.json\"}]"}, - }}, + wantResult: true, + }, { + desc: "task with no image resource", + task: &v1alpha1.Task{ + ObjectMeta: metav1.ObjectMeta{ + Name: "task1", + Namespace: "marshmallow", + }, + Spec: v1alpha1.TaskSpec{ + Inputs: &v1alpha1.Inputs{ + Resources: []v1alpha1.TaskResource{{ + Name: "source", + Type: "git", + }}, + }, + Outputs: &v1alpha1.Outputs{ + Resources: []v1alpha1.TaskResource{{ + Name: "source", + Type: "git", + }}, + }, + }, + }, + taskRun: &v1alpha1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-taskrun-run-output-steps", + Namespace: "marshmallow", + }, + Spec: v1alpha1.TaskRunSpec{ + Inputs: v1alpha1.TaskRunInputs{ + Resources: []v1alpha1.TaskResourceBinding{{ + Name: "source", + ResourceRef: v1alpha1.PipelineResourceRef{ + Name: "source-git-1", + }, + }}, + }, + Outputs: v1alpha1.TaskRunOutputs{ + Resources: []v1alpha1.TaskResourceBinding{{ + Name: "source", + ResourceRef: v1alpha1.PipelineResourceRef{ + Name: "source-git-1", + }, + }}, + }, + }, + }, + wantResult: false, }} { t.Run(c.desc, func(t *testing.T) { - outputImageResourceSetup() - err := AddOutputImageDigestExporter(c.taskRun, &c.task.Spec, outputpipelineImageResourceLister, logger) - if err != nil { - t.Fatalf("Failed to declare output resources for test %q: error %v", c.desc, err) + gr := func(name string) (*v1alpha1.PipelineResource, error) { + var r *v1alpha1.PipelineResource + if name == "source-image-1" { + r = &v1alpha1.PipelineResource{ + ObjectMeta: metav1.ObjectMeta{ + Name: "source-image-1", + Namespace: "marshmallow", + }, + Spec: v1alpha1.PipelineResourceSpec{ + Type: "image", + Params: []v1alpha1.Param{{ + Name: "url", + Value: "gcr.io/some-image-1", + }, { + Name: "digest", + Value: "", + }, { + Name: "OutputImagePath", + Value: "/workspace/source-image-1/index.json", + }}, + }, + } + } else if name == "source-git-1" { + r = &v1alpha1.PipelineResource{ + ObjectMeta: metav1.ObjectMeta{ + Name: "source-git-1", + Namespace: "marshmallow", + }, + Spec: v1alpha1.PipelineResourceSpec{ + Type: "git", + Params: []v1alpha1.Param{{ + Name: "url", + Value: "github.com/repo", + }, + }, + }, + } + } + return r, nil } + result := TaskRunHasOutputImageResource(gr, c.taskRun) - if d := cmp.Diff(c.task.Spec.Steps, c.wantSteps); d != "" { + if d := cmp.Diff(result, c.wantResult); d != "" { t.Fatalf("post build steps mismatch: %s", d) } }) diff --git a/pkg/reconciler/v1alpha1/taskrun/taskrun.go b/pkg/reconciler/v1alpha1/taskrun/taskrun.go index 89794cd9ff5..cf9a8440ef3 100644 --- a/pkg/reconciler/v1alpha1/taskrun/taskrun.go +++ b/pkg/reconciler/v1alpha1/taskrun/taskrun.go @@ -18,9 +18,9 @@ package taskrun import ( "context" - "encoding/json" "fmt" "reflect" + "strings" "time" "github.com/knative/pkg/apis" @@ -318,9 +318,7 @@ func (c *Reconciler) reconcile(ctx context.Context, tr *v1alpha1.TaskRun) error before := tr.Status.GetCondition(apis.ConditionSucceeded) - c.timeoutHandler.StatusLock(tr) updateStatusFromPod(tr, pod, c.resourceLister, c.KubeClientSet, c.Logger) - c.timeoutHandler.StatusUnlock(tr) after := tr.Status.GetCondition(apis.ConditionSucceeded) @@ -331,21 +329,6 @@ func (c *Reconciler) reconcile(ctx context.Context, tr *v1alpha1.TaskRun) error return nil } -func taskRunHasOutputImageResource(resourceLister listers.PipelineResourceLister, taskRun *v1alpha1.TaskRun) bool { - if len(taskRun.Spec.Outputs.Resources) > 0 { - for _, r := range taskRun.Spec.Outputs.Resources { - resource, err := resourceLister.PipelineResources(taskRun.Namespace).Get(r.ResourceRef.Name) - if err != nil { - return false - } - if resource.Spec.Type == v1alpha1.PipelineResourceTypeImage { - return true - } - } - } - return false -} - func updateStatusFromPod(taskRun *v1alpha1.TaskRun, pod *corev1.Pod, resourceLister listers.PipelineResourceLister, kubeclient kubernetes.Interface, logger *zap.SugaredLogger) { if taskRun.Status.GetCondition(apis.ConditionSucceeded) == nil || taskRun.Status.GetCondition(apis.ConditionSucceeded).Status == corev1.ConditionUnknown { // If the taskRunStatus doesn't exist yet, it's because we just started running @@ -400,15 +383,19 @@ func updateStatusFromPod(taskRun *v1alpha1.TaskRun, pod *corev1.Pod, resourceLis taskRun.Status.CompletionTime = &metav1.Time{Time: time.Now()} } - if taskRunHasOutputImageResource(resourceLister, taskRun) && taskRun.IsSuccessful() { - req := kubeclient.CoreV1().Pods(taskRun.Namespace).GetLogs(pod.Name, &corev1.PodLogOptions{Container: imageDigestExporterContainerName}) - 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 = json.Unmarshal(logContent, &taskRun.Status.ResourcesResult) - if err != nil { - logger.Errorf("Error getting output from image-digest-exporter for %s/%s: %s", taskRun.Name, taskRun.Namespace, err) + 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) + if err != nil { + logger.Errorf("Error getting output from image-digest-exporter for %s/%s: %s", taskRun.Name, taskRun.Namespace, err) + } + } } } } @@ -486,21 +473,22 @@ func (c *Reconciler) updateLabels(tr *v1alpha1.TaskRun) (*v1alpha1.TaskRun, erro // volumeMount func (c *Reconciler) createPod(tr *v1alpha1.TaskRun, ts *v1alpha1.TaskSpec, taskName string) (*corev1.Pod, error) { ts = ts.DeepCopy() - ts, err := resources.AddInputResource(c.KubeClientSet, taskName, ts, tr, c.resourceLister, c.Logger) + + err := resources.AddOutputImageDigestExporter(tr, ts, c.resourceLister.PipelineResources(tr.Namespace).Get, c.Logger) if err != nil { - c.Logger.Errorf("Failed to create a build for taskrun: %s due to input resource error %v", tr.Name, err) + c.Logger.Errorf("Failed to create a build for taskrun: %s due to output image resource error %v", tr.Name, err) return nil, err } - err = resources.AddOutputResources(c.KubeClientSet, taskName, ts, tr, c.resourceLister, c.Logger) + ts, err = resources.AddInputResource(c.KubeClientSet, taskName, ts, tr, c.resourceLister, c.Logger) if err != nil { - c.Logger.Errorf("Failed to create a build for taskrun: %s due to output resource error %v", tr.Name, err) + c.Logger.Errorf("Failed to create a build for taskrun: %s due to input resource error %v", tr.Name, err) return nil, err } - err = resources.AddOutputImageDigestExporter(tr, ts, c.resourceLister, c.Logger) + err = resources.AddOutputResources(c.KubeClientSet, taskName, ts, tr, c.resourceLister, c.Logger) if err != nil { - c.Logger.Errorf("Failed to create a build for taskrun: %s due to output image resource error %v", tr.Name, err) + c.Logger.Errorf("Failed to create a build for taskrun: %s due to output resource error %v", tr.Name, err) return nil, err } diff --git a/pkg/reconciler/v1alpha1/taskrun/taskrun_test.go b/pkg/reconciler/v1alpha1/taskrun/taskrun_test.go index a38a55b708a..dde51375ded 100644 --- a/pkg/reconciler/v1alpha1/taskrun/taskrun_test.go +++ b/pkg/reconciler/v1alpha1/taskrun/taskrun_test.go @@ -446,9 +446,9 @@ func TestReconcile(t *testing.T) { }, }, toolsVolume, workspaceVolume, homeVolume), tb.PodRestartPolicy(corev1.RestartPolicyNever), - getCredentialsInitContainer("mz4c7"), + getCredentialsInitContainer("78c5n"), getPlaceToolsInitContainer(), - tb.PodContainer("build-step-git-source-git-resource-9l9zj", "override-with-git:latest", + tb.PodContainer("build-step-git-source-git-resource-mssqb", "override-with-git:latest", tb.Command(entrypointLocation), tb.Args("-wait_file", "", "-post_file", "/builder/tools/0", "-entrypoint", "/ko-app/git-init", "--", "-url", "https://foo.git", "-revision", "master", "-path", "/workspace/workspace"), @@ -479,9 +479,24 @@ func TestReconcile(t *testing.T) { tb.EphemeralStorage("0"), )), ), + tb.PodContainer("build-step-image-digest-exporter-mycontainer-9l9zj", "override-with-imagedigest-exporter-image:latest", + tb.Command("/builder/tools/entrypoint"), + tb.Args("-wait_file", "/builder/tools/1", "-post_file", "/builder/tools/2", "-entrypoint", "/ko-app/imagedigestexporter", "--", + "-images", "[{\"name\":\"image-resource\",\"type\":\"image\",\"url\":\"gcr.io/kristoff/sven\",\"digest\":\"\",\"OutputImagePath\":\"\"}]"), + tb.WorkingDir(workspaceDir), + tb.EnvVar("HOME", "/builder/home"), + tb.VolumeMount("tools", "/builder/tools"), + tb.VolumeMount("workspace", workspaceDir), + tb.VolumeMount("home", "/builder/home"), + tb.Resources(tb.Requests( + tb.CPU("0"), + tb.Memory("0"), + tb.EphemeralStorage("0"), + )), + ), tb.PodContainer("build-step-myothercontainer", "myotherimage", tb.Command(entrypointLocation), - tb.Args("-wait_file", "/builder/tools/1", "-post_file", "/builder/tools/2", "-entrypoint", "/mycmd", "--", + tb.Args("-wait_file", "/builder/tools/2", "-post_file", "/builder/tools/3", "-entrypoint", "/mycmd", "--", "--my-other-arg=https://foo.git"), tb.WorkingDir(workspaceDir), tb.EnvVar("HOME", "/builder/home"), @@ -494,10 +509,10 @@ func TestReconcile(t *testing.T) { tb.EphemeralStorage("0"), )), ), - tb.PodContainer("build-step-image-digest-exporter", "override-with-imagedigest-exporter-image:latest", + tb.PodContainer("build-step-image-digest-exporter-myothercontainer-mz4c7", "override-with-imagedigest-exporter-image:latest", tb.Command(entrypointLocation), - tb.Args("-wait_file", "/builder/tools/2", "-post_file", "/builder/tools/3", "-entrypoint", "/ko-app/imagedigestexporter", "--", - "-images", "[{\"name\":\"image-resource\",\"type\":\"image\",\"url\":\"gcr.io/kristoff/sven\",\"digest\":\"\",\"indexpath\":\"\"}]"), + tb.Args("-wait_file", "/builder/tools/3", "-post_file", "/builder/tools/4", "-entrypoint", "/ko-app/imagedigestexporter", "--", + "-images", "[{\"name\":\"image-resource\",\"type\":\"image\",\"url\":\"gcr.io/kristoff/sven\",\"digest\":\"\",\"OutputImagePath\":\"\"}]"), tb.WorkingDir(workspaceDir), tb.EnvVar("HOME", "/builder/home"), tb.VolumeMount("tools", "/builder/tools"), @@ -511,7 +526,7 @@ func TestReconcile(t *testing.T) { ), tb.PodContainer("nop", "override-with-nop:latest", tb.Command("/builder/tools/entrypoint"), - tb.Args("-wait_file", "/builder/tools/3", "-post_file", "/builder/tools/4", "-entrypoint", "/ko-app/nop", "--"), + tb.Args("-wait_file", "/builder/tools/4", "-post_file", "/builder/tools/5", "-entrypoint", "/ko-app/nop", "--"), tb.VolumeMount(entrypoint.MountName, entrypoint.MountPoint), ), ),