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",
}

// 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
151 changes: 151 additions & 0 deletions internal/common/oci/spec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// 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.
// Panics on unknown labels since callers use compile-time-known string literals.
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))
}

// 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, "export OCI_ANNOTATION_PREFIX=%q\n", s.AnnotationPrefix)

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

for _, ak := range s.Annotations.Layer.Custom {
fmt.Fprintf(&b, "export OCI_LAYER_ANN_%s=%q\n", ak.Var, s.AnnotationPrefix+ak.Key)
}
for _, ak := range s.Annotations.Layer.Standard {
fmt.Fprintf(&b, "export 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, "export OCI_REFERRER_TYPE_%s=%q\n", r.Var, r.ArtifactType)
fmt.Fprintf(&b, "export 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, "export %s%s=%q\n", prefix, strings.ToUpper(k), m[k])
}
}
Loading
Loading