diff --git a/pkg/apis/build/v1alpha2/build_pod.go b/pkg/apis/build/v1alpha2/build_pod.go index 556020e12..925ed78ef 100644 --- a/pkg/apis/build/v1alpha2/build_pod.go +++ b/pkg/apis/build/v1alpha2/build_pod.go @@ -3,7 +3,9 @@ package v1alpha2 import ( "fmt" "path/filepath" + "strconv" "strings" + "time" "github.com/Masterminds/semver/v3" "github.com/google/go-containerregistry/pkg/name" @@ -338,6 +340,12 @@ func (b *Build) BuildPod(images BuildPodImages, buildContext BuildContext) (*cor SecurityContext: containerSecurityContext(buildContext.BuildPodBuilderConfig), } detectContainerMods := ifWindows(buildContext.os(), addNetworkWaitLauncherVolume(), useNetworkWaitLauncher(dnsProbeHost)) + + dateTime, err := parseTime(b.Spec.CreationTime) + if err != nil { + return nil, errors.Wrapf(err, "parsing creation time %s", b.Spec.CreationTime) + } + return &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: b.PodName(), @@ -592,6 +600,12 @@ func (b *Build) BuildPod(images BuildPodImages, buildContext BuildContext) (*cor homeEnv, platformApiVersionEnvVar, }, + func() corev1.EnvVar { + if dateTime != nil { + return corev1.EnvVar{Name: "SOURCE_DATE_EPOCH", Value: strconv.Itoa(int(dateTime.Unix()))} + } + return corev1.EnvVar{Name:"", Value:""} + }(), func() corev1.EnvVar { return corev1.EnvVar{ Name: "CNB_RUN_IMAGE", @@ -1211,9 +1225,29 @@ func cosignSecretArgs(secret corev1.Secret) []string { return cosignArgs } -func envs(envs []corev1.EnvVar, envVar corev1.EnvVar) []corev1.EnvVar { - if envVar.Name != "" && envVar.Value != "" { - envs = append(envs, envVar) +func envs(envs []corev1.EnvVar, envVars ...corev1.EnvVar) []corev1.EnvVar { + for _, envVar := range envVars { + if envVar.Name != "" && envVar.Value != "" { + envs = append(envs, envVar) + } } return envs } + + +func parseTime(providedTime string) (*time.Time, error) { + var parsedTime time.Time + switch providedTime { + case "": + return nil, nil + case "now": + parsedTime = time.Now().UTC() + default: + intTime, err := strconv.ParseInt(providedTime, 10, 64) + if err != nil { + return nil, errors.Wrap(err, "parsing unix timestamp") + } + parsedTime = time.Unix(intTime, 0).UTC() + } + return &parsedTime, nil +} diff --git a/pkg/apis/build/v1alpha2/build_pod_test.go b/pkg/apis/build/v1alpha2/build_pod_test.go index 93d45be38..cc1c455c7 100644 --- a/pkg/apis/build/v1alpha2/build_pod_test.go +++ b/pkg/apis/build/v1alpha2/build_pod_test.go @@ -334,6 +334,7 @@ func testBuildPod(t *testing.T, when spec.G, it spec.S) { Tolerations: []corev1.Toleration{{Key: "some-key"}}, NodeSelector: map[string]string{"foo": "bar"}, Affinity: &corev1.Affinity{}, + CreationTime: "now", }, } }) @@ -1069,6 +1070,9 @@ func testBuildPod(t *testing.T, when spec.G, it spec.S) { assert.Equal(t, pod.Spec.InitContainers[5].Name, "export") assert.Equal(t, pod.Spec.InitContainers[5].Image, builderImage) assert.Contains(t, pod.Spec.InitContainers[5].Env, corev1.EnvVar{Name: "CNB_PLATFORM_API", Value: "0.8"}) + _, ok := fetchEnvVar(pod.Spec.InitContainers[5].Env, "SOURCE_DATE_EPOCH") + assert.Equal(t, true, ok) + assert.Contains(t, pod.Spec.InitContainers[5].Env, corev1.EnvVar{Name: "CNB_RUN_IMAGE", Value: "builderregistry.io/run"}) assert.ElementsMatch(t, names(pod.Spec.InitContainers[5].VolumeMounts), []string{ "layers-dir", "workspace-dir", diff --git a/pkg/apis/build/v1alpha2/build_types.go b/pkg/apis/build/v1alpha2/build_types.go index 053fb2e8e..83b54e4cc 100644 --- a/pkg/apis/build/v1alpha2/build_types.go +++ b/pkg/apis/build/v1alpha2/build_types.go @@ -77,6 +77,7 @@ type BuildSpec struct { RuntimeClassName *string `json:"runtimeClassName,omitempty"` SchedulerName string `json:"schedulerName,omitempty"` PriorityClassName string `json:"priorityClassName,omitempty"` + CreationTime string `json:"creationTime,omitempty"` } func (bs *BuildSpec) RegistryCacheTag() string { diff --git a/pkg/apis/build/v1alpha2/image_builds.go b/pkg/apis/build/v1alpha2/image_builds.go index ce203a6fc..e24979009 100644 --- a/pkg/apis/build/v1alpha2/image_builds.go +++ b/pkg/apis/build/v1alpha2/image_builds.go @@ -80,6 +80,7 @@ func (im *Image) Build(sourceResolver *SourceResolver, builder BuilderResource, SchedulerName: im.SchedulerName(), PriorityClassName: priorityClass, ActiveDeadlineSeconds: im.BuildTimeout(), + CreationTime: im.Spec.Build.CreationTime, }, } } diff --git a/pkg/apis/build/v1alpha2/image_builds_test.go b/pkg/apis/build/v1alpha2/image_builds_test.go index 7b6ddf982..ac6f810f7 100644 --- a/pkg/apis/build/v1alpha2/image_builds_test.go +++ b/pkg/apis/build/v1alpha2/image_builds_test.go @@ -35,6 +35,9 @@ func testImageBuilds(t *testing.T, when spec.G, it spec.S) { Kind: "Builder", Name: "builder-name", }, + Build: &ImageBuild{ + CreationTime: "", + }, }, } diff --git a/pkg/apis/build/v1alpha2/image_types.go b/pkg/apis/build/v1alpha2/image_types.go index 39556d1ff..f8553df62 100644 --- a/pkg/apis/build/v1alpha2/image_types.go +++ b/pkg/apis/build/v1alpha2/image_types.go @@ -78,6 +78,7 @@ type ImageBuild struct { RuntimeClassName *string `json:"runtimeClassName,omitempty"` SchedulerName string `json:"schedulerName,omitempty"` BuildTimeout *int64 `json:"buildTimeout,omitempty"` + CreationTime string `json:"creationTime,omitempty"` } // +k8s:openapi-gen=true diff --git a/pkg/apis/build/v1alpha2/image_validation.go b/pkg/apis/build/v1alpha2/image_validation.go index 4c4d94b3c..4e362f958 100644 --- a/pkg/apis/build/v1alpha2/image_validation.go +++ b/pkg/apis/build/v1alpha2/image_validation.go @@ -3,6 +3,7 @@ package v1alpha2 import ( "context" "fmt" + "strconv" "strings" "github.com/google/go-containerregistry/pkg/name" @@ -166,6 +167,14 @@ func (ib *ImageBuild) Validate(ctx context.Context) *apis.FieldError { } } + if ib.CreationTime != "" && ib.CreationTime != "now" { + // check that the timestamp in CreationTime is in valid format + _, err := strconv.ParseInt(ib.CreationTime, 10, 64) + if err != nil { + return apis.ErrInvalidValue(ib.CreationTime, "creationTime") + } + } + return ib.Services.Validate(ctx).ViaField("services"). Also(validateCnbBindings(ctx, ib.CNBBindings).ViaField("cnbBindings")) } diff --git a/pkg/apis/build/v1alpha2/image_validation_test.go b/pkg/apis/build/v1alpha2/image_validation_test.go index 073f865c9..b74d85d98 100644 --- a/pkg/apis/build/v1alpha2/image_validation_test.go +++ b/pkg/apis/build/v1alpha2/image_validation_test.go @@ -261,6 +261,25 @@ func testImageValidation(t *testing.T, when spec.G, it spec.S) { assertValidationError(image, ctx, apis.ErrMissingField("spec.build.services[0].name")) }) + when("validates the creation time", func() { + it("pass if it sets to 'now'", func() { + image.Spec.Build.CreationTime = "now" + err := image.Validate(ctx) + assert.Nil(t, err) + }) + + it ("pass if it sets to a valid timestamp", func() { + image.Spec.Build.CreationTime = "1566172801" //Mon Aug 19 2019 00:00:01 GMT+0000 + err := image.Validate(ctx) + assert.Nil(t, err) + }) + + it("fails if the creation time is not 'now' or a valid timestamp", func() { + image.Spec.Build.CreationTime = "invalidTimestamp" + assertValidationError(image, ctx, apis.ErrInvalidValue("invalidTimestamp", "creationTime").ViaField("spec", "build")) + }) + }) + it("image name is too long", func() { image.ObjectMeta.Name = "this-image-name-that-is-too-long-some-sha-that-is-long-82cb521d636b282340378d80a6307a08e3d4a4c4" assertValidationError(image, ctx, errors.New("invalid image name: this-image-name-that-is-too-long-some-sha-that-is-long-82cb521d636b282340378d80a6307a08e3d4a4c4, name must be a a valid label: metadata.name\nmust be no more than 63 characters"))