Skip to content

Preserve layer annotations from parent images during commit - #6972

Open
simek-m wants to merge 1 commit into
podman-container-tools:mainfrom
simek-m:RUN-5088-layer-annotations
Open

Preserve layer annotations from parent images during commit#6972
simek-m wants to merge 1 commit into
podman-container-tools:mainfrom
simek-m:RUN-5088-layer-annotations

Conversation

@simek-m

@simek-m simek-m commented Jul 20, 2026

Copy link
Copy Markdown

What type of PR is this?

/kind bug

What this PR does / why we need it:

When building an image using FROM with a source that contains
layer annotations, the final committed image loses those
annotations.

Propagate the per-layer annotations to addLayer() for OCI
images to be present in the final committed image.
The annotations are only preserved if the layer
content doesn't change as their lookup relies
on the layer uncompressed digest not changing.

Introduce a new flag --inherit-layer-annotations for
the `buildah build` command to control inheritance
of per-layer annotations (default=true).

Extend the existing `buildah config --annotation` flag
to support "layer:" prefix for per-layer annotations
on the top-most (read-write) layer. The flag works
the same for per-layer annotation as it does for
image ones (set/unset key/clear **all).**

How to verify it

  1. Pick an image with layer annotations, e.g.:
skopeo inspect -n docker://quay.io/fedora/fedora-bootc | jq -c '.LayersData[].Annotations' | head
  1. Build from it:
buildah from quay.io/fedora/fedora-bootc
buildah run fedora-bootc-working-container touch /var/foo
img=$(buildah commit fedora-bootc-working-container)
  1. Check annotations on the new image:
skopeo inspect containers-storage:$img | jq '.LayersData[].Annotations' | head

Which issue(s) this PR fixes:

Fixes #6652

Special notes for your reviewer:

I'm adding notes inline.

Does this PR introduce a user-facing change?

When building an image using FROM with a source that contains layer annotations, commit now preserves the per-layer annotations.
The `buildah build` command now accepts an optional flag `--inherit-layer-annotations` (default true) that controls inheritance for per-layer annotations.

Comment thread tests/commit.bats Outdated

