Skip to content

Commit 1e6bb46

Browse files
committed
Add --metadata-file
Add a MetadataFile field to BuildOptions, to which we write a dictionary of information about a just-committed image. Pay more attention to sourceDateEpoch than to timestamp when we're tagging an existing image with the intended destination name. Signed-off-by: Nalin Dahyabhai <[email protected]>
1 parent 45da9cf commit 1e6bb46

File tree

14 files changed

+261
-90
lines changed

14 files changed

+261
-90
lines changed

cmd/buildah/commit.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ type commitInputOptions struct {
6969
unsetAnnotation []string
7070
annotation []string
7171
createdAnnotation bool
72+
metadataFile string
7273
}
7374

7475
func init() {
@@ -123,6 +124,9 @@ func commitListFlagSet(cmd *cobra.Command, opts *commitInputOptions) {
123124
_ = cmd.RegisterFlagCompletionFunc("manifest", completion.AutocompleteNone)
124125
flags.StringVar(&opts.iidfile, "iidfile", "", "write the image ID to the file")
125126
_ = cmd.RegisterFlagCompletionFunc("iidfile", completion.AutocompleteDefault)
127+
flags.StringVar(&opts.metadataFile, "metadata-file", "", "`file` to write metadata about the image to")
128+
_ = cmd.RegisterFlagCompletionFunc("metadata-file", completion.AutocompleteDefault)
129+
126130
flags.BoolVar(&opts.omitTimestamp, "omit-timestamp", false, "set created timestamp to epoch 0 to allow for deterministic builds")
127131
sourceDateEpochUsageDefault := "current time"
128132
if v := os.Getenv(internal.SourceDateEpochName); v != "" {
@@ -401,10 +405,12 @@ func commitCmd(c *cobra.Command, args []string, iopts commitInputOptions) error
401405
if !iopts.quiet {
402406
options.ReportWriter = os.Stderr
403407
}
404-
id, ref, _, err := builder.Commit(ctx, dest, options)
408+
results, err := builder.CommitResults(ctx, dest, options)
405409
if err != nil {
406410
return util.GetFailureCause(err, fmt.Errorf("committing container %q to %q: %w", builder.Container, image, err))
407411
}
412+
ref := results.Canonical
413+
id := results.ImageID
408414
if ref != nil && id != "" {
409415
logrus.Debugf("wrote image %s with ID %s", ref, id)
410416
} else if ref != nil {
@@ -417,6 +423,15 @@ func commitCmd(c *cobra.Command, args []string, iopts commitInputOptions) error
417423
if options.IIDFile == "" && id != "" {
418424
fmt.Printf("%s\n", id)
419425
}
426+
if iopts.metadataFile != "" {
427+
metadataBytes, err := json.Marshal(results.Metadata)
428+
if err != nil {
429+
return fmt.Errorf("encoding contents for %q: %w", iopts.metadataFile, err)
430+
}
431+
if err := os.WriteFile(iopts.metadataFile, metadataBytes, 0o644); err != nil {
432+
return err
433+
}
434+
}
420435

421436
if iopts.rm {
422437
return builder.Delete()

commit.go

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"strings"
1111
"time"
1212

13+
"github.com/containers/buildah/internal/metadata"
1314
"github.com/containers/buildah/pkg/blobcache"
1415
"github.com/containers/buildah/util"
1516
encconfig "github.com/containers/ocicrypt/config"
@@ -76,7 +77,8 @@ type CommitOptions struct {
7677
// github.com/containers/image/types SystemContext to hold credentials
7778
// and other authentication/authorization information.
7879
SystemContext *types.SystemContext
79-
// IIDFile tells the builder to write the image ID to the specified file
80+
// IIDFile tells the builder to write the image's ID, preceded by
81+
// "sha256:", to the specified file.
8082
IIDFile string
8183
// Squash tells the builder to produce an image with a single layer
8284
// instead of with possibly more than one layer.
@@ -308,9 +310,10 @@ func (b *Builder) addManifest(ctx context.Context, manifestName string, imageSpe
308310
type CommitResults struct {
309311
ImageID string // a local image ID, or part of the digest of the image's config blob
310312
Canonical reference.Canonical // set if destination included a DockerReference
311-
MediaType string // always returned
312-
ImageManifest []byte // always returned
313-
Digest digest.Digest // always returned
313+
MediaType string // image manifest MIME type, always returned
314+
ImageManifest []byte // raw image manifest, always returned
315+
Digest digest.Digest // digest of the manifest, always returned
316+
Metadata map[string]any // always returned, format is flexible
314317
}
315318

316319
// Commit writes the contents of the container, along with its updated
@@ -555,13 +558,13 @@ func (b *Builder) CommitResults(ctx context.Context, dest types.ImageReference,
555558
if err != nil {
556559
return nil, fmt.Errorf("computing digest of manifest of new image %q: %w", transports.ImageName(dest), err)
557560
}
558-
if imgID == "" {
559-
parsedManifest, err := manifest.FromBlob(manifestBytes, manifest.GuessMIMEType(manifestBytes))
560-
if err != nil {
561-
return nil, fmt.Errorf("parsing written manifest to determine the image's ID: %w", err)
562-
}
563-
configInfo := parsedManifest.ConfigInfo()
564-
if configInfo.Size > 2 && configInfo.Digest.Validate() == nil { // don't be returning a digest of "" or "{}"
561+
parsedManifest, err := manifest.FromBlob(manifestBytes, manifest.GuessMIMEType(manifestBytes))
562+
if err != nil {
563+
return nil, fmt.Errorf("parsing written manifest to determine the image's ID: %w", err)
564+
}
565+
configInfo := parsedManifest.ConfigInfo()
566+
if configInfo.Size > 2 && configInfo.Digest.Validate() == nil { // don't be returning a digest of "" or "{}"
567+
if imgID == "" {
565568
imgID = configInfo.Digest.Encoded()
566569
}
567570
}
@@ -582,12 +585,23 @@ func (b *Builder) CommitResults(ctx context.Context, dest types.ImageReference,
582585
logrus.Debugf("added imgID %s to manifestID %s", imgID, manifestID)
583586
}
584587

588+
descriptor := v1.Descriptor{
589+
MediaType: manifest.GuessMIMEType(manifestBytes),
590+
Digest: manifestDigest,
591+
Size: int64(len(manifestBytes)),
592+
}
593+
metadata, err := metadata.Build(configInfo.Digest, descriptor)
594+
if err != nil {
595+
return nil, fmt.Errorf("building metadata map for image: %w", err)
596+
}
597+
585598
results := CommitResults{
586599
ImageID: imgID,
587600
Canonical: ref,
588-
MediaType: manifest.GuessMIMEType(manifestBytes),
601+
MediaType: descriptor.MediaType,
589602
ImageManifest: manifestBytes,
590603
Digest: manifestDigest,
604+
Metadata: metadata,
591605
}
592606
return &results, nil
593607
}

define/build.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,4 +418,7 @@ type BuildOptions struct {
418418
// CreatedAnnotation controls whether or not an "org.opencontainers.image.created"
419419
// annotation is present in the output image.
420420
CreatedAnnotation types.OptionalBool
421+
// MetadataFile is the name of a file to which the builder should write a JSON map
422+
// containing metadata about the built image.
423+
MetadataFile string
421424
}

docker/types.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,3 +257,10 @@ type V2S2Manifest struct {
257257
// configuration.
258258
Layers []V2S2Descriptor `json:"layers"`
259259
}
260+
261+
const (
262+
// github.com/moby/buildkit/exporter/containerimage/exptypes/types.go
263+
ExporterImageDigestKey = "containerimage.digest"
264+
ExporterImageConfigDigestKey = "containerimage.config.digest"
265+
ExporterImageDescriptorKey = "containerimage.descriptor"
266+
)

docs/buildah-build.1.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,12 @@ The format of `LIMIT` is `<number>[<unit>]`. Unit can be `b` (bytes),
625625
`k` (kilobytes), `m` (megabytes), or `g` (gigabytes). If you don't specify a
626626
unit, `b` is used. Set LIMIT to `-1` to enable unlimited swap.
627627

628+
**--metadata-file** *MetadataFile*
629+
630+
Write information about the built image to the named file. When `--platform`
631+
is specified more than once, attempting to use this option will trigger an
632+
error.
633+
628634
**--network**, **--net**=*mode*
629635

630636
Sets the configuration for network namespaces when handling `RUN` instructions.

docs/buildah-commit.1.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,10 @@ Write the image ID to the file.
176176
Name of the manifest list to which the built image will be added. Creates the manifest list
177177
if it does not exist. This option is useful for building multi architecture images.
178178

179+
**--metadata-file** *MetadataFile*
180+
181+
Write information about the committed image to the named file.
182+
179183
**--omit-history** *bool-value*
180184

181185
Omit build history information in the built image. (default false).

imagebuildah/build.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ func BuildDockerfiles(ctx context.Context, store storage.Store, options define.B
8686
if len(options.Platforms) > 1 && options.IIDFile != "" {
8787
return "", nil, fmt.Errorf("building multiple images, but iidfile %q can only be used to store one image ID", options.IIDFile)
8888
}
89+
if len(options.Platforms) > 1 && options.MetadataFile != "" {
90+
return "", nil, fmt.Errorf("building multiple images, but metadata file %q can only be used to store information about one image", options.MetadataFile)
91+
}
8992

9093
logger := logrus.New()
9194
if options.Err != nil {

imagebuildah/executor.go

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package imagebuildah
22

33
import (
44
"context"
5+
"encoding/json"
56
"errors"
67
"fmt"
78
"io"
@@ -15,6 +16,7 @@ import (
1516
"github.com/containers/buildah"
1617
"github.com/containers/buildah/define"
1718
"github.com/containers/buildah/internal"
19+
"github.com/containers/buildah/internal/metadata"
1820
internalUtil "github.com/containers/buildah/internal/util"
1921
"github.com/containers/buildah/pkg/parse"
2022
"github.com/containers/buildah/pkg/sshagent"
@@ -172,6 +174,7 @@ type executor struct {
172174
sourceDateEpoch *time.Time
173175
rewriteTimestamp bool
174176
createdAnnotation types.OptionalBool
177+
metadataFile string
175178
}
176179

177180
type imageTypeAndHistoryAndDiffIDs struct {
@@ -346,6 +349,7 @@ func newExecutor(logger *logrus.Logger, logPrefix string, store storage.Store, o
346349
sourceDateEpoch: options.SourceDateEpoch,
347350
rewriteTimestamp: options.RewriteTimestamp,
348351
createdAnnotation: options.CreatedAnnotation,
352+
metadataFile: options.MetadataFile,
349353
}
350354
// sort unsetAnnotations because we will later write these
351355
// values to the history of the image therefore we want to
@@ -519,7 +523,7 @@ func (b *executor) getImageTypeAndHistoryAndDiffIDs(ctx context.Context, imageID
519523
return oci.OS, oci.Architecture, manifestFormat, oci.History, oci.RootFS.DiffIDs, nil
520524
}
521525

522-
func (b *executor) buildStage(ctx context.Context, cleanupStages map[int]*stageExecutor, stages imagebuilder.Stages, stageIndex int) (imageID string, ref reference.Canonical, onlyBaseImage bool, err error) {
526+
func (b *executor) buildStage(ctx context.Context, cleanupStages map[int]*stageExecutor, stages imagebuilder.Stages, stageIndex int) (imageID string, commitResults *buildah.CommitResults, onlyBaseImage bool, err error) {
523527
stage := stages[stageIndex]
524528
ib := stage.Builder
525529
node := stage.Node
@@ -616,7 +620,7 @@ func (b *executor) buildStage(ctx context.Context, cleanupStages map[int]*stageE
616620
}
617621

618622
// Build this stage.
619-
if imageID, ref, onlyBaseImage, err = stageExecutor.execute(ctx, base); err != nil {
623+
if imageID, commitResults, onlyBaseImage, err = stageExecutor.execute(ctx, base); err != nil {
620624
return "", nil, onlyBaseImage, err
621625
}
622626

@@ -630,7 +634,7 @@ func (b *executor) buildStage(ctx context.Context, cleanupStages map[int]*stageE
630634
b.stagesLock.Unlock()
631635
}
632636

633-
return imageID, ref, onlyBaseImage, nil
637+
return imageID, commitResults, onlyBaseImage, nil
634638
}
635639

636640
type stageDependencyInfo struct {
@@ -922,7 +926,7 @@ func (b *executor) Build(ctx context.Context, stages imagebuilder.Stages) (image
922926
Index int
923927
ImageID string
924928
OnlyBaseImage bool
925-
Ref reference.Canonical
929+
CommitResults buildah.CommitResults
926930
Error error
927931
}
928932

@@ -935,6 +939,7 @@ func (b *executor) Build(ctx context.Context, stages imagebuilder.Stages) (image
935939
var wg sync.WaitGroup
936940
wg.Add(len(stages))
937941

942+
var commitResults buildah.CommitResults
938943
go func() {
939944
cancel := false
940945
for stageIndex := range stages {
@@ -983,7 +988,7 @@ func (b *executor) Build(ctx context.Context, stages imagebuilder.Stages) (image
983988
return
984989
}
985990
}
986-
stageID, stageRef, stageOnlyBaseImage, stageErr := b.buildStage(ctx, cleanupStages, stages, index)
991+
stageID, stageResults, stageOnlyBaseImage, stageErr := b.buildStage(ctx, cleanupStages, stages, index)
987992
if stageErr != nil {
988993
cancel = true
989994
ch <- Result{
@@ -997,7 +1002,7 @@ func (b *executor) Build(ctx context.Context, stages imagebuilder.Stages) (image
9971002
ch <- Result{
9981003
Index: index,
9991004
ImageID: stageID,
1000-
Ref: stageRef,
1005+
CommitResults: *stageResults,
10011006
OnlyBaseImage: stageOnlyBaseImage,
10021007
Error: nil,
10031008
}
@@ -1037,7 +1042,8 @@ func (b *executor) Build(ctx context.Context, stages imagebuilder.Stages) (image
10371042
}
10381043
if r.Index == len(stages)-1 {
10391044
imageID = r.ImageID
1040-
ref = r.Ref
1045+
commitResults = r.CommitResults
1046+
ref = commitResults.Canonical
10411047
}
10421048
b.stagesLock.Unlock()
10431049
}
@@ -1088,7 +1094,11 @@ func (b *executor) Build(ctx context.Context, stages imagebuilder.Stages) (image
10881094
if b.iidfile != "" {
10891095
iid := imageID
10901096
if iid != "" {
1091-
iid = "sha256:" + iid // only prepend a digest algorithm name if we actually got a value back
1097+
cdigest, err := digest.Parse("sha256:" + imageID)
1098+
if err != nil {
1099+
return imageID, ref, fmt.Errorf("coercing image ID into a digest structure: %w", err)
1100+
}
1101+
iid = cdigest.String()
10921102
}
10931103
if err = os.WriteFile(b.iidfile, []byte(iid), 0o644); err != nil {
10941104
return imageID, ref, fmt.Errorf("failed to write image ID to file %q: %w", b.iidfile, err)
@@ -1098,6 +1108,29 @@ func (b *executor) Build(ctx context.Context, stages imagebuilder.Stages) (image
10981108
return imageID, ref, fmt.Errorf("failed to write image ID to stdout: %w", err)
10991109
}
11001110
}
1111+
if b.metadataFile != "" {
1112+
var cdigest digest.Digest
1113+
if imageID != "" {
1114+
if cdigest, err = digest.Parse("sha256:" + imageID); err != nil {
1115+
return imageID, ref, fmt.Errorf("coercing image ID into a digest structure: %w", err)
1116+
}
1117+
}
1118+
metadata, err := metadata.Build(cdigest, v1.Descriptor{
1119+
MediaType: commitResults.MediaType,
1120+
Digest: commitResults.Digest,
1121+
Size: int64(len(commitResults.ImageManifest)),
1122+
})
1123+
if err != nil {
1124+
return imageID, ref, fmt.Errorf("building metadata for metadata file: %w", err)
1125+
}
1126+
metadataBytes, err := json.Marshal(metadata)
1127+
if err != nil {
1128+
return imageID, ref, fmt.Errorf("encoding metadata for metadata file: %w", err)
1129+
}
1130+
if err = os.WriteFile(b.metadataFile, metadataBytes, 0o644); err != nil {
1131+
return imageID, ref, fmt.Errorf("failed to write image metadata to file %q: %w", b.metadataFile, err)
1132+
}
1133+
}
11011134
return imageID, ref, nil
11021135
}
11031136

0 commit comments

Comments
 (0)