Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/apko_file.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ annotations:
foo: bar
bar: baz

# optional layer descriptor annotations
layer-annotations:
dev.chainguard.layer.source: apko

# optional layering strategy
layering:
strategy: origin
Expand Down Expand Up @@ -247,6 +251,21 @@ Patches to improve the parsing to make it more flexible are welcome.

`annotations` defines the set of annotations that should be applied to images and indexes.

### Layer Annotations

`layer-annotations` defines the set of annotations that should be applied to every layer descriptor
in the image manifest. This is useful for attaching metadata to layers for provenance tracking or
tooling integration.

Example:

```yaml
layer-annotations:
dev.chainguard.layer.source: apko
```

Layer annotations can also be set via the `--layer-annotations` CLI flag using `key:value` format.

### Layering

`layering` defines a strategy for splitting the filesystem contents into layers.
Expand Down
7 changes: 7 additions & 0 deletions internal/cli/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func buildCmd() *cobra.Command {
var extraRepos []string
var extraPackages []string
var rawAnnotations []string
var rawLayerAnnotations []string
var cacheDir string
var offline bool
var lockfile string
Expand Down Expand Up @@ -86,6 +87,10 @@ Along the image, apko will generate SBOMs (software bill of materials) describin
if err != nil {
return fmt.Errorf("parsing annotations from command line: %w", err)
}
layerAnnotations, err := parseAnnotations(rawLayerAnnotations)
if err != nil {
return fmt.Errorf("parsing layer annotations from command line: %w", err)
}

var sbomGenerators []generator.Generator
if writeSBOM && len(sbomFormats) > 0 {
Expand Down Expand Up @@ -113,6 +118,7 @@ Along the image, apko will generate SBOMs (software bill of materials) describin
build.WithTags(args[1]),
build.WithVCS(withVCS),
build.WithAnnotations(annotations),
build.WithLayerAnnotations(layerAnnotations),
build.WithCache(cacheDir, offline, apk.NewCache(true)),
build.WithLockFile(lockfile),
build.WithTempDir(tmp),
Expand All @@ -134,6 +140,7 @@ Along the image, apko will generate SBOMs (software bill of materials) describin
cmd.Flags().StringSliceVarP(&extraRepos, "repository-append", "r", []string{}, "path to extra repositories to include")
cmd.Flags().StringSliceVarP(&extraPackages, "package-append", "p", []string{}, "extra packages to include")
cmd.Flags().StringSliceVar(&rawAnnotations, "annotations", []string{}, "OCI annotations to add. Separate with colon (key:value)")
cmd.Flags().StringSliceVar(&rawLayerAnnotations, "layer-annotations", []string{}, "OCI annotations to add to every layer descriptor. Separate with colon (key:value)")
cmd.Flags().StringVar(&cacheDir, "cache-dir", "", "directory to use for caching apk packages and indexes (default '' means to use system-defined cache directory)")
cmd.Flags().BoolVar(&offline, "offline", false, "do not use network to fetch packages (cache must be pre-populated)")
cmd.Flags().StringVar(&lockfile, "lockfile", "", "a path to .lock.json file (e.g. produced by apko lock) that constraints versions of packages to the listed ones (default '' means no additional constraints)")
Expand Down
7 changes: 7 additions & 0 deletions internal/cli/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func publish() *cobra.Command {
var extraRepos []string
var extraPackages []string
var rawAnnotations []string
var rawLayerAnnotations []string
var withVCS bool
var writeSBOM bool
var local bool
Expand Down Expand Up @@ -79,6 +80,10 @@ in a keychain.`,
if err != nil {
return fmt.Errorf("parsing annotations from command line: %w", err)
}
layerAnnotations, err := parseAnnotations(rawLayerAnnotations)
if err != nil {
return fmt.Errorf("parsing layer annotations from command line: %w", err)
}

keychain := authn.NewMultiKeychain(
authn.DefaultKeychain,
Expand Down Expand Up @@ -118,6 +123,7 @@ in a keychain.`,
build.WithTags(args[1:]...),
build.WithVCS(withVCS),
build.WithAnnotations(annotations),
build.WithLayerAnnotations(layerAnnotations),
build.WithCache(cacheDir, offline, apk.NewCache(true)),
build.WithLockFile(lockfile),
build.WithTempDir(tmp),
Expand Down Expand Up @@ -146,6 +152,7 @@ in a keychain.`,
cmd.Flags().StringSliceVarP(&extraRepos, "repository-append", "r", []string{}, "path to extra repositories to include")
cmd.Flags().StringSliceVarP(&extraPackages, "package-append", "p", []string{}, "extra packages to include")
cmd.Flags().StringSliceVar(&rawAnnotations, "annotations", []string{}, "OCI annotations to add. Separate with colon (key:value)")
cmd.Flags().StringSliceVar(&rawLayerAnnotations, "layer-annotations", []string{}, "OCI annotations to add to every layer descriptor. Separate with colon (key:value)")
cmd.Flags().StringVar(&cacheDir, "cache-dir", "", "directory to use for caching apk packages and indexes (default '' means to use system-defined cache directory)")
cmd.Flags().BoolVar(&offline, "offline", false, "do not use network to fetch packages (cache must be pre-populated)")
cmd.Flags().StringVar(&lockfile, "lockfile", "", "a path to .lock.json file (e.g. produced by apko lock) that constraints versions of packages to the listed ones (default '' means no additional constraints)")
Expand Down
7 changes: 7 additions & 0 deletions pkg/build/oci/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ func BuildImageFromLayers(ctx context.Context, baseImage v1.Image, layers []v1.L
log.Infof("layer digest: %v", digest)
log.Infof("layer diffID: %v", diffid)

// Apply uniform layer annotations if configured.
var layerAnns map[string]string
if len(ic.LayerAnnotations) > 0 {
layerAnns = maps.Clone(ic.LayerAnnotations)
}

adds = append(adds, mutate.Addendum{
Layer: layer,
History: v1.History{
Expand All @@ -84,6 +90,7 @@ func BuildImageFromLayers(ctx context.Context, baseImage v1.Image, layers []v1.L
CreatedBy: "apko",
Created: v1.Time{Time: created}, // TODO: Consider per-layer creation time?
},
Annotations: layerAnns,
})
}

Expand Down
47 changes: 47 additions & 0 deletions pkg/build/oci/image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,50 @@ func TestBuildImageFromLayer(t *testing.T) {
})
}
}

func TestBuildImageFromLayersWithAnnotations(t *testing.T) {
layer1 := static.NewLayer([]byte("layer1"), ggcrtypes.OCILayer)
layer2 := static.NewLayer([]byte("layer2"), ggcrtypes.OCILayer)

now := time.Now()
ctx := context.Background()

for _, tc := range []struct {
desc string
cfg types.ImageConfiguration
wantLayerAnns []map[string]string
}{{
desc: "uniform layer annotations",
cfg: types.ImageConfiguration{
LayerAnnotations: map[string]string{
"dev.chainguard.layer.source": "apko",
},
},
wantLayerAnns: []map[string]string{
{"dev.chainguard.layer.source": "apko"},
{"dev.chainguard.layer.source": "apko"},
},
}, {
desc: "no annotations",
cfg: types.ImageConfiguration{},
wantLayerAnns: []map[string]string{nil, nil},
}} {
t.Run(tc.desc, func(t *testing.T) {
layers := []v1.Layer{layer1, layer2}
img, err := BuildImageFromLayers(ctx, empty.Image, layers, tc.cfg, now, types.ParseArchitecture(""))
require.NoError(t, err)

manifest, err := img.Manifest()
require.NoError(t, err)

require.Len(t, manifest.Layers, 2)
for i, desc := range manifest.Layers {
if tc.wantLayerAnns[i] == nil {
require.Empty(t, desc.Annotations, "layer %d should have no annotations", i)
} else {
require.Equal(t, tc.wantLayerAnns[i], desc.Annotations, "layer %d annotations mismatch", i)
}
}
})
}
}
12 changes: 12 additions & 0 deletions pkg/build/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,18 @@ func WithAnnotations(annotations map[string]string) Option {
}
}

// WithLayerAnnotations adds layer annotations from commandline to those in the config.
// Commandline layer annotations take precedence.
func WithLayerAnnotations(annotations map[string]string) Option {
return func(bc *Context) error {
if bc.ic.LayerAnnotations == nil {
bc.ic.LayerAnnotations = make(map[string]string)
}
maps.Copy(bc.ic.LayerAnnotations, annotations)
return nil
}
}

// WithCache set the cache directory to use
func WithCache(cacheDir string, offline bool, shared *apk.Cache) Option {
return func(bc *Context) error {
Expand Down
9 changes: 9 additions & 0 deletions pkg/build/types/image_configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,15 @@ func (ic *ImageConfiguration) MergeInto(target *ImageConfiguration) error {
}
}
}
if target.LayerAnnotations == nil && ic.LayerAnnotations != nil {
target.LayerAnnotations = maps.Clone(ic.LayerAnnotations)
} else {
for k, v := range ic.LayerAnnotations {
if _, ok := target.LayerAnnotations[k]; !ok {
target.LayerAnnotations[k] = v
}
}
}

target.Volumes = slices.Concat(ic.Volumes, target.Volumes)

Expand Down
7 changes: 7 additions & 0 deletions pkg/build/types/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,13 @@
"type": "object",
"description": "Optional: Annotations to apply to the images manifests"
},
"layer-annotations": {
"additionalProperties": {
"type": "string"
},
"type": "object",
"description": "Optional: Annotations to apply to every layer descriptor in the image manifest"
},
"include": {
"type": "string",
"description": "Optional: Path to a local file containing additional image configuration\n\nThe included configuration is deep merged with the parent configuration\n\nDeprecated: This will be removed in a future release."
Expand Down
2 changes: 2 additions & 0 deletions pkg/build/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@ type ImageConfiguration struct {
VCSUrl string `json:"vcs-url,omitempty" yaml:"vcs-url,omitempty"`
// Optional: Annotations to apply to the images manifests
Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"`
// Optional: Annotations to apply to every layer descriptor in the image manifest
LayerAnnotations map[string]string `json:"layer-annotations,omitempty" yaml:"layer-annotations,omitempty"`
// Optional: Path to a local file containing additional image configuration
//
// The included configuration is deep merged with the parent configuration
Expand Down
Loading