Skip to content

Commit 6c82e7e

Browse files
committed
commit: add --source-date-epoch and --rewrite-timestamp flags
Add a --source-date-epoch flag, defaulting to $SOURCE_DATE_EPOCH if set, which sets the created-on date and the timestamp for the new history entries, but does not default to modifying the timestamps on contents in new layers. Add a --rewrite-timestamp flag, which "clamps" timestamps in the new layers to not be later than the --source-date-epoch value if both the --rewrite-timestamp and --source-date-epoch flags were set. Signed-off-by: Nalin Dahyabhai <[email protected]>
1 parent 2d32c9a commit 6c82e7e

File tree

9 files changed

+332
-82
lines changed

9 files changed

+332
-82
lines changed

cmd/buildah/commit.go

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import (
55
"errors"
66
"fmt"
77
"os"
8+
"strconv"
89
"strings"
910
"time"
1011

1112
"github.com/containers/buildah"
1213
"github.com/containers/buildah/define"
14+
"github.com/containers/buildah/internal"
1315
"github.com/containers/buildah/pkg/cli"
1416
"github.com/containers/buildah/pkg/parse"
1517
"github.com/containers/buildah/util"
@@ -39,6 +41,8 @@ type commitInputOptions struct {
3941
manifest string
4042
omitTimestamp bool
4143
timestamp int64
44+
sourceDateEpoch string
45+
rewriteTimestamp bool
4246
quiet bool
4347
referenceTime string
4448
rm bool
@@ -117,7 +121,14 @@ func commitListFlagSet(cmd *cobra.Command, opts *commitInputOptions) {
117121
flags.StringVar(&opts.iidfile, "iidfile", "", "write the image ID to the file")
118122
_ = cmd.RegisterFlagCompletionFunc("iidfile", completion.AutocompleteDefault)
119123
flags.BoolVar(&opts.omitTimestamp, "omit-timestamp", false, "set created timestamp to epoch 0 to allow for deterministic builds")
120-
flags.Int64Var(&opts.timestamp, "timestamp", 0, "set created timestamp to epoch seconds to allow for deterministic builds, defaults to current time")
124+
sourceDateEpochUsageDefault := "current time"
125+
if v := os.Getenv(internal.SourceDateEpochName); v != "" {
126+
sourceDateEpochUsageDefault = fmt.Sprintf("%q", v)
127+
}
128+
flags.StringVar(&opts.sourceDateEpoch, "source-date-epoch", os.Getenv(internal.SourceDateEpochName), "set new timestamps in image info to `seconds` after the epoch, defaults to "+sourceDateEpochUsageDefault)
129+
_ = cmd.RegisterFlagCompletionFunc("source-date-epoch", completion.AutocompleteNone)
130+
flags.BoolVar(&opts.rewriteTimestamp, "rewrite-timestamp", false, "set timestamps in layer to no later than the value for --source-date-epoch")
131+
flags.Int64Var(&opts.timestamp, "timestamp", 0, "set new timestamps in image info and layer to `seconds` after the epoch, defaults to current times")
121132
_ = cmd.RegisterFlagCompletionFunc("timestamp", completion.AutocompleteNone)
122133
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "don't output progress information when writing images")
123134
flags.StringVar(&opts.referenceTime, "reference-time", "", "set the timestamp on the image to match the named `file`")
@@ -317,6 +328,16 @@ func commitCmd(c *cobra.Command, args []string, iopts commitInputOptions) error
317328
timestamp := finfo.ModTime().UTC()
318329
options.HistoryTimestamp = &timestamp
319330
}
331+
if iopts.sourceDateEpoch != "" {
332+
exclusiveFlags++
333+
sourceDateEpochVal, err := strconv.ParseInt(iopts.sourceDateEpoch, 10, 64)
334+
if err != nil {
335+
return fmt.Errorf("parsing source date epoch %q: %w", iopts.sourceDateEpoch, err)
336+
}
337+
sourceDateEpoch := time.Unix(sourceDateEpochVal, 0).UTC()
338+
options.SourceDateEpoch = &sourceDateEpoch
339+
}
340+
options.RewriteTimestamp = iopts.rewriteTimestamp
320341
if c.Flag("timestamp").Changed {
321342
exclusiveFlags++
322343
timestamp := time.Unix(iopts.timestamp, 0).UTC()
@@ -327,6 +348,9 @@ func commitCmd(c *cobra.Command, args []string, iopts commitInputOptions) error
327348
timestamp := time.Unix(0, 0).UTC()
328349
options.HistoryTimestamp = &timestamp
329350
}
351+
if exclusiveFlags > 1 {
352+
return errors.New("cannot use more then one timestamp option at at time")
353+
}
330354

331355
if iopts.cwOptions != "" {
332356
confidentialWorkloadOptions, err := parse.GetConfidentialWorkloadOptions(iopts.cwOptions)
@@ -352,10 +376,6 @@ func commitCmd(c *cobra.Command, args []string, iopts commitInputOptions) error
352376
options.SBOMScanOptions = sbomOptions
353377
}
354378

355-
if exclusiveFlags > 1 {
356-
return errors.New("can not use more then one timestamp option at at time")
357-
}
358-
359379
if !iopts.quiet {
360380
options.ReportWriter = os.Stderr
361381
}

commit.go

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,20 @@ type CommitOptions struct {
5858
// ReportWriter is an io.Writer which will be used to log the writing
5959
// of the new image.
6060
ReportWriter io.Writer
61-
// HistoryTimestamp is the timestamp used when creating new items in the
62-
// image's history. If unset, the current time will be used.
61+
// HistoryTimestamp specifies a timestamp to use for the image's
62+
// created-on date, the corresponding field in new history entries, and
63+
// the timestamps to set on contents in new layer diffs. If left
64+
// unset, the current time is used for the configuration and manifest,
65+
// and timestamps of layer contents are used as-is.
6366
HistoryTimestamp *time.Time
67+
// SourceDateEpoch specifies a timestamp to use for the image's
68+
// created-on date and the corresponding field in new history entries.
69+
// If left unset, the current time is used for the configuration and
70+
// manifest.
71+
SourceDateEpoch *time.Time
72+
// RewriteTimestamp, if set, forces timestamps in generated layers to
73+
// not be later than the SourceDateEpoch, if it is set.
74+
RewriteTimestamp bool
6475
// github.com/containers/image/types SystemContext to hold credentials
6576
// and other authentication/authorization information.
6677
SystemContext *types.SystemContext
@@ -274,8 +285,9 @@ func (b *Builder) addManifest(ctx context.Context, manifestName string, imageSpe
274285
// if commit was successful and the image destination was local.
275286
func (b *Builder) Commit(ctx context.Context, dest types.ImageReference, options CommitOptions) (string, reference.Canonical, digest.Digest, error) {
276287
var (
277-
imgID string
278-
src types.ImageReference
288+
imgID string
289+
src types.ImageReference
290+
destinationTimestamp *time.Time
279291
)
280292

281293
// If we weren't given a name, build a destination reference using a
@@ -293,6 +305,10 @@ func (b *Builder) Commit(ctx context.Context, dest types.ImageReference, options
293305
timestamp := time.Unix(0, 0).UTC()
294306
options.HistoryTimestamp = &timestamp
295307
}
308+
destinationTimestamp = options.HistoryTimestamp
309+
if options.SourceDateEpoch != nil {
310+
destinationTimestamp = options.SourceDateEpoch
311+
}
296312
nameToRemove := ""
297313
if dest == nil {
298314
nameToRemove = stringid.GenerateRandomID() + "-tmp"
@@ -415,7 +431,7 @@ func (b *Builder) Commit(ctx context.Context, dest types.ImageReference, options
415431
}
416432

417433
var manifestBytes []byte
418-
if manifestBytes, err = retryCopyImage(ctx, policyContext, maybeCachedDest, maybeCachedSrc, dest, getCopyOptions(b.store, options.ReportWriter, nil, systemContext, "", false, options.SignBy, options.OciEncryptLayers, options.OciEncryptConfig, nil, options.HistoryTimestamp), options.MaxRetries, options.RetryDelay); err != nil {
434+
if manifestBytes, err = retryCopyImage(ctx, policyContext, maybeCachedDest, maybeCachedSrc, dest, getCopyOptions(b.store, options.ReportWriter, nil, systemContext, "", false, options.SignBy, options.OciEncryptLayers, options.OciEncryptConfig, nil, destinationTimestamp), options.MaxRetries, options.RetryDelay); err != nil {
419435
return imgID, nil, "", fmt.Errorf("copying layers and metadata for container %q: %w", b.ContainerID, err)
420436
}
421437
// If we've got more names to attach, and we know how to do that for

commit_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,9 @@ func TestCommitLinkedLayers(t *testing.T) {
174174
}
175175
b.AddAppendedLinkedLayer(nil, imageName(layerNumber+6), "", "", ninthArchiveFile)
176176
ref, err = imageStorage.Transport.ParseStoreReference(store, imageName(layerNumber))
177-
require.NoError(t, err, "parsing reference for to-be-committed image", imageName(layerNumber))
177+
require.NoErrorf(t, err, "parsing reference for to-be-committed image %q", imageName(layerNumber))
178178
_, _, _, err = b.Commit(ctx, ref, commitOptions)
179-
require.NoError(t, err, "committing", imageName(layerNumber))
179+
require.NoErrorf(t, err, "committing %q", imageName(layerNumber))
180180

181181
// Build one last image based on the previous one.
182182
builderOptions.FromImage = imageName(layerNumber)

digester.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ func newTarFilterer(writeCloser io.WriteCloser, filter func(hdr *tar.Header) (sk
133133
}
134134
hdr, err = tarReader.Next()
135135
}
136-
if err != io.EOF {
136+
if !errors.Is(err, io.EOF) {
137137
filterer.err = fmt.Errorf("reading tar archive: %w", err)
138138
break
139139
}
@@ -146,7 +146,11 @@ func newTarFilterer(writeCloser io.WriteCloser, filter func(hdr *tar.Header) (sk
146146
if err == nil {
147147
err = err1
148148
}
149-
pipeReader.CloseWithError(err)
149+
if err != nil {
150+
pipeReader.CloseWithError(err)
151+
} else {
152+
pipeReader.Close()
153+
}
150154
filterer.wg.Done()
151155
}()
152156
return filterer

docs/buildah-commit.1.md

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ The image ID of the image that was created. On error, 1 is returned and errno i
2626
Read the contents of the file `source` and add it to the committed image as a
2727
file at `destination`. If `destination` is not specified, the path of `source`
2828
will be used. The new file will be owned by UID 0, GID 0, have 0644
29-
permissions, and be given a current timestamp unless the **--timestamp** option
30-
is also specified. This option can be specified multiple times.
29+
permissions, and be given the timestamp specified to the **--timestamp** option
30+
if it is specified. This option can be specified multiple times.
3131

3232
**--authfile** *path*
3333

@@ -196,6 +196,13 @@ the image is not present locally.
196196

197197
When writing the output image, suppress progress output.
198198

199+
**--rewrite-timestamp**
200+
201+
When generating the new layer for the image, ensure that no newly added content
202+
bears a timestamp later than the value used by the **--source-date-epoch**
203+
flag, if one was provided, by replacing any timestamps which are later than
204+
that value, with that value.
205+
199206
**--rm**
200207
Remove the working container and its contents after creating the image.
201208
Default leaves the container and its content in place.
@@ -295,16 +302,41 @@ Generate SBOMs using the specified scanner image.
295302

296303
Sign the new image using the GPG key that matches the specified fingerprint.
297304

305+
**--source-date-epoch** *seconds*
306+
307+
Set the "created" timestamp for the image to this number of seconds since the
308+
epoch (Unix time 0, i.e., 00:00:00 UTC on 1 January 1970) to make it easier to
309+
create deterministic builds (defaults to $SOURCE_DATE_EPOCH if set, otherwise
310+
the current time will be used).
311+
312+
The "created" timestamp is written into the image's configuration and manifest
313+
when the image is committed, so committing the same working container at two
314+
different times will produce images with different sha256 hashes, even if no
315+
other changes were made to the working container in between.
316+
317+
When --source-date-epoch is set, the "created" timestamp is always set to the time
318+
specified, which should allow for identical images to be committed at different
319+
times.
320+
298321
**--squash**
299322

300323
Squash all of the new image's layers (including those inherited from a base image) into a single new layer.
301324

302325
**--timestamp** *seconds*
303326

304-
Set the create timestamp to seconds since epoch to allow for deterministic builds (defaults to current time).
305-
By default, the created timestamp is changed and written into the image manifest with every commit,
306-
causing the image's sha256 hash to be different even if the sources are exactly the same otherwise.
307-
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.
327+
Set the "created" timestamp for the image to this number of seconds since the
328+
epoch (Unix time 0, i.e., 00:00:00 UTC on 1 January 1970) to make it easier to
329+
create deterministic builds (defaults to current time).
330+
331+
The "created" timestamp is written into the image's configuration and manifest
332+
when the image is committed, so committing the same working container at two
333+
different times will produce images with different sha256 hashes, even if no
334+
other changes were made to the working container in between.
335+
336+
When --timestamp is set, the "created" timestamp is always set to the time
337+
specified, which should allow for identical images to be committed at different
338+
times. All content in the new layer added as part of the image will also bear
339+
this timestamp.
308340

309341
**--tls-verify** *bool-value*
310342

0 commit comments

Comments
 (0)