From 0a174a9cdcfcfb93441b3d9243f2d3c8cb39eec4 Mon Sep 17 00:00:00 2001 From: Juan Antonio Osorio Date: Sat, 7 Feb 2026 08:39:20 +0200 Subject: [PATCH] Migrate custom OCI types to ocispec in oci/skills package Replace the dual type system where the packager used ocispec types internally but the store, registry, and helpers used custom types (ImageConfig, ImageIndex, IndexDescriptor, Platform). This eliminates friction like needing JSON re-serialization between the packager's ocispec.Image and the store's ImageConfig. Removed 7 custom struct types and 5 redundant media type constants in favor of their ocispec equivalents. Updated ParsePlatform to support os/arch/variant format, and replaced Platform.String() method with PlatformString() free function. Also fixed platform variant propagation in packager config and index construction, and added -copyright_file to mockgen directives so generated mocks include SPDX headers. Closes #24 Co-Authored-By: Claude Opus 4.6 (1M context) --- env/env.go | 2 +- env/mocks/mock_reader.go | 5 +- oci/skills/interfaces.go | 7 +- oci/skills/mediatypes.go | 108 +++++++--------------------- oci/skills/mediatypes_test.go | 90 +++++++++++++++++------ oci/skills/mocks/mock_interfaces.go | 6 +- oci/skills/packager.go | 19 ++--- oci/skills/packager_test.go | 2 +- oci/skills/registry.go | 8 +-- oci/skills/registry_test.go | 60 ++++++++-------- oci/skills/store.go | 7 +- oci/skills/store_test.go | 26 +++---- 12 files changed, 167 insertions(+), 173 deletions(-) diff --git a/env/env.go b/env/env.go index 5ff54c5..6d6481d 100644 --- a/env/env.go +++ b/env/env.go @@ -3,7 +3,7 @@ package env -//go:generate mockgen -source=env.go -destination=mocks/mock_reader.go -package=mocks Reader +//go:generate mockgen -copyright_file=../.github/license-header.txt -source=env.go -destination=mocks/mock_reader.go -package=mocks Reader import "os" diff --git a/env/mocks/mock_reader.go b/env/mocks/mock_reader.go index ed02979..17358bb 100644 --- a/env/mocks/mock_reader.go +++ b/env/mocks/mock_reader.go @@ -1,12 +1,13 @@ -// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc. +// SPDX-FileCopyrightText: Copyright 2026 Stacklok, Inc. // SPDX-License-Identifier: Apache-2.0 +// // Code generated by MockGen. DO NOT EDIT. // Source: env.go // // Generated by this command: // -// mockgen -source=env.go -destination=mocks/mock_reader.go -package=mocks Reader +// mockgen -copyright_file=../.github/license-header.txt -source=env.go -destination=mocks/mock_reader.go -package=mocks Reader // // Package mocks is a generated GoMock package. diff --git a/oci/skills/interfaces.go b/oci/skills/interfaces.go index 8acd9cc..6d32777 100644 --- a/oci/skills/interfaces.go +++ b/oci/skills/interfaces.go @@ -3,13 +3,14 @@ package skills -//go:generate mockgen -source=interfaces.go -destination=mocks/mock_interfaces.go -package=mocks +//go:generate mockgen -copyright_file=../../.github/license-header.txt -source=interfaces.go -destination=mocks/mock_interfaces.go -package=mocks import ( "context" "time" "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) // RegistryClient provides remote OCI registry operations for skills. @@ -34,7 +35,7 @@ type PackageOptions struct { // Platforms specifies target platforms for the image index. // If empty, defaults to DefaultPlatforms. - Platforms []Platform + Platforms []ocispec.Platform } // PackageResult contains the result of packaging a skill. @@ -44,5 +45,5 @@ type PackageResult struct { ConfigDigest digest.Digest LayerDigest digest.Digest Config *SkillConfig - Platforms []Platform + Platforms []ocispec.Platform } diff --git a/oci/skills/mediatypes.go b/oci/skills/mediatypes.go index 5fd7149..633473a 100644 --- a/oci/skills/mediatypes.go +++ b/oci/skills/mediatypes.go @@ -7,6 +7,8 @@ import ( "encoding/json" "fmt" "strings" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) // Artifact type for skill identification. @@ -15,29 +17,8 @@ const ( ArtifactTypeSkill = "dev.toolhive.skills.v1" ) -// OCI Image Index media type. -const ( - // MediaTypeImageIndex is the OCI image index media type. - MediaTypeImageIndex = "application/vnd.oci.image.index.v1+json" -) - -// Standard OCI media types for Kubernetes image volume compatibility. -const ( - // MediaTypeImageManifest is the OCI image manifest media type. - MediaTypeImageManifest = "application/vnd.oci.image.manifest.v1+json" - - // MediaTypeImageConfig is the standard OCI image config media type. - MediaTypeImageConfig = "application/vnd.oci.image.config.v1+json" - - // MediaTypeImageLayer is the standard OCI image layer media type. - MediaTypeImageLayer = "application/vnd.oci.image.layer.v1.tar+gzip" -) - // Annotation keys for skill metadata in manifests. const ( - // AnnotationCreated is the OCI standard annotation for creation time. - AnnotationCreated = "org.opencontainers.image.created" - // AnnotationSkillName is the annotation key for skill name. AnnotationSkillName = "dev.toolhive.skills.name" @@ -84,35 +65,8 @@ type SkillConfig struct { Files []string `json:"files"` } -// ImageConfig represents a standard OCI image configuration. -// This structure is required for Kubernetes image volume compatibility. -type ImageConfig struct { - Architecture string `json:"architecture"` - OS string `json:"os"` - Config ImageConfigData `json:"config,omitempty"` - RootFS RootFS `json:"rootfs"` - History []HistoryEntry `json:"history,omitempty"` -} - -// ImageConfigData contains container configuration including labels. -type ImageConfigData struct { - Labels map[string]string `json:"Labels,omitempty"` -} - -// RootFS describes the rootfs of the image. -type RootFS struct { - Type string `json:"type"` - DiffIDs []string `json:"diff_ids"` -} - -// HistoryEntry describes a layer in the image history. -type HistoryEntry struct { - Created string `json:"created,omitempty"` - CreatedBy string `json:"created_by,omitempty"` -} - // SkillConfigFromImageConfig extracts SkillConfig from OCI image config labels. -func SkillConfigFromImageConfig(imgConfig *ImageConfig) (*SkillConfig, error) { +func SkillConfigFromImageConfig(imgConfig *ocispec.Image) (*SkillConfig, error) { if imgConfig == nil { return nil, fmt.Errorf("image config is nil") } @@ -149,56 +103,44 @@ func SkillConfigFromImageConfig(imgConfig *ImageConfig) (*SkillConfig, error) { return config, nil } -// Platform represents a target platform for OCI artifacts. -type Platform struct { - Architecture string `json:"architecture"` - OS string `json:"os"` -} - -// String returns the platform in "os/arch" format. -func (p Platform) String() string { - return p.OS + "/" + p.Architecture +// PlatformString returns the platform in "os/arch" or "os/arch/variant" format. +func PlatformString(p ocispec.Platform) string { + s := p.OS + "/" + p.Architecture + if p.Variant != "" { + s += "/" + p.Variant + } + return s } -// ParsePlatform parses a platform string in "os/arch" format. -func ParsePlatform(s string) (Platform, error) { +// ParsePlatform parses a platform string in "os/arch" or "os/arch/variant" format. +func ParsePlatform(s string) (ocispec.Platform, error) { parts := strings.Split(s, "/") - if len(parts) != 2 { - return Platform{}, fmt.Errorf("invalid platform format: %q (expected os/arch)", s) + if len(parts) < 2 || len(parts) > 3 { + return ocispec.Platform{}, fmt.Errorf("invalid platform format: %q (expected os/arch or os/arch/variant)", s) } osName := strings.TrimSpace(parts[0]) arch := strings.TrimSpace(parts[1]) if osName == "" || arch == "" { - return Platform{}, fmt.Errorf("invalid platform format: %q (os and arch cannot be empty)", s) + return ocispec.Platform{}, fmt.Errorf("invalid platform format: %q (os and arch cannot be empty)", s) } - return Platform{OS: osName, Architecture: arch}, nil + p := ocispec.Platform{OS: osName, Architecture: arch} + if len(parts) == 3 { + variant := strings.TrimSpace(parts[2]) + if variant == "" { + return ocispec.Platform{}, fmt.Errorf("invalid platform format: %q (variant cannot be empty)", s) + } + p.Variant = variant + } + return p, nil } // DefaultPlatforms are the default platforms for skill artifacts. // These cover most Kubernetes clusters. -var DefaultPlatforms = []Platform{ +var DefaultPlatforms = []ocispec.Platform{ {OS: "linux", Architecture: "amd64"}, {OS: "linux", Architecture: "arm64"}, } -// ImageIndex represents an OCI image index (multi-platform manifest list). -type ImageIndex struct { - SchemaVersion int `json:"schemaVersion"` - MediaType string `json:"mediaType"` - ArtifactType string `json:"artifactType,omitempty"` - Manifests []IndexDescriptor `json:"manifests"` - Annotations map[string]string `json:"annotations,omitempty"` -} - -// IndexDescriptor describes a manifest in an image index. -type IndexDescriptor struct { - MediaType string `json:"mediaType"` - Digest string `json:"digest"` - Size int64 `json:"size"` - Platform *Platform `json:"platform,omitempty"` - Annotations map[string]string `json:"annotations,omitempty"` -} - // ParseRequiresAnnotation parses skill dependency references from manifest annotations. // Returns nil if the annotation is missing or invalid. func ParseRequiresAnnotation(annotations map[string]string) []string { diff --git a/oci/skills/mediatypes_test.go b/oci/skills/mediatypes_test.go index 59fd9dd..6e43dff 100644 --- a/oci/skills/mediatypes_test.go +++ b/oci/skills/mediatypes_test.go @@ -6,6 +6,7 @@ package skills import ( "testing" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -15,7 +16,7 @@ func TestSkillConfigFromImageConfig(t *testing.T) { tests := []struct { name string - config *ImageConfig + config *ocispec.Image wantName string wantErr bool wantTools []string @@ -23,8 +24,8 @@ func TestSkillConfigFromImageConfig(t *testing.T) { }{ { name: "all fields populated", - config: &ImageConfig{ - Config: ImageConfigData{ + config: &ocispec.Image{ + Config: ocispec.ImageConfig{ Labels: map[string]string{ LabelSkillName: "my-skill", LabelSkillDescription: "A test skill", @@ -41,8 +42,8 @@ func TestSkillConfigFromImageConfig(t *testing.T) { }, { name: "minimal config", - config: &ImageConfig{ - Config: ImageConfigData{ + config: &ocispec.Image{ + Config: ocispec.ImageConfig{ Labels: map[string]string{ LabelSkillName: "minimal-skill", }, @@ -57,15 +58,15 @@ func TestSkillConfigFromImageConfig(t *testing.T) { }, { name: "nil labels", - config: &ImageConfig{ - Config: ImageConfigData{Labels: nil}, + config: &ocispec.Image{ + Config: ocispec.ImageConfig{Labels: nil}, }, wantErr: true, }, { name: "missing name", - config: &ImageConfig{ - Config: ImageConfigData{ + config: &ocispec.Image{ + Config: ocispec.ImageConfig{ Labels: map[string]string{ LabelSkillDescription: "no name", }, @@ -75,8 +76,8 @@ func TestSkillConfigFromImageConfig(t *testing.T) { }, { name: "invalid allowed tools JSON", - config: &ImageConfig{ - Config: ImageConfigData{ + config: &ocispec.Image{ + Config: ocispec.ImageConfig{ Labels: map[string]string{ LabelSkillName: "bad-tools", LabelSkillAllowedTools: "not-json", @@ -87,8 +88,8 @@ func TestSkillConfigFromImageConfig(t *testing.T) { }, { name: "invalid files JSON", - config: &ImageConfig{ - Config: ImageConfigData{ + config: &ocispec.Image{ + Config: ocispec.ImageConfig{ Labels: map[string]string{ LabelSkillName: "bad-files", LabelSkillFiles: "not-json", @@ -126,18 +127,23 @@ func TestParsePlatform(t *testing.T) { tests := []struct { name string input string - want Platform + want ocispec.Platform wantErr bool }{ { name: "linux/amd64", input: "linux/amd64", - want: Platform{OS: "linux", Architecture: "amd64"}, + want: ocispec.Platform{OS: "linux", Architecture: "amd64"}, }, { name: "linux/arm64", input: "linux/arm64", - want: Platform{OS: "linux", Architecture: "arm64"}, + want: ocispec.Platform{OS: "linux", Architecture: "arm64"}, + }, + { + name: "linux/arm/v7", + input: "linux/arm/v7", + want: ocispec.Platform{OS: "linux", Architecture: "arm", Variant: "v7"}, }, { name: "no slash", @@ -146,7 +152,7 @@ func TestParsePlatform(t *testing.T) { }, { name: "too many parts", - input: "linux/amd64/v8", + input: "linux/amd64/v8/extra", wantErr: true, }, { @@ -159,6 +165,11 @@ func TestParsePlatform(t *testing.T) { input: "linux/", wantErr: true, }, + { + name: "empty variant", + input: "linux/arm/", + wantErr: true, + }, } for _, tt := range tests { @@ -179,8 +190,45 @@ func TestParsePlatform(t *testing.T) { func TestPlatformString(t *testing.T) { t.Parallel() - p := Platform{OS: "linux", Architecture: "amd64"} - assert.Equal(t, "linux/amd64", p.String()) + tests := []struct { + name string + platform ocispec.Platform + want string + }{ + { + name: "os/arch", + platform: ocispec.Platform{OS: "linux", Architecture: "amd64"}, + want: "linux/amd64", + }, + { + name: "os/arch/variant", + platform: ocispec.Platform{OS: "linux", Architecture: "arm", Variant: "v7"}, + want: "linux/arm/v7", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tt.want, PlatformString(tt.platform)) + }) + } +} + +func TestParsePlatform_PlatformString_Roundtrip(t *testing.T) { + t.Parallel() + + platforms := []ocispec.Platform{ + {OS: "linux", Architecture: "amd64"}, + {OS: "linux", Architecture: "arm64"}, + {OS: "linux", Architecture: "arm", Variant: "v7"}, + } + + for _, p := range platforms { + parsed, err := ParsePlatform(PlatformString(p)) + require.NoError(t, err) + assert.Equal(t, p, parsed) + } } func TestParseRequiresAnnotation(t *testing.T) { @@ -238,6 +286,6 @@ func TestDefaultPlatforms(t *testing.T) { t.Parallel() require.Len(t, DefaultPlatforms, 2) - assert.Equal(t, Platform{OS: "linux", Architecture: "amd64"}, DefaultPlatforms[0]) - assert.Equal(t, Platform{OS: "linux", Architecture: "arm64"}, DefaultPlatforms[1]) + assert.Equal(t, ocispec.Platform{OS: "linux", Architecture: "amd64"}, DefaultPlatforms[0]) + assert.Equal(t, ocispec.Platform{OS: "linux", Architecture: "arm64"}, DefaultPlatforms[1]) } diff --git a/oci/skills/mocks/mock_interfaces.go b/oci/skills/mocks/mock_interfaces.go index 5d234f8..ddf42e6 100644 --- a/oci/skills/mocks/mock_interfaces.go +++ b/oci/skills/mocks/mock_interfaces.go @@ -1,9 +1,13 @@ +// SPDX-FileCopyrightText: Copyright 2026 Stacklok, Inc. +// SPDX-License-Identifier: Apache-2.0 +// + // Code generated by MockGen. DO NOT EDIT. // Source: interfaces.go // // Generated by this command: // -// mockgen -source=interfaces.go -destination=mocks/mock_interfaces.go -package=mocks +// mockgen -copyright_file=../../.github/license-header.txt -source=interfaces.go -destination=mocks/mock_interfaces.go -package=mocks // // Package mocks is a generated GoMock package. diff --git a/oci/skills/packager.go b/oci/skills/packager.go index 58e2bdd..df97944 100644 --- a/oci/skills/packager.go +++ b/oci/skills/packager.go @@ -155,7 +155,7 @@ func (p *Packager) Package(ctx context.Context, skillDir string, opts PackageOpt var manifestAnnotations map[string]string for i, platform := range opts.Platforms { - platformStr := platform.String() + platformStr := PlatformString(platform) ociConfig, cfg := createOCIConfig(content, uncompressedTar, platform, opts) configBytes, err := json.Marshal(ociConfig) @@ -414,7 +414,7 @@ func createContentLayer(content *skillDirContent, opts PackageOptions) (compress func createOCIConfig( content *skillDirContent, uncompressedTar []byte, - platform Platform, + platform ocispec.Platform, opts PackageOptions, ) (*ocispec.Image, *SkillConfig) { // Collect all file paths @@ -441,11 +441,8 @@ func createOCIConfig( epoch := opts.Epoch ociConfig := &ocispec.Image{ - Created: &epoch, - Platform: ocispec.Platform{ - Architecture: platform.Architecture, - OS: platform.OS, - }, + Created: &epoch, + Platform: platform, Config: ocispec.ImageConfig{ Labels: map[string]string{ LabelSkillName: skillConfig.Name, @@ -534,20 +531,18 @@ func (p *Packager) createIndex( ) (digest.Digest, error) { manifests := make([]ocispec.Descriptor, 0, len(opts.Platforms)) for _, platform := range opts.Platforms { - platformStr := platform.String() + platformStr := PlatformString(platform) info, ok := platformManifests[platformStr] if !ok { return "", fmt.Errorf("missing manifest for platform %s", platformStr) } + p := platform // copy for pointer manifests = append(manifests, ocispec.Descriptor{ MediaType: ocispec.MediaTypeImageManifest, Digest: info.digest, Size: info.size, - Platform: &ocispec.Platform{ - Architecture: platform.Architecture, - OS: platform.OS, - }, + Platform: &p, }) } diff --git a/oci/skills/packager_test.go b/oci/skills/packager_test.go index fd860cd..03bc995 100644 --- a/oci/skills/packager_test.go +++ b/oci/skills/packager_test.go @@ -208,7 +208,7 @@ func TestPackager_Package_MultiPlatformConfigMatch(t *testing.T) { require.NoError(t, err) packager := NewPackager(store) - platforms := []Platform{ + platforms := []ocispec.Platform{ {OS: "linux", Architecture: "amd64"}, {OS: "linux", Architecture: "arm64"}, } diff --git a/oci/skills/registry.go b/oci/skills/registry.go index e8a6a4e..333279d 100644 --- a/oci/skills/registry.go +++ b/oci/skills/registry.go @@ -346,8 +346,8 @@ func (v *validatingTarget) Push(ctx context.Context, desc ocispec.Descriptor, co // validateManifestCounts checks layer/manifest counts for resource exhaustion prevention. func validateManifestCounts(mediaType string, data []byte) error { switch mediaType { - case MediaTypeImageIndex: - var index ImageIndex + case ocispec.MediaTypeImageIndex: + var index ocispec.Index if err := json.Unmarshal(data, &index); err != nil { return fmt.Errorf("parsing index: %w", err) } @@ -357,7 +357,7 @@ func validateManifestCounts(mediaType string, data []byte) error { len(index.Manifests), maxIndexManifests, ) } - case MediaTypeImageManifest: + case ocispec.MediaTypeImageManifest: var manifest ocispec.Manifest if err := json.Unmarshal(data, &manifest); err != nil { return fmt.Errorf("parsing manifest: %w", err) @@ -375,7 +375,7 @@ func validateManifestCounts(mediaType string, data []byte) error { // isManifestMediaType returns true if the media type is a manifest or index type. func isManifestMediaType(mediaType string) bool { switch mediaType { - case MediaTypeImageManifest, MediaTypeImageIndex, + case ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex, "application/vnd.docker.distribution.manifest.v2+json", "application/vnd.docker.distribution.manifest.list.v2+json": return true diff --git a/oci/skills/registry_test.go b/oci/skills/registry_test.go index 5734693..5478c7e 100644 --- a/oci/skills/registry_test.go +++ b/oci/skills/registry_test.go @@ -72,8 +72,8 @@ func TestIsManifestMediaType(t *testing.T) { mediaType string want bool }{ - {"OCI manifest", MediaTypeImageManifest, true}, - {"OCI index", MediaTypeImageIndex, true}, + {"OCI manifest", ocispec.MediaTypeImageManifest, true}, + {"OCI index", ocispec.MediaTypeImageIndex, true}, {"Docker manifest", "application/vnd.docker.distribution.manifest.v2+json", true}, {"Docker manifest list", "application/vnd.docker.distribution.manifest.list.v2+json", true}, {"OCI config", "application/vnd.oci.image.config.v1+json", false}, @@ -99,7 +99,7 @@ func TestValidatingTarget_RejectOversizedContent(t *testing.T) { oversized := make([]byte, MaxManifestSize+1) desc := ocispec.Descriptor{ - MediaType: MediaTypeImageManifest, + MediaType: ocispec.MediaTypeImageManifest, Digest: digest.FromBytes(oversized), Size: int64(len(oversized)), } @@ -117,7 +117,7 @@ func TestValidatingTarget_RejectLyingDescriptor(t *testing.T) { oversized := make([]byte, MaxManifestSize+1) desc := ocispec.Descriptor{ - MediaType: MediaTypeImageManifest, + MediaType: ocispec.MediaTypeImageManifest, Digest: digest.FromBytes(oversized), Size: 10, // lying } @@ -134,7 +134,7 @@ func TestValidatingTarget_RejectNegativeSize(t *testing.T) { vt := newValidatingTarget(memory.New()) desc := ocispec.Descriptor{ - MediaType: MediaTypeImageManifest, + MediaType: ocispec.MediaTypeImageManifest, Digest: digest.FromString("test"), Size: -1, } @@ -153,7 +153,7 @@ func TestValidatingTarget_AcceptValidContent(t *testing.T) { content := []byte(`{"schemaVersion": 2}`) desc := ocispec.Descriptor{ - MediaType: MediaTypeImageManifest, + MediaType: ocispec.MediaTypeImageManifest, Digest: digest.FromBytes(content), Size: int64(len(content)), } @@ -171,15 +171,15 @@ func TestValidateManifestCounts(t *testing.T) { t.Run("too many manifests in index", func(t *testing.T) { t.Parallel() - index := ImageIndex{ - SchemaVersion: 2, - MediaType: MediaTypeImageIndex, - Manifests: make([]IndexDescriptor, maxIndexManifests+1), + index := ocispec.Index{ + MediaType: ocispec.MediaTypeImageIndex, + Manifests: make([]ocispec.Descriptor, maxIndexManifests+1), } + index.SchemaVersion = 2 data, err := json.Marshal(index) require.NoError(t, err) - err = validateManifestCounts(MediaTypeImageIndex, data) + err = validateManifestCounts(ocispec.MediaTypeImageIndex, data) require.Error(t, err) assert.Contains(t, err.Error(), "exceeds maximum") }) @@ -187,13 +187,13 @@ func TestValidateManifestCounts(t *testing.T) { t.Run("too many layers in manifest", func(t *testing.T) { t.Parallel() manifest := ocispec.Manifest{ - MediaType: MediaTypeImageManifest, + MediaType: ocispec.MediaTypeImageManifest, Layers: make([]ocispec.Descriptor, maxManifestLayers+1), } data, err := json.Marshal(manifest) require.NoError(t, err) - err = validateManifestCounts(MediaTypeImageManifest, data) + err = validateManifestCounts(ocispec.MediaTypeImageManifest, data) require.Error(t, err) assert.Contains(t, err.Error(), "exceeds maximum") }) @@ -201,13 +201,13 @@ func TestValidateManifestCounts(t *testing.T) { t.Run("valid counts", func(t *testing.T) { t.Parallel() manifest := ocispec.Manifest{ - MediaType: MediaTypeImageManifest, + MediaType: ocispec.MediaTypeImageManifest, Layers: make([]ocispec.Descriptor, 2), } data, err := json.Marshal(manifest) require.NoError(t, err) - err = validateManifestCounts(MediaTypeImageManifest, data) + err = validateManifestCounts(ocispec.MediaTypeImageManifest, data) require.NoError(t, err) }) } @@ -266,7 +266,7 @@ func TestStoreAdapter_ResolveAndTag(t *testing.T) { adapter := newStoreAdapter(store) // Build and store a manifest - manifest := ocispec.Manifest{MediaType: MediaTypeImageManifest} + manifest := ocispec.Manifest{MediaType: ocispec.MediaTypeImageManifest} manifestBytes, err := json.Marshal(manifest) require.NoError(t, err) @@ -275,7 +275,7 @@ func TestStoreAdapter_ResolveAndTag(t *testing.T) { // Tag via adapter desc := ocispec.Descriptor{ - MediaType: MediaTypeImageManifest, + MediaType: ocispec.MediaTypeImageManifest, Digest: manifestDigest, Size: int64(len(manifestBytes)), } @@ -286,7 +286,7 @@ func TestStoreAdapter_ResolveAndTag(t *testing.T) { resolved, err := adapter.Resolve(ctx, "my-tag") require.NoError(t, err) assert.Equal(t, manifestDigest, resolved.Digest) - assert.Equal(t, MediaTypeImageManifest, resolved.MediaType) + assert.Equal(t, ocispec.MediaTypeImageManifest, resolved.MediaType) } // --- Integration tests using in-memory target --- @@ -313,16 +313,16 @@ func buildTestManifest(t *testing.T, store *Store) (digest.Digest, []byte) { require.NoError(t, err) manifest := ocispec.Manifest{ - MediaType: MediaTypeImageManifest, + MediaType: ocispec.MediaTypeImageManifest, ArtifactType: ArtifactTypeSkill, Config: ocispec.Descriptor{ - MediaType: MediaTypeImageConfig, + MediaType: ocispec.MediaTypeImageConfig, Digest: configDigest, Size: int64(len(configContent)), }, Layers: []ocispec.Descriptor{ { - MediaType: MediaTypeImageLayer, + MediaType: ocispec.MediaTypeImageLayerGzip, Digest: layerDigest, Size: int64(len(layerContent)), }, @@ -385,19 +385,19 @@ func TestPushPull_IndexRoundTrip(t *testing.T) { manifestDigest, manifestBytes := buildTestManifest(t, localStore) - index := ImageIndex{ - SchemaVersion: 2, - MediaType: MediaTypeImageIndex, - ArtifactType: ArtifactTypeSkill, - Manifests: []IndexDescriptor{ + index := ocispec.Index{ + MediaType: ocispec.MediaTypeImageIndex, + ArtifactType: ArtifactTypeSkill, + Manifests: []ocispec.Descriptor{ { - MediaType: MediaTypeImageManifest, - Digest: manifestDigest.String(), + MediaType: ocispec.MediaTypeImageManifest, + Digest: manifestDigest, Size: int64(len(manifestBytes)), - Platform: &Platform{OS: "linux", Architecture: "amd64"}, + Platform: &ocispec.Platform{OS: "linux", Architecture: "amd64"}, }, }, } + index.SchemaVersion = 2 indexBytes, err := json.Marshal(index) require.NoError(t, err) @@ -426,7 +426,7 @@ func TestPushPull_IndexRoundTrip(t *testing.T) { pulledIndex, err := pullStore.GetIndex(ctx, pulledDigest) require.NoError(t, err) require.Len(t, pulledIndex.Manifests, 1) - assert.Equal(t, manifestDigest.String(), pulledIndex.Manifests[0].Digest) + assert.Equal(t, manifestDigest, pulledIndex.Manifests[0].Digest) // Verify manifest is also present pulledManifest, err := pullStore.GetManifest(ctx, manifestDigest) diff --git a/oci/skills/store.go b/oci/skills/store.go index 381f9ba..3ea88db 100644 --- a/oci/skills/store.go +++ b/oci/skills/store.go @@ -13,6 +13,7 @@ import ( "github.com/adrg/xdg" "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) // Store provides local OCI artifact storage. @@ -179,7 +180,7 @@ func (s *Store) ListTags(_ context.Context) ([]string, error) { } // GetIndex retrieves and parses an image index by digest. -func (s *Store) GetIndex(_ context.Context, d digest.Digest) (*ImageIndex, error) { +func (s *Store) GetIndex(_ context.Context, d digest.Digest) (*ocispec.Index, error) { s.mu.RLock() defer s.mu.RUnlock() @@ -188,7 +189,7 @@ func (s *Store) GetIndex(_ context.Context, d digest.Digest) (*ImageIndex, error return nil, fmt.Errorf("getting index: %w", err) } - var index ImageIndex + var index ocispec.Index if err := json.Unmarshal(data, &index); err != nil { return nil, fmt.Errorf("parsing index: %w", err) } @@ -214,7 +215,7 @@ func (s *Store) IsIndex(_ context.Context, d digest.Digest) (bool, error) { return false, fmt.Errorf("parsing media type: %w", err) } - return header.MediaType == MediaTypeImageIndex, nil + return header.MediaType == ocispec.MediaTypeImageIndex, nil } // Root returns the store root directory. diff --git a/oci/skills/store_test.go b/oci/skills/store_test.go index d481b49..a106bcc 100644 --- a/oci/skills/store_test.go +++ b/oci/skills/store_test.go @@ -12,6 +12,7 @@ import ( "testing" "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -207,18 +208,18 @@ func TestStore_GetIndex(t *testing.T) { ctx := context.Background() - idx := &ImageIndex{ - SchemaVersion: 2, - MediaType: MediaTypeImageIndex, - Manifests: []IndexDescriptor{ + idx := &ocispec.Index{ + MediaType: ocispec.MediaTypeImageIndex, + Manifests: []ocispec.Descriptor{ { - MediaType: MediaTypeImageManifest, - Digest: "sha256:abc123", + MediaType: ocispec.MediaTypeImageManifest, + Digest: digest.FromString("test"), Size: 100, - Platform: &Platform{OS: "linux", Architecture: "amd64"}, + Platform: &ocispec.Platform{OS: "linux", Architecture: "amd64"}, }, }, } + idx.SchemaVersion = 2 data, err := json.Marshal(idx) require.NoError(t, err) @@ -229,7 +230,7 @@ func TestStore_GetIndex(t *testing.T) { got, err := store.GetIndex(ctx, d) require.NoError(t, err) assert.Equal(t, 2, got.SchemaVersion) - assert.Equal(t, MediaTypeImageIndex, got.MediaType) + assert.Equal(t, ocispec.MediaTypeImageIndex, got.MediaType) require.Len(t, got.Manifests, 1) assert.Equal(t, "linux", got.Manifests[0].Platform.OS) assert.Equal(t, "amd64", got.Manifests[0].Platform.Architecture) @@ -244,10 +245,11 @@ func TestStore_IsIndex(t *testing.T) { ctx := context.Background() // Store an image index - indexData, err := json.Marshal(ImageIndex{ - SchemaVersion: 2, - MediaType: MediaTypeImageIndex, - }) + idx := ocispec.Index{ + MediaType: ocispec.MediaTypeImageIndex, + } + idx.SchemaVersion = 2 + indexData, err := json.Marshal(idx) require.NoError(t, err) indexDigest, err := store.PutManifest(ctx, indexData)