Skip to content
Merged
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
59 changes: 59 additions & 0 deletions pkg/content/artifact.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package content

import (
"encoding/json"
"fmt"

v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/partial"
)

// artifactImage wraps a v1.Image so its serialized manifest includes an
// "artifactType" field. The underlying config and layers are preserved
// unchanged, which is required for tarball round-trips (the Docker tarball
// format relies on DiffIDs in the config to map layers).
//
// This is necessary because go-containerregistry's v1.Manifest struct does not
// have an artifactType field.
//
// See https://github.com/opencontainers/image-spec/blob/v1.1.1/manifest.md#guidelines-for-artifact-usage
type artifactImage struct {
v1.Image
artifactType string
}

// NewArtifactImage wraps an image so its serialized manifest includes the
// given artifactType.
func NewArtifactImage(base v1.Image, artifactType string) v1.Image {
return &artifactImage{Image: base, artifactType: artifactType}
}

// RawManifest returns the manifest with artifactType injected.
func (a *artifactImage) RawManifest() ([]byte, error) {
raw, err := a.Image.RawManifest()
if err != nil {
return nil, fmt.Errorf("getting raw manifest: %w", err)
}

var manifest map[string]json.RawMessage
if err := json.Unmarshal(raw, &manifest); err != nil {
return nil, fmt.Errorf("unmarshaling manifest: %w", err)
}

at, err := json.Marshal(a.artifactType)
if err != nil {
return nil, fmt.Errorf("marshaling artifactType: %w", err)
}
manifest["artifactType"] = at

return json.Marshal(manifest)
}

// Digest returns the sha256 of the modified manifest.
func (a *artifactImage) Digest() (v1.Hash, error) { return partial.Digest(a) }

// Manifest parses the modified raw manifest into a v1.Manifest.
func (a *artifactImage) Manifest() (*v1.Manifest, error) { return partial.Manifest(a) }

// Size returns the size of the modified manifest.
func (a *artifactImage) Size() (int64, error) { return partial.Size(a) }
50 changes: 50 additions & 0 deletions pkg/content/artifact_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package content

import (
"encoding/json"
"testing"

"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/static"
"github.com/google/go-containerregistry/pkg/v1/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestNewArtifactImage(t *testing.T) {
t.Parallel()

const testArtifactType = "application/vnd.test.artifact+json"

layer := static.NewLayer([]byte("test content"), "application/yaml")
base, err := mutate.AppendLayers(empty.Image, layer)
require.NoError(t, err)
base = mutate.MediaType(base, types.OCIManifestSchema1)

artifact := NewArtifactImage(base, testArtifactType)

// Manifest must contain artifactType.
raw, err := artifact.RawManifest()
require.NoError(t, err)

var manifest map[string]json.RawMessage
require.NoError(t, json.Unmarshal(raw, &manifest))

var got string
require.Contains(t, manifest, "artifactType")
require.NoError(t, json.Unmarshal(manifest["artifactType"], &got))
assert.Equal(t, testArtifactType, got)

// Config must be preserved from the base image (not replaced with {}).
rawConfig, err := artifact.RawConfigFile()
require.NoError(t, err)
baseConfig, err := base.RawConfigFile()
require.NoError(t, err)
assert.Equal(t, baseConfig, rawConfig)

// Layers must still be accessible.
layers, err := artifact.Layers()
require.NoError(t, err)
assert.Len(t, layers, 1)
}
4 changes: 4 additions & 0 deletions pkg/remote/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ func Push(reference string) error {
img = mutate.Annotations(img, metadata.Annotations).(v1.Image)
}

// Wrap as a spec-compliant OCI artifact so the pushed manifest includes
// artifactType and an empty config descriptor.
img = content.NewArtifactImage(img, "application/vnd.docker.cagent.config.v1+json")

ref, err := name.ParseReference(reference)
if err != nil {
return fmt.Errorf("parsing registry reference %s: %w", reference, err)
Expand Down
Loading