@test "commit preserves layer annotations" {
# Build a minimal OCI image with layer annotations.
mkdir -p $TEST_SCRATCH_DIR/blobs/sha256

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to use quay.io/fedora/fedora-bootc, but I've been running into resource limitations - the image is large and the test was slow. Unfortunately, I couldn't find a smaller existing image with layer annotations.

Comment thread buildah.go Outdated
Comment thread commit_test.go
Comment on lines +669 to +684
ctx := context.TODO()
graphDriverName := os.Getenv("STORAGE_DRIVER")
if graphDriverName == "" {
graphDriverName = "vfs"
}
t.Logf("using storage driver %q", graphDriverName)
store, err := storage.GetStore(storageTypes.StoreOptions{
RunRoot: t.TempDir(),
GraphRoot: t.TempDir(),
GraphDriverName: graphDriverName,
})
require.NoError(t, err, "initializing storage")
t.Cleanup(func() { _, err := store.Shutdown(true); assert.NoError(t, err) })

// Build a source image with one layer.
b, err := NewBuilder(ctx, store, BuilderOptions{

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole setup is repeated multiple times in the tests and could be extracted. I didn't want to do it in the scope of this change.

Comment thread config.go Outdated
}

// Get layer annotations if present.
if len(v1Manifest.Layers) > len(b.OCIv1.RootFS.DiffIDs) {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not confident this is a correct way of getting the annotations (well, it works), but couldn't think of anything better.

The length check should not be normally needed, but I wanted to be safe.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if using the blob info cache (go.podman.io/image/v5/pkg/blobinfocache.DefaultCache()) to try to correlate digest values from the manifest and diffID values from the config blob would be more resilient. It would certainly be more complicated.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, missed this comment.
Interesting, I can try to implement it to see what it practically means. It definitely looks more complicated and I'm not sure if there are cases when the cache would get a miss, while this index-based approach would work (maybe there are none).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried that and it looks easier than I expected. Tests pass, so kept it with a fallback to the index-based lookup.

@simonbrauner

Copy link
Copy Markdown

Does this PR introduce a user-facing change?
None

This should be a user-facing change, right? The user will see that the bug is no longer there.

images to be present in the final commited image. (commit message)

Typo. committed

@simek-m
simek-m force-pushed the RUN-5088-layer-annotations branch from 50d72c3 to 15ae7d8 Compare July 20, 2026 13:18
@simek-m

simek-m commented Jul 20, 2026

Copy link
Copy Markdown
Author

@simonbrauner

images to be present in the final commited image. (commit message)

Typo. committed

Thank you, fixed.

Does this PR introduce a user-facing change?
None

This should be a user-facing change, right? The user will see that the bug is no longer there.

I'm not sure. Looking at past PRs, I can see both. It won't hurt so I added the change log.

@nalind nalind left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would need some documentation, and possibly a flag to revert to discarding these annotations, as --inherit-annotations offers control of those at the level of the image manifest.
The docs would also be a good place to note that these aren't (can't be?) preserved when the diffIDs for layers change, for example when --rewrite-timestamp is used, or when all layers are being squashed, as with --cw.

Comment thread image.go Outdated
Comment thread buildah.go Outdated
Comment thread tests/commit.bats
Comment thread tests/commit.bats
@simek-m
simek-m force-pushed the RUN-5088-layer-annotations branch from 15ae7d8 to 85ceb8a Compare July 21, 2026 10:36
@simek-m

simek-m commented Jul 21, 2026

Copy link
Copy Markdown
Author

@nalind

This would need some documentation, and possibly a flag to revert to discarding these annotations, as --inherit-annotations offers control of those at the level of the image manifest. The docs would also be a good place to note that these aren't (can't be?) preserved when the diffIDs for layers change, for example when --rewrite-timestamp is used, or when all layers are being squashed, as with --cw.

Thank you for the feedback. I've addressed the obvious ones. Regarding these points, I'm thinking how it should work regarding flags to control annotation preservation. With image annotations I:

  • The --inherit-annotations is used by buildah build to control image annotations inheritance.
  • buildah commit uses additionally --unsetannotation to explicitly disable inheritance for specific image annotations.


I think that there should be a separate flag like --inherit-layer-annotations, but I'm not that sure about where it should be placed:

- Both buildah build and buildah from could use it to clear the annotations. The build command would be consistent with the existing flag for image annotations, but the worflow in this bug uses from that has currently no equivalent.

- Adding a flag to the commit command would IMHO make sense, but it's not consistent with the existing granular --unsetannotations flag. Unless it's per-annotation too and that would be more cumbersome to use.




What do you think? I might be leaning towards --inherit-layer-annotations for commit, but it's not the most consistent and I could be missing a lot of things with my limited knowledge.


There could be also more support for the layer annotations implemented (set, ...), but more than the inheritance is probably out-of-scope here.


The docs would also be a good place to note that these aren't (can't be?) preserved when the diffIDs for layers change, for example when --rewrite-timestamp is used, or when all layers are being squashed, as with --cw.

Yes, the implementation is definitely limited and it needs to be documented.

@nalind

nalind commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

The --unset flags being present in buildah commit is arguably a misfeature, as they duplicate functionality that's already exposed by buildah config, and we really don't want to just have buildah commit duplicate every flag that buildah config has, differing only in whether or not they affect the defaults for an image committed from the working container.

Likewise, buildah from isn't a good place to duplicate buildah config functionality. It ended up being the place to set CommonBuildOpts values that, at the time, we decided would persist between buildah from and buildah run, and now it's a mixture of things that would probably be better split between buildah commit and buildah run, and all shared with buildah build.

The --inherit flags in buildah build make a bit more sense there because we can make the equivalent changes without having to keep track of the state of the flags in between invocations -- there's no "undo that unset" logic like we'd have to add for a hypothetical buildah config --inherit-annotations=true invoked after a buildah config --inherit-annotations=false. So I'd suggest adding a --inherit... flag to buildah build.

