Skip to content
Closed
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
4 changes: 3 additions & 1 deletion cmd/caib/common/oci_artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import (
"github.com/containers/image/v5/oci/layout"
"github.com/containers/image/v5/signature"
"github.com/containers/image/v5/types"

"github.com/centos-automotive-suite/automotive-dev-operator/internal/common/oci"
)

// PullOCIArtifact pulls and extracts an OCI artifact to local destination.
Expand Down Expand Up @@ -183,7 +185,7 @@ func extractOCIArtifactBlob(ociLayoutPath, destPath string) error {
return fmt.Errorf("no layers found in manifest")
}

annotationMultiLayer := manifest.Annotations["automotive.sdv.cloud.redhat.com/multi-layer"] == "true"
annotationMultiLayer := manifest.Annotations[oci.Get().AnnotationKey("multi-layer")] == "true"
isMultiLayer := annotationMultiLayer || len(manifest.Layers) > 1
if isMultiLayer {
if !annotationMultiLayer && len(manifest.Layers) > 1 {
Expand Down
78 changes: 35 additions & 43 deletions cmd/caib/inspectcmd/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,34 +25,26 @@ import (

caibcommon "github.com/centos-automotive-suite/automotive-dev-operator/cmd/caib/common"
"github.com/centos-automotive-suite/automotive-dev-operator/cmd/caib/registryauth"
"github.com/centos-automotive-suite/automotive-dev-operator/internal/common/oci"
)

const annotationPrefix = "automotive.sdv.cloud.redhat.com/"

var knownAnnotations = []struct {
key string
label string
}{
{"distro", "Distro"},
{"target", "Target"},
{"arch", "Arch"},
{"automotive-image-builder", "AIB Image"},
{"builder-image", "Builder Image"},
{"aib-version", "AIB Version"},
{"task-bundle-ref", "Task Bundle"},
{"custom-defines", "Custom Defines"},
{"aib-extra-args", "AIB Extra Args"},
{"export-format", "Export Format"},
{"aib-command", "AIB Command"},
}

var knownReferrerTypes = []struct {
artifactType string
label string
}{
{"application/vnd.automotive.manifest.v1+yaml", "AIB Manifest"},
{"application/vnd.automotive.sources.v1+tar+gzip", "Build Sources"},
{"application/vnd.osbuild.manifest.v1+json", "osbuild Manifest"},
var ociSpec = oci.Get()

// annotationDisplayLabels maps spec annotation keys to human-readable labels
// for provenance display. Keys not listed here (parts, multi-layer,
// default-partitions) are tooling-only metadata and intentionally omitted.
var annotationDisplayLabels = map[string]string{
"distro": "Distro",
"target": "Target",
"arch": "Arch",
"automotive-image-builder": "AIB Image",
"builder-image": "Builder Image",
"aib-version": "AIB Version",
"task-bundle-ref": "Task Bundle",
"custom-defines": "Custom Defines",
"aib-extra-args": "AIB Extra Args",
"export-format": "Export Format",
"aib-command": "AIB Command",
}

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.

annotationDisplayLabels map has 11 entries but the spec defines 14 annotation keys, the three missing keys (parts, multi-layer, default-partitions) are silently skipped.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

the 3 others are not for provenance, but for tools (FLS)


// Options wires inspect handler dependencies.
Expand Down Expand Up @@ -156,8 +148,8 @@ func (h *Handler) RunInspect(_ *cobra.Command, args []string) {
func (h *Handler) printStructured(format, ociRef, digest string, annotations map[string]string, referrers []referrerInfo, referrerTypes map[string]bool) {
stripped := make(map[string]string)
for k, v := range annotations {
if strings.HasPrefix(k, annotationPrefix) {
stripped[strings.TrimPrefix(k, annotationPrefix)] = v
if strings.HasPrefix(k, ociSpec.AnnotationPrefix) {
stripped[strings.TrimPrefix(k, ociSpec.AnnotationPrefix)] = v
}
}

Expand Down Expand Up @@ -322,11 +314,15 @@ func (h *Handler) printProvenance(ociRef, digest string, annotations map[string]
fmt.Println()

hasAnnotations := false
for _, a := range knownAnnotations {
val := annotations[annotationPrefix+a.key]
for _, ak := range ociSpec.AllManifestAnnotationKeys() {
label := annotationDisplayLabels[ak.Key]
if label == "" {
continue
}
val := annotations[ociSpec.AnnotationKey(ak.Key)]
if val != "" {
hasAnnotations = true
fmt.Printf(" %-16s %s\n", bold(a.label+":"), green(val))
fmt.Printf(" %-16s %s\n", bold(label+":"), green(val))
}
}
if !hasAnnotations {
Expand All @@ -337,11 +333,11 @@ func (h *Handler) printProvenance(ociRef, digest string, annotations map[string]
fmt.Println(bold("Saved Artifacts"))
fmt.Println(bold(strings.Repeat("═", 50)))

for _, rt := range knownReferrerTypes {
if referrerTypes[rt.artifactType] {
fmt.Printf(" %s %s (%s)\n", green("✓"), bold(rt.label), rt.artifactType)
for _, rt := range ociSpec.ReferrerTypes {
if referrerTypes[rt.ArtifactType] {
fmt.Printf(" %s %s (%s)\n", green("✓"), bold(rt.Label), rt.ArtifactType)
} else {
fmt.Printf(" %s %s\n", yellow("✗"), rt.label)
fmt.Printf(" %s %s\n", yellow("✗"), rt.Label)
}
}

Expand All @@ -355,7 +351,7 @@ func (h *Handler) printProvenance(ociRef, digest string, annotations map[string]
}

func buildRebuildCommand(ociRef, digest string, annotations map[string]string, referrerTypes map[string]bool) string {
get := func(key string) string { return annotations[annotationPrefix+key] }
get := func(key string) string { return annotations[ociSpec.AnnotationKey(key)] }

aibCmd := get("aib-command")
isDevBuild := strings.HasPrefix(aibCmd, "aib-dev")
Expand All @@ -367,7 +363,7 @@ func buildRebuildCommand(ociRef, digest string, annotations map[string]string, r
parts = append(parts, "caib image build")
}

hasManifest := referrerTypes["application/vnd.automotive.manifest.v1+yaml"]
hasManifest := referrerTypes[ociSpec.ReferrerArtifactTypeByLabel("AIB Manifest")]
if hasManifest {
parts = append(parts, "manifest.aib.yml")
} else {
Expand Down Expand Up @@ -408,7 +404,7 @@ func buildRebuildCommand(ociRef, digest string, annotations map[string]string, r
}
}
}
hasSources := referrerTypes["application/vnd.automotive.sources.v1+tar+gzip"]
hasSources := referrerTypes[ociSpec.ReferrerArtifactTypeByLabel("Build Sources")]
taskBundleRef := get("task-bundle-ref")
if taskBundleRef != "" {
parts = append(parts, fmt.Sprintf(" --task-bundle-ref %s", taskBundleRef))
Expand Down Expand Up @@ -439,11 +435,7 @@ func (h *Handler) downloadReferrers(ociRef, _ string, referrers []referrerInfo,
repo := splitReference(ociRef)
insecure := h.opts.InsecureSkipTLS != nil && *h.opts.InsecureSkipTLS

fileMap := map[string]string{
"application/vnd.automotive.manifest.v1+yaml": "manifest.aib.yml",
"application/vnd.automotive.sources.v1+tar+gzip": "build-sources.tar.gz",
"application/vnd.osbuild.manifest.v1+json": "image.json",
}
fileMap := ociSpec.ReferrerFileMap()

for _, ref := range referrers {
filename, known := fileMap[ref.ArtifactType]
Expand Down
30 changes: 15 additions & 15 deletions cmd/caib/inspectcmd/inspect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ func TestSplitReference(t *testing.T) {

func fullAnnotations() map[string]string {
return map[string]string{
annotationPrefix + "distro": "autosd",
annotationPrefix + "target": "qemu",
annotationPrefix + "arch": "amd64",
annotationPrefix + "automotive-image-builder": "quay.io/aib@sha256:abc",
annotationPrefix + "builder-image": "quay.io/builder@sha256:def",
annotationPrefix + "aib-version": "1.3.0",
annotationPrefix + "task-bundle-ref": "quay.io/tasks@sha256:789",
annotationPrefix + "aib-command": "aib build --distro autosd --target qemu",
ociSpec.AnnotationPrefix + "distro": "autosd",
ociSpec.AnnotationPrefix + "target": "qemu",
ociSpec.AnnotationPrefix + "arch": "amd64",
ociSpec.AnnotationPrefix + "automotive-image-builder": "quay.io/aib@sha256:abc",
ociSpec.AnnotationPrefix + "builder-image": "quay.io/builder@sha256:def",
ociSpec.AnnotationPrefix + "aib-version": "1.3.0",
ociSpec.AnnotationPrefix + "task-bundle-ref": "quay.io/tasks@sha256:789",
ociSpec.AnnotationPrefix + "aib-command": "aib build --distro autosd --target qemu",
}
}

Expand Down Expand Up @@ -82,7 +82,7 @@ func TestBuildRebuildCommand_Bootc(t *testing.T) {

func TestBuildRebuildCommand_DevBuild(t *testing.T) {
annotations := fullAnnotations()
annotations[annotationPrefix+"aib-command"] = "aib-dev --verbose build --distro autosd"
annotations[ociSpec.AnnotationPrefix+"aib-command"] = "aib-dev --verbose build --distro autosd"
referrerTypes := map[string]bool{}

cmd := buildRebuildCommand("quay.io/org/repo:v1", "sha256:abc123", annotations, referrerTypes)
Expand All @@ -100,7 +100,7 @@ func TestBuildRebuildCommand_DevBuild(t *testing.T) {

func TestBuildRebuildCommand_NoSecure(t *testing.T) {
annotations := fullAnnotations()
delete(annotations, annotationPrefix+"task-bundle-ref")
delete(annotations, ociSpec.AnnotationPrefix+"task-bundle-ref")
referrerTypes := map[string]bool{}

cmd := buildRebuildCommand("quay.io/org/repo:v1", "sha256:abc123", annotations, referrerTypes)
Expand All @@ -112,7 +112,7 @@ func TestBuildRebuildCommand_NoSecure(t *testing.T) {

func TestBuildRebuildCommand_CustomDefines(t *testing.T) {
annotations := fullAnnotations()
annotations[annotationPrefix+"custom-defines"] = "use_debug=true\nfoo=bar"
annotations[ociSpec.AnnotationPrefix+"custom-defines"] = "use_debug=true\nfoo=bar"
referrerTypes := map[string]bool{}

cmd := buildRebuildCommand("quay.io/org/repo:v1", "sha256:abc123", annotations, referrerTypes)
Expand All @@ -127,7 +127,7 @@ func TestBuildRebuildCommand_CustomDefines(t *testing.T) {

func TestBuildRebuildCommand_ExtraArgs(t *testing.T) {
annotations := fullAnnotations()
annotations[annotationPrefix+"aib-extra-args"] = "--verbose\n--cache-max-size=unlimited"
annotations[ociSpec.AnnotationPrefix+"aib-extra-args"] = "--verbose\n--cache-max-size=unlimited"
referrerTypes := map[string]bool{}

cmd := buildRebuildCommand("quay.io/org/repo:v1", "sha256:abc123", annotations, referrerTypes)
Expand All @@ -142,7 +142,7 @@ func TestBuildRebuildCommand_ExtraArgs(t *testing.T) {

func TestBuildRebuildCommand_ExportFormat(t *testing.T) {
annotations := fullAnnotations()
annotations[annotationPrefix+"export-format"] = "simg"
annotations[ociSpec.AnnotationPrefix+"export-format"] = "simg"
referrerTypes := map[string]bool{}

cmd := buildRebuildCommand("quay.io/org/repo:v1", "sha256:abc123", annotations, referrerTypes)
Expand Down Expand Up @@ -181,7 +181,7 @@ func TestBuildRebuildCommand_NoRestoreSourcesWithoutReferrer(t *testing.T) {

func TestBuildRebuildCommand_NoBuilderImage(t *testing.T) {
annotations := fullAnnotations()
delete(annotations, annotationPrefix+"builder-image")
delete(annotations, ociSpec.AnnotationPrefix+"builder-image")
referrerTypes := map[string]bool{}

cmd := buildRebuildCommand("quay.io/org/repo:v1", "sha256:abc123", annotations, referrerTypes)
Expand Down Expand Up @@ -311,7 +311,7 @@ func TestPrintProvenance_FullAIBCommand(t *testing.T) {

longCmd := strings.Repeat("x", 200)
annotations := map[string]string{
annotationPrefix + "aib-command": longCmd,
ociSpec.AnnotationPrefix + "aib-command": longCmd,
}
referrerTypes := map[string]bool{}

Expand Down
150 changes: 150 additions & 0 deletions internal/common/oci/spec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Package oci defines the shared OCI artifact contract (media types, annotations, referrer types).
package oci

import (
_ "embed"
"encoding/json"
"fmt"
"sort"
"strings"
)

//go:embed spec.json
var specJSON []byte

var spec Spec

func init() {
if err := json.Unmarshal(specJSON, &spec); err != nil {
panic(fmt.Sprintf("oci: failed to parse spec.json: %v", err))
}
}

// Spec is the top-level OCI artifact specification.
type Spec struct {
AnnotationPrefix string `json:"annotationPrefix"`
Annotations AnnotationSpec `json:"annotations"`
MediaTypes MediaTypeSpec `json:"mediaTypes"`
ReferrerTypes []ReferrerType `json:"referrerTypes"`
}

// AnnotationSpec groups manifest-level and layer-level annotation keys.
type AnnotationSpec struct {
Manifest ManifestAnnotations `json:"manifest"`
Layer LayerAnnotations `json:"layer"`
}

// ManifestAnnotations separates required from optional annotation keys.
type ManifestAnnotations struct {
Required []AnnotationKey `json:"required"`
Optional []AnnotationKey `json:"optional"`
}

// LayerAnnotations separates custom (prefixed) from standard OCI keys.
type LayerAnnotations struct {
Custom []AnnotationKey `json:"custom"`
Standard []AnnotationKey `json:"standard"`
}

// AnnotationKey pairs an annotation key with its shell variable name.
type AnnotationKey struct {
Key string `json:"key"`
Var string `json:"var"`
}

// MediaTypeSpec contains all media type families.
type MediaTypeSpec struct {
DiskFormats map[string]string `json:"diskFormats"`
CompressionSuffixes map[string]string `json:"compressionSuffixes"`
ContainerLayers map[string]string `json:"containerLayers"`
Generic map[string]string `json:"generic"`
}

// ReferrerType defines an OCI referrer artifact type with display label and default filename.
type ReferrerType struct {
ArtifactType string `json:"artifactType"`
Label string `json:"label"`
Var string `json:"var"`
Filename string `json:"filename"`
}

// Get returns the parsed OCI artifact specification.
func Get() *Spec { return &spec }

// AnnotationKey returns a fully-qualified annotation key (prefix + short name).
func (s *Spec) AnnotationKey(short string) string {
return s.AnnotationPrefix + short
}

// ReferrerFileMap returns a map from artifact type to default filename.
func (s *Spec) ReferrerFileMap() map[string]string {
m := make(map[string]string, len(s.ReferrerTypes))
for _, r := range s.ReferrerTypes {
m[r.ArtifactType] = r.Filename
}
return m
}

// AllManifestAnnotationKeys returns all annotation keys (required + optional).
func (s *Spec) AllManifestAnnotationKeys() []AnnotationKey {
result := make([]AnnotationKey, 0, len(s.Annotations.Manifest.Required)+len(s.Annotations.Manifest.Optional))
result = append(result, s.Annotations.Manifest.Required...)
result = append(result, s.Annotations.Manifest.Optional...)
return result
}

// ReferrerArtifactTypeByLabel returns the artifact type string for a given display label.
func (s *Spec) ReferrerArtifactTypeByLabel(label string) string {
for _, r := range s.ReferrerTypes {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ReferrerArtifactTypeByLabel returns "" on a miss with no signal that the label was invalid. Since the callers in inspect.go use compile-time-known string literals ("AIB Manifest", "Build Sources"), a typo or spec.json label rename will silently evaluate referrerTypes[""] to false, hiding the bug.

Consider either panicking on unknown labels (these are effectively internal constants, not user input):

func (s *Spec) ReferrerArtifactTypeByLabel(label string) string {
	for _, r := range s.ReferrerTypes {
		if r.Label == label {
			return r.ArtifactType
		}
	}
	panic(fmt.Sprintf("oci: unknown referrer label %q", label))
}

Or switching callers to use the artifact type directly from ociSpec.ReferrerTypes — the label indirection adds a fragile string coupling that the spec was meant to eliminate.

if r.Label == label {
return r.ArtifactType
}
}
return ""
}

// ShellVars generates deterministic shell variable assignments for all OCI constants.
func (s *Spec) ShellVars() string {
var b strings.Builder
b.WriteString("# --- OCI Artifact Constants (generated from spec.json) ---\n")

fmt.Fprintf(&b, "OCI_ANNOTATION_PREFIX=%q\n", s.AnnotationPrefix)

for _, ak := range s.Annotations.Manifest.Required {
fmt.Fprintf(&b, "OCI_ANN_%s=%q\n", ak.Var, s.AnnotationPrefix+ak.Key)
}
for _, ak := range s.Annotations.Manifest.Optional {
fmt.Fprintf(&b, "OCI_ANN_%s=%q\n", ak.Var, s.AnnotationPrefix+ak.Key)
}

for _, ak := range s.Annotations.Layer.Custom {
fmt.Fprintf(&b, "OCI_LAYER_ANN_%s=%q\n", ak.Var, s.AnnotationPrefix+ak.Key)
}
for _, ak := range s.Annotations.Layer.Standard {
fmt.Fprintf(&b, "OCI_LAYER_ANN_%s=%q\n", ak.Var, ak.Key)
}

writeMapSorted(&b, "OCI_MEDIA_DISK_", s.MediaTypes.DiskFormats)
writeMapSorted(&b, "OCI_COMPRESS_SUFFIX_", s.MediaTypes.CompressionSuffixes)
writeMapSorted(&b, "OCI_MEDIA_LAYER_", s.MediaTypes.ContainerLayers)
writeMapSorted(&b, "OCI_MEDIA_", s.MediaTypes.Generic)

for _, r := range s.ReferrerTypes {
fmt.Fprintf(&b, "OCI_REFERRER_TYPE_%s=%q\n", r.Var, r.ArtifactType)
fmt.Fprintf(&b, "OCI_REFERRER_FILE_%s=%q\n", r.Var, r.Filename)
}

b.WriteString("# --- End OCI Artifact Constants ---\n")
return b.String()
}

func writeMapSorted(b *strings.Builder, prefix string, m map[string]string) {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Fprintf(b, "%s%s=%q\n", prefix, strings.ToUpper(k), m[k])
}
}
Loading