diff --git a/pkg/apis/build/v1alpha1/build_types.go b/pkg/apis/build/v1alpha1/build_types.go index 4b10b12223..36da3dc68d 100644 --- a/pkg/apis/build/v1alpha1/build_types.go +++ b/pkg/apis/build/v1alpha1/build_types.go @@ -108,6 +108,17 @@ const ( AnnotationBuildVerifyRepository = BuildDomain + "/verify.repository" ) +const ( + // OutputImageZeroTimestamp indicates that the UNIX timestamp 0 is to be used + OutputImageZeroTimestamp = "Zero" + + // OutputImageSourceTimestamp indicates that the timestamp of the respective source it to be used + OutputImageSourceTimestamp = "SourceTimestamp" + + // OutputImageBuildTimestamp indicates that the current timestamp of the build run itself is to be used + OutputImageBuildTimestamp = "BuildTimestamp" +) + // BuildSpec defines the desired state of Build type BuildSpec struct { // Source refers to the Git repository containing the diff --git a/pkg/apis/build/v1beta1/build_types.go b/pkg/apis/build/v1beta1/build_types.go index 5231835608..75a8109e81 100644 --- a/pkg/apis/build/v1beta1/build_types.go +++ b/pkg/apis/build/v1beta1/build_types.go @@ -70,6 +70,10 @@ const ( TriggerInvalidImage BuildReason = "TriggerInvalidImage" // TriggerInvalidPipeline indicates the trigger type Pipeline is invalid TriggerInvalidPipeline BuildReason = "TriggerInvalidPipeline" + // OutputTimestampValueNotSupported indicates that an unsupported output timestamp setting was used + OutputTimestampValueNotSupported BuildReason = "OutputTimestampValueNotSupported" + // OutputTimestampValueNotValid indicates that the output timestamp value is not valid + OutputTimestampValueNotValid BuildReason = "OutputTimestampValueNotValid" // AllValidationsSucceeded indicates a Build was successfully validated AllValidationsSucceeded = "all validations succeeded" @@ -105,6 +109,17 @@ const ( AnnotationBuildVerifyRepository = BuildDomain + "/verify.repository" ) +const ( + // OutputImageZeroTimestamp indicates that the UNIX timestamp 0 is to be used + OutputImageZeroTimestamp = "Zero" + + // OutputImageSourceTimestamp indicates that the timestamp of the respective source it to be used + OutputImageSourceTimestamp = "SourceTimestamp" + + // OutputImageBuildTimestamp indicates that the current timestamp of the build run itself is to be used + OutputImageBuildTimestamp = "BuildTimestamp" +) + // BuildSpec defines the desired state of Build type BuildSpec struct { // Source refers to the location where the source code is, diff --git a/pkg/image/mutate.go b/pkg/image/mutate.go index 2f87ac30ef..10de77aa59 100644 --- a/pkg/image/mutate.go +++ b/pkg/image/mutate.go @@ -6,12 +6,32 @@ package image import ( "errors" + "time" containerreg "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/types" ) +// replaceManifestIn replaces a manifest in an index, because there is no +// mutate.ReplaceManifests, so therefore, it removes the old one first, +// and then add the new one +func replaceManifestIn(imageIndex containerreg.ImageIndex, descriptor containerreg.Descriptor, replacement mutate.Appendable) containerreg.ImageIndex { + imageIndex = mutate.RemoveManifests(imageIndex, func(ref containerreg.Descriptor) bool { + return ref.Digest.String() == descriptor.Digest.String() + }) + + return mutate.AppendManifests(imageIndex, mutate.IndexAddendum{ + Add: replacement, + Descriptor: containerreg.Descriptor{ + Annotations: descriptor.Annotations, + MediaType: descriptor.MediaType, + Platform: descriptor.Platform, + URLs: descriptor.URLs, + }, + }) +} + // MutateImageOrImageIndex mutates an image or image index with additional annotations and labels func MutateImageOrImageIndex(image containerreg.Image, imageIndex containerreg.ImageIndex, annotations map[string]string, labels map[string]string) (containerreg.Image, containerreg.ImageIndex, error) { if imageIndex != nil { @@ -22,12 +42,9 @@ func MutateImageOrImageIndex(image containerreg.Image, imageIndex containerreg.I if len(labels) > 0 || len(annotations) > 0 { for _, descriptor := range indexManifest.Manifests { - digest := descriptor.Digest - var appendable mutate.Appendable - switch descriptor.MediaType { case types.OCIImageIndex, types.DockerManifestList: - childImageIndex, err := imageIndex.ImageIndex(digest) + childImageIndex, err := imageIndex.ImageIndex(descriptor.Digest) if err != nil { return nil, nil, err } @@ -36,9 +53,10 @@ func MutateImageOrImageIndex(image containerreg.Image, imageIndex containerreg.I return nil, nil, err } - appendable = childImageIndex + imageIndex = replaceManifestIn(imageIndex, descriptor, childImageIndex) + case types.OCIManifestSchema1, types.DockerManifestSchema2: - image, err := imageIndex.Image(digest) + image, err := imageIndex.Image(descriptor.Digest) if err != nil { return nil, nil, err } @@ -48,25 +66,8 @@ func MutateImageOrImageIndex(image containerreg.Image, imageIndex containerreg.I return nil, nil, err } - appendable = image - default: - continue + imageIndex = replaceManifestIn(imageIndex, descriptor, image) } - - // there is no mutate.ReplaceManifests, therefore, remove the old one first, and then add the new one - imageIndex = mutate.RemoveManifests(imageIndex, func(desc containerreg.Descriptor) bool { - return desc.Digest.String() == digest.String() - }) - - imageIndex = mutate.AppendManifests(imageIndex, mutate.IndexAddendum{ - Add: appendable, - Descriptor: containerreg.Descriptor{ - Annotations: descriptor.Annotations, - MediaType: descriptor.MediaType, - Platform: descriptor.Platform, - URLs: descriptor.URLs, - }, - }) } } @@ -120,3 +121,61 @@ func mutateImage(image containerreg.Image, annotations map[string]string, labels return image, nil } + +func MutateImageOrImageIndexTimestamp(image containerreg.Image, imageIndex containerreg.ImageIndex, timestamp time.Time) (containerreg.Image, containerreg.ImageIndex, error) { + if image != nil { + image, err := mutateImageTimestamp(image, timestamp) + return image, nil, err + } + + imageIndex, err := mutateImageIndexTimestamp(imageIndex, timestamp) + return nil, imageIndex, err +} + +func mutateImageTimestamp(image containerreg.Image, timestamp time.Time) (containerreg.Image, error) { + image, err := mutate.Time(image, timestamp) + if err != nil { + return nil, err + } + + return image, nil +} + +func mutateImageIndexTimestamp(imageIndex containerreg.ImageIndex, timestamp time.Time) (containerreg.ImageIndex, error) { + indexManifest, err := imageIndex.IndexManifest() + if err != nil { + return nil, err + } + + for _, desc := range indexManifest.Manifests { + switch desc.MediaType { + case types.OCIImageIndex, types.DockerManifestList: + childImageIndex, err := imageIndex.ImageIndex(desc.Digest) + if err != nil { + return nil, err + } + + childImageIndex, err = mutateImageIndexTimestamp(childImageIndex, timestamp) + if err != nil { + return nil, err + } + + imageIndex = replaceManifestIn(imageIndex, desc, childImageIndex) + + case types.OCIManifestSchema1, types.DockerManifestSchema2: + image, err := imageIndex.Image(desc.Digest) + if err != nil { + return nil, err + } + + image, err = mutateImageTimestamp(image, timestamp) + if err != nil { + return nil, err + } + + imageIndex = replaceManifestIn(imageIndex, desc, image) + } + } + + return imageIndex, nil +} diff --git a/pkg/image/mutate_test.go b/pkg/image/mutate_test.go index 444f39b504..3c56da3f2a 100644 --- a/pkg/image/mutate_test.go +++ b/pkg/image/mutate_test.go @@ -5,6 +5,9 @@ package image_test import ( + "fmt" + "time" + containerreg "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/random" @@ -157,4 +160,76 @@ var _ = Describe("MutateImageOrImageIndex", func() { } }) }) + + Context("mutate creation timestamp", func() { + referenceTime := time.Unix(1700000000, 0) + + creationTimeOf := func(img containerreg.Image) time.Time { + GinkgoHelper() + cfg, err := img.ConfigFile() + Expect(err).ToNot(HaveOccurred()) + return cfg.Created.Time + } + + imagesOf := func(index containerreg.ImageIndex) (result []containerreg.Image) { + indexManifest, err := index.IndexManifest() + Expect(err).ToNot(HaveOccurred()) + + for _, desc := range indexManifest.Manifests { + img, err := index.Image(desc.Digest) + Expect(err).ToNot(HaveOccurred()) + result = append(result, img) + } + + return result + } + + Context("mutating timestamp of an image", func() { + var img containerreg.Image + + BeforeEach(func() { + var err error + img, err = random.Image(1024, 1) + Expect(err).ToNot(HaveOccurred()) + Expect(creationTimeOf(img)).ToNot(BeTemporally("==", referenceTime)) + }) + + It("should mutate an image by setting a creation timestamp", func() { + img, _, err := image.MutateImageOrImageIndexTimestamp(img, nil, referenceTime) + Expect(err).ToNot(HaveOccurred()) + Expect(creationTimeOf(img)).To(BeTemporally("==", referenceTime)) + }) + }) + + Context("mutating timestamp of an image index", func() { + var index containerreg.ImageIndex + + BeforeEach(func() { + var err error + index, err = random.Index(1024, 2, 2) + Expect(err).ToNot(HaveOccurred()) + + for _, img := range imagesOf(index) { + fmt.Fprintf(GinkgoWriter, "%v=%v\n", + func() string { + digest, err := img.Digest() + Expect(err).ToNot(HaveOccurred()) + return digest.String() + }(), + creationTimeOf(img), + ) + Expect(creationTimeOf(img)).ToNot(BeTemporally("==", referenceTime)) + } + }) + + It("should mutate an image by setting a creation timestamp", func() { + _, index, err := image.MutateImageOrImageIndexTimestamp(nil, index, referenceTime) + Expect(err).ToNot(HaveOccurred()) + + for _, img := range imagesOf(index) { + Expect(creationTimeOf(img)).To(BeTemporally("==", referenceTime)) + } + }) + }) + }) }) diff --git a/pkg/reconciler/build/build.go b/pkg/reconciler/build/build.go index ea4d246ea9..b45c376b7d 100644 --- a/pkg/reconciler/build/build.go +++ b/pkg/reconciler/build/build.go @@ -29,6 +29,7 @@ var validationTypes = [...]string{ validate.Secrets, validate.Strategies, validate.Source, + validate.Output, validate.BuildName, validate.Envs, validate.Triggers, diff --git a/pkg/reconciler/build/build_test.go b/pkg/reconciler/build/build_test.go index bd4a14ac15..3e08dd213b 100644 --- a/pkg/reconciler/build/build_test.go +++ b/pkg/reconciler/build/build_test.go @@ -552,5 +552,42 @@ var _ = Describe("Reconcile Build", func() { Expect(err).ToNot(BeNil()) }) }) + + Context("when build object has output timestamp defined", func() { + It("should fail build validation due to unsupported combination of empty source with output image timestamp set to be the source timestamp", func() { + buildSample.Spec.Output.Timestamp = pointer.String(build.OutputImageSourceTimestamp) + buildSample.Spec.Output.PushSecret = nil + buildSample.Spec.Source = &build.Source{} + + statusWriter.UpdateCalls(func(ctx context.Context, o crc.Object, sruo ...crc.SubResourceUpdateOption) error { + Expect(o).To(BeAssignableToTypeOf(&build.Build{})) + build := o.(*build.Build) + Expect(*build.Status.Reason).To(BeEquivalentTo("OutputTimestampValueNotSupported")) + Expect(*build.Status.Message).To(BeEquivalentTo("cannot use SourceTimestamp output image setting with an empty build source")) + + return nil + }) + + _, err := reconciler.Reconcile(context.TODO(), request) + Expect(err).ToNot(HaveOccurred()) + }) + + It("should fail when the output timestamp is not a parsable number", func() { + buildSample.Spec.Output.Timestamp = pointer.String("forty-two") + buildSample.Spec.Output.PushSecret = nil + + statusWriter.UpdateCalls(func(ctx context.Context, o crc.Object, sruo ...crc.SubResourceUpdateOption) error { + Expect(o).To(BeAssignableToTypeOf(&build.Build{})) + build := o.(*build.Build) + Expect(*build.Status.Reason).To(BeEquivalentTo("OutputTimestampValueNotValid")) + Expect(*build.Status.Message).To(BeEquivalentTo("output timestamp value is invalid, must be Zero, SourceTimestamp, BuildTimestamp, or number")) + + return nil + }) + + _, err := reconciler.Reconcile(context.TODO(), request) + Expect(err).ToNot(HaveOccurred()) + }) + }) }) }) diff --git a/pkg/reconciler/buildrun/resources/image_processing.go b/pkg/reconciler/buildrun/resources/image_processing.go index 6ad80852e2..28058e58d2 100644 --- a/pkg/reconciler/buildrun/resources/image_processing.go +++ b/pkg/reconciler/buildrun/resources/image_processing.go @@ -6,12 +6,15 @@ package resources import ( "fmt" + "strconv" + "time" + + core "k8s.io/api/core/v1" build "github.com/shipwright-io/build/pkg/apis/build/v1beta1" "github.com/shipwright-io/build/pkg/config" "github.com/shipwright-io/build/pkg/reconciler/buildrun/resources/sources" pipelineapi "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" - core "k8s.io/api/core/v1" ) const ( @@ -21,7 +24,7 @@ const ( ) // SetupImageProcessing appends the image-processing step to a TaskRun if desired -func SetupImageProcessing(taskRun *pipelineapi.TaskRun, cfg *config.Config, buildOutput, buildRunOutput build.Image) { +func SetupImageProcessing(taskRun *pipelineapi.TaskRun, cfg *config.Config, creationTimestamp time.Time, buildOutput, buildRunOutput build.Image) error { stepArgs := []string{} // Check if any build step references the output-directory system parameter. If that is the case, @@ -82,6 +85,32 @@ func SetupImageProcessing(taskRun *pipelineapi.TaskRun, cfg *config.Config, buil stepArgs = append(stepArgs, convertMutateArgs("--label", labels)...) } + // check if we need to set image timestamp + if imageTimestamp := getImageTimestamp(buildOutput, buildRunOutput); imageTimestamp != nil { + switch *imageTimestamp { + case build.OutputImageZeroTimestamp: + stepArgs = append(stepArgs, "--image-timestamp", "0") + + case build.OutputImageSourceTimestamp: + if !hasTaskSpecResult(taskRun, "shp-source-default-source-timestamp") { + return fmt.Errorf("cannot use SourceTimestamp setting, because there is no source timestamp available for this source") + } + + stepArgs = append(stepArgs, "--image-timestamp-file", "$(results.shp-source-default-source-timestamp.path)") + + case build.OutputImageBuildTimestamp: + stepArgs = append(stepArgs, "--image-timestamp", strconv.FormatInt(creationTimestamp.Unix(), 10)) + + default: + if _, err := strconv.ParseInt(*imageTimestamp, 10, 64); err != nil { + return fmt.Errorf("cannot parse output timestamp %s as a number, must be %s, %s, %s, or a an integer", + *imageTimestamp, build.OutputImageZeroTimestamp, build.OutputImageSourceTimestamp, build.OutputImageBuildTimestamp) + } + + stepArgs = append(stepArgs, "--image-timestamp", *imageTimestamp) + } + } + // check if there is anything to do if len(stepArgs) > 0 { // add the image argument @@ -138,6 +167,8 @@ func SetupImageProcessing(taskRun *pipelineapi.TaskRun, cfg *config.Config, buil // append the mutate step taskRun.Spec.TaskSpec.Steps = append(taskRun.Spec.TaskSpec.Steps, imageProcessingStep) } + + return nil } // convertMutateArgs to convert the argument map to comma separated values @@ -162,3 +193,26 @@ func mergeMaps(first map[string]string, second map[string]string) map[string]str } return first } + +func getImageTimestamp(buildOutput, buildRunOutput build.Image) *string { + switch { + case buildRunOutput.Timestamp != nil: + return buildRunOutput.Timestamp + + case buildOutput.Timestamp != nil: + return buildOutput.Timestamp + + default: + return nil + } +} + +func hasTaskSpecResult(taskRun *pipelineapi.TaskRun, name string) bool { + for _, result := range taskRun.Spec.TaskSpec.Results { + if result.Name == name { + return true + } + } + + return false +} diff --git a/pkg/reconciler/buildrun/resources/image_processing_test.go b/pkg/reconciler/buildrun/resources/image_processing_test.go index a96d9ac208..fb9a6bc9f5 100644 --- a/pkg/reconciler/buildrun/resources/image_processing_test.go +++ b/pkg/reconciler/buildrun/resources/image_processing_test.go @@ -5,6 +5,8 @@ package resources_test import ( + "time" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -12,6 +14,7 @@ import ( "github.com/shipwright-io/build/pkg/config" "github.com/shipwright-io/build/pkg/reconciler/buildrun/resources" utils "github.com/shipwright-io/build/test/utils/v1beta1" + pipelineapi "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" ) @@ -20,6 +23,9 @@ var _ = Describe("Image Processing overrides", func() { config := config.NewDefaultConfig() var processedTaskRun *pipelineapi.TaskRun + // just a fixed reference timestamp for the setup function + refTimestamp := time.Unix(1234567890, 0) + Context("for a TaskRun that does not reference the output directory", func() { taskRun := &pipelineapi.TaskRun{ Spec: pipelineapi.TaskRunSpec{ @@ -36,9 +42,13 @@ var _ = Describe("Image Processing overrides", func() { Context("for a build without labels and annotation in the output", func() { BeforeEach(func() { processedTaskRun = taskRun.DeepCopy() - resources.SetupImageProcessing(processedTaskRun, config, buildv1beta1.Image{ - Image: "some-registry/some-namespace/some-image", - }, buildv1beta1.Image{}) + Expect(resources.SetupImageProcessing( + processedTaskRun, + config, + refTimestamp, + buildv1beta1.Image{Image: "some-registry/some-namespace/some-image"}, + buildv1beta1.Image{}, + )).To(Succeed()) }) It("does not add the image-processing step", func() { @@ -50,12 +60,18 @@ var _ = Describe("Image Processing overrides", func() { Context("for a build with a label in the output", func() { BeforeEach(func() { processedTaskRun = taskRun.DeepCopy() - resources.SetupImageProcessing(processedTaskRun, config, buildv1beta1.Image{ - Image: "some-registry/some-namespace/some-image", - Labels: map[string]string{ - "aKey": "aLabel", + Expect(resources.SetupImageProcessing( + processedTaskRun, + config, + refTimestamp, + buildv1beta1.Image{ + Image: "some-registry/some-namespace/some-image", + Labels: map[string]string{ + "aKey": "aLabel", + }, }, - }, buildv1beta1.Image{}) + buildv1beta1.Image{}, + )).To(Succeed()) }) It("adds the image-processing step", func() { @@ -101,16 +117,22 @@ var _ = Describe("Image Processing overrides", func() { Context("for a build with label and annotation in the output", func() { BeforeEach(func() { processedTaskRun = taskRun.DeepCopy() - resources.SetupImageProcessing(processedTaskRun, config, buildv1beta1.Image{ - Image: "some-registry/some-namespace/some-image", - Labels: map[string]string{ - "a-label": "a-value", + Expect(resources.SetupImageProcessing( + processedTaskRun, + config, + refTimestamp, + buildv1beta1.Image{ + Image: "some-registry/some-namespace/some-image", + Labels: map[string]string{ + "a-label": "a-value", + }, }, - }, buildv1beta1.Image{ - Annotations: map[string]string{ - "an-annotation": "some-value", + buildv1beta1.Image{ + Annotations: map[string]string{ + "an-annotation": "some-value", + }, }, - }) + )).To(Succeed()) }) It("adds the output-directory parameter", func() { @@ -149,9 +171,13 @@ var _ = Describe("Image Processing overrides", func() { Context("for a build without labels and annotation in the output", func() { BeforeEach(func() { processedTaskRun = taskRun.DeepCopy() - resources.SetupImageProcessing(processedTaskRun, config, buildv1beta1.Image{ - Image: "some-registry/some-namespace/some-image", - }, buildv1beta1.Image{}) + Expect(resources.SetupImageProcessing( + processedTaskRun, + config, + refTimestamp, + buildv1beta1.Image{Image: "some-registry/some-namespace/some-image"}, + buildv1beta1.Image{}, + )).To(Succeed()) }) It("adds the output-directory parameter", func() { @@ -188,10 +214,16 @@ var _ = Describe("Image Processing overrides", func() { BeforeEach(func() { processedTaskRun = taskRun.DeepCopy() someSecret := "some-secret" - resources.SetupImageProcessing(processedTaskRun, config, buildv1beta1.Image{ - Image: "some-registry/some-namespace/some-image", - PushSecret: &someSecret, - }, buildv1beta1.Image{}) + Expect(resources.SetupImageProcessing( + processedTaskRun, + config, + refTimestamp, + buildv1beta1.Image{ + Image: "some-registry/some-namespace/some-image", + PushSecret: &someSecret, + }, + buildv1beta1.Image{}, + )).To(Succeed()) }) It("adds the output-directory parameter", func() { diff --git a/pkg/reconciler/buildrun/resources/taskrun.go b/pkg/reconciler/buildrun/resources/taskrun.go index 5a4991c9a3..40387490dc 100644 --- a/pkg/reconciler/buildrun/resources/taskrun.go +++ b/pkg/reconciler/buildrun/resources/taskrun.go @@ -324,7 +324,12 @@ func GenerateTaskRun( if buildRunOutput == nil { buildRunOutput = &buildv1beta1.Image{} } - SetupImageProcessing(expectedTaskRun, cfg, build.Spec.Output, *buildRunOutput) + + // Make sure that image-processing is setup in case it is needed, which can fail with an error + // in case some assumptions cannot be met, i.e. illegal combiniation of fields + if err := SetupImageProcessing(expectedTaskRun, cfg, buildRun.CreationTimestamp.Time, build.Spec.Output, *buildRunOutput); err != nil { + return nil, err + } return expectedTaskRun, nil } diff --git a/pkg/validate/output.go b/pkg/validate/output.go new file mode 100644 index 0000000000..054dd72cb4 --- /dev/null +++ b/pkg/validate/output.go @@ -0,0 +1,56 @@ +// Copyright The Shipwright Contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package validate + +import ( + "context" + "strconv" + + build "github.com/shipwright-io/build/pkg/apis/build/v1beta1" + "k8s.io/utils/pointer" +) + +// BuildSpecOutputValidator implements validation interface to add validations for `build.spec.output`. +type BuildSpecOutputValidator struct { + Build *build.Build // build instance for analysis +} + +var _ BuildPath = &BuildSpecOutputValidator{} + +func (b *BuildSpecOutputValidator) ValidatePath(_ context.Context) error { + if b.Build.Spec.Output.Timestamp != nil { + switch *b.Build.Spec.Output.Timestamp { + case "": + // no validation required + + case build.OutputImageZeroTimestamp: + // no validation required + + case build.OutputImageSourceTimestamp: + // check that there is a source defined that can be used in combination with source timestamp + if b.isEmptySource() { + b.Build.Status.Reason = build.BuildReasonPtr(build.OutputTimestampValueNotSupported) + b.Build.Status.Message = pointer.String("cannot use SourceTimestamp output image setting with an empty build source") + } + + case build.OutputImageBuildTimestamp: + // no validation required + + default: + // check that value is parsable integer + if _, err := strconv.ParseInt(*b.Build.Spec.Output.Timestamp, 10, 64); err != nil { + b.Build.Status.Reason = build.BuildReasonPtr(build.OutputTimestampValueNotValid) + b.Build.Status.Message = pointer.String("output timestamp value is invalid, must be Zero, SourceTimestamp, BuildTimestamp, or number") + } + } + } + + return nil +} + +func (b *BuildSpecOutputValidator) isEmptySource() bool { + return b.Build.Spec.Source == nil || + b.Build.Spec.Source.Git == nil && b.Build.Spec.Source.OCIArtifact == nil && b.Build.Spec.Source.Local == nil +} diff --git a/pkg/validate/output_test.go b/pkg/validate/output_test.go new file mode 100644 index 0000000000..f04171e5c6 --- /dev/null +++ b/pkg/validate/output_test.go @@ -0,0 +1,102 @@ +// Copyright The Shipwright Contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package validate_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + corev1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + . "github.com/shipwright-io/build/pkg/apis/build/v1beta1" + "github.com/shipwright-io/build/pkg/validate" +) + +var _ = Describe("BuildSpecOutputValidator", func() { + var ctx context.Context + + BeforeEach(func() { + ctx = context.TODO() + }) + + var validate = func(build *Build) { + GinkgoHelper() + + var validator = &validate.BuildSpecOutputValidator{Build: build} + Expect(validator.ValidatePath(ctx)).To(Succeed()) + } + + Context("output timestamp is specified", func() { + var sampleBuild = func(timestamp string) *Build { + return &Build{ + ObjectMeta: corev1.ObjectMeta{ + Namespace: "foo", + Name: "bar", + }, + Spec: BuildSpec{ + Source: &Source{ + Type: GitType, + Git: &Git{ + URL: "https://github.com/shipwright-io/sample-go", + }, + }, + Strategy: Strategy{ + Name: "magic", + }, + Output: Image{ + Timestamp: ×tamp, + }, + }, + } + } + + It("should pass an empty string", func() { + build := sampleBuild("") + validate(build) + Expect(build.Status.Reason).To(BeNil()) + Expect(build.Status.Message).To(BeNil()) + }) + + It("should pass with string Zero", func() { + build := sampleBuild(OutputImageZeroTimestamp) + validate(build) + Expect(build.Status.Reason).To(BeNil()) + Expect(build.Status.Message).To(BeNil()) + }) + + It("should pass with string SourceTimestamp", func() { + build := sampleBuild(OutputImageSourceTimestamp) + validate(build) + Expect(build.Status.Reason).To(BeNil()) + Expect(build.Status.Message).To(BeNil()) + }) + + It("should pass with string BuildTimestamp", func() { + build := sampleBuild(OutputImageBuildTimestamp) + validate(build) + Expect(build.Status.Reason).To(BeNil()) + Expect(build.Status.Message).To(BeNil()) + }) + + It("should fail with string SourceTimestamp in case there are no sources", func() { + build := sampleBuild(OutputImageSourceTimestamp) + build.Spec.Source = nil + + validate(build) + Expect(*build.Status.Reason).To(Equal(OutputTimestampValueNotSupported)) + Expect(*build.Status.Message).To(ContainSubstring("cannot use SourceTimestamp")) + }) + + It("should fail when invalid timestamp is used", func() { + build := sampleBuild("WrongValue") + + validate(build) + Expect(*build.Status.Reason).To(Equal(OutputTimestampValueNotValid)) + Expect(*build.Status.Message).To(ContainSubstring("output timestamp value is invalid")) + }) + }) +}) diff --git a/pkg/validate/validate.go b/pkg/validate/validate.go index 7e308152e0..91d15db46b 100644 --- a/pkg/validate/validate.go +++ b/pkg/validate/validate.go @@ -24,6 +24,8 @@ const ( SourceURL = "sourceurl" // Sources for validating `spec.sources` entries Source = "source" + // Output for validating `spec.output` entry + Output = "output" // BuildName for validating `metadata.name` entry BuildName = "buildname" // Envs for validating `spec.env` entries @@ -65,6 +67,8 @@ func NewValidation( return &OwnerRef{Build: build, Client: client, Scheme: scheme}, nil case Source: return &SourceRef{Build: build}, nil + case Output: + return &BuildSpecOutputValidator{Build: build}, nil case BuildName: return &BuildNameRef{Build: build}, nil case Envs: diff --git a/test/e2e/v1alpha1/common_suite_test.go b/test/e2e/v1alpha1/common_suite_test.go index 2c7475342d..d7c1a18f7f 100644 --- a/test/e2e/v1alpha1/common_suite_test.go +++ b/test/e2e/v1alpha1/common_suite_test.go @@ -13,6 +13,7 @@ import ( "time" . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" core "k8s.io/api/core/v1" meta "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -78,6 +79,11 @@ func (b *buildPrototype) SourceGit(repository string) *buildPrototype { return b } +func (b *buildPrototype) SourceGitRevision(revision string) *buildPrototype { + b.build.Spec.Source.Revision = pointer.String(revision) + return b +} + func (b *buildPrototype) SourceBundle(image string) *buildPrototype { if b.build.Spec.Source.BundleContainer == nil { b.build.Spec.Source.BundleContainer = &buildv1alpha1.BundleContainer{} @@ -104,11 +110,6 @@ func (b *buildPrototype) Dockerfile(dockerfile string) *buildPrototype { return b } -func (b *buildPrototype) OutputImage(image string) *buildPrototype { - b.build.Spec.Output.Image = image - return b -} - func (b *buildPrototype) determineParameterIndex(name string) int { index := -1 for i, paramValue := range b.build.Spec.ParamValues { @@ -207,6 +208,11 @@ func (b *buildPrototype) StringParamValueFromSecret(name string, secretName stri return b } +func (b *buildPrototype) OutputImage(image string) *buildPrototype { + b.build.Spec.Output.Image = image + return b +} + func (b *buildPrototype) OutputImageCredentials(name string) *buildPrototype { if name != "" { b.build.Spec.Output.Credentials = &core.LocalObjectReference{Name: name} @@ -221,6 +227,11 @@ func (b *buildPrototype) OutputImageInsecure(insecure bool) *buildPrototype { return b } +func (b *buildPrototype) OutputTimestamp(timestampString string) *buildPrototype { + b.build.Spec.Output.Timestamp = ×tampString + return b +} + func (b buildPrototype) Create() (build *buildv1alpha1.Build, err error) { ctx := context.Background() @@ -390,6 +401,16 @@ func (b *buildRunPrototype) Create() (*buildv1alpha1.BuildRun, error) { Create(context.Background(), &b.buildRun, meta.CreateOptions{}) } +func (b *buildRunPrototype) MustCreate() *buildv1alpha1.BuildRun { + GinkgoHelper() + + buildrun, err := b.Create() + Expect(err).ToNot(HaveOccurred()) + Expect(buildrun).ToNot(BeNil()) + + return buildrun +} + // Logf logs data func Logf(format string, args ...interface{}) { currentTime := time.Now().UTC().Format(time.RFC3339) diff --git a/test/e2e/v1alpha1/e2e_image_mutate_test.go b/test/e2e/v1alpha1/e2e_image_mutate_test.go index d2fdacb5f0..024edf608f 100644 --- a/test/e2e/v1alpha1/e2e_image_mutate_test.go +++ b/test/e2e/v1alpha1/e2e_image_mutate_test.go @@ -5,10 +5,17 @@ package e2e_test import ( - containerreg "github.com/google/go-containerregistry/pkg/v1" + "fmt" + "os" + "strconv" + "time" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/google/go-containerregistry/pkg/name" + containerreg "github.com/google/go-containerregistry/pkg/v1" + buildv1alpha1 "github.com/shipwright-io/build/pkg/apis/build/v1alpha1" ) @@ -20,6 +27,27 @@ var _ = Describe("For a Kubernetes cluster with Tekton and build installed", fun buildRun *buildv1alpha1.BuildRun ) + annotationsOf := func(img containerreg.Image) map[string]string { + GinkgoHelper() + manifest, err := img.Manifest() + Expect(err).To(BeNil()) + return manifest.Annotations + } + + labelsOf := func(img containerreg.Image) map[string]string { + GinkgoHelper() + config, err := img.ConfigFile() + Expect(err).To(BeNil()) + return config.Config.Labels + } + + creationTimeOf := func(img containerreg.Image) time.Time { + GinkgoHelper() + cfg, err := img.ConfigFile() + Expect(err).ToNot(HaveOccurred()) + return cfg.Created.Time + } + AfterEach(func() { if CurrentSpecReport().Failed() { printTestFailureDebugInfo(testBuild, testBuild.Namespace, testID) @@ -62,28 +90,182 @@ var _ = Describe("For a Kubernetes cluster with Tekton and build installed", fun testBuild.ValidateImageDigest(buildRun) image := testBuild.GetImage(buildRun) + Expect(annotationsOf(image)).To(HaveKeyWithValue("org.opencontainers.image.url", "https://my-company.com/images")) + Expect(labelsOf(image)).To(HaveKeyWithValue("maintainer", "team@my-company.com")) + }) + }) - Expect( - getImageAnnotation(image, "org.opencontainers.image.url"), - ).To(Equal("https://my-company.com/images")) + Context("mutate image timestamp", func() { + var outputImage name.Reference - Expect( - getImageLabel(image, "maintainer"), - ).To(Equal("team@my-company.com")) + var insecure = func() bool { + if val, ok := os.LookupEnv(EnvVarImageRepoInsecure); ok { + if result, err := strconv.ParseBool(val); err == nil { + return result + } + } + + return false + }() + + BeforeEach(func() { + testID = generateTestID("timestamp") + + outputImage, err = name.ParseReference(fmt.Sprintf("%s/%s:%s", + os.Getenv(EnvVarImageRepo), + testID, + "latest", + )) + Expect(err).ToNot(HaveOccurred()) }) - }) -}) -func getImageAnnotation(img containerreg.Image, annotation string) string { - manifest, err := img.Manifest() - Expect(err).To(BeNil()) + Context("when using BuildKit based Dockerfile build", func() { + var sampleBuildRun = func(outputTimestamp string) *buildv1alpha1.BuildRun { + return NewBuildRunPrototype(). + Namespace(testBuild.Namespace). + Name(testID). + WithBuildSpec(NewBuildPrototype(). + ClusterBuildStrategy("buildkit"). + Namespace(testBuild.Namespace). + Name(testID). + SourceGit("https://github.com/shipwright-io/sample-nodejs"). + SourceGitRevision("0be20591d7096bef165949c22f6059f5d8eb6a85"). + SourceContextDir("docker-build"). + Dockerfile("Dockerfile"). + OutputImage(outputImage.String()). + OutputImageCredentials(os.Getenv(EnvVarImageRepoSecret)). + OutputImageInsecure(insecure). + OutputTimestamp(outputTimestamp). + BuildSpec()). + MustCreate() + } - return manifest.Annotations[annotation] -} + It("should create an image with creation timestamp set to unix epoch timestamp zero", func() { + buildRun := validateBuildRunToSucceed(testBuild, sampleBuildRun(buildv1alpha1.OutputImageZeroTimestamp)) + image := testBuild.GetImage(buildRun) + Expect(creationTimeOf(image)).To(BeTemporally("==", time.Unix(0, 0))) + }) -func getImageLabel(img containerreg.Image, label string) string { - config, err := img.ConfigFile() - Expect(err).To(BeNil()) + It("should create an image with creation timestamp set to the source timestamp", func() { + buildRun := validateBuildRunToSucceed(testBuild, sampleBuildRun(buildv1alpha1.OutputImageSourceTimestamp)) + image := testBuild.GetImage(buildRun) + Expect(creationTimeOf(image)).To(BeTemporally("==", time.Unix(1699261787, 0))) + }) - return config.Config.Labels[label] -} + It("should create an image with creation timestamp set to the build timestamp", func() { + buildRun := validateBuildRunToSucceed(testBuild, sampleBuildRun(buildv1alpha1.OutputImageBuildTimestamp)) + image := testBuild.GetImage(buildRun) + Expect(creationTimeOf(image)).To(BeTemporally("==", buildRun.CreationTimestamp.Time)) + }) + + It("should create an image with creation timestamp set to a custom timestamp", func() { + buildRun := validateBuildRunToSucceed(testBuild, sampleBuildRun("1691691691")) + image := testBuild.GetImage(buildRun) + Expect(creationTimeOf(image)).To(BeTemporally("==", time.Unix(1691691691, 0))) + }) + }) + + Context("when using Buildpacks build", func() { + var sampleBuildRun = func(outputTimestamp string) *buildv1alpha1.BuildRun { + return NewBuildRunPrototype(). + Namespace(testBuild.Namespace). + Name(testID). + WithBuildSpec(NewBuildPrototype(). + ClusterBuildStrategy("buildpacks-v3"). + Namespace(testBuild.Namespace). + Name(testID). + SourceGit("https://github.com/shipwright-io/sample-nodejs"). + SourceGitRevision("0be20591d7096bef165949c22f6059f5d8eb6a85"). + SourceContextDir("source-build"). + OutputImage(outputImage.String()). + OutputImageCredentials(os.Getenv(EnvVarImageRepoSecret)). + OutputImageInsecure(insecure). + OutputTimestamp(outputTimestamp). + BuildSpec()). + MustCreate() + } + + It("should create an image with creation timestamp set to unix epoch timestamp zero", func() { + buildRun := validateBuildRunToSucceed(testBuild, sampleBuildRun(buildv1alpha1.OutputImageZeroTimestamp)) + image := testBuild.GetImage(buildRun) + Expect(creationTimeOf(image)).To(BeTemporally("==", time.Unix(0, 0))) + }) + + It("should create an image with creation timestamp set to the source timestamp", func() { + buildRun := validateBuildRunToSucceed(testBuild, sampleBuildRun(buildv1alpha1.OutputImageSourceTimestamp)) + image := testBuild.GetImage(buildRun) + Expect(creationTimeOf(image)).To(BeTemporally("==", time.Unix(1699261787, 0))) + }) + + It("should create an image with creation timestamp set to the build timestamp", func() { + buildRun := validateBuildRunToSucceed(testBuild, sampleBuildRun(buildv1alpha1.OutputImageBuildTimestamp)) + image := testBuild.GetImage(buildRun) + Expect(creationTimeOf(image)).To(BeTemporally("==", buildRun.CreationTimestamp.Time)) + }) + + It("should create an image with creation timestamp set to a custom timestamp", func() { + buildRun := validateBuildRunToSucceed(testBuild, sampleBuildRun("1691691691")) + image := testBuild.GetImage(buildRun) + Expect(creationTimeOf(image)).To(BeTemporally("==", time.Unix(1691691691, 0))) + }) + }) + + Context("edge cases", func() { + It("should fail run a build run when source timestamp is used with an empty source", func() { + buildRun = NewBuildRunPrototype(). + Namespace(testBuild.Namespace). + Name(testID). + WithBuildSpec(NewBuildPrototype(). + ClusterBuildStrategy("buildkit"). + Namespace(testBuild.Namespace). + Name(testID). + OutputImage(outputImage.String()). + OutputImageCredentials(os.Getenv(EnvVarImageRepoSecret)). + OutputImageInsecure(insecure). + OutputTimestamp(buildv1alpha1.OutputImageSourceTimestamp). + BuildSpec()). + MustCreate() + + Expect(testBuild.CreateBR(buildRun)).ToNot(Succeed()) + + buildRun, err = testBuild.GetBRTillCompletion(buildRun.Name) + Expect(err).ToNot(HaveOccurred()) + + condition := buildRun.Status.GetCondition(buildv1alpha1.Succeeded) + Expect(condition).ToNot(BeNil()) + Expect(condition.Reason).To(ContainSubstring("TaskRunGenerationFailed")) + Expect(condition.Message).To(ContainSubstring("cannot use SourceTimestamp setting")) + }) + + It("should fail fail when output timestamp value is not valid", func() { + buildRun = NewBuildRunPrototype(). + Namespace(testBuild.Namespace). + Name(testID). + WithBuildSpec(NewBuildPrototype(). + ClusterBuildStrategy("buildkit"). + Namespace(testBuild.Namespace). + Name(testID). + SourceGit("https://github.com/shipwright-io/sample-nodejs"). + SourceGitRevision("0be20591d7096bef165949c22f6059f5d8eb6a85"). + SourceContextDir("docker-build"). + Dockerfile("Dockerfile"). + OutputImage(outputImage.String()). + OutputImageCredentials(os.Getenv(EnvVarImageRepoSecret)). + OutputImageInsecure(insecure). + OutputTimestamp("WrongValue"). + BuildSpec()). + MustCreate() + + Expect(testBuild.CreateBR(buildRun)).ToNot(Succeed()) + + buildRun, err = testBuild.GetBRTillCompletion(buildRun.Name) + Expect(err).ToNot(HaveOccurred()) + + condition := buildRun.Status.GetCondition(buildv1alpha1.Succeeded) + Expect(condition).ToNot(BeNil()) + Expect(condition.Reason).To(ContainSubstring("Failed")) + Expect(condition.Message).To(ContainSubstring("cannot parse output timestamp")) + }) + }) + }) +}) diff --git a/test/e2e/v1beta1/common_suite_test.go b/test/e2e/v1beta1/common_suite_test.go index a5dc8f8d25..699e0c9a4d 100644 --- a/test/e2e/v1beta1/common_suite_test.go +++ b/test/e2e/v1beta1/common_suite_test.go @@ -13,6 +13,7 @@ import ( "time" . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" core "k8s.io/api/core/v1" meta "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -101,6 +102,14 @@ func (b *buildPrototype) SourceGit(repository string) *buildPrototype { return b } +func (b *buildPrototype) SourceGitRevision(revision string) *buildPrototype { + if b.build.Spec.Source.Git == nil { + b.build.Spec.Source.Git = &buildv1beta1.Git{} + } + b.build.Spec.Source.Git.Revision = &revision + return b +} + func (b *buildPrototype) SourceBundle(image string) *buildPrototype { if b.build.Spec.Source == nil { b.build.Spec.Source = &buildv1beta1.Source{} @@ -256,6 +265,11 @@ func (b *buildPrototype) OutputImageInsecure(insecure bool) *buildPrototype { return b } +func (b *buildPrototype) OutputTimestamp(timestampString string) *buildPrototype { + b.build.Spec.Output.Timestamp = ×tampString + return b +} + func (b buildPrototype) Create() (build *buildv1beta1.Build, err error) { ctx := context.Background() @@ -425,6 +439,16 @@ func (b *buildRunPrototype) Create() (*buildv1beta1.BuildRun, error) { Create(context.Background(), &b.buildRun, meta.CreateOptions{}) } +func (b *buildRunPrototype) MustCreate() *buildv1beta1.BuildRun { + GinkgoHelper() + + buildrun, err := b.Create() + Expect(err).ToNot(HaveOccurred()) + Expect(buildrun).ToNot(BeNil()) + + return buildrun +} + // Logf logs data func Logf(format string, args ...interface{}) { currentTime := time.Now().UTC().Format(time.RFC3339) diff --git a/test/e2e/v1beta1/e2e_image_mutate_test.go b/test/e2e/v1beta1/e2e_image_mutate_test.go index 965a58f1ac..8a36253c94 100644 --- a/test/e2e/v1beta1/e2e_image_mutate_test.go +++ b/test/e2e/v1beta1/e2e_image_mutate_test.go @@ -5,10 +5,17 @@ package e2e_test import ( - containerreg "github.com/google/go-containerregistry/pkg/v1" + "fmt" + "os" + "strconv" + "time" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/google/go-containerregistry/pkg/name" + containerreg "github.com/google/go-containerregistry/pkg/v1" + buildv1beta1 "github.com/shipwright-io/build/pkg/apis/build/v1beta1" ) @@ -20,6 +27,27 @@ var _ = Describe("For a Kubernetes cluster with Tekton and build installed", fun buildRun *buildv1beta1.BuildRun ) + annotationsOf := func(img containerreg.Image) map[string]string { + GinkgoHelper() + manifest, err := img.Manifest() + Expect(err).To(BeNil()) + return manifest.Annotations + } + + labelsOf := func(img containerreg.Image) map[string]string { + GinkgoHelper() + config, err := img.ConfigFile() + Expect(err).To(BeNil()) + return config.Config.Labels + } + + creationTimeOf := func(img containerreg.Image) time.Time { + GinkgoHelper() + cfg, err := img.ConfigFile() + Expect(err).ToNot(HaveOccurred()) + return cfg.Created.Time + } + AfterEach(func() { if CurrentSpecReport().Failed() { printTestFailureDebugInfo(testBuild, testBuild.Namespace, testID) @@ -62,28 +90,182 @@ var _ = Describe("For a Kubernetes cluster with Tekton and build installed", fun testBuild.ValidateImageDigest(buildRun) image := testBuild.GetImage(buildRun) + Expect(annotationsOf(image)).To(HaveKeyWithValue("org.opencontainers.image.url", "https://my-company.com/images")) + Expect(labelsOf(image)).To(HaveKeyWithValue("maintainer", "team@my-company.com")) + }) + }) - Expect( - getImageAnnotation(image, "org.opencontainers.image.url"), - ).To(Equal("https://my-company.com/images")) + Context("mutate image timestamp", func() { + var outputImage name.Reference - Expect( - getImageLabel(image, "maintainer"), - ).To(Equal("team@my-company.com")) + var insecure = func() bool { + if val, ok := os.LookupEnv(EnvVarImageRepoInsecure); ok { + if result, err := strconv.ParseBool(val); err == nil { + return result + } + } + + return false + }() + + BeforeEach(func() { + testID = generateTestID("timestamp") + + outputImage, err = name.ParseReference(fmt.Sprintf("%s/%s:%s", + os.Getenv(EnvVarImageRepo), + testID, + "latest", + )) + Expect(err).ToNot(HaveOccurred()) }) - }) -}) -func getImageAnnotation(img containerreg.Image, annotation string) string { - manifest, err := img.Manifest() - Expect(err).To(BeNil()) + Context("when using BuildKit based Dockerfile build", func() { + var sampleBuildRun = func(outputTimestamp string) *buildv1beta1.BuildRun { + return NewBuildRunPrototype(). + Namespace(testBuild.Namespace). + Name(testID). + WithBuildSpec(NewBuildPrototype(). + ClusterBuildStrategy("buildkit"). + Namespace(testBuild.Namespace). + Name(testID). + SourceGit("https://github.com/shipwright-io/sample-nodejs"). + SourceGitRevision("0be20591d7096bef165949c22f6059f5d8eb6a85"). + SourceContextDir("docker-build"). + Dockerfile("Dockerfile"). + OutputImage(outputImage.String()). + OutputImageCredentials(os.Getenv(EnvVarImageRepoSecret)). + OutputImageInsecure(insecure). + OutputTimestamp(outputTimestamp). + BuildSpec()). + MustCreate() + } - return manifest.Annotations[annotation] -} + It("should create an image with creation timestamp set to unix epoch timestamp zero", func() { + buildRun := validateBuildRunToSucceed(testBuild, sampleBuildRun(buildv1beta1.OutputImageZeroTimestamp)) + image := testBuild.GetImage(buildRun) + Expect(creationTimeOf(image)).To(BeTemporally("==", time.Unix(0, 0))) + }) -func getImageLabel(img containerreg.Image, label string) string { - config, err := img.ConfigFile() - Expect(err).To(BeNil()) + It("should create an image with creation timestamp set to the source timestamp", func() { + buildRun := validateBuildRunToSucceed(testBuild, sampleBuildRun(buildv1beta1.OutputImageSourceTimestamp)) + image := testBuild.GetImage(buildRun) + Expect(creationTimeOf(image)).To(BeTemporally("==", time.Unix(1699261787, 0))) + }) - return config.Config.Labels[label] -} + It("should create an image with creation timestamp set to the build timestamp", func() { + buildRun := validateBuildRunToSucceed(testBuild, sampleBuildRun(buildv1beta1.OutputImageBuildTimestamp)) + image := testBuild.GetImage(buildRun) + Expect(creationTimeOf(image)).To(BeTemporally("==", buildRun.CreationTimestamp.Time)) + }) + + It("should create an image with creation timestamp set to a custom timestamp", func() { + buildRun := validateBuildRunToSucceed(testBuild, sampleBuildRun("1691691691")) + image := testBuild.GetImage(buildRun) + Expect(creationTimeOf(image)).To(BeTemporally("==", time.Unix(1691691691, 0))) + }) + }) + + Context("when using Buildpacks build", func() { + var sampleBuildRun = func(outputTimestamp string) *buildv1beta1.BuildRun { + return NewBuildRunPrototype(). + Namespace(testBuild.Namespace). + Name(testID). + WithBuildSpec(NewBuildPrototype(). + ClusterBuildStrategy("buildpacks-v3"). + Namespace(testBuild.Namespace). + Name(testID). + SourceGit("https://github.com/shipwright-io/sample-nodejs"). + SourceGitRevision("0be20591d7096bef165949c22f6059f5d8eb6a85"). + SourceContextDir("source-build"). + OutputImage(outputImage.String()). + OutputImageCredentials(os.Getenv(EnvVarImageRepoSecret)). + OutputImageInsecure(insecure). + OutputTimestamp(outputTimestamp). + BuildSpec()). + MustCreate() + } + + It("should create an image with creation timestamp set to unix epoch timestamp zero", func() { + buildRun := validateBuildRunToSucceed(testBuild, sampleBuildRun(buildv1beta1.OutputImageZeroTimestamp)) + image := testBuild.GetImage(buildRun) + Expect(creationTimeOf(image)).To(BeTemporally("==", time.Unix(0, 0))) + }) + + It("should create an image with creation timestamp set to the source timestamp", func() { + buildRun := validateBuildRunToSucceed(testBuild, sampleBuildRun(buildv1beta1.OutputImageSourceTimestamp)) + image := testBuild.GetImage(buildRun) + Expect(creationTimeOf(image)).To(BeTemporally("==", time.Unix(1699261787, 0))) + }) + + It("should create an image with creation timestamp set to the build timestamp", func() { + buildRun := validateBuildRunToSucceed(testBuild, sampleBuildRun(buildv1beta1.OutputImageBuildTimestamp)) + image := testBuild.GetImage(buildRun) + Expect(creationTimeOf(image)).To(BeTemporally("==", buildRun.CreationTimestamp.Time)) + }) + + It("should create an image with creation timestamp set to a custom timestamp", func() { + buildRun := validateBuildRunToSucceed(testBuild, sampleBuildRun("1691691691")) + image := testBuild.GetImage(buildRun) + Expect(creationTimeOf(image)).To(BeTemporally("==", time.Unix(1691691691, 0))) + }) + }) + + Context("edge cases", func() { + It("should fail run a build run when source timestamp is used with an empty source", func() { + buildRun = NewBuildRunPrototype(). + Namespace(testBuild.Namespace). + Name(testID). + WithBuildSpec(NewBuildPrototype(). + ClusterBuildStrategy("buildkit"). + Namespace(testBuild.Namespace). + Name(testID). + OutputImage(outputImage.String()). + OutputImageCredentials(os.Getenv(EnvVarImageRepoSecret)). + OutputImageInsecure(insecure). + OutputTimestamp(buildv1beta1.OutputImageSourceTimestamp). + BuildSpec()). + MustCreate() + + Expect(testBuild.CreateBR(buildRun)).ToNot(Succeed()) + + buildRun, err = testBuild.GetBRTillCompletion(buildRun.Name) + Expect(err).ToNot(HaveOccurred()) + + condition := buildRun.Status.GetCondition(buildv1beta1.Succeeded) + Expect(condition).ToNot(BeNil()) + Expect(condition.Reason).To(ContainSubstring("TaskRunGenerationFailed")) + Expect(condition.Message).To(ContainSubstring("cannot use SourceTimestamp setting")) + }) + + It("should fail fail when output timestamp value is not valid", func() { + buildRun = NewBuildRunPrototype(). + Namespace(testBuild.Namespace). + Name(testID). + WithBuildSpec(NewBuildPrototype(). + ClusterBuildStrategy("buildkit"). + Namespace(testBuild.Namespace). + Name(testID). + SourceGit("https://github.com/shipwright-io/sample-nodejs"). + SourceGitRevision("0be20591d7096bef165949c22f6059f5d8eb6a85"). + SourceContextDir("docker-build"). + Dockerfile("Dockerfile"). + OutputImage(outputImage.String()). + OutputImageCredentials(os.Getenv(EnvVarImageRepoSecret)). + OutputImageInsecure(insecure). + OutputTimestamp("WrongValue"). + BuildSpec()). + MustCreate() + + Expect(testBuild.CreateBR(buildRun)).ToNot(Succeed()) + + buildRun, err = testBuild.GetBRTillCompletion(buildRun.Name) + Expect(err).ToNot(HaveOccurred()) + + condition := buildRun.Status.GetCondition(buildv1beta1.Succeeded) + Expect(condition).ToNot(BeNil()) + Expect(condition.Reason).To(ContainSubstring("Failed")) + Expect(condition.Message).To(ContainSubstring("cannot parse output timestamp")) + }) + }) + }) +}) diff --git a/vendor/github.com/google/go-containerregistry/internal/windows/windows.go b/vendor/github.com/google/go-containerregistry/internal/windows/windows.go new file mode 100644 index 0000000000..62d04cfb30 --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/internal/windows/windows.go @@ -0,0 +1,114 @@ +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package windows + +import ( + "archive/tar" + "bytes" + "errors" + "fmt" + "io" + "path" + "strings" + + "github.com/google/go-containerregistry/internal/gzip" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/tarball" +) + +// userOwnerAndGroupSID is a magic value needed to make the binary executable +// in a Windows container. +// +// owner: BUILTIN/Users group: BUILTIN/Users ($sddlValue="O:BUG:BU") +const userOwnerAndGroupSID = "AQAAgBQAAAAkAAAAAAAAAAAAAAABAgAAAAAABSAAAAAhAgAAAQIAAAAAAAUgAAAAIQIAAA==" + +// Windows returns a Layer that is converted to be pullable on Windows. +func Windows(layer v1.Layer) (v1.Layer, error) { + // TODO: do this lazily. + + layerReader, err := layer.Uncompressed() + if err != nil { + return nil, fmt.Errorf("getting layer: %w", err) + } + defer layerReader.Close() + tarReader := tar.NewReader(layerReader) + w := new(bytes.Buffer) + tarWriter := tar.NewWriter(w) + defer tarWriter.Close() + + for _, dir := range []string{"Files", "Hives"} { + if err := tarWriter.WriteHeader(&tar.Header{ + Name: dir, + Typeflag: tar.TypeDir, + // Use a fixed Mode, so that this isn't sensitive to the directory and umask + // under which it was created. Additionally, windows can only set 0222, + // 0444, or 0666, none of which are executable. + Mode: 0555, + Format: tar.FormatPAX, + }); err != nil { + return nil, fmt.Errorf("writing %s directory: %w", dir, err) + } + } + + for { + header, err := tarReader.Next() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return nil, fmt.Errorf("reading layer: %w", err) + } + + if strings.HasPrefix(header.Name, "Files/") { + return nil, fmt.Errorf("file path %q already suitable for Windows", header.Name) + } + + header.Name = path.Join("Files", header.Name) + header.Format = tar.FormatPAX + + // TODO: this seems to make the file executable on Windows; + // only do this if the file should be executable. + if header.PAXRecords == nil { + header.PAXRecords = map[string]string{} + } + header.PAXRecords["MSWINDOWS.rawsd"] = userOwnerAndGroupSID + + if err := tarWriter.WriteHeader(header); err != nil { + return nil, fmt.Errorf("writing tar header: %w", err) + } + + if header.Typeflag == tar.TypeReg { + if _, err = io.Copy(tarWriter, tarReader); err != nil { + return nil, fmt.Errorf("writing layer file: %w", err) + } + } + } + + if err := tarWriter.Close(); err != nil { + return nil, err + } + + b := w.Bytes() + // gzip the contents, then create the layer + opener := func() (io.ReadCloser, error) { + return gzip.ReadCloser(io.NopCloser(bytes.NewReader(b))), nil + } + layer, err = tarball.LayerFromOpener(opener) + if err != nil { + return nil, fmt.Errorf("creating layer: %w", err) + } + + return layer, nil +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/append.go b/vendor/github.com/google/go-containerregistry/pkg/crane/append.go new file mode 100644 index 0000000000..f1c2ef69ae --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/crane/append.go @@ -0,0 +1,114 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "fmt" + "os" + + "github.com/google/go-containerregistry/internal/windows" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/stream" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +func isWindows(img v1.Image) (bool, error) { + cfg, err := img.ConfigFile() + if err != nil { + return false, err + } + return cfg != nil && cfg.OS == "windows", nil +} + +// Append reads a layer from path and appends it the the v1.Image base. +// +// If the base image is a Windows base image (i.e., its config.OS is +// "windows"), the contents of the tarballs will be modified to be suitable for +// a Windows container image.`, +func Append(base v1.Image, paths ...string) (v1.Image, error) { + if base == nil { + return nil, fmt.Errorf("invalid argument: base") + } + + win, err := isWindows(base) + if err != nil { + return nil, fmt.Errorf("getting base image: %w", err) + } + + baseMediaType, err := base.MediaType() + + if err != nil { + return nil, fmt.Errorf("getting base image media type: %w", err) + } + + layerType := types.DockerLayer + + if baseMediaType == types.OCIManifestSchema1 { + layerType = types.OCILayer + } + + layers := make([]v1.Layer, 0, len(paths)) + for _, path := range paths { + layer, err := getLayer(path, layerType) + if err != nil { + return nil, fmt.Errorf("reading layer %q: %w", path, err) + } + + if win { + layer, err = windows.Windows(layer) + if err != nil { + return nil, fmt.Errorf("converting %q for Windows: %w", path, err) + } + } + + layers = append(layers, layer) + } + + return mutate.AppendLayers(base, layers...) +} + +func getLayer(path string, layerType types.MediaType) (v1.Layer, error) { + f, err := streamFile(path) + if err != nil { + return nil, err + } + if f != nil { + return stream.NewLayer(f, stream.WithMediaType(layerType)), nil + } + + return tarball.LayerFromFile(path, tarball.WithMediaType(layerType)) +} + +// If we're dealing with a named pipe, trying to open it multiple times will +// fail, so we need to do a streaming upload. +// +// returns nil, nil for non-streaming files +func streamFile(path string) (*os.File, error) { + if path == "-" { + return os.Stdin, nil + } + fi, err := os.Stat(path) + if err != nil { + return nil, err + } + + if !fi.Mode().IsRegular() { + return os.Open(path) + } + + return nil, nil +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/catalog.go b/vendor/github.com/google/go-containerregistry/pkg/crane/catalog.go new file mode 100644 index 0000000000..f30800cca3 --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/crane/catalog.go @@ -0,0 +1,35 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "context" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +// Catalog returns the repositories in a registry's catalog. +func Catalog(src string, opt ...Option) (res []string, err error) { + o := makeOptions(opt...) + reg, err := name.NewRegistry(src, o.Name...) + if err != nil { + return nil, err + } + + // This context gets overridden by remote.WithContext, which is set by + // crane.WithContext. + return remote.Catalog(context.Background(), reg, o.Remote...) +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/config.go b/vendor/github.com/google/go-containerregistry/pkg/crane/config.go new file mode 100644 index 0000000000..3e55cc93a7 --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/crane/config.go @@ -0,0 +1,24 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +// Config returns the config file for the remote image ref. +func Config(ref string, opt ...Option) ([]byte, error) { + i, _, err := getImage(ref, opt...) + if err != nil { + return nil, err + } + return i.RawConfigFile() +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/copy.go b/vendor/github.com/google/go-containerregistry/pkg/crane/copy.go new file mode 100644 index 0000000000..bbdf5481fd --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/crane/copy.go @@ -0,0 +1,181 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "errors" + "fmt" + "net/http" + + "github.com/google/go-containerregistry/pkg/logs" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" + "golang.org/x/sync/errgroup" +) + +// Copy copies a remote image or index from src to dst. +func Copy(src, dst string, opt ...Option) error { + o := makeOptions(opt...) + srcRef, err := name.ParseReference(src, o.Name...) + if err != nil { + return fmt.Errorf("parsing reference %q: %w", src, err) + } + + dstRef, err := name.ParseReference(dst, o.Name...) + if err != nil { + return fmt.Errorf("parsing reference for %q: %w", dst, err) + } + + puller, err := remote.NewPuller(o.Remote...) + if err != nil { + return err + } + + if tag, ok := dstRef.(name.Tag); ok { + if o.noclobber { + logs.Progress.Printf("Checking existing tag %v", tag) + head, err := puller.Head(o.ctx, tag) + var terr *transport.Error + if errors.As(err, &terr) { + if terr.StatusCode != http.StatusNotFound && terr.StatusCode != http.StatusForbidden { + return err + } + } else if err != nil { + return err + } + + if head != nil { + return fmt.Errorf("refusing to clobber existing tag %s@%s", tag, head.Digest) + } + } + } + + pusher, err := remote.NewPusher(o.Remote...) + if err != nil { + return err + } + + logs.Progress.Printf("Copying from %v to %v", srcRef, dstRef) + desc, err := puller.Get(o.ctx, srcRef) + if err != nil { + return fmt.Errorf("fetching %q: %w", src, err) + } + + if o.Platform == nil { + return pusher.Push(o.ctx, dstRef, desc) + } + + // If platform is explicitly set, don't copy the whole index, just the appropriate image. + img, err := desc.Image() + if err != nil { + return err + } + return pusher.Push(o.ctx, dstRef, img) +} + +// CopyRepository copies every tag from src to dst. +func CopyRepository(src, dst string, opt ...Option) error { + o := makeOptions(opt...) + + srcRepo, err := name.NewRepository(src, o.Name...) + if err != nil { + return err + } + + dstRepo, err := name.NewRepository(dst, o.Name...) + if err != nil { + return fmt.Errorf("parsing reference for %q: %w", dst, err) + } + + puller, err := remote.NewPuller(o.Remote...) + if err != nil { + return err + } + + ignoredTags := map[string]struct{}{} + if o.noclobber { + // TODO: It would be good to propagate noclobber down into remote so we can use Etags. + have, err := puller.List(o.ctx, dstRepo) + if err != nil { + var terr *transport.Error + if errors.As(err, &terr) { + // Some registries create repository on first push, so listing tags will fail. + // If we see 404 or 403, assume we failed because the repository hasn't been created yet. + if !(terr.StatusCode == http.StatusNotFound || terr.StatusCode == http.StatusForbidden) { + return err + } + } else { + return err + } + } + for _, tag := range have { + ignoredTags[tag] = struct{}{} + } + } + + pusher, err := remote.NewPusher(o.Remote...) + if err != nil { + return err + } + + lister, err := puller.Lister(o.ctx, srcRepo) + if err != nil { + return err + } + + g, ctx := errgroup.WithContext(o.ctx) + g.SetLimit(o.jobs) + + for lister.HasNext() { + tags, err := lister.Next(ctx) + if err != nil { + return err + } + + for _, tag := range tags.Tags { + tag := tag + + if o.noclobber { + if _, ok := ignoredTags[tag]; ok { + logs.Progress.Printf("Skipping %s due to no-clobber", tag) + continue + } + } + + g.Go(func() error { + srcTag, err := name.ParseReference(src+":"+tag, o.Name...) + if err != nil { + return fmt.Errorf("failed to parse tag: %w", err) + } + dstTag, err := name.ParseReference(dst+":"+tag, o.Name...) + if err != nil { + return fmt.Errorf("failed to parse tag: %w", err) + } + + logs.Progress.Printf("Fetching %s", srcTag) + desc, err := puller.Get(ctx, srcTag) + if err != nil { + return err + } + + logs.Progress.Printf("Pushing %s", dstTag) + return pusher.Push(ctx, dstTag, desc) + }) + } + } + + return g.Wait() +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/delete.go b/vendor/github.com/google/go-containerregistry/pkg/crane/delete.go new file mode 100644 index 0000000000..58a8be1f0b --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/crane/delete.go @@ -0,0 +1,33 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "fmt" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +// Delete deletes the remote reference at src. +func Delete(src string, opt ...Option) error { + o := makeOptions(opt...) + ref, err := name.ParseReference(src, o.Name...) + if err != nil { + return fmt.Errorf("parsing reference %q: %w", src, err) + } + + return remote.Delete(ref, o.Remote...) +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/digest.go b/vendor/github.com/google/go-containerregistry/pkg/crane/digest.go new file mode 100644 index 0000000000..868a57010d --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/crane/digest.go @@ -0,0 +1,52 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import "github.com/google/go-containerregistry/pkg/logs" + +// Digest returns the sha256 hash of the remote image at ref. +func Digest(ref string, opt ...Option) (string, error) { + o := makeOptions(opt...) + if o.Platform != nil { + desc, err := getManifest(ref, opt...) + if err != nil { + return "", err + } + if !desc.MediaType.IsIndex() { + return desc.Digest.String(), nil + } + + // TODO: does not work for indexes which contain schema v1 manifests + img, err := desc.Image() + if err != nil { + return "", err + } + digest, err := img.Digest() + if err != nil { + return "", err + } + return digest.String(), nil + } + desc, err := Head(ref, opt...) + if err != nil { + logs.Warn.Printf("HEAD request failed, falling back on GET: %v", err) + rdesc, err := getManifest(ref, opt...) + if err != nil { + return "", err + } + return rdesc.Digest.String(), nil + } + return desc.Digest.String(), nil +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/doc.go b/vendor/github.com/google/go-containerregistry/pkg/crane/doc.go new file mode 100644 index 0000000000..7602d7953f --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/crane/doc.go @@ -0,0 +1,16 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package crane holds libraries used to implement the crane CLI. +package crane diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/export.go b/vendor/github.com/google/go-containerregistry/pkg/crane/export.go new file mode 100644 index 0000000000..b5e1296d21 --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/crane/export.go @@ -0,0 +1,54 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "io" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/mutate" +) + +// Export writes the filesystem contents (as a tarball) of img to w. +// If img has a single layer, just write the (uncompressed) contents to w so +// that this "just works" for images that just wrap a single blob. +func Export(img v1.Image, w io.Writer) error { + layers, err := img.Layers() + if err != nil { + return err + } + if len(layers) == 1 { + // If it's a single layer... + l := layers[0] + mt, err := l.MediaType() + if err != nil { + return err + } + + if !mt.IsLayer() { + // ...and isn't an OCI mediaType, we don't have to flatten it. + // This lets export work for single layer, non-tarball images. + rc, err := l.Uncompressed() + if err != nil { + return err + } + _, err = io.Copy(w, rc) + return err + } + } + fs := mutate.Extract(img) + _, err = io.Copy(w, fs) + return err +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/filemap.go b/vendor/github.com/google/go-containerregistry/pkg/crane/filemap.go new file mode 100644 index 0000000000..36dfc2a644 --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/crane/filemap.go @@ -0,0 +1,72 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "archive/tar" + "bytes" + "io" + "sort" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/tarball" +) + +// Layer creates a layer from a single file map. These layers are reproducible and consistent. +// A filemap is a path -> file content map representing a file system. +func Layer(filemap map[string][]byte) (v1.Layer, error) { + b := &bytes.Buffer{} + w := tar.NewWriter(b) + + fn := []string{} + for f := range filemap { + fn = append(fn, f) + } + sort.Strings(fn) + + for _, f := range fn { + c := filemap[f] + if err := w.WriteHeader(&tar.Header{ + Name: f, + Size: int64(len(c)), + }); err != nil { + return nil, err + } + if _, err := w.Write(c); err != nil { + return nil, err + } + } + if err := w.Close(); err != nil { + return nil, err + } + + // Return a new copy of the buffer each time it's opened. + return tarball.LayerFromOpener(func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewBuffer(b.Bytes())), nil + }) +} + +// Image creates a image with the given filemaps as its contents. These images are reproducible and consistent. +// A filemap is a path -> file content map representing a file system. +func Image(filemap map[string][]byte) (v1.Image, error) { + y, err := Layer(filemap) + if err != nil { + return nil, err + } + + return mutate.AppendLayers(empty.Image, y) +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/get.go b/vendor/github.com/google/go-containerregistry/pkg/crane/get.go new file mode 100644 index 0000000000..98a2e8933e --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/crane/get.go @@ -0,0 +1,61 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "fmt" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +func getImage(r string, opt ...Option) (v1.Image, name.Reference, error) { + o := makeOptions(opt...) + ref, err := name.ParseReference(r, o.Name...) + if err != nil { + return nil, nil, fmt.Errorf("parsing reference %q: %w", r, err) + } + img, err := remote.Image(ref, o.Remote...) + if err != nil { + return nil, nil, fmt.Errorf("reading image %q: %w", ref, err) + } + return img, ref, nil +} + +func getManifest(r string, opt ...Option) (*remote.Descriptor, error) { + o := makeOptions(opt...) + ref, err := name.ParseReference(r, o.Name...) + if err != nil { + return nil, fmt.Errorf("parsing reference %q: %w", r, err) + } + return remote.Get(ref, o.Remote...) +} + +// Get calls remote.Get and returns an uninterpreted response. +func Get(r string, opt ...Option) (*remote.Descriptor, error) { + return getManifest(r, opt...) +} + +// Head performs a HEAD request for a manifest and returns a content descriptor +// based on the registry's response. +func Head(r string, opt ...Option) (*v1.Descriptor, error) { + o := makeOptions(opt...) + ref, err := name.ParseReference(r, o.Name...) + if err != nil { + return nil, err + } + return remote.Head(ref, o.Remote...) +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/list.go b/vendor/github.com/google/go-containerregistry/pkg/crane/list.go new file mode 100644 index 0000000000..38352153bb --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/crane/list.go @@ -0,0 +1,33 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "fmt" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +// ListTags returns the tags in repository src. +func ListTags(src string, opt ...Option) ([]string, error) { + o := makeOptions(opt...) + repo, err := name.NewRepository(src, o.Name...) + if err != nil { + return nil, fmt.Errorf("parsing repo %q: %w", src, err) + } + + return remote.List(repo, o.Remote...) +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/manifest.go b/vendor/github.com/google/go-containerregistry/pkg/crane/manifest.go new file mode 100644 index 0000000000..a54926aef3 --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/crane/manifest.go @@ -0,0 +1,32 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +// Manifest returns the manifest for the remote image or index ref. +func Manifest(ref string, opt ...Option) ([]byte, error) { + desc, err := getManifest(ref, opt...) + if err != nil { + return nil, err + } + o := makeOptions(opt...) + if o.Platform != nil { + img, err := desc.Image() + if err != nil { + return nil, err + } + return img.RawManifest() + } + return desc.Manifest, nil +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/options.go b/vendor/github.com/google/go-containerregistry/pkg/crane/options.go new file mode 100644 index 0000000000..d9d4417619 --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/crane/options.go @@ -0,0 +1,178 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "context" + "crypto/tls" + "net/http" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +// Options hold the options that crane uses when calling other packages. +type Options struct { + Name []name.Option + Remote []remote.Option + Platform *v1.Platform + Keychain authn.Keychain + Transport http.RoundTripper + + auth authn.Authenticator + insecure bool + jobs int + noclobber bool + ctx context.Context +} + +// GetOptions exposes the underlying []remote.Option, []name.Option, and +// platform, based on the passed Option. Generally, you shouldn't need to use +// this unless you've painted yourself into a dependency corner as we have +// with the crane and gcrane cli packages. +func GetOptions(opts ...Option) Options { + return makeOptions(opts...) +} + +func makeOptions(opts ...Option) Options { + opt := Options{ + Remote: []remote.Option{ + remote.WithAuthFromKeychain(authn.DefaultKeychain), + }, + Keychain: authn.DefaultKeychain, + jobs: 4, + ctx: context.Background(), + } + + for _, o := range opts { + o(&opt) + } + + // Allow for untrusted certificates if the user + // passed Insecure but no custom transport. + if opt.insecure && opt.Transport == nil { + transport := remote.DefaultTransport.(*http.Transport).Clone() + transport.TLSClientConfig = &tls.Config{ + InsecureSkipVerify: true, //nolint: gosec + } + + WithTransport(transport)(&opt) + } else if opt.Transport == nil { + opt.Transport = remote.DefaultTransport + } + + return opt +} + +// Option is a functional option for crane. +type Option func(*Options) + +// WithTransport is a functional option for overriding the default transport +// for remote operations. Setting a transport will override the Insecure option's +// configuration allowing for image registries to use untrusted certificates. +func WithTransport(t http.RoundTripper) Option { + return func(o *Options) { + o.Remote = append(o.Remote, remote.WithTransport(t)) + o.Transport = t + } +} + +// Insecure is an Option that allows image references to be fetched without TLS. +// This will also allow for untrusted (e.g. self-signed) certificates in cases where +// the default transport is used (i.e. when WithTransport is not used). +func Insecure(o *Options) { + o.Name = append(o.Name, name.Insecure) + o.insecure = true +} + +// WithPlatform is an Option to specify the platform. +func WithPlatform(platform *v1.Platform) Option { + return func(o *Options) { + if platform != nil { + o.Remote = append(o.Remote, remote.WithPlatform(*platform)) + } + o.Platform = platform + } +} + +// WithAuthFromKeychain is a functional option for overriding the default +// authenticator for remote operations, using an authn.Keychain to find +// credentials. +// +// By default, crane will use authn.DefaultKeychain. +func WithAuthFromKeychain(keys authn.Keychain) Option { + return func(o *Options) { + // Replace the default keychain at position 0. + o.Remote[0] = remote.WithAuthFromKeychain(keys) + o.Keychain = keys + } +} + +// WithAuth is a functional option for overriding the default authenticator +// for remote operations. +// +// By default, crane will use authn.DefaultKeychain. +func WithAuth(auth authn.Authenticator) Option { + return func(o *Options) { + // Replace the default keychain at position 0. + o.Remote[0] = remote.WithAuth(auth) + o.auth = auth + } +} + +// WithUserAgent adds the given string to the User-Agent header for any HTTP +// requests. +func WithUserAgent(ua string) Option { + return func(o *Options) { + o.Remote = append(o.Remote, remote.WithUserAgent(ua)) + } +} + +// WithNondistributable is an option that allows pushing non-distributable +// layers. +func WithNondistributable() Option { + return func(o *Options) { + o.Remote = append(o.Remote, remote.WithNondistributable) + } +} + +// WithContext is a functional option for setting the context. +func WithContext(ctx context.Context) Option { + return func(o *Options) { + o.ctx = ctx + o.Remote = append(o.Remote, remote.WithContext(ctx)) + } +} + +// WithJobs sets the number of concurrent jobs to run. +// +// The default number of jobs is GOMAXPROCS. +func WithJobs(jobs int) Option { + return func(o *Options) { + if jobs > 0 { + o.jobs = jobs + } + o.Remote = append(o.Remote, remote.WithJobs(o.jobs)) + } +} + +// WithNoClobber modifies behavior to avoid overwriting existing tags, if possible. +func WithNoClobber(noclobber bool) Option { + return func(o *Options) { + o.noclobber = noclobber + } +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/pull.go b/vendor/github.com/google/go-containerregistry/pkg/crane/pull.go new file mode 100644 index 0000000000..7e6e5b7b6e --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/crane/pull.go @@ -0,0 +1,142 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "fmt" + "os" + + legacy "github.com/google/go-containerregistry/pkg/legacy/tarball" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/layout" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/tarball" +) + +// Tag applied to images that were pulled by digest. This denotes that the +// image was (probably) never tagged with this, but lets us avoid applying the +// ":latest" tag which might be misleading. +const iWasADigestTag = "i-was-a-digest" + +// Pull returns a v1.Image of the remote image src. +func Pull(src string, opt ...Option) (v1.Image, error) { + o := makeOptions(opt...) + ref, err := name.ParseReference(src, o.Name...) + if err != nil { + return nil, fmt.Errorf("parsing reference %q: %w", src, err) + } + + return remote.Image(ref, o.Remote...) +} + +// Save writes the v1.Image img as a tarball at path with tag src. +func Save(img v1.Image, src, path string) error { + imgMap := map[string]v1.Image{src: img} + return MultiSave(imgMap, path) +} + +// MultiSave writes collection of v1.Image img with tag as a tarball. +func MultiSave(imgMap map[string]v1.Image, path string, opt ...Option) error { + o := makeOptions(opt...) + tagToImage := map[name.Tag]v1.Image{} + + for src, img := range imgMap { + ref, err := name.ParseReference(src, o.Name...) + if err != nil { + return fmt.Errorf("parsing ref %q: %w", src, err) + } + + // WriteToFile wants a tag to write to the tarball, but we might have + // been given a digest. + // If the original ref was a tag, use that. Otherwise, if it was a + // digest, tag the image with :i-was-a-digest instead. + tag, ok := ref.(name.Tag) + if !ok { + d, ok := ref.(name.Digest) + if !ok { + return fmt.Errorf("ref wasn't a tag or digest") + } + tag = d.Repository.Tag(iWasADigestTag) + } + tagToImage[tag] = img + } + // no progress channel (for now) + return tarball.MultiWriteToFile(path, tagToImage) +} + +// PullLayer returns the given layer from a registry. +func PullLayer(ref string, opt ...Option) (v1.Layer, error) { + o := makeOptions(opt...) + digest, err := name.NewDigest(ref, o.Name...) + if err != nil { + return nil, err + } + + return remote.Layer(digest, o.Remote...) +} + +// SaveLegacy writes the v1.Image img as a legacy tarball at path with tag src. +func SaveLegacy(img v1.Image, src, path string) error { + imgMap := map[string]v1.Image{src: img} + return MultiSave(imgMap, path) +} + +// MultiSaveLegacy writes collection of v1.Image img with tag as a legacy tarball. +func MultiSaveLegacy(imgMap map[string]v1.Image, path string) error { + refToImage := map[name.Reference]v1.Image{} + + for src, img := range imgMap { + ref, err := name.ParseReference(src) + if err != nil { + return fmt.Errorf("parsing ref %q: %w", src, err) + } + refToImage[ref] = img + } + + w, err := os.Create(path) + if err != nil { + return err + } + defer w.Close() + + return legacy.MultiWrite(refToImage, w) +} + +// SaveOCI writes the v1.Image img as an OCI Image Layout at path. If a layout +// already exists at that path, it will add the image to the index. +func SaveOCI(img v1.Image, path string) error { + imgMap := map[string]v1.Image{"": img} + return MultiSaveOCI(imgMap, path) +} + +// MultiSaveOCI writes collection of v1.Image img as an OCI Image Layout at path. If a layout +// already exists at that path, it will add the image to the index. +func MultiSaveOCI(imgMap map[string]v1.Image, path string) error { + p, err := layout.FromPath(path) + if err != nil { + p, err = layout.Write(path, empty.Index) + if err != nil { + return err + } + } + for _, img := range imgMap { + if err = p.AppendImage(img); err != nil { + return err + } + } + return nil +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/push.go b/vendor/github.com/google/go-containerregistry/pkg/crane/push.go new file mode 100644 index 0000000000..90a0585021 --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/crane/push.go @@ -0,0 +1,65 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "fmt" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/tarball" +) + +// Load reads the tarball at path as a v1.Image. +func Load(path string, opt ...Option) (v1.Image, error) { + return LoadTag(path, "", opt...) +} + +// LoadTag reads a tag from the tarball at path as a v1.Image. +// If tag is "", will attempt to read the tarball as a single image. +func LoadTag(path, tag string, opt ...Option) (v1.Image, error) { + if tag == "" { + return tarball.ImageFromPath(path, nil) + } + + o := makeOptions(opt...) + t, err := name.NewTag(tag, o.Name...) + if err != nil { + return nil, fmt.Errorf("parsing tag %q: %w", tag, err) + } + return tarball.ImageFromPath(path, &t) +} + +// Push pushes the v1.Image img to a registry as dst. +func Push(img v1.Image, dst string, opt ...Option) error { + o := makeOptions(opt...) + tag, err := name.ParseReference(dst, o.Name...) + if err != nil { + return fmt.Errorf("parsing reference %q: %w", dst, err) + } + return remote.Write(tag, img, o.Remote...) +} + +// Upload pushes the v1.Layer to a given repo. +func Upload(layer v1.Layer, repo string, opt ...Option) error { + o := makeOptions(opt...) + ref, err := name.NewRepository(repo, o.Name...) + if err != nil { + return fmt.Errorf("parsing repo %q: %w", repo, err) + } + + return remote.WriteLayer(ref, layer, o.Remote...) +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/tag.go b/vendor/github.com/google/go-containerregistry/pkg/crane/tag.go new file mode 100644 index 0000000000..13bc395872 --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/crane/tag.go @@ -0,0 +1,39 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "fmt" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +// Tag adds tag to the remote img. +func Tag(img, tag string, opt ...Option) error { + o := makeOptions(opt...) + ref, err := name.ParseReference(img, o.Name...) + if err != nil { + return fmt.Errorf("parsing reference %q: %w", img, err) + } + desc, err := remote.Get(ref, o.Remote...) + if err != nil { + return fmt.Errorf("fetching %q: %w", img, err) + } + + dst := ref.Context().Tag(tag) + + return remote.Tag(dst, desc, o.Remote...) +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/legacy/config.go b/vendor/github.com/google/go-containerregistry/pkg/legacy/config.go new file mode 100644 index 0000000000..3364bec61c --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/legacy/config.go @@ -0,0 +1,33 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package legacy + +import ( + v1 "github.com/google/go-containerregistry/pkg/v1" +) + +// LayerConfigFile is the configuration file that holds the metadata describing +// a v1 layer. See: +// https://github.com/moby/moby/blob/master/image/spec/v1.md +type LayerConfigFile struct { + v1.ConfigFile + + ContainerConfig v1.Config `json:"container_config,omitempty"` + + ID string `json:"id,omitempty"` + Parent string `json:"parent,omitempty"` + Throwaway bool `json:"throwaway,omitempty"` + Comment string `json:"comment,omitempty"` +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/legacy/doc.go b/vendor/github.com/google/go-containerregistry/pkg/legacy/doc.go new file mode 100644 index 0000000000..1d1668887a --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/legacy/doc.go @@ -0,0 +1,18 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package legacy provides functionality to work with docker images in the v1 +// format. +// See: https://github.com/moby/moby/blob/master/image/spec/v1.md +package legacy diff --git a/vendor/github.com/google/go-containerregistry/pkg/legacy/tarball/README.md b/vendor/github.com/google/go-containerregistry/pkg/legacy/tarball/README.md new file mode 100644 index 0000000000..90b88c7578 --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/legacy/tarball/README.md @@ -0,0 +1,6 @@ +# `legacy/tarball` + +[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/legacy/tarball?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/legacy/tarball) + +This package implements support for writing legacy tarballs, as described +[here](https://github.com/moby/moby/blob/749d90e10f989802638ae542daf54257f3bf71f2/image/spec/v1.2.md#combined-image-json--filesystem-changeset-format). diff --git a/vendor/github.com/google/go-containerregistry/pkg/legacy/tarball/doc.go b/vendor/github.com/google/go-containerregistry/pkg/legacy/tarball/doc.go new file mode 100644 index 0000000000..62684d6e72 --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/legacy/tarball/doc.go @@ -0,0 +1,18 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package tarball provides facilities for writing v1 docker images +// (https://github.com/moby/moby/blob/master/image/spec/v1.md) from/to a tarball +// on-disk. +package tarball diff --git a/vendor/github.com/google/go-containerregistry/pkg/legacy/tarball/write.go b/vendor/github.com/google/go-containerregistry/pkg/legacy/tarball/write.go new file mode 100644 index 0000000000..627bfbfdb1 --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/legacy/tarball/write.go @@ -0,0 +1,371 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tarball + +import ( + "archive/tar" + "bytes" + "encoding/json" + "fmt" + "io" + "sort" + "strings" + + "github.com/google/go-containerregistry/pkg/legacy" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/tarball" +) + +// repositoriesTarDescriptor represents the repositories file inside a `docker save` tarball. +type repositoriesTarDescriptor map[string]map[string]string + +// v1Layer represents a layer with metadata needed by the v1 image spec https://github.com/moby/moby/blob/master/image/spec/v1.md. +type v1Layer struct { + // config is the layer metadata. + config *legacy.LayerConfigFile + // layer is the v1.Layer object this v1Layer represents. + layer v1.Layer +} + +// json returns the raw bytes of the json metadata of the given v1Layer. +func (l *v1Layer) json() ([]byte, error) { + return json.Marshal(l.config) +} + +// version returns the raw bytes of the "VERSION" file of the given v1Layer. +func (l *v1Layer) version() []byte { + return []byte("1.0") +} + +// v1LayerID computes the v1 image format layer id for the given v1.Layer with the given v1 parent ID and raw image config. +func v1LayerID(layer v1.Layer, parentID string, rawConfig []byte) (string, error) { + d, err := layer.Digest() + if err != nil { + return "", fmt.Errorf("unable to get layer digest to generate v1 layer ID: %w", err) + } + s := fmt.Sprintf("%s %s", d.Hex, parentID) + if len(rawConfig) != 0 { + s = fmt.Sprintf("%s %s", s, string(rawConfig)) + } + + h, _, _ := v1.SHA256(strings.NewReader(s)) + return h.Hex, nil +} + +// newTopV1Layer creates a new v1Layer for a layer other than the top layer in a v1 image tarball. +func newV1Layer(layer v1.Layer, parent *v1Layer, history v1.History) (*v1Layer, error) { + parentID := "" + if parent != nil { + parentID = parent.config.ID + } + id, err := v1LayerID(layer, parentID, nil) + if err != nil { + return nil, fmt.Errorf("unable to generate v1 layer ID: %w", err) + } + result := &v1Layer{ + layer: layer, + config: &legacy.LayerConfigFile{ + ConfigFile: v1.ConfigFile{ + Created: history.Created, + Author: history.Author, + }, + ContainerConfig: v1.Config{ + Cmd: []string{history.CreatedBy}, + }, + ID: id, + Parent: parentID, + Throwaway: history.EmptyLayer, + Comment: history.Comment, + }, + } + return result, nil +} + +// newTopV1Layer creates a new v1Layer for the top layer in a v1 image tarball. +func newTopV1Layer(layer v1.Layer, parent *v1Layer, history v1.History, imgConfig *v1.ConfigFile, rawConfig []byte) (*v1Layer, error) { + result, err := newV1Layer(layer, parent, history) + if err != nil { + return nil, err + } + id, err := v1LayerID(layer, result.config.Parent, rawConfig) + if err != nil { + return nil, fmt.Errorf("unable to generate v1 layer ID for top layer: %w", err) + } + result.config.ID = id + result.config.Architecture = imgConfig.Architecture + result.config.Container = imgConfig.Container + result.config.DockerVersion = imgConfig.DockerVersion + result.config.OS = imgConfig.OS + result.config.Config = imgConfig.Config + result.config.Created = imgConfig.Created + return result, nil +} + +// splitTag splits the given tagged image name /: +// into / and . +func splitTag(name string) (string, string) { + // Split on ":" + parts := strings.Split(name, ":") + // Verify that we aren't confusing a tag for a hostname w/ port for the purposes of weak validation. + if len(parts) > 1 && !strings.Contains(parts[len(parts)-1], "/") { + base := strings.Join(parts[:len(parts)-1], ":") + tag := parts[len(parts)-1] + return base, tag + } + return name, "" +} + +// addTags adds the given image tags to the given "repositories" file descriptor in a v1 image tarball. +func addTags(repos repositoriesTarDescriptor, tags []string, topLayerID string) { + for _, t := range tags { + base, tag := splitTag(t) + tagToID, ok := repos[base] + if !ok { + tagToID = make(map[string]string) + repos[base] = tagToID + } + tagToID[tag] = topLayerID + } +} + +// updateLayerSources updates the given layer digest to descriptor map with the descriptor of the given layer in the given image if it's an undistributable layer. +func updateLayerSources(layerSources map[v1.Hash]v1.Descriptor, layer v1.Layer, img v1.Image) error { + d, err := layer.Digest() + if err != nil { + return err + } + // Add to LayerSources if it's a foreign layer. + desc, err := partial.BlobDescriptor(img, d) + if err != nil { + return err + } + if !desc.MediaType.IsDistributable() { + diffid, err := partial.BlobToDiffID(img, d) + if err != nil { + return err + } + layerSources[diffid] = *desc + } + return nil +} + +// Write is a wrapper to write a single image in V1 format and tag to a tarball. +func Write(ref name.Reference, img v1.Image, w io.Writer) error { + return MultiWrite(map[name.Reference]v1.Image{ref: img}, w) +} + +// filterEmpty filters out the history corresponding to empty layers from the +// given history. +func filterEmpty(h []v1.History) []v1.History { + result := []v1.History{} + for _, i := range h { + if i.EmptyLayer { + continue + } + result = append(result, i) + } + return result +} + +// MultiWrite writes the contents of each image to the provided reader, in the V1 image tarball format. +// The contents are written in the following format: +// One manifest.json file at the top level containing information about several images. +// One repositories file mapping from the image / to to the id of the top most layer. +// For every layer, a directory named with the layer ID is created with the following contents: +// +// layer.tar - The uncompressed layer tarball. +// .json- Layer metadata json. +// VERSION- Schema version string. Always set to "1.0". +// +// One file for the config blob, named after its SHA. +func MultiWrite(refToImage map[name.Reference]v1.Image, w io.Writer) error { + tf := tar.NewWriter(w) + defer tf.Close() + + sortedImages, imageToTags := dedupRefToImage(refToImage) + var m tarball.Manifest + repos := make(repositoriesTarDescriptor) + + seenLayerIDs := make(map[string]struct{}) + for _, img := range sortedImages { + tags := imageToTags[img] + + // Write the config. + cfgName, err := img.ConfigName() + if err != nil { + return err + } + cfgFileName := fmt.Sprintf("%s.json", cfgName.Hex) + cfgBlob, err := img.RawConfigFile() + if err != nil { + return err + } + if err := writeTarEntry(tf, cfgFileName, bytes.NewReader(cfgBlob), int64(len(cfgBlob))); err != nil { + return err + } + cfg, err := img.ConfigFile() + if err != nil { + return err + } + + // Store foreign layer info. + layerSources := make(map[v1.Hash]v1.Descriptor) + + // Write the layers. + layers, err := img.Layers() + if err != nil { + return err + } + history := filterEmpty(cfg.History) + // Create a blank config history if the config didn't have a history. + if len(history) == 0 && len(layers) != 0 { + history = make([]v1.History, len(layers)) + } else if len(layers) != len(history) { + return fmt.Errorf("image config had layer history which did not match the number of layers, got len(history)=%d, len(layers)=%d, want len(history)=len(layers)", len(history), len(layers)) + } + layerFiles := make([]string, len(layers)) + var prev *v1Layer + for i, l := range layers { + if err := updateLayerSources(layerSources, l, img); err != nil { + return fmt.Errorf("unable to update image metadata to include undistributable layer source information: %w", err) + } + var cur *v1Layer + if i < (len(layers) - 1) { + cur, err = newV1Layer(l, prev, history[i]) + } else { + cur, err = newTopV1Layer(l, prev, history[i], cfg, cfgBlob) + } + if err != nil { + return err + } + layerFiles[i] = fmt.Sprintf("%s/layer.tar", cur.config.ID) + if _, ok := seenLayerIDs[cur.config.ID]; ok { + prev = cur + continue + } + seenLayerIDs[cur.config.ID] = struct{}{} + + // If the v1.Layer implements UncompressedSize efficiently, use that + // for the tar header. Otherwise, this iterates over Uncompressed(). + // NOTE: If using a streaming layer, this may consume the layer. + size, err := partial.UncompressedSize(l) + if err != nil { + return err + } + u, err := l.Uncompressed() + if err != nil { + return err + } + defer u.Close() + if err := writeTarEntry(tf, layerFiles[i], u, size); err != nil { + return err + } + + j, err := cur.json() + if err != nil { + return err + } + if err := writeTarEntry(tf, fmt.Sprintf("%s/json", cur.config.ID), bytes.NewReader(j), int64(len(j))); err != nil { + return err + } + v := cur.version() + if err := writeTarEntry(tf, fmt.Sprintf("%s/VERSION", cur.config.ID), bytes.NewReader(v), int64(len(v))); err != nil { + return err + } + prev = cur + } + + // Generate the tar descriptor and write it. + m = append(m, tarball.Descriptor{ + Config: cfgFileName, + RepoTags: tags, + Layers: layerFiles, + LayerSources: layerSources, + }) + // prev should be the top layer here. Use it to add the image tags + // to the tarball repositories file. + addTags(repos, tags, prev.config.ID) + } + + mBytes, err := json.Marshal(m) + if err != nil { + return err + } + + if err := writeTarEntry(tf, "manifest.json", bytes.NewReader(mBytes), int64(len(mBytes))); err != nil { + return err + } + reposBytes, err := json.Marshal(&repos) + if err != nil { + return err + } + return writeTarEntry(tf, "repositories", bytes.NewReader(reposBytes), int64(len(reposBytes))) +} + +func dedupRefToImage(refToImage map[name.Reference]v1.Image) ([]v1.Image, map[v1.Image][]string) { + imageToTags := make(map[v1.Image][]string) + + for ref, img := range refToImage { + if tag, ok := ref.(name.Tag); ok { + if tags, ok := imageToTags[img]; ok && tags != nil { + imageToTags[img] = append(tags, tag.String()) + } else { + imageToTags[img] = []string{tag.String()} + } + } else { + if _, ok := imageToTags[img]; !ok { + imageToTags[img] = nil + } + } + } + + // Force specific order on tags + imgs := []v1.Image{} + for img, tags := range imageToTags { + sort.Strings(tags) + imgs = append(imgs, img) + } + + sort.Slice(imgs, func(i, j int) bool { + cfI, err := imgs[i].ConfigName() + if err != nil { + return false + } + cfJ, err := imgs[j].ConfigName() + if err != nil { + return false + } + return cfI.Hex < cfJ.Hex + }) + + return imgs, imageToTags +} + +// Writes a file to the provided writer with a corresponding tar header +func writeTarEntry(tf *tar.Writer, path string, r io.Reader, size int64) error { + hdr := &tar.Header{ + Mode: 0644, + Typeflag: tar.TypeReg, + Size: size, + Name: path, + } + if err := tf.WriteHeader(hdr); err != nil { + return err + } + _, err := io.Copy(tf, r) + return err +} diff --git a/vendor/modules.txt b/vendor/modules.txt index c81f78f325..93056d7bb9 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -244,9 +244,13 @@ github.com/google/go-containerregistry/internal/redact github.com/google/go-containerregistry/internal/retry github.com/google/go-containerregistry/internal/retry/wait github.com/google/go-containerregistry/internal/verify +github.com/google/go-containerregistry/internal/windows github.com/google/go-containerregistry/internal/zstd github.com/google/go-containerregistry/pkg/authn github.com/google/go-containerregistry/pkg/compression +github.com/google/go-containerregistry/pkg/crane +github.com/google/go-containerregistry/pkg/legacy +github.com/google/go-containerregistry/pkg/legacy/tarball github.com/google/go-containerregistry/pkg/logs github.com/google/go-containerregistry/pkg/name github.com/google/go-containerregistry/pkg/registry