I don't know what the CLI would look like for exposing an ability to set or unset annotations for individual layers in a multi-layer buildah build. For a single-layer buildah build, or a case where "commit" is going to be used to create one layer, either brand-new flags for buildah config, or enhancing the existing flags so that they interpret the values they're passed that match the form "level:key=value", as #6322 was toying with, perhaps with "layer" or "descriptor" as the scope, would probably be enough. I guess that multi-layer case could look like "descriptor[n]:", in keeping with the idea of other levels being able to optionally include a platform specifier.

So, shorter version of the above: a --inherit... flag for buildah build, and either new flags to set/unset layer annotations, or extending the current flag parsing, in buildah config/buildah commit/buildah build. Hopefully that'd be at least vaguely consistent with expectations we've created.

@simek-m
simek-m force-pushed the RUN-5088-layer-annotations branch 2 times, most recently from 9825a0a to 285f4b3 Compare July 22, 2026 11:17
@simek-m

simek-m commented Jul 22, 2026

Copy link
Copy Markdown
Author

@nalind

The --unset flags being present in buildah commit is arguably a misfeature, as they duplicate functionality that's already exposed by buildah config, and we really don't want to just have buildah commit duplicate every flag that buildah config has, differing only in whether or not they affect the defaults for an image committed from the working container.

Likewise, buildah from isn't a good place to duplicate buildah config functionality. It ended up being the place to set CommonBuildOpts values that, at the time, we decided would persist between buildah from and buildah run, and now it's a mixture of things that would probably be better split between buildah commit and buildah run, and all shared with buildah build.

The --inherit flags in buildah build make a bit more sense there because we can make the equivalent changes without having to keep track of the state of the flags in between invocations -- there's no "undo that unset" logic like we'd have to add for a hypothetical buildah config --inherit-annotations=true invoked after a buildah config --inherit-annotations=false. So I'd suggest adding a --inherit... flag to buildah build.

I don't know what the CLI would look like for exposing an ability to set or unset annotations for individual layers in a multi-layer buildah build. For a single-layer buildah build, or a case where "commit" is going to be used to create one layer, either brand-new flags for buildah config, or enhancing the existing flags so that they interpret the values they're passed that match the form "level:key=value", as #6322 was toying with, perhaps with "layer" or "descriptor" as the scope, would probably be enough. I guess that multi-layer case could look like "descriptor[n]:", in keeping with the idea of other levels being able to optionally include a platform specifier.

So, shorter version of the above: a --inherit... flag for buildah build, and either new flags to set/unset layer annotations, or extending the current flag parsing, in buildah config/buildah commit/buildah build. Hopefully that'd be at least vaguely consistent with expectations we've created.

Thank you for this thorough answer. It is even more complex than I initially thought.

I updated this PR and added a --inherit-layer-annotations flag for buildah build.

I looked at your draft in #6322 and I think it's the way to go.
I'm thinking of a limited, yet future-proof version of extending existing annotation flags in buildah config in this PR's scope:

  • Mimicking how the --created-by flag works, buildah config would allow to affect per-layer annotations only for the top-most layer.
  • I'd borrow the prefix syntax and have --annotation layer:[key]=[value] and --unset-annotation [key].
  • Note: I realized that there's also this (that should not change anything, though):

    If annotation has a trailing -, then the annotation is removed from the config.
    If the annotation is set to "-" then all annotations are removed from the config.

  • It should be possible to extend this syntax to allow for specifying different layers in multi-layer images how you show above.

What do you think? Does it make any sense like this and would it be sufficient?

@nalind

nalind commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Yes, I think that all makes sense. It's more than I think the original issue requested, but it should meet their needs, too.

Comment thread docs/buildah-build.1.md Outdated
@simek-m
simek-m force-pushed the RUN-5088-layer-annotations branch from 285f4b3 to 61ad151 Compare July 23, 2026 08:55

@nalind nalind left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with one nit.

Comment thread image.go Outdated
mb.addLayer(destHasher.Digest(), size, srcHasher.Digest())

diffID := srcHasher.Digest()
if layerAnnotations == nil && i.layerAnnotations != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second check here shouldn't be necessary.

