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 1, 2024
1 parent 5705c65 commit 167305e
Show file tree
Hide file tree
Showing 37 changed files with 2,423 additions and 95 deletions.
2 changes: 2 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,8 @@ const (
TriggerInvalidImage BuildReason = "TriggerInvalidImage"
// TriggerInvalidPipeline indicates the trigger type Pipeline is invalid
TriggerInvalidPipeline BuildReason = "TriggerInvalidPipeline"
// OutputTimestampValueNotSupportedForBuild indicates that an unsupported output timestamp setting was used
OutputTimestampValueNotSupportedForBuild BuildReason = "OutputTimestampValueNotSupportedForBuild"

// AllValidationsSucceeded indicates a Build was successfully validated
AllValidationsSucceeded = "all validations succeeded"
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
21 changes: 21 additions & 0 deletions pkg/reconciler/build/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -552,5 +552,26 @@ var _ = Describe("Reconcile Build", func() {
Expect(err).ToNot(BeNil())
})
})

Context("when build object does not have sources, but output timestamp is set to be the source timestamp", func() {
It("should fail build validation due to unsupported combination of no source but output image timestamp set to be the source timestamp", func() {
buildSample.Spec.Output.Timestamp = pointer.String("SourceTimestamp")
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("OutputTimestampValueNotSupportedForBuild"))
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())
})
})
})
})
53 changes: 51 additions & 2 deletions pkg/reconciler/buildrun/resources/image_processing.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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,
Expand Down Expand Up @@ -82,6 +85,27 @@ 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 "Zero":
stepArgs = append(stepArgs, "--image-timestamp", "0")

case "SourceTimestamp":
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 "BuildTimestamp":
stepArgs = append(stepArgs, "--image-timestamp", strconv.FormatInt(creationTimestamp.Unix(), 10))

default:
stepArgs = append(stepArgs, "--image-timestamp", *imageTimestamp)
}
}

// check if there is anything to do
if len(stepArgs) > 0 {
// add the image argument
Expand Down Expand Up @@ -138,6 +162,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
Expand All @@ -162,3 +188,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
}
Loading

0 comments on commit 167305e

Please sign in to comment.