1
0
mirror of https://github.com/opencontainers/umoci.git synced 2026-02-06 21:44:54 +01:00

stat: include manifest information

This information is arguably a bit too "internal" but given that tools
like [1] exist now, maybe just providing this information anyway could
be useful. (Our tests would benefit from being able to pull out the
digests of the configs and layers directly.)

[1]: https://oci.dag.dev/

Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
This commit is contained in:
Aleksa Sarai
2025-09-18 01:10:49 +10:00
parent 4bf1adcabe
commit fa4d5be735
3 changed files with 113 additions and 11 deletions

View File

@@ -41,6 +41,22 @@ function digest_to_path() {
statFile="$(setup_tmpdir)/stat"
echo "$output" > "$statFile"
# .manifest.descriptor should describe a config blob
sane_run jq -SMr '.manifest.descriptor.mediaType' "$statFile"
[ "$status" -eq 0 ]
[[ "$output" == "application/vnd.oci.image.manifest.v1+json" ]]
# .manifest.blob should match .manifest.descriptor data
sane_run jq -SMr '.manifest.descriptor.digest' "$statFile"
[ "$status" -eq 0 ]
manifest_digest="$output"
sane_run jq -SMr '.manifest.blob' "$statFile"
[ "$status" -eq 0 ]
manifest_data="$output"
sane_run jq -SMr '.' "$(digest_to_path "$IMAGE" "$manifest_digest")"
[ "$status" -eq 0 ]
[[ "$output" == "$manifest_data" ]]
# .config.descriptor should describe a config blob
sane_run jq -SMr '.config.descriptor.mediaType' "$statFile"
[ "$status" -eq 0 ]
@@ -73,13 +89,21 @@ function digest_to_path() {
# We can't really test the output for non-JSON output, but we can smoke test it.
@test "umoci stat [smoke]" {
# Set some values to make sure they show up in stat properly.
umoci config --config.user "foobar" --image "${IMAGE}:${TAG}"
umoci config --image "${IMAGE}:${TAG}" \
--config.user "foobar" \
--manifest.annotation "org.opencontainers.umoci.test=foo"
[ "$status" -eq 0 ]
# Make sure that stat looks about right.
umoci stat --image "${IMAGE}:${TAG}"
[ "$status" -eq 0 ]
# We should have some manifest information.
echo "$output" | grep "== MANIFEST =="
echo "$output" | grep "Media Type: application/vnd.oci.image.manifest.v1+json"
echo "$output" | grep "org.opencontainers.umoci.test: foo"
echo "$output" | grep "org.opencontainers.image.ref.name: ${TAG}"
# We should have some config information.
echo "$output" | grep "== CONFIG =="
echo "$output" | grep "Media Type: application/vnd.oci.image.config.v1+json"
@@ -97,6 +121,21 @@ function digest_to_path() {
}
BLANK_IMAGE_STAT="$(cat <<EOF
== MANIFEST ==
Schema Version: 2
Media Type: application/vnd.oci.image.manifest.v1+json
Config:
Descriptor:
Media Type: application/vnd.oci.image.config.v1+json
Digest: sha256:e5101a46118c740a7709af8eaeec19cbc50a567f4fe7741f8420af39a3779a77
Size: 135B
Descriptor:
Media Type: application/vnd.oci.image.manifest.v1+json
Digest: sha256:98a4b5d5fe4ea076a0a9059075dad54741e055fd0fa016903a8e2b858dcbad80
Size: 249B
Annotations:
org.opencontainers.image.ref.name: latest
== CONFIG ==
Created: 2025-09-05T13:05:10.12345+10:00
Author: ""

View File

@@ -98,13 +98,14 @@ function teardown() {
[ "$status" -eq 0 ]
image-verify "${IMAGE}"
# Compare the stats.
# Compare the stats -- aside from the refname annotation, they should be
# identical.
umoci stat --image "${IMAGE}:${TAG}" --json
[ "$status" -eq 0 ]
oldOutput="$output"
oldOutput="$(jq -rM 'del(.manifest.descriptor.annotations["org.opencontainers.image.ref.name"])' <<<"$output")"
umoci stat --image "${IMAGE}:${NEW_TAG}" --json
[ "$status" -eq 0 ]
newOutput="$output"
newOutput="$(jq -rM 'del(.manifest.descriptor.annotations["org.opencontainers.image.ref.name"])' <<<"$output")"
[[ "$oldOutput" == "$newOutput" ]]
@@ -190,13 +191,14 @@ function teardown() {
[ "$status" -eq 0 ]
image-verify "${IMAGE}"
# Compare the stats.
# Compare the stats -- aside from the refname annotation, they should be
# identical.
umoci stat --image "${IMAGE}:${TAG}" --json
[ "$status" -eq 0 ]
oldOutput="$output"
oldOutput="$(jq -rM 'del(.manifest.descriptor.annotations["org.opencontainers.image.ref.name"])' <<<"$output")"
umoci stat --image "${IMAGE}:${NEW_TAG}" --json
[ "$status" -eq 0 ]
newOutput="$output"
newOutput="$(jq -rM 'del(.manifest.descriptor.annotations["org.opencontainers.image.ref.name"])' <<<"$output")"
[[ "$oldOutput" == "$newOutput" ]]

View File

@@ -154,9 +154,7 @@ func ReadBundleMeta(bundle string) (_ Meta, Err error) {
// TODO: Implement support for manifest lists, this should also be able to
// contain stat information for a list of manifests.
type ManifestStat struct {
// TODO: Flesh this out. Currently it's only really being used to get an
// equivalent of docker-history(1). We really need to add more
// information about it.
Manifest manifestStat `json:"manifest"`
// Config stores information about the configuration of a manifest.
Config configStat `json:"config"`
@@ -294,7 +292,14 @@ func pprintDescriptor(w io.Writer, prefix string, descriptor ispec.Descriptor) e
// define their own custom templates for different blocks (meaning that this
// should use text/template rather than using tabwriters manually.
func (ms ManifestStat) Format(w io.Writer) error {
if _, err := fmt.Fprintln(w, "== CONFIG =="); err != nil {
if _, err := fmt.Fprintln(w, "== MANIFEST =="); err != nil {
return err
}
if err := ms.Manifest.pprint(w); err != nil {
return err
}
if _, err := fmt.Fprintln(w, "\n== CONFIG =="); err != nil {
return err
}
if err := ms.Config.pprint(w); err != nil {
@@ -311,6 +316,57 @@ func (ms ManifestStat) Format(w io.Writer) error {
return nil
}
// manifestStat contains information about the image manifest.
type manifestStat struct {
// Descriptor is the descriptor for the configuration JSON.
Descriptor ispec.Descriptor `json:"descriptor"`
// Manifest is the contents of the image manifest.
Manifest ispec.Manifest `json:"-"`
// RawData is the raw data stream of the blob, which is output when we
// provide JSON output (to make sure no information is lost in --json
// mode).
RawData json.RawMessage `json:"blob"`
}
func (m manifestStat) pprint(w io.Writer) error {
manifest := m.Manifest
if err := pprint(w, "", "Schema Version", strconv.Itoa(manifest.SchemaVersion)); err != nil {
return err
}
if err := pprint(w, "", "Media Type", manifest.MediaType); err != nil {
return err
}
// TODO(image-spec v1.1): manifest.ArtifactType
// TODO(image-spec v1.1): manifest.Subject
if err := pprint(w, "", "Config"); err != nil {
return err
}
if err := pprintDescriptor(w, "\t", manifest.Config); err != nil {
return err
}
if len(manifest.Layers) > 0 {
if err := pprint(w, "", "Layers"); err != nil {
return err
}
for _, layer := range manifest.Layers {
if err := pprintDescriptor(w, "\t", layer); err != nil {
return err
}
}
}
if len(manifest.Annotations) > 0 {
if err := pprintMap(w, "", "Annotations", manifest.Annotations); err != nil {
return err
}
}
if err := pprintDescriptor(w, "", m.Descriptor); err != nil {
return err
}
return nil
}
// configStat contains information about the image configuration of this
// manifest.
type configStat struct {
@@ -470,6 +526,11 @@ func Stat(ctx context.Context, engine casext.Engine, manifestDescriptor ispec.De
// Should _never_ be reached.
return stat, fmt.Errorf("[internal error] unknown manifest blob type: %s", manifestBlob.Descriptor.MediaType)
}
stat.Manifest = manifestStat{
Descriptor: manifestDescriptor,
Manifest: manifest,
RawData: manifestBlob.RawData,
}
// Now get the config.
configBlob, err := engine.FromDescriptor(ctx, manifest.Config)