diff --git a/acceptance/analyzer_test.go b/acceptance/analyzer_test.go index 3d9eb4640..fff640317 100644 --- a/acceptance/analyzer_test.go +++ b/acceptance/analyzer_test.go @@ -112,7 +112,9 @@ func testAnalyzerFunc(platformAPI string) func(t *testing.T, when spec.G, it spe when("called with skip layers", func() { it("errors", func() { - h.SkipIf(t, api.MustParse(platformAPI).LessThan("0.7"), "Platform API < 0.7 accepts a -skip-layers flag") + h.SkipIf(t, + api.MustParse(platformAPI).LessThan("0.7") || api.MustParse(platformAPI).AtLeast("0.9"), + "Platform API < 0.7 or Platform API > 0.9 accepts a -skip-layers flag") cmd := exec.Command( "docker", "run", "--rm", "--env", "CNB_PLATFORM_API="+platformAPI, diff --git a/acceptance/exporter_test.go b/acceptance/exporter_test.go index 13ebc145d..5facbf31f 100644 --- a/acceptance/exporter_test.go +++ b/acceptance/exporter_test.go @@ -5,6 +5,7 @@ package acceptance import ( "context" + "fmt" "math/rand" "os/exec" "path/filepath" @@ -16,6 +17,8 @@ import ( "github.com/sclevine/spec" "github.com/sclevine/spec/report" + "github.com/buildpacks/imgutil" + "github.com/buildpacks/lifecycle/api" "github.com/buildpacks/lifecycle/internal/encoding" "github.com/buildpacks/lifecycle/platform" @@ -90,10 +93,40 @@ func testExporterFunc(platformAPI string) func(t *testing.T, when spec.G, it spe ) h.AssertStringContains(t, output, "Saving "+exportedImageName) - assertImageOSAndArch(t, exportedImageName, exportTest) + assertImageOSAndArchAndCreatedAt(t, exportedImageName, exportTest, imgutil.NormalizedDateTime) }) }) }) + when("SOURCE_DATE_EPOCH is set", func() { + it("Image CreatedAt is set to SOURCE_DATE_EPOCH", func() { + h.SkipIf(t, api.MustParse(platformAPI).LessThan("0.9"), "SOURCE_DATE_EPOCH support added in 0.9") + expectedTime := time.Date(2022, 1, 5, 5, 5, 5, 0, time.UTC) + + exportFlags := []string{"-daemon"} + if api.MustParse(platformAPI).LessThan("0.7") { + exportFlags = append(exportFlags, []string{"-run-image", exportRegFixtures.ReadOnlyRunImage}...) + } + + exportArgs := append([]string{ctrPath(exporterPath)}, exportFlags...) + exportedImageName = "some-exported-image-" + h.RandString(10) + exportArgs = append(exportArgs, exportedImageName) + + output := h.DockerRun(t, + exportImage, + h.WithFlags(append( + dockerSocketMount, + "--env", "CNB_PLATFORM_API="+platformAPI, + "--env", "CNB_REGISTRY_AUTH="+exportRegAuthConfig, + "--env", "SOURCE_DATE_EPOCH="+fmt.Sprintf("%d", expectedTime.Unix()), + "--network", exportRegNetwork, + )...), + h.WithArgs(exportArgs...), + ) + h.AssertStringContains(t, output, "Saving "+exportedImageName) + + assertImageOSAndArchAndCreatedAt(t, exportedImageName, exportTest, expectedTime) + }) + }) }) when("registry case", func() { @@ -121,7 +154,37 @@ func testExporterFunc(platformAPI string) func(t *testing.T, when spec.G, it spe h.AssertStringContains(t, output, "Saving "+exportedImageName) h.Run(t, exec.Command("docker", "pull", exportedImageName)) - assertImageOSAndArch(t, exportedImageName, exportTest) + assertImageOSAndArchAndCreatedAt(t, exportedImageName, exportTest, imgutil.NormalizedDateTime) + }) + }) + when("SOURCE_DATE_EPOCH is set", func() { + it("Image CreatedAt is set to SOURCE_DATE_EPOCH", func() { + h.SkipIf(t, api.MustParse(platformAPI).LessThan("0.9"), "SOURCE_DATE_EPOCH support added in 0.9") + expectedTime := time.Date(2022, 1, 5, 5, 5, 5, 0, time.UTC) + + var exportFlags []string + if api.MustParse(platformAPI).LessThan("0.7") { + exportFlags = append(exportFlags, []string{"-run-image", exportRegFixtures.ReadOnlyRunImage}...) + } + + exportArgs := append([]string{ctrPath(exporterPath)}, exportFlags...) + exportedImageName = exportTest.RegRepoName("some-exported-image-" + h.RandString(10)) + exportArgs = append(exportArgs, exportedImageName) + + output := h.DockerRun(t, + exportImage, + h.WithFlags( + "--env", "CNB_PLATFORM_API="+platformAPI, + "--env", "CNB_REGISTRY_AUTH="+exportRegAuthConfig, + "--env", "SOURCE_DATE_EPOCH="+fmt.Sprintf("%d", expectedTime.Unix()), + "--network", exportRegNetwork, + ), + h.WithArgs(exportArgs...), + ) + h.AssertStringContains(t, output, "Saving "+exportedImageName) + + h.Run(t, exec.Command("docker", "pull", exportedImageName)) + assertImageOSAndArchAndCreatedAt(t, exportedImageName, exportTest, expectedTime) }) }) when("cache", func() { @@ -149,7 +212,7 @@ func testExporterFunc(platformAPI string) func(t *testing.T, when spec.G, it spe h.AssertStringContains(t, output, "Saving "+exportedImageName) h.Run(t, exec.Command("docker", "pull", exportedImageName)) - assertImageOSAndArch(t, exportedImageName, exportTest) + assertImageOSAndArchAndCreatedAt(t, exportedImageName, exportTest, imgutil.NormalizedDateTime) }) }) }) @@ -165,6 +228,14 @@ func assertImageOSAndArch(t *testing.T, imageName string, phaseTest *PhaseTest) h.AssertEq(t, inspect.Architecture, phaseTest.targetDaemon.arch) } +func assertImageOSAndArchAndCreatedAt(t *testing.T, imageName string, phaseTest *PhaseTest, expectedCreatedAt time.Time) { + inspect, _, err := h.DockerCli(t).ImageInspectWithRaw(context.TODO(), imageName) + h.AssertNil(t, err) + h.AssertEq(t, inspect.Os, phaseTest.targetDaemon.os) + h.AssertEq(t, inspect.Architecture, phaseTest.targetDaemon.arch) + h.AssertEq(t, inspect.Created, expectedCreatedAt.Format(time.RFC3339)) +} + func updateAnalyzedTOMLFixturesWithRegRepoName(t *testing.T, phaseTest *PhaseTest) { placeHolderPath := filepath.Join("testdata", "exporter", "container", "layers", "analyzed.toml.placeholder") analyzedMD := assertAnalyzedMetadata(t, placeHolderPath) diff --git a/api/apis.go b/api/apis.go index 92efbe6fd..0dccd1719 100644 --- a/api/apis.go +++ b/api/apis.go @@ -8,7 +8,7 @@ import ( ) var ( - Platform = newApisMustParse([]string{"0.3", "0.4", "0.5", "0.6", "0.7", "0.8"}, nil) + Platform = newApisMustParse([]string{"0.3", "0.4", "0.5", "0.6", "0.7", "0.8", "0.9"}, nil) Buildpack = newApisMustParse([]string{"0.2", "0.3", "0.4", "0.5", "0.6", "0.7", "0.8"}, nil) ) diff --git a/cmd/lifecycle/exporter.go b/cmd/lifecycle/exporter.go index 24449253e..85afb6a3b 100644 --- a/cmd/lifecycle/exporter.go +++ b/cmd/lifecycle/exporter.go @@ -4,6 +4,8 @@ import ( "fmt" "io/ioutil" "os" + "strconv" + "time" "github.com/BurntSushi/toml" "github.com/buildpacks/imgutil" @@ -334,6 +336,10 @@ func (ea exportArgs) initDaemonAppImage(analyzedMD platform.AnalyzedMetadata) (i opts = append(opts, local.WithPreviousImage(analyzedMD.PreviousImage.Reference)) } + if !ea.customSourceDateEpoch().IsZero() { + opts = append(opts, local.WithCreatedAt(ea.customSourceDateEpoch())) + } + var appImage imgutil.Image appImage, err := local.NewImage( ea.imageNames[0], @@ -377,6 +383,10 @@ func (ea exportArgs) initRemoteAppImage(analyzedMD platform.AnalyzedMetadata) (i opts = append(opts, remote.WithPreviousImage(analyzedMD.PreviousImage.Reference)) } + if !ea.customSourceDateEpoch().IsZero() { + opts = append(opts, remote.WithCreatedAt(ea.customSourceDateEpoch())) + } + appImage, err := remote.NewImage( ea.imageNames[0], ea.keychain, @@ -427,3 +437,19 @@ func parseAnalyzedMD(logger lifecycle.Logger, path string) (platform.AnalyzedMet return analyzedMD, nil } + +func (ea exportArgs) customSourceDateEpoch() time.Time { + if ea.platform.API().LessThan("0.9") { + return time.Time{} + } + + if epoch := os.Getenv("SOURCE_DATE_EPOCH"); epoch != "" { + seconds, err := strconv.ParseInt(epoch, 10, 64) + if err != nil { + cmd.DefaultLogger.Warn("Ignoring invalid SOURCE_DATE_EPOCH") + return time.Time{} + } + return time.Unix(seconds, 0) + } + return time.Time{} +} diff --git a/go.mod b/go.mod index 8fd7eb385..a4ec15348 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/buildpacks/lifecycle require ( github.com/BurntSushi/toml v1.0.0 github.com/apex/log v1.9.0 - github.com/buildpacks/imgutil v0.0.0-20211203200417-76206845baac + github.com/buildpacks/imgutil v0.0.0-20220310160537-4dd8bc60eaff github.com/docker/docker v20.10.12+incompatible github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.5.7 diff --git a/go.sum b/go.sum index 1bf62a3ac..a71e4eed3 100644 --- a/go.sum +++ b/go.sum @@ -106,8 +106,8 @@ github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edY github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= -github.com/buildpacks/imgutil v0.0.0-20211203200417-76206845baac h1:XrKr6axRUBHEQdyyo7uffYDwWurOdeyH8MpNRJuBdIw= -github.com/buildpacks/imgutil v0.0.0-20211203200417-76206845baac/go.mod h1:YZReWjuSxwyvuN92Vlcul+WgaCXylpecgFn7T3rNang= +github.com/buildpacks/imgutil v0.0.0-20220310160537-4dd8bc60eaff h1:sP0G3fOfWMSDabqIuPY1o6aeiX35eQz9mWhYCMqgp08= +github.com/buildpacks/imgutil v0.0.0-20220310160537-4dd8bc60eaff/go.mod h1:zjdTnysBSl9Jeiz2J/B7Nf621dsDaEGkMfySlPqXNtY= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -200,7 +200,6 @@ github.com/docker/cli v20.10.12+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hH github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v20.10.10+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker v20.10.11+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v20.10.12+incompatible h1:CEeNmFM0QZIsJCZKMkZx0ZcahTiewkrgiwfYD+dfl1U= github.com/docker/docker v20.10.12+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.6.4 h1:axCks+yV+2MR3/kZhAmy07yC56WZ2Pwu/fKWtKuZB0o= diff --git a/platform/platform_test.go b/platform/platform_test.go index dd5e4a939..4d9586d9c 100644 --- a/platform/platform_test.go +++ b/platform/platform_test.go @@ -45,6 +45,10 @@ func testPlatform(t *testing.T, when spec.G, it spec.S) { version: "0.8", exiter: &platform.DefaultExiter{}, }, + { + version: "0.9", + exiter: &platform.DefaultExiter{}, + }, } for _, apiVersion := range api.Platform.Supported { for _, expectedPlatform := range toTest {