Skip to content

Commit

Permalink
feat: add support for SOURCE_DATE_EPOCH and build-date flag
Browse files Browse the repository at this point in the history
  • Loading branch information
kruskall committed Mar 4, 2022
1 parent 26bbc0e commit d591f1f
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 26 deletions.
8 changes: 3 additions & 5 deletions pkg/build/apk.go
Expand Up @@ -23,7 +23,6 @@ import (
"os"
"path/filepath"
"strings"
"time"

"github.com/pkg/errors"
"go.lsp.dev/uri"
Expand Down Expand Up @@ -201,7 +200,6 @@ func (bc *Context) normalizeApkScriptsTar() error {

defer outfile.Close()


tr := tar.NewReader(f)
tw := tar.NewWriter(outfile)

Expand All @@ -219,9 +217,9 @@ func (bc *Context) normalizeApkScriptsTar() error {
}

// zero out timestamps for reproducibility
header.AccessTime = time.Time{}
header.ModTime = time.Time{}
header.ChangeTime = time.Time{}
header.AccessTime = bc.SourceDataEpoch
header.ModTime = bc.SourceDataEpoch
header.ChangeTime = bc.SourceDataEpoch

if err := tw.WriteHeader(header); err != nil {
return err
Expand Down
4 changes: 3 additions & 1 deletion pkg/build/build.go
Expand Up @@ -17,6 +17,7 @@ package build
import (
"log"
"os"
"time"

"chainguard.dev/apko/pkg/build/types"
"chainguard.dev/apko/pkg/tarball"
Expand All @@ -29,6 +30,7 @@ type Context struct {
WorkDir string
TarballPath string
UseProot bool
SourceDataEpoch time.Time
}

func (bc *Context) Summarize() {
Expand All @@ -52,7 +54,7 @@ func (bc *Context) BuildTarball() (string, error) {
}
defer outfile.Close()

err = tarball.WriteArchive(bc.WorkDir, outfile)
err = tarball.WriteArchive(bc.WorkDir, outfile, bc.SourceDataEpoch)
if err != nil {
return "", errors.Wrap(err, "failed to generate tarball for image")
}
Expand Down
12 changes: 6 additions & 6 deletions pkg/build/oci/oci.go
Expand Up @@ -35,7 +35,7 @@ import (
"github.com/pkg/errors"
)

func buildImageFromLayer(layerTarGZ string, ic types.ImageConfiguration) (v1.Image, error) {
func buildImageFromLayer(layerTarGZ string, ic types.ImageConfiguration, created time.Time) (v1.Image, error) {
log.Printf("building OCI image from layer '%s'", layerTarGZ)

v1Layer, err := v1tar.LayerFromFile(layerTarGZ)
Expand Down Expand Up @@ -63,7 +63,7 @@ func buildImageFromLayer(layerTarGZ string, ic types.ImageConfiguration) (v1.Ima
Author: "apko",
Comment: "This is an apko single-layer image",
CreatedBy: "apko",
Created: v1.Time{Time: time.Time{}},
Created: v1.Time{Time: created},
},
})

Expand Down Expand Up @@ -100,8 +100,8 @@ func buildImageFromLayer(layerTarGZ string, ic types.ImageConfiguration) (v1.Ima
return v1Image, nil
}

func BuildImageTarballFromLayer(imageRef string, layerTarGZ string, outputTarGZ string, ic types.ImageConfiguration) error {
v1Image, err := buildImageFromLayer(layerTarGZ, ic)
func BuildImageTarballFromLayer(imageRef string, layerTarGZ string, outputTarGZ string, ic types.ImageConfiguration, created time.Time) error {
v1Image, err := buildImageFromLayer(layerTarGZ, ic, created)
if err != nil {
return err
}
Expand Down Expand Up @@ -133,8 +133,8 @@ func publishTagFromImage(image v1.Image, imageRef string, hash v1.Hash, kc authn
return imgRef.Context().Digest(hash.String()), nil
}

func PublishImageFromLayer(layerTarGZ string, ic types.ImageConfiguration, tags ...string) (name.Digest, error) {
v1Image, err := buildImageFromLayer(layerTarGZ, ic)
func PublishImageFromLayer(layerTarGZ string, ic types.ImageConfiguration, created time.Time, tags ...string) (name.Digest, error) {
v1Image, err := buildImageFromLayer(layerTarGZ, ic, created)
if err != nil {
return name.Digest{}, err
}
Expand Down
31 changes: 29 additions & 2 deletions pkg/cli/build-minirootfs.go
Expand Up @@ -18,6 +18,8 @@ import (
"context"
"log"
"os"
"strconv"
"time"

"chainguard.dev/apko/pkg/build"
"chainguard.dev/apko/pkg/build/types"
Expand All @@ -27,6 +29,7 @@ import (

func BuildMinirootFS() *cobra.Command {
var useProot bool
var buildDate string

cmd := &cobra.Command{
Use: "build-minirootfs",
Expand All @@ -35,16 +38,17 @@ func BuildMinirootFS() *cobra.Command {
Example: ` apko build-minirootfs <config.yaml> <output.tar.gz>`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
return BuildMinirootFSCmd(cmd.Context(), args[0], args[1], useProot)
return BuildMinirootFSCmd(cmd.Context(), args[0], args[1], useProot, buildDate)
},
}

cmd.Flags().BoolVar(&useProot, "use-proot", false, "use proot to simulate privileged operations")
cmd.Flags().StringVar(&buildDate, "build-date", "now", "date used for the timestamps of the files inside the image")

return cmd
}

func BuildMinirootFSCmd(ctx context.Context, configFile string, outputTarGZ string, useProot bool) error {
func BuildMinirootFSCmd(ctx context.Context, configFile string, outputTarGZ string, useProot bool, buildDate string) error {
log.Printf("building minirootfs '%s' from config file '%s'", outputTarGZ, configFile)

ic := types.ImageConfiguration{}
Expand All @@ -64,6 +68,29 @@ func BuildMinirootFSCmd(ctx context.Context, configFile string, outputTarGZ stri
WorkDir: wd,
TarballPath: outputTarGZ,
UseProot: useProot,
SourceDataEpoch: time.Now(),
}

if v, ok := os.LookupEnv("SOURCE_DATE_EPOCH"); ok {
// The value MUST be an ASCII representation of an integer
// with no fractional component, identical to the output
// format of date +%s.
sec, err := strconv.ParseInt(v, 10, 64)
if err != nil {
// If the value is malformed, the build process
// SHOULD exit with a non-zero error code.
return err
}

bc.SourceDataEpoch = time.Unix(sec, 0)
} else if buildDate == "now" {
bc.SourceDataEpoch = time.Now()
} else {
sec, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return err
}
bc.SourceDataEpoch = time.Unix(sec, 0)
}

layerTarGZ, err := bc.BuildLayer()
Expand Down
33 changes: 30 additions & 3 deletions pkg/cli/build.go
Expand Up @@ -18,6 +18,8 @@ import (
"context"
"log"
"os"
"strconv"
"time"

"chainguard.dev/apko/pkg/build"
"chainguard.dev/apko/pkg/build/oci"
Expand All @@ -28,6 +30,7 @@ import (

func Build() *cobra.Command {
var useProot bool
var buildDate string

cmd := &cobra.Command{
Use: "build",
Expand All @@ -41,16 +44,17 @@ command, e.g.
Example: ` apko build <config.yaml> <tag> <output.tar>`,
Args: cobra.ExactArgs(3),
RunE: func(cmd *cobra.Command, args []string) error {
return BuildCmd(cmd.Context(), args[0], args[1], args[2], useProot)
return BuildCmd(cmd.Context(), args[0], args[1], args[2], useProot, buildDate)
},
}

cmd.Flags().BoolVar(&useProot, "use-proot", false, "use proot to simulate privileged operations")
cmd.Flags().StringVar(&buildDate, "build-date", "now", "date used for the timestamps of the files inside the image")

return cmd
}

func BuildCmd(ctx context.Context, configFile string, imageRef string, outputTarGZ string, useProot bool) error {
func BuildCmd(ctx context.Context, configFile string, imageRef string, outputTarGZ string, useProot bool, buildDate string) error {
log.Printf("building image '%s' from config file '%s'", imageRef, configFile)

ic := types.ImageConfiguration{}
Expand All @@ -69,6 +73,29 @@ func BuildCmd(ctx context.Context, configFile string, imageRef string, outputTar
ImageConfiguration: ic,
WorkDir: wd,
UseProot: useProot,
SourceDataEpoch: time.Now(),
}

if v, ok := os.LookupEnv("SOURCE_DATE_EPOCH"); ok {
// The value MUST be an ASCII representation of an integer
// with no fractional component, identical to the output
// format of date +%s.
sec, err := strconv.ParseInt(v, 10, 64)
if err != nil {
// If the value is malformed, the build process
// SHOULD exit with a non-zero error code.
return err
}

bc.SourceDataEpoch = time.Unix(sec, 0)
} else if buildDate == "now" {
bc.SourceDataEpoch = time.Now()
} else {
sec, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return err
}
bc.SourceDataEpoch = time.Unix(sec, 0)
}

layerTarGZ, err := bc.BuildLayer()
Expand All @@ -77,7 +104,7 @@ func BuildCmd(ctx context.Context, configFile string, imageRef string, outputTar
}
defer os.Remove(layerTarGZ)

err = oci.BuildImageTarballFromLayer(imageRef, layerTarGZ, outputTarGZ, bc.ImageConfiguration)
err = oci.BuildImageTarballFromLayer(imageRef, layerTarGZ, outputTarGZ, bc.ImageConfiguration, bc.SourceDataEpoch)
if err != nil {
return errors.Wrap(err, "failed to build OCI image")
}
Expand Down
33 changes: 30 additions & 3 deletions pkg/cli/publish.go
Expand Up @@ -19,6 +19,8 @@ import (
"fmt"
"log"
"os"
"strconv"
"time"

"chainguard.dev/apko/pkg/build"
"chainguard.dev/apko/pkg/build/oci"
Expand All @@ -30,6 +32,7 @@ import (
func Publish() *cobra.Command {
var imageRefs string
var useProot bool
var buildDate string

cmd := &cobra.Command{
Use: "publish",
Expand All @@ -41,7 +44,7 @@ in a keychain.`,
Example: ` apko publish <config.yaml> <tag...>`,
Args: cobra.MinimumNArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
err := PublishCmd(cmd.Context(), args[0], imageRefs, useProot, args[1:]...)
err := PublishCmd(cmd.Context(), args[0], imageRefs, useProot, buildDate, args[1:]...)
if err != nil {
return err
}
Expand All @@ -51,11 +54,12 @@ in a keychain.`,

cmd.Flags().StringVar(&imageRefs, "image-refs", "", "path to file where a list of the published image references will be written")
cmd.Flags().BoolVar(&useProot, "use-proot", false, "use proot to simulate privileged operations")
cmd.Flags().StringVar(&buildDate, "build-date", "now", "date used for the timestamps of the files inside the image")

return cmd
}

func PublishCmd(ctx context.Context, configFile string, outputRefs string, useProot bool, tags ...string) error {
func PublishCmd(ctx context.Context, configFile string, outputRefs string, useProot bool, buildDate string, tags ...string) error {
log.Printf("building tags %v from config file '%s'", tags, configFile)

ic := types.ImageConfiguration{}
Expand All @@ -76,13 +80,36 @@ func PublishCmd(ctx context.Context, configFile string, outputRefs string, usePr
UseProot: useProot,
}

// SOURCE_DATE_EPOCH takes priority
if v, ok := os.LookupEnv("SOURCE_DATE_EPOCH"); ok {
// The value MUST be an ASCII representation of an integer
// with no fractional component, identical to the output
// format of date +%s.
sec, err := strconv.ParseInt(v, 10, 64)
if err != nil {
// If the value is malformed, the build process
// SHOULD exit with a non-zero error code.
return err
}

bc.SourceDataEpoch = time.Unix(sec, 0)
} else if buildDate == "now" {
bc.SourceDataEpoch = time.Now()
} else {
sec, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return err
}
bc.SourceDataEpoch = time.Unix(sec, 0)
}

layerTarGZ, err := bc.BuildLayer()
if err != nil {
return errors.Wrap(err, "failed to build layer image")
}
defer os.Remove(layerTarGZ)

digest, err := oci.PublishImageFromLayer(layerTarGZ, bc.ImageConfiguration, tags...)
digest, err := oci.PublishImageFromLayer(layerTarGZ, bc.ImageConfiguration, bc.SourceDataEpoch, tags...)
if err != nil {
return errors.Wrap(err, "failed to build OCI image")
}
Expand Down
12 changes: 6 additions & 6 deletions pkg/tarball/tarball.go
Expand Up @@ -27,7 +27,7 @@ import (
)

// Writes a raw TAR archive to out, given an fs.FS.
func WriteArchiveFromFS(base string, fsys fs.FS, out io.Writer) error {
func WriteArchiveFromFS(base string, fsys fs.FS, out io.Writer, sourceDateEpoch time.Time) error {
gzw := gzip.NewWriter(out)
defer gzw.Close()

Expand Down Expand Up @@ -60,9 +60,9 @@ func WriteArchiveFromFS(base string, fsys fs.FS, out io.Writer) error {
header.Name = path

// zero out timestamps for reproducibility
header.AccessTime = time.Time{}
header.ModTime = time.Time{}
header.ChangeTime = time.Time{}
header.AccessTime = sourceDateEpoch
header.ModTime = sourceDateEpoch
header.ChangeTime = sourceDateEpoch

if err := tw.WriteHeader(header); err != nil {
return err
Expand Down Expand Up @@ -92,9 +92,9 @@ func WriteArchiveFromFS(base string, fsys fs.FS, out io.Writer) error {

// Writes a tarball to a temporary file. Caller's responsibility to
// clean it up when it's done with it.
func WriteArchive(src string, w io.Writer) error {
func WriteArchive(src string, w io.Writer, sourceDateEpoch time.Time) error {
fs := os.DirFS(src)
err := WriteArchiveFromFS(src, fs, w)
err := WriteArchiveFromFS(src, fs, w, sourceDateEpoch)
if err != nil {
return errors.Wrap(err, "writing TAR archive failed")
}
Expand Down

0 comments on commit d591f1f

Please sign in to comment.