Skip to content

Commit 865dace

Browse files
committed
build: add --source-date-epoch and --rewrite-timestamp flags
Use $SOURCE_DATE_EPOCH as the default for the --source-date-epoch flag to the "build" CLI. When a source-date-epoch is set, we'll use it when writing new history entries, force timestamps in data written for --output to the specified timestamp, and populate a "SOURCE_DATE_EPOCH" ARG that we treat as always being set, and which we don't complain about being left unused. By default, this will not affect timestamps in newly-added layers. Add a --rewrite-timestamp flag, which "clamps" timestamps in newly-added layers to not be later than the --source-date-epoch value if the --source-date-epoch flag is set, but has no effect otherwise. Signed-off-by: Nalin Dahyabhai <[email protected]>
1 parent b8d8cc3 commit 865dace

File tree

13 files changed

+359
-56
lines changed

13 files changed

+359
-56
lines changed

define/build.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,9 +295,23 @@ type BuildOptions struct {
295295
SignBy string
296296
// Architecture specifies the target architecture of the image to be built.
297297
Architecture string
298-
// Timestamp sets the created timestamp to the specified time, allowing
299-
// for deterministic, content-addressable builds.
298+
// Timestamp specifies a timestamp to use for the image's created-on
299+
// date, the corresponding field in new history entries, the timestamps
300+
// to set on contents in new layer diffs, and the timestamps to set on
301+
// contents written as specified in the BuildOutput field. If left
302+
// unset, the current time is used for the configuration and manifest,
303+
// and layer contents are recorded as-is.
300304
Timestamp *time.Time
305+
// SourceDateEpoch specifies a timestamp to use for the image's
306+
// created-on date and the corrsponding field in new history entries,
307+
// and any content written as specified in the BuildOutput field. If
308+
// left unset, the current time is used for the configuration and
309+
// manifest, and layer and BuildOutput contents retain their original
310+
// timestamps.
311+
SourceDateEpoch *time.Time
312+
// RewriteTimestamp, if set, forces timestamps in generated layers to
313+
// not be later than the SourceDateEpoch, if it is also set.
314+
RewriteTimestamp bool
301315
// OS is the specifies the operating system of the image to be built.
302316
OS string
303317
// MaxPullPushRetries is the maximum number of attempts we'll make to pull or push any one

docs/buildah-build.1.md

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -715,24 +715,34 @@ Windows base images, so using this option is usually unnecessary.
715715

716716
**--output**, **-o**=""
717717

718-
Output destination (format: type=local,dest=path)
718+
Additional output (format: type=local,dest=path)
719719

720-
The --output (or -o) option extends the default behavior of building a container image by allowing users to export the contents of the image as files on the local filesystem, which can be useful for generating local binaries, code generation, etc.
720+
The --output (or -o) option supplements the default behavior of building a
721+
container image by allowing users to export the image's contents as files on
722+
the local filesystem, which can be useful for generating local binaries, code
723+
generation, etc.
721724

722-
The value for --output is a comma-separated sequence of key=value pairs, defining the output type and options.
725+
The value for --output is a comma-separated sequence of key=value pairs,
726+
defining the output type and options.
723727

724728
Supported _keys_ are:
725-
- **dest**: Destination path for exported output. Valid value is absolute or relative path, `-` means the standard output.
726-
- **type**: Defines the type of output to be used. Valid values is documented below.
729+
**dest**: Destination for exported output. Can be set to `-` to indicate standard output, or to an absolute or relative path.
730+
**type**: Defines the type of output to be written. Must be one of the values listed below.
727731

728732
Valid _type_ values are:
729-
- **local**: write the resulting build files to a directory on the client-side.
730-
- **tar**: write the resulting files as a single tarball (.tar).
733+
**local**: write the resulting build files to a directory on the client-side.
734+
**tar**: write the resulting files as a single tarball (.tar).
731735

732-
If no type is specified, the value defaults to **local**.
733-
Alternatively, instead of a comma-separated sequence, the value of **--output** can be just a destination (in the `**dest**` format) (e.g. `--output some-path`, `--output -`) where `--output some-path` is treated as if **type=local** and `--output -` is treated as if **type=tar**.
736+
Alternatively, instead of a comma-separated sequence, the value of **--output**
737+
can be just the destination (in the `**dest**` format) (e.g. `--output
738+
some-path`, `--output -`), and the **type** will be inferred to be **tar** if
739+
the output destination is `-`, and **local** otherwise.
734740

735-
Note: The **--tag** option can also be used to change the file image format to supported `containers-transports(5)`.
741+
Timestamps on the output contents will be set to exactly match the value
742+
specified using the **--timestamp** flag, or to exactly match the value
743+
specified for the **--source-date-epoch** flag, if either are specified.
744+
745+
Note that the **--tag** option can also be used to write the image to any location described by `containers-transports(5)`.
736746

737747
**--pid** *how*
738748

@@ -802,6 +812,13 @@ Duration of delay between retry attempts in case of failure when performing push
802812

803813
Defaults to `2s`.
804814

815+
**--rewrite-timestamp**
816+
817+
When generating new layers for the image, ensure that no newly added content
818+
bears a timestamp later than the value used by the **--source-date-epoch**
819+
flag, if one was provided, by replacing any timestamps which are later than
820+
that value, with that value.
821+
805822
**--rm** *bool-value*
806823

807824
Remove intermediate containers after a successful build (default true).
@@ -970,6 +987,31 @@ Sign the built image using the GPG key that matches the specified fingerprint.
970987

971988
Skip stages in multi-stage builds which don't affect the target stage. (Default is `true`).
972989

990+
**--source-date-epoch** *seconds*
991+
992+
Set the "created" timestamp for the built image to this number of seconds since
993+
the epoch (Unix time 0, i.e., 00:00:00 UTC on 1 January 1970) (default is to
994+
use the value set in the `SOURCE_DATE_EPOCH` environment variable, or the
995+
current time if it is not set).
996+
997+
The "created" timestamp is written into the image's configuration and manifest
998+
when the image is committed, so running the same build two different times
999+
will ordinarily produce images with different sha256 hashes, even if no other
1000+
changes were made to the Containerfile and build context.
1001+
1002+
When this flag is set, a `SOURCE_DATE_EPOCH` build arg will provide its value
1003+
for a stage in which it is declared.
1004+
1005+
When this flag is set, the image configuration's "created" timestamp is always
1006+
set to the time specified, which should allow for identical images to be built
1007+
at different times using the same set of inputs.
1008+
1009+
When this flag is set, output written as specified to the **--output** flag
1010+
will bear exactly the specified timestamp.
1011+
1012+
Conflicts with the similar **--timestamp** flag, which also sets its specified
1013+
time on the contents of new layers.
1014+
9731015
**--squash**
9741016

9751017
Squash all layers, including those from base image(s), into one single layer. (Default is false).
@@ -1013,10 +1055,25 @@ Commands after the target stage will be skipped.
10131055

10141056
**--timestamp** *seconds*
10151057

1016-
Set the create timestamp to seconds since epoch to allow for deterministic builds (defaults to current time).
1017-
By default, the created timestamp is changed and written into the image manifest with every commit,
1018-
causing the image's sha256 hash to be different even if the sources are exactly the same otherwise.
1019-
When --timestamp is set, the created timestamp is always set to the time specified and therefore not changed, allowing the image's sha256 to remain the same. All files committed to the layers of the image will be created with the timestamp.
1058+
Set the "created" timestamp for the built image to this number of seconds since
1059+
the epoch (Unix time 0, i.e., 00:00:00 UTC on 1 January 1970) (defaults to
1060+
current time).
1061+
1062+
The "created" timestamp is written into the image's configuration and manifest
1063+
when the image is committed, so running the same build two different times
1064+
will ordinarily produce images with different sha256 hashes, even if no other
1065+
changes were made to the Containerfile and build context.
1066+
1067+
When --timestamp is set, the "created" timestamp is always set to the time
1068+
specified, which should allow for identical images to be built at different
1069+
times using the same set of inputs.
1070+
1071+
When --timestamp is set, all content in layers created as part of the build,
1072+
and output written as specified to the **--output** flag, will also bear this
1073+
same timestamp.
1074+
1075+
Conflicts with the similar **--source-date-epoch** flag, which by default does
1076+
not affect the timestamps of layer contents.
10201077

10211078
**--tls-verify** *bool-value*
10221079

docs/buildah-commit.1.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,9 @@ When --source-date-epoch is set, the "created" timestamp is always set to the ti
318318
specified, which should allow for identical images to be committed at different
319319
times.
320320

321+
Conflicts with the similar **--timestamp** flag, which also sets its specified
322+
time on layer contents.
323+
321324
**--squash**
322325

323326
Squash all of the new image's layers (including those inherited from a base image) into a single new layer.
@@ -338,6 +341,9 @@ specified, which should allow for identical images to be committed at different
338341
times. All content in the new layer added as part of the image will also bear
339342
this timestamp.
340343

344+
Conflicts with the similar **--source-date-epoch** flag, which by default does
345+
not affect the timestamps of layer contents.
346+
341347
**--tls-verify** *bool-value*
342348

343349
Require HTTPS and verification of certificates when talking to container registries (defaults to true). TLS verification cannot be used when talking to an insecure registry.

image.go

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,10 @@ const (
5252
// control whether various information like the like setuid and setgid bits and
5353
// xattrs are preserved when extracting file system objects.
5454
type ExtractRootfsOptions struct {
55-
StripSetuidBit bool // strip the setuid bit off of items being extracted.
56-
StripSetgidBit bool // strip the setgid bit off of items being extracted.
57-
StripXattrs bool // don't record extended attributes of items being extracted.
55+
StripSetuidBit bool // strip the setuid bit off of items being extracted.
56+
StripSetgidBit bool // strip the setgid bit off of items being extracted.
57+
StripXattrs bool // don't record extended attributes of items being extracted.
58+
ForceTimestamp *time.Time // force timestamps in output content
5859
}
5960

6061
type containerImageRef struct {
@@ -226,13 +227,14 @@ func (i *containerImageRef) extractRootfs(opts ExtractRootfsOptions) (io.ReadClo
226227
pipeReader, pipeWriter := io.Pipe()
227228
errChan := make(chan error, 1)
228229
go func() {
230+
defer pipeWriter.Close()
229231
defer close(errChan)
230232
if len(i.extraImageContent) > 0 {
231233
// Abuse the tar format and _prepend_ the synthesized
232234
// data items to the archive we'll get from
233235
// copier.Get(), in a way that looks right to a reader
234236
// as long as we DON'T Close() the tar Writer.
235-
filename, _, _, err := i.makeExtraImageContentDiff(false)
237+
filename, _, _, err := i.makeExtraImageContentDiff(false, opts.ForceTimestamp)
236238
if err != nil {
237239
errChan <- fmt.Errorf("creating part of archive with extra content: %w", err)
238240
return
@@ -257,10 +259,10 @@ func (i *containerImageRef) extractRootfs(opts ExtractRootfsOptions) (io.ReadClo
257259
StripSetuidBit: opts.StripSetuidBit,
258260
StripSetgidBit: opts.StripSetgidBit,
259261
StripXattrs: opts.StripXattrs,
262+
Timestamp: opts.ForceTimestamp,
260263
}
261264
err := copier.Get(mountPoint, mountPoint, copierOptions, []string{"."}, pipeWriter)
262265
errChan <- err
263-
pipeWriter.Close()
264266
}()
265267
return ioutils.NewReadCloserWrapper(pipeReader, func() error {
266268
if err = pipeReader.Close(); err != nil {
@@ -849,7 +851,7 @@ func (i *containerImageRef) NewImageSource(_ context.Context, _ *types.SystemCon
849851
layerUncompressedSize = apiLayers[apiLayerIndex].size
850852
} else if layerID == synthesizedLayerID {
851853
// layer diff consisting of extra files to synthesize into a layer
852-
diffFilename, digest, size, err := i.makeExtraImageContentDiff(true)
854+
diffFilename, digest, size, err := i.makeExtraImageContentDiff(true, nil)
853855
if err != nil {
854856
return nil, fmt.Errorf("unable to generate layer for additional content: %w", err)
855857
}
@@ -1152,7 +1154,7 @@ func (i *containerImageSource) GetBlob(_ context.Context, blob types.BlobInfo, _
11521154
// makeExtraImageContentDiff creates an archive file containing the contents of
11531155
// files named in i.extraImageContent. The footer that marks the end of the
11541156
// archive may be omitted.
1155-
func (i *containerImageRef) makeExtraImageContentDiff(includeFooter bool) (_ string, _ digest.Digest, _ int64, retErr error) {
1157+
func (i *containerImageRef) makeExtraImageContentDiff(includeFooter bool, timestamp *time.Time) (_ string, _ digest.Digest, _ int64, retErr error) {
11561158
cdir, err := i.store.ContainerDirectory(i.containerID)
11571159
if err != nil {
11581160
return "", "", -1, err
@@ -1170,9 +1172,12 @@ func (i *containerImageRef) makeExtraImageContentDiff(includeFooter bool) (_ str
11701172
digester := digest.Canonical.Digester()
11711173
counter := ioutils.NewWriteCounter(digester.Hash())
11721174
tw := tar.NewWriter(io.MultiWriter(diff, counter))
1173-
created := time.Now()
1174-
if i.created != nil {
1175-
created = *i.created
1175+
if timestamp == nil {
1176+
now := time.Now()
1177+
timestamp = &now
1178+
if i.created != nil {
1179+
timestamp = i.created
1180+
}
11761181
}
11771182
for path, contents := range i.extraImageContent {
11781183
if err := func() error {
@@ -1189,7 +1194,7 @@ func (i *containerImageRef) makeExtraImageContentDiff(includeFooter bool) (_ str
11891194
Name: path,
11901195
Typeflag: tar.TypeReg,
11911196
Mode: 0o644,
1192-
ModTime: created,
1197+
ModTime: *timestamp,
11931198
Size: st.Size(),
11941199
}); err != nil {
11951200
return err

imagebuildah/build.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/containerd/platforms"
2222
"github.com/containers/buildah"
2323
"github.com/containers/buildah/define"
24+
"github.com/containers/buildah/internal"
2425
internalUtil "github.com/containers/buildah/internal/util"
2526
"github.com/containers/buildah/pkg/parse"
2627
"github.com/containers/buildah/util"
@@ -259,6 +260,16 @@ func BuildDockerfiles(ctx context.Context, store storage.Store, options define.B
259260
}
260261
// Deep copy args to prevent concurrent read/writes over Args.
261262
platformOptions.Args = maps.Clone(options.Args)
263+
264+
if options.SourceDateEpoch != nil {
265+
if options.Timestamp != nil {
266+
return "", nil, errors.New("timestamp and source-date-epoch would be ambiguous if allowed together")
267+
}
268+
if _, alreadySet := platformOptions.Args[internal.SourceDateEpochName]; !alreadySet {
269+
platformOptions.Args[internal.SourceDateEpochName] = fmt.Sprintf("%d", options.SourceDateEpoch.Unix())
270+
}
271+
}
272+
262273
builds.Go(func() error {
263274
loggerPerPlatform := logger
264275
if platformOptions.LogFile != "" && platformOptions.LogSplitByPlatform {

imagebuildah/executor.go

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
"github.com/containers/buildah"
1616
"github.com/containers/buildah/define"
17+
"github.com/containers/buildah/internal"
1718
internalUtil "github.com/containers/buildah/internal/util"
1819
"github.com/containers/buildah/pkg/parse"
1920
"github.com/containers/buildah/pkg/sshagent"
@@ -43,18 +44,19 @@ import (
4344
// instruction in the Dockerfile, since that's usually an indication of a user
4445
// error, but for these values we make exceptions and ignore them.
4546
var builtinAllowedBuildArgs = map[string]struct{}{
46-
"HTTP_PROXY": {},
47-
"http_proxy": {},
48-
"HTTPS_PROXY": {},
49-
"https_proxy": {},
50-
"FTP_PROXY": {},
51-
"ftp_proxy": {},
52-
"NO_PROXY": {},
53-
"no_proxy": {},
54-
"TARGETARCH": {},
55-
"TARGETOS": {},
56-
"TARGETPLATFORM": {},
57-
"TARGETVARIANT": {},
47+
"HTTP_PROXY": {},
48+
"http_proxy": {},
49+
"HTTPS_PROXY": {},
50+
"https_proxy": {},
51+
"FTP_PROXY": {},
52+
"ftp_proxy": {},
53+
"NO_PROXY": {},
54+
"no_proxy": {},
55+
"TARGETARCH": {},
56+
"TARGETOS": {},
57+
"TARGETPLATFORM": {},
58+
"TARGETVARIANT": {},
59+
internal.SourceDateEpochName: {},
5860
}
5961

6062
// Executor is a buildah-based implementation of the imagebuilder.Executor
@@ -164,6 +166,8 @@ type Executor struct {
164166
compatVolumes types.OptionalBool
165167
compatScratchConfig types.OptionalBool
166168
noPivotRoot bool
169+
sourceDateEpoch *time.Time
170+
rewriteTimestamp bool
167171
}
168172

169173
type imageTypeAndHistoryAndDiffIDs struct {
@@ -330,6 +334,8 @@ func newExecutor(logger *logrus.Logger, logPrefix string, store storage.Store, o
330334
compatVolumes: options.CompatVolumes,
331335
compatScratchConfig: options.CompatScratchConfig,
332336
noPivotRoot: options.NoPivotRoot,
337+
sourceDateEpoch: options.SourceDateEpoch,
338+
rewriteTimestamp: options.RewriteTimestamp,
333339
}
334340
if exec.err == nil {
335341
exec.err = os.Stderr

imagebuildah/stage_executor.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2457,6 +2457,8 @@ func (s *StageExecutor) commit(ctx context.Context, createdBy string, emptyLayer
24572457
HistoryTimestamp: s.executor.timestamp,
24582458
Manifest: s.executor.manifest,
24592459
CompatSetParent: s.executor.compatSetParent,
2460+
SourceDateEpoch: s.executor.sourceDateEpoch,
2461+
RewriteTimestamp: s.executor.rewriteTimestamp,
24602462
}
24612463
if finalInstruction {
24622464
options.ConfidentialWorkloadOptions = s.executor.confidentialWorkload
@@ -2478,7 +2480,13 @@ func (s *StageExecutor) commit(ctx context.Context, createdBy string, emptyLayer
24782480
}
24792481

24802482
func (s *StageExecutor) generateBuildOutput(buildOutputOpts define.BuildOutputOption) error {
2481-
extractRootfsOpts := buildah.ExtractRootfsOptions{}
2483+
forceTimestamp := s.executor.timestamp
2484+
if s.executor.sourceDateEpoch != nil {
2485+
forceTimestamp = s.executor.sourceDateEpoch
2486+
}
2487+
extractRootfsOpts := buildah.ExtractRootfsOptions{
2488+
ForceTimestamp: forceTimestamp,
2489+
}
24822490
if unshare.IsRootless() {
24832491
// In order to maintain as much parity as possible
24842492
// with buildkit's version of --output and to avoid
@@ -2492,7 +2500,11 @@ func (s *StageExecutor) generateBuildOutput(buildOutputOpts define.BuildOutputOp
24922500
extractRootfsOpts.StripSetgidBit = true
24932501
extractRootfsOpts.StripXattrs = true
24942502
}
2495-
rc, errChan, err := s.builder.ExtractRootfs(buildah.CommitOptions{}, extractRootfsOpts)
2503+
rc, errChan, err := s.builder.ExtractRootfs(buildah.CommitOptions{
2504+
HistoryTimestamp: s.executor.timestamp,
2505+
SourceDateEpoch: s.executor.sourceDateEpoch,
2506+
RewriteTimestamp: s.executor.rewriteTimestamp,
2507+
}, extractRootfsOpts)
24962508
if err != nil {
24972509
return fmt.Errorf("failed to extract rootfs from given container image: %w", err)
24982510
}

internal/types.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ const (
66
// external items which are downloaded for a build, typically a tarball
77
// being used as an additional build context.
88
BuildahExternalArtifactsDir = "buildah-external-artifacts"
9-
// SourceDateEpochName is the name of the SOURCE_DATE_EPOCH environment
10-
// variable when it's read from the environment by our main().
9+
// SourceDateEpochName is both the name of the SOURCE_DATE_EPOCH
10+
// environment variable and the built-in ARG that carries its value,
11+
// whether it's read from the environment by our main(), or passed in
12+
// via CLI or API flags.
1113
SourceDateEpochName = "SOURCE_DATE_EPOCH"
1214
)
1315

0 commit comments

Comments
 (0)