diff --git a/docs/buildah-build.1.md b/docs/buildah-build.1.md index dce977a48f5..203a35e8562 100644 --- a/docs/buildah-build.1.md +++ b/docs/buildah-build.1.md @@ -49,6 +49,19 @@ Instead of building for a set of platforms specified using the **--platform** op Add an image *annotation* (e.g. annotation=*value*) to the image metadata. Can be used multiple times. If *annotation* is named, but neither `=` nor a `value` is provided, then the *annotation* is set to an empty value. +If the annotation name is prefixed with "manifest:", the prefix will be stripped from it and the annotation will +be added to the built image(s). + +If the annotation name is prefixed with "manifest-descriptor:", and the **--manifest** flag is being used, the prefix will +be stripped from it and the annotation will be set on new entries which are added to the list of instances in the image index +instead of to the image metadata. If the **--manifest** flag is not being used, this will trigger an error. + +If the annotation name is prefixed with "index:", and the **--manifest** flag is being used, the prefix will be stripped +from it and the annotation will be set in the image index instead of in the image metadata. If the **--manifest** flag is not being used, this value will trigger an error. + +One or more of the "manifest:", "manifest-descriptor:", and "index:" prefixes can be combined +into a comma-separated list, for example as "manifest,manifest-descriptor:". + Note: this information is not present in Docker image formats, so it is discarded when writing images in Docker formats. **--arch**="ARCH" @@ -1125,6 +1138,9 @@ include: Unset the image annotation, causing the annotation not to be inherited from the base image. +If the annotation name is prefixed with "manifest:", the prefix will be stripped from it and +the rest of the argument will be treated as the name of the annotation. + **--unsetenv** *env* Unset environment variables from the final image. diff --git a/docs/buildah-commit.1.md b/docs/buildah-commit.1.md index a69429823bf..1e087cb95bf 100644 --- a/docs/buildah-commit.1.md +++ b/docs/buildah-commit.1.md @@ -34,6 +34,8 @@ if it is specified. This option can be specified multiple times. Add an image *annotation* (e.g. annotation=*value*) to the image metadata. Can be used multiple times. If *annotation* is named, but neither `=` nor a `value` is provided, then the *annotation* is set to an empty value. +If the annotation name is prefixed with "manifest:", the prefix will be stripped from it. + Note: this information is not present in Docker image formats, so it is discarded when writing images in Docker formats. **--authfile** *path* @@ -371,6 +373,8 @@ Require HTTPS and verification of certificates when talking to container registr Unset the image annotation, causing the annotation not to be inherited from the base image. +If the annotation name is prefixed with "manifest:", the prefix will be stripped from it. + **--unsetenv** *env* Unset environment variables from the final image. diff --git a/image.go b/image.go index 998076b6e4d..5a91c9fc962 100644 --- a/image.go +++ b/image.go @@ -607,10 +607,24 @@ func (i *containerImageRef) newOCIManifestBuilder() (manifestBuilder, error) { annotations[v1.AnnotationCreated] = created.UTC().Format(time.RFC3339Nano) } for _, k := range i.unsetAnnotations { + levelSpec, key, levelsSpecified := strings.Cut(k, ":") + if levelsSpecified { + k = key + if !slices.Contains(strings.Split(levelSpec, ","), "manifest") { + return nil, fmt.Errorf("can't unset non-manifest (%q) annotation %q", levelSpec, k) + } + } delete(annotations, k) } for _, kv := range i.setAnnotations { k, v, _ := strings.Cut(kv, "=") + levelSpec, key, levelsSpecified := strings.Cut(k, ":") + if levelsSpecified { + k = key + if !slices.Contains(strings.Split(levelSpec, ","), "manifest") { + return nil, fmt.Errorf("can't set non-manifest (%q) annotation %q", levelSpec, k) + } + } annotations[k] = v } return &ociManifestBuilder{ diff --git a/imagebuildah/build.go b/imagebuildah/build.go index eb3c5e2409d..4aec108087c 100644 --- a/imagebuildah/build.go +++ b/imagebuildah/build.go @@ -64,6 +64,59 @@ type Mount = specs.Mount type BuildOptions = define.BuildOptions +// selectAnnotations selects annotations that were meant for a particular level, and +// returns them in bare "k=v" form, without the levels (list) prefix +func selectAnnotations(inputs []string, level string, includeNoLevels bool) []string { + if len(inputs) == 0 { + return slices.Clone(inputs) + } + var s []string + for _, kv := range inputs { + k, v, equals := strings.Cut(kv, "=") + levelSpec, key, levelSpecified := strings.Cut(k, ":") + if levelSpecified { + if !slices.Contains(strings.Split(levelSpec, ","), level) { + continue + } + k = key + } else { + if !includeNoLevels { + continue + } + } + if equals { + s = append(s, k+"="+v) + } else { + s = append(s, k) + } + } + return s +} + +// requireAnnotationLevels checks if there are any annotations that were meant for levels other than +// those that were passed in, and returns an error if it finds any +func requireAnnotationLevels(inputs []string, allowedLevels []string) error { + if len(inputs) == 0 { + return nil + } + const noLevel = "manifest" + allowedFunc := func(s string) bool { return slices.Contains(allowedLevels, s) } + for _, kv := range inputs { + k, _, _ := strings.Cut(kv, "=") + levelSpec, _, levelSpecified := strings.Cut(k, ":") + if levelSpecified { + if !slices.ContainsFunc(strings.Split(levelSpec, ","), allowedFunc) { + return fmt.Errorf("disallowed annotation level %q in %q: only %q allowed", levelSpec, k, allowedLevels) + } + } else { + if !allowedFunc(noLevel) { + return fmt.Errorf("disallowed unspecified annotation level %q in %q", noLevel, k) + } + } + } + return nil +} + // BuildDockerfiles parses a set of one or more Dockerfiles (which may be // URLs), creates one or more new Executors, and then runs // Prepare/Execute/Commit/Delete over the entire set of instructions. @@ -188,6 +241,22 @@ func BuildDockerfiles(ctx context.Context, store storage.Store, options define.B } } + if options.Manifest != "" { + if err := requireAnnotationLevels(options.Annotations, []string{"manifest", "manifest-descriptor", "index"}); err != nil { + return "", nil, err + } + if err := requireAnnotationLevels(options.UnsetAnnotations, []string{"manifest"}); err != nil { + return "", nil, err + } + } else { + if err := requireAnnotationLevels(options.Annotations, []string{"manifest"}); err != nil { + return "", nil, err + } + if err := requireAnnotationLevels(options.UnsetAnnotations, []string{"manifest"}); err != nil { + return "", nil, err + } + } + manifestList := options.Manifest options.Manifest = "" type instance struct { @@ -264,6 +333,8 @@ func BuildDockerfiles(ctx context.Context, store storage.Store, options define.B platformOptions.SystemContext = &platformContext platformOptions.OS = platformContext.OSChoice platformOptions.Architecture = platformContext.ArchitectureChoice + platformOptions.Annotations = selectAnnotations(options.Annotations, "manifest", true) + platformOptions.UnsetAnnotations = selectAnnotations(options.UnsetAnnotations, "manifest", true) logPrefix := "" if len(options.Platforms) > 1 { logPrefix = "[" + platforms.Format(platformSpec) + "] " @@ -358,9 +429,59 @@ func BuildDockerfiles(ctx context.Context, store storage.Store, options define.B if err != nil { return "", nil, err } + // In case it already exists, pull up its annotations. + inspectData, err := list.Inspect() + if err != nil { + return "", nil, err + } + // If we're adding annotations to the index, add them now. + indexAnnotations := maps.Clone(inspectData.Annotations) + for _, k := range selectAnnotations(options.UnsetAnnotations, "index", false) { + delete(indexAnnotations, k) + } + for _, kv := range selectAnnotations(options.Annotations, "index", false) { + k, v, _ := strings.Cut(kv, "=") + if v != "" { + if indexAnnotations == nil { + indexAnnotations = make(map[string]string) + } + indexAnnotations[k] = v + } else { + delete(indexAnnotations, k) + } + } + err = list.AnnotateInstance("", &libimage.ManifestListAnnotateOptions{ + IndexAnnotations: indexAnnotations, + }) + if err != nil { + return "", nil, err + } // Add each instance to the list in turn. storeTransportName := istorage.Transport.Name() for _, instance := range instances { + // If we're adding annotations to the instance, build them now. + var instanceAnnotations map[string]string + for _, k := range selectAnnotations(options.UnsetAnnotations, "manifest-descriptor", false) { + delete(instanceAnnotations, k) + } + for _, kv := range selectAnnotations(options.Annotations, "manifest-descriptor", false) { + k, v, _ := strings.Cut(kv, "=") + if v != "" { + if instanceAnnotations == nil { + instanceAnnotations = make(map[string]string) + } + instanceAnnotations[k] = v + } else { + delete(instanceAnnotations, k) + } + } + err = list.AnnotateInstance("", &libimage.ManifestListAnnotateOptions{ + IndexAnnotations: indexAnnotations, + }) + if err != nil { + return "", nil, err + } + // Add the instance and set the things we want to set in it. instanceDigest, err := list.Add(ctx, storeTransportName+":"+instance.ID, nil) if err != nil { return "", nil, err @@ -369,6 +490,7 @@ func BuildDockerfiles(ctx context.Context, store storage.Store, options define.B Architecture: instance.Architecture, OS: instance.OS, Variant: instance.Variant, + Annotations: instanceAnnotations, }) if err != nil { return "", nil, err diff --git a/tests/bud.bats b/tests/bud.bats index f44690f82b0..8322427393d 100644 --- a/tests/bud.bats +++ b/tests/bud.bats @@ -8844,3 +8844,94 @@ _EOF run_buildah build --layers ${contextdir} run_buildah build ${contextdir} } + +@test "bud-with-annotation-levels" { + _prefetch busybox + local contextdir=${TEST_SCRATCH_DIR}/context + mkdir $contextdir + cat > $contextdir/Dockerfile << EOF +FROM busybox +RUN pwd > pwd.txt +EOF + cat > $contextdir/Dockerfile2 << EOF +FROM localhost/foo +RUN pwd > pwd.txt +EOF + for level in "" manifest: manifest-descriptor: index: ; do + local annotation=a=b + run_buildah build --annotation ${level}${annotation} --annotation image=annotation --annotation manifest:manifest=annotation --manifest foo ${contextdir} + case "$level" in + "manifest:") + run_buildah inspect -t image foo + run jq -r '.ImageAnnotations["'${annotation%%=*}'"]' <<< "$output" + assert $status = 0 + assert "$output" = ${annotation##*=} + run_buildah manifest inspect foo + run jq -r '.manifests[0].annotations["'${annotation%%=*}'"]' <<< "$output" + assert $status = 0 + assert "$output" = null + run_buildah manifest inspect foo + run jq -r '.annotations["'${annotation%%=*}'"]' <<< "$output" + assert $status = 0 + assert "$output" = null + ;; + "manifest-descriptor:") + run_buildah inspect -t image foo + run jq -r '.ImageAnnotations["'${annotation%%=*}'"]' <<< "$output" + assert $status = 0 + assert "$output" = null + run_buildah manifest inspect foo + run jq -r '.manifests[0].annotations["'${annotation%%=*}'"]' <<< "$output" + assert $status = 0 + assert "$output" = ${annotation##*=} + run_buildah manifest inspect foo + run jq -r '.annotations["'${annotation%%=*}'"]' <<< "$output" + assert $status = 0 + assert "$output" = null + ;; + "index:") + run_buildah inspect -t image foo + run jq -r '.ImageAnnotations["'${annotation%%=*}'"]' <<< "$output" + assert $status = 0 + assert "$output" = null + run_buildah manifest inspect foo + run jq -r '.manifests[0].annotations["'${annotation%%=*}'"]' <<< "$output" + assert $status = 0 + assert "$output" = null + run_buildah manifest inspect foo + run jq -r '.annotations["'${annotation%%=*}'"]' <<< "$output" + assert $status = 0 + assert "$output" = ${annotation##*=} + ;; + esac + run_buildah build --unsetannotation ${annotation%%=*} --manifest foo2 -f ${contextdir}/Dockerfile2 ${contextdir} + run_buildah inspect -t image foo2 + run jq -r '.ImageAnnotations["'${annotation%%=*}'"]' <<< "$output" + assert $status = 0 + assert "$output" = null + run_buildah inspect -t image foo2 + run jq -r '.ImageAnnotations["image"]' <<< "$output" + assert $status = 0 + assert "$output" = annotation + run_buildah inspect -t image foo2 + run jq -r '.ImageAnnotations["manifest"]' <<< "$output" + assert $status = 0 + assert "$output" = annotation + run_buildah manifest rm foo2 + run_buildah manifest rm foo + done + run_buildah 125 build --annotation manifest-descriptor:a=b ${contextdir} + assert "$output" =~ "disallowed annotation level" + run_buildah 125 build --annotation index:a=b ${contextdir} + assert "$output" =~ "disallowed annotation level" + run_buildah 125 build --unsetannotation index:a=b ${contextdir} + assert "$output" =~ "disallowed annotation level" + run_buildah 125 build --annotation index-descriptor:a=b ${contextdir} + assert "$output" =~ "disallowed annotation level" + run_buildah 125 build --unsetannotation index-descriptor:a=b ${contextdir} + assert "$output" =~ "disallowed annotation level" + run_buildah 125 build --annotation made-up:a=b ${contextdir} + assert "$output" =~ "disallowed annotation level" + run_buildah 125 build --unsetannotation made-up:a=b ${contextdir} + assert "$output" =~ "disallowed annotation level" +} diff --git a/tests/commit.bats b/tests/commit.bats index 942897e1bb0..47f475bdb91 100644 --- a/tests/commit.bats +++ b/tests/commit.bats @@ -624,3 +624,35 @@ load helpers fi done } + +@test "commit-with-annotation-levels" { + _prefetch busybox + run_buildah from -q busybox + local cid="$output" + for level in "" manifest: ; do + for annotation in a=b c=d ; do + local subdir=${level%:}${annotation%%=*} + run_buildah commit --annotation ${level}${annotation} "$cid" oci:${TEST_SCRATCH_DIR}/$subdir + local manifest=${TEST_SCRATCH_DIR}/$subdir/$(oci_image_manifest ${TEST_SCRATCH_DIR}/$subdir) + run jq -r '.annotations["'${annotation%%=*}'"]' "$manifest" + assert $status -eq 0 + echo "$output" + assert "$output" = ${annotation##*=} + run_buildah from --quiet oci:${TEST_SCRATCH_DIR}/$subdir + subcid="$output" + run_buildah commit --unsetannotation ${level}${annotation%%=*} "$subcid" oci:${TEST_SCRATCH_DIR}/cleaned-$subdir + manifest=${TEST_SCRATCH_DIR}/cleaned-$subdir/$(oci_image_manifest ${TEST_SCRATCH_DIR}/cleaned-$subdir) + run jq -r '.annotations["'${annotation%%=*}'"]' "$manifest" + echo "$output" + assert "$output" = null + done + done + run_buildah 125 commit --annotation manifest-descriptor:a=b "$cid" oci:${TEST_SCRATCH_DIR}/nonce + assert "$output" =~ "can't set non-manifest.*annotation" + run_buildah 125 commit --annotation index:a=b "$cid" oci:${TEST_SCRATCH_DIR}/nonce + assert "$output" =~ "can't set non-manifest.*annotation" + run_buildah 125 commit --annotation index-descriptor:a=b "$cid" oci:${TEST_SCRATCH_DIR}/nonce + assert "$output" =~ "can't set non-manifest.*annotation" + run_buildah 125 commit --annotation made-up:a=b "$cid" oci:${TEST_SCRATCH_DIR}/nonce + assert "$output" =~ "can't set non-manifest.*annotation" +}