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)