diff --git a/README.md b/README.md index 357ccb9bc8..97fddad325 100644 --- a/README.md +++ b/README.md @@ -214,6 +214,13 @@ you can include Git commit information in your image with: ln -s -r .git/HEAD ./cmd/app/kodata/ ``` +Also note that `http.FileServer` will not serve the `Last-Modified` header +(or validate `If-Modified-Since` request headers) because `ko` does not embed +timestamps by default. + +This can be supported by manually setting the `KO_DATA_DATE_EPOCH` environment +variable during build ([See below](#Why-are-my-images-all-created-in-1970)). + # Kubernetes Integration You could stop at just building and pushing images. @@ -335,21 +342,27 @@ GOFLAGS="-ldflags=-X=main.version=1.2.3" ko publish . ## Why are my images all created in 1970? In order to support [reproducible builds](https://reproducible-builds.org), `ko` -doesn't embed timestamps in the images it produces by default; however, `ko` -does respect the -[`SOURCE_DATE_EPOCH`](https://reproducible-builds.org/docs/source-date-epoch/) -environment variable. +doesn't embed timestamps in the images it produces by default. + +However, `ko` does respect the [`SOURCE_DATE_EPOCH`](https://reproducible-builds.org/docs/source-date-epoch/) +environment variable, which will set the container image's timestamp +accordingly. + +Similarly, the `KO_DATA_DATE_EPOCH` environment variable can be used to set +the _modtime_ timestamp of the files in `KO_DATA_PATH`. -For example, you can set this to the current timestamp by executing: +For example, you can set the container image's timestamp to the current +timestamp by executing: ``` export SOURCE_DATE_EPOCH=$(date +%s) ``` -or to the latest git commit's timestamp with: +or set the timestamp of the files in `KO_DATA_PATH` to the latest git commit's +timestamp with: ``` -export SOURCE_DATE_EPOCH=$(git log -1 --format='%ct') +export KO_DATA_DATE_EPOCH=$(git log -1 --format='%ct') ``` ## Can I optimize images for [eStargz support](https://github.com/containerd/stargz-snapshotter/blob/v0.2.0/docs/stargz-estargz.md)? diff --git a/pkg/build/gobuild.go b/pkg/build/gobuild.go index dbcfc5de79..4d77de4f23 100644 --- a/pkg/build/gobuild.go +++ b/pkg/build/gobuild.go @@ -81,6 +81,7 @@ type platformMatcher struct { type gobuild struct { getBase GetBase creationTime v1.Time + kodataCreationTime v1.Time build builder disableOptimizations bool mod *modules @@ -96,6 +97,7 @@ type Option func(*gobuildOpener) error type gobuildOpener struct { getBase GetBase creationTime v1.Time + kodataCreationTime v1.Time build builder disableOptimizations bool mod *modules @@ -116,6 +118,7 @@ func (gbo *gobuildOpener) Open() (Interface, error) { return &gobuild{ getBase: gbo.getBase, creationTime: gbo.creationTime, + kodataCreationTime: gbo.kodataCreationTime, build: gbo.build, disableOptimizations: gbo.disableOptimizations, mod: gbo.mod, @@ -414,13 +417,13 @@ func appFilename(importpath string) string { return base } -func tarAddDirectories(tw *tar.Writer, dir string) error { +func tarAddDirectories(tw *tar.Writer, dir string, creationTime v1.Time) error { if dir == "." || dir == string(filepath.Separator) { return nil } // Write parent directories first - if err := tarAddDirectories(tw, filepath.Dir(dir)); err != nil { + if err := tarAddDirectories(tw, filepath.Dir(dir), creationTime); err != nil { return err } @@ -431,7 +434,8 @@ func tarAddDirectories(tw *tar.Writer, dir string) error { // 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, + Mode: 0555, + ModTime: creationTime.Time, }); err != nil { return err } @@ -439,13 +443,13 @@ func tarAddDirectories(tw *tar.Writer, dir string) error { return nil } -func tarBinary(name, binary string) (*bytes.Buffer, error) { +func tarBinary(name, binary string, creationTime v1.Time) (*bytes.Buffer, error) { buf := bytes.NewBuffer(nil) tw := tar.NewWriter(buf) defer tw.Close() // write the parent directories to the tarball archive - if err := tarAddDirectories(tw, path.Dir(name)); err != nil { + if err := tarAddDirectories(tw, path.Dir(name), creationTime); err != nil { return nil, err } @@ -465,7 +469,8 @@ func tarBinary(name, binary string) (*bytes.Buffer, error) { // 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, + Mode: 0555, + ModTime: creationTime.Time, } // write the header to the tarball archive if err := tw.WriteHeader(header); err != nil { @@ -493,7 +498,7 @@ const kodataRoot = "/var/run/ko" // walkRecursive performs a filepath.Walk of the given root directory adding it // to the provided tar.Writer with root -> chroot. All symlinks are dereferenced, // which is what leads to recursion when we encounter a directory symlink. -func walkRecursive(tw *tar.Writer, root, chroot string) error { +func walkRecursive(tw *tar.Writer, root, chroot string, creationTime v1.Time) error { return filepath.Walk(root, func(hostPath string, info os.FileInfo, err error) error { if hostPath == root { // Add an entry for the root directory of our walk. @@ -503,7 +508,8 @@ func walkRecursive(tw *tar.Writer, root, chroot string) error { // 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, + Mode: 0555, + ModTime: creationTime.Time, }) } if err != nil { @@ -527,7 +533,7 @@ func walkRecursive(tw *tar.Writer, root, chroot string) error { } // Skip other directories. if info.Mode().IsDir() { - return walkRecursive(tw, evalPath, newPath) + return walkRecursive(tw, evalPath, newPath, creationTime) } // Open the file to copy it into the tarball. @@ -545,7 +551,8 @@ func walkRecursive(tw *tar.Writer, root, chroot string) error { // 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, + Mode: 0555, + ModTime: creationTime.Time, }); err != nil { return fmt.Errorf("tar.Writer.WriteHeader(%q): %w", newPath, err) } @@ -566,7 +573,9 @@ func (g *gobuild) tarKoData(ref reference) (*bytes.Buffer, error) { return nil, err } - return buf, walkRecursive(tw, root, kodataRoot) + creationTime := g.kodataCreationTime + + return buf, walkRecursive(tw, root, kodataRoot, creationTime) } func (g *gobuild) buildOne(ctx context.Context, s string, base v1.Image, platform *v1.Platform) (v1.Image, error) { @@ -616,7 +625,7 @@ func (g *gobuild) buildOne(ctx context.Context, s string, base v1.Image, platfor appPath := path.Join(appDir, appFilename(ref.Path())) // Construct a tarball with the binary and produce a layer. - binaryLayerBuf, err := tarBinary(appPath, file) + binaryLayerBuf, err := tarBinary(appPath, file, v1.Time{}) if err != nil { return nil, err } diff --git a/pkg/build/options.go b/pkg/build/options.go index a8a765811b..e79f9a1fe7 100644 --- a/pkg/build/options.go +++ b/pkg/build/options.go @@ -36,6 +36,15 @@ func WithCreationTime(t v1.Time) Option { } } +// WithKoDataCreationTime is a functional option for overriding the creation +// time given to the files in the kodata directory. +func WithKoDataCreationTime(t v1.Time) Option { + return func(gbo *gobuildOpener) error { + gbo.kodataCreationTime = t + return nil + } +} + // WithDisabledOptimizations is a functional option for disabling optimizations // when compiling. func WithDisabledOptimizations() Option { diff --git a/pkg/commands/config.go b/pkg/commands/config.go index 1448c95c85..f90a0cbeec 100644 --- a/pkg/commands/config.go +++ b/pkg/commands/config.go @@ -127,19 +127,27 @@ func getBaseImage(platform string, bo *options.BuildOptions) build.GetBase { } } -func getCreationTime() (*v1.Time, error) { - epoch := os.Getenv("SOURCE_DATE_EPOCH") +func getTimeFromEnv(env string) (*v1.Time, error) { + epoch := os.Getenv(env) if epoch == "" { return nil, nil } seconds, err := strconv.ParseInt(epoch, 10, 64) if err != nil { - return nil, fmt.Errorf("the environment variable SOURCE_DATE_EPOCH should be the number of seconds since January 1st 1970, 00:00 UTC, got: %v", err) + return nil, fmt.Errorf("the environment variable %s should be the number of seconds since January 1st 1970, 00:00 UTC, got: %v", env, err) } return &v1.Time{Time: time.Unix(seconds, 0)}, nil } +func getCreationTime() (*v1.Time, error) { + return getTimeFromEnv("SOURCE_DATE_EPOCH") +} + +func getKoDataCreationTime() (*v1.Time, error) { + return getTimeFromEnv("KO_DATA_DATE_EPOCH") +} + func createCancellableContext() context.Context { signals := make(chan os.Signal) signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) diff --git a/pkg/commands/resolver.go b/pkg/commands/resolver.go index e72627a746..1e0442fe42 100644 --- a/pkg/commands/resolver.go +++ b/pkg/commands/resolver.go @@ -55,6 +55,11 @@ func gobuildOptions(bo *options.BuildOptions) ([]build.Option, error) { return nil, err } + kodataCreationTime, err := getKoDataCreationTime() + if err != nil { + return nil, err + } + platform := bo.Platform if platform == "" { platform = "linux/amd64" @@ -86,6 +91,9 @@ func gobuildOptions(bo *options.BuildOptions) ([]build.Option, error) { if creationTime != nil { opts = append(opts, build.WithCreationTime(*creationTime)) } + if kodataCreationTime != nil { + opts = append(opts, build.WithKoDataCreationTime(*kodataCreationTime)) + } if bo.DisableOptimizations { opts = append(opts, build.WithDisabledOptimizations()) }