Skip to content

Commit

Permalink
Add timestamp setup to main taskrun logic
Browse files Browse the repository at this point in the history
Introduce functions to mutate timestamp of an image or image index.

Extend image-processing step to include required timestamp values based
on the configured Build or BuildRun output settings.

Add test E2E cases for timestamp values for:
- Zero
- SourceTimestamp
- BuildTimestamp
- custom timestamp
  • Loading branch information
HeavyWombat committed Mar 8, 2024
1 parent a2cdd87 commit f4890eb
Show file tree
Hide file tree
Showing 38 changed files with 2,544 additions and 93 deletions.
11 changes: 11 additions & 0 deletions pkg/apis/build/v1alpha1/build_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions pkg/apis/build/v1beta1/build_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down
107 changes: 83 additions & 24 deletions pkg/image/mutate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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,
},
})
}
}

Expand Down Expand Up @@ -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
}
75 changes: 75 additions & 0 deletions pkg/image/mutate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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))
}
})
})
})
})
1 change: 1 addition & 0 deletions pkg/reconciler/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ var validationTypes = [...]string{
validate.Secrets,
validate.Strategies,
validate.Source,
validate.Output,
validate.BuildName,
validate.Envs,
validate.Triggers,
Expand Down
37 changes: 37 additions & 0 deletions pkg/reconciler/build/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
})
})
})
})
Loading

0 comments on commit f4890eb

Please sign in to comment.