@simek-m

simek-m commented Jul 23, 2026

Copy link
Copy Markdown
Author

Yes, I think that all makes sense. It's more than I think the original issue requested, but it should meet their needs, too.

@nalind Thank you, the buildah config --annotation layer:k=v implementation looks good (haven't committed it yet).

However, when implementing the CLI, I realized that buildah config --unsetannotation without specifying a layer for unsetting an inherited annotation (discussed above) is unusable.
The per-layer annotations in the fedora-bootc image look like this - same key is repeated for different layers:

{"ostree.components":"python3-botocore"}
{"ostree.components":"nvidia-gpu-firmware"}
{"ostree.components":"kernel-modules"}
{"ostree.components":"kernel-modules-core"}
{"ostree.components":"linux-firmware"}
{"ostree.components":"python3-libs"}
{"ostree.components":"podman"}
{"ostree.components":"atheros-firmware"}



A hacky way could allow removing a specific annotation matching a key-value pair regardless of a layer like --unsetannotation layer:ostree.components=podman or it can be done properly with the layer[digest]:key or layer[index]:key matching an exact layer (can be a different level name).


@simek-m
simek-m force-pushed the RUN-5088-layer-annotations branch 3 times, most recently from c8ea96c to d6b6021 Compare July 27, 2026 13:26
@simek-m

simek-m commented Jul 27, 2026

Copy link
Copy Markdown
Author

I updated the PR with the buildah config --annotation layer: support (+ the BlobInfoCache mentioned above).

I haven't implemented the --unsetannotation flag support for per-layer annotations as it would need properly identifying the layers to be usable.

@simonbrauner simonbrauner left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added two comments.

Other than that, LGTM

Comment thread image.go
Comment on lines +1230 to +1232
if layerAnnotations == nil {
layerAnnotations = make(map[string]string)
}

@simonbrauner simonbrauner Aug 10, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure whether this could cause problems in this particular case, but wouldn't it be better practice to clone the map instead of rewriting the original, as the structure i refers to outlives that code block?

Suggested change
if layerAnnotations == nil {
layerAnnotations = make(map[string]string)
}
if layerAnnotations == nil {
layerAnnotations = make(map[string]string)
} else {
layerAnnotations = maps.Clone(layerAnnotations)
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thank you, great catch.
It's cloned in other places and it should be done here too. I just did it where it's populated:

+++ b/image.go
@@ -1040,7 +1040,7 @@ func (i *containerImageRef) NewImageSource(ctx context.Context, _ *types.SystemC
                        layerUncompressedSize = layer.UncompressedSize
                }
                // Get annotations for the layer if present.
-               layerAnnotations := i.layerAnnotations[layerUncompressedDigest]
+               layerAnnotations := maps.Clone(i.layerAnnotations[layerUncompressedDigest])
                // We already know the digest of the contents of parent layers,
                // so if this is a parent layer, and we know its digest, reuse
                // its blobsum, diff ID, and size.
@@ -1223,7 +1223,7 @@ func (i *containerImageRef) NewImageSource(ctx context.Context, _ *types.SystemC

                diffID := srcHasher.Digest()
                if layerAnnotations == nil {
-                       layerAnnotations = i.layerAnnotations[diffID]
+                       layerAnnotations = maps.Clone(i.layerAnnotations[diffID])
                }
                // Set layer annotations for the top layer.
                if layerID == i.layerID && len(i.topLayerAnnotations) > 0 {

(I also fixed some more instances of commited in the config.bats tests.)

}
// Add new annotations to the last step.
for _, annotationSpec := range s.executor.annotations {
annotationk, annotationv, _ := strings.Cut(annotationSpec, "=")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There seems to be an introduced inconsistency between commands. Perhaps it would be a good idea to unify the behavior?

buildah config --annotation layer:key=val

Adds the annotation to a layer.

cid=$(./bin/buildah from --quiet scratch)
./bin/buildah config --annotation layer:mykey=myval "$cid"
./bin/buildah copy "$cid" /dev/null /somefile
./bin/buildah commit "$cid" repro-config

echo "=== Image Annotations ==="
./bin/buildah inspect --type=image --format '{{.Manifest}}' repro-config | jq '.annotations'

echo "=== Layer Annotations ==="
./bin/buildah inspect --type=image --format '{{.Manifest}}' repro-config | jq '.layers[].annotations'

./bin/buildah rm "$cid"
./bin/buildah rmi repro-config
=== Image Annotations ===
{
  "org.opencontainers.image.base.digest": "",
  "org.opencontainers.image.base.name": "",
  "org.opencontainers.image.created": "2026-08-10T14:09:24.9320504Z"
}
=== Layer Annotations ===
{
  "mykey": "myval"
}

buildah build --annotation layer:key=val

Adds the annotation to an image, treats layer: prefix as part of the key.

dir=$(mktemp -d)
echo hello > "$dir/somefile"
cat > "$dir/Containerfile" <<EOF
FROM scratch
COPY somefile /
EOF

./bin/buildah build --annotation layer:mykey=myval -t repro-build "$dir"

echo "=== Image Annotations ==="
./bin/buildah inspect --type=image --format '{{.Manifest}}' repro-build | jq '.annotations'

echo "=== Layer Annotations ==="
./bin/buildah inspect --type=image --format '{{.Manifest}}' repro-build | jq '.layers[].annotations'

./bin/buildah rmi repro-build
rm -rf "$dir"
=== Image Annotations ===
{
  "layer:mykey": "myval",
  "org.opencontainers.image.base.digest": "",
  "org.opencontainers.image.base.name": "",
  "org.opencontainers.image.created": "2026-08-10T14:12:17.63929102Z"
}
=== Layer Annotations ===
null

@simek-m simek-m Aug 11, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, that's a very good point and you're right that this is a confusing behavior. Unfortunately, there are other inconsistencies too.

I'm afraid it's a bit more tricky with buildah build, though. The layer: prefix in buildah config refers to the top-most layer and it makes sense there.
However, there can be multiple layers with buildah build and IMHO the options are:

  1. Document properly that it's not supported
  2. Warn if the layer: prefix is used (documentation too)
  3. Refer only to the top-most layer for the annotations like with config
  4. Do it properly and let the user to refer to specific layers.

The last option (discussed in this PR) is most complex, because it's less clear how to design the CLI for it to be user-friendly (layer[idx]: or layer[digest]:) and I wanted to leave it out of scope of this PR. I think it's quite a niche feature and I'm not that sure of actual use cases needed to support.

@nalind What do you think?

When building an image using FROM with a source that contains
layer annotations, the final committed image loses those
annotations.

Propagate the per-layer annotations to addLayer() for OCI
images to be present in the final committed image.
The annotations are only preserved if the layer
content doesn't change as their lookup relies
on the layer uncompressed digest not changing.

Introduce a new flag --inherit-layer-annotations for
the `buildah build` command to control inheritance
of per-layer annotations (default=true).

Extend the existing `buildah config --annotation` flag
to support "layer:" prefix for per-layer annotations
on the top-most (read-write) layer. The flag works
the same for per-layer annotation as it does for
image ones (set/unset key/clear all).

Fixes: podman-container-tools#6652
Fixes: https://redhat.atlassian.net/browse/RUN-5088
Signed-off-by: Marek Simek <msimek@redhat.com>
@simek-m
simek-m force-pushed the RUN-5088-layer-annotations branch from d6b6021 to 326bc45 Compare August 11, 2026 12:11
@TomSweeneyRedHat

Copy link
Copy Markdown
Contributor

You might consider tweaking the release note to include something about config --annotation too

Comment thread docs/buildah-config.1.md
of any images which will be built using the specified container or add a per-layer
annotation for the top-most layer with the "layer:" prefix before the key. Can be used
multiple times. If *annotation* has a trailing `-`, then the *annotation* is removed
from the config. If the *annotation* is set to "-" then all annotations are removed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
from the config. If the *annotation* is set to "-" then all annotations are removed
from the config. If the *annotation* is set to "-" then all annotations at that level are removed

I think

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Layer annotations from parent images are not preserved during commit

4 participants