Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: implement artifact manifest type #29

Merged
merged 5 commits into from
Oct 27, 2022
Merged
Show file tree
Hide file tree
Changes from 4 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
118 changes: 118 additions & 0 deletions manifest/ociartifact/artifact_manifest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package ociartifact

import (
"encoding/json"
"errors"
"fmt"

"github.com/distribution/distribution/v3"
"github.com/opencontainers/go-digest"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)

func init() {
artifactFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
m := new(DeserializedManifest)
err := m.UnmarshalJSON(b)
if err != nil {
return nil, distribution.Descriptor{}, err
}

dgst := digest.FromBytes(b)
return m, distribution.Descriptor{Digest: dgst, Size: int64(len(b)), MediaType: v1.MediaTypeArtifactManifest}, err
}
err := distribution.RegisterManifestSchema(v1.MediaTypeArtifactManifest, artifactFunc)
if err != nil {
panic(fmt.Sprintf("Unable to register artifact manifest: %s", err))
}
}

// Manifest defines an ocischema artifact manifest.
type Manifest struct {
// MediaType must be application/vnd.oci.artifact.manifest.v1+json.
MediaType string `json:"mediaType"`

// ArtifactType contains the mediaType of the referenced artifact.
// If defined, the value MUST comply with RFC 6838, including the naming
// requirements in its section 4.2, and MAY be registered with IANA.
ArtifactType string `json:"artifactType,omitempty"`

// Blobs lists descriptors for the blobs referenced by the artifact.
Blobs []distribution.Descriptor `json:"blobs,omitempty"`

// Subject specifies the descriptor of another manifest. This value is
// used by the referrers API.
Subject distribution.Descriptor `json:"subject,omitempty"`

// Annotations contains arbitrary metadata for the artifact manifest.
Annotations map[string]string `json:"annotations,omitempty"`
}

// References returns the descriptors of this artifact manifest references.
func (m Manifest) References() []distribution.Descriptor {
var references []distribution.Descriptor
references = append(references, m.Blobs...)
// if Subject exists, append it to references, this part needs more design
if m.Subject.Digest != "" {
references = append(references, m.Subject)
}
return references
}

// DeserializedManifest wraps Manifest with a copy of the original JSON.
// It satisfies the distribution.Manifest interface.
type DeserializedManifest struct {
Manifest

// canonical is the canonical byte representation of the Manifest.
canonical []byte
}

// FromStruct takes an Manifest structure, marshals it to JSON, and returns a
// DeserializedManifest which contains the manifest and its JSON representation.
func FromStruct(m Manifest) (*DeserializedManifest, error) {
var deserialized DeserializedManifest
deserialized.Manifest = m

var err error
deserialized.canonical, err = json.MarshalIndent(&m, "", " ")
return &deserialized, err
}

// UnmarshalJSON populates a new Manifest struct from JSON data.
func (m *DeserializedManifest) UnmarshalJSON(b []byte) error {
m.canonical = make([]byte, len(b))
// store manifest in canonical
copy(m.canonical, b)

// Unmarshal canonical JSON into an Manifest object
var manifest Manifest
if err := json.Unmarshal(m.canonical, &manifest); err != nil {
return err
}

if manifest.MediaType != v1.MediaTypeArtifactManifest {
return fmt.Errorf("mediaType in manifest should be '%s' not '%s'",
v1.MediaTypeArtifactManifest, manifest.MediaType)
}

m.Manifest = manifest

return nil
}

// MarshalJSON returns the contents of canonical. If canonical is empty,
// marshals the inner contents.
func (m *DeserializedManifest) MarshalJSON() ([]byte, error) {
if len(m.canonical) > 0 {
return m.canonical, nil
}

return nil, errors.New("JSON representation not initialized in DeserializedManifest")
}

// Payload returns the raw content of the artifact manifest. The contents can be used to
// calculate the content identifier.
func (m DeserializedManifest) Payload() (string, []byte, error) {
return v1.MediaTypeArtifactManifest, m.canonical, nil
}
99 changes: 99 additions & 0 deletions manifest/ociartifact/artifact_manifest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package ociartifact

import (
"bytes"
"encoding/json"
"reflect"
"testing"

"github.com/distribution/distribution/v3"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)

// Example showing an artifact manifest for an example SBOM referencing an image,
// taken from https://github.com/opencontainers/image-spec/blob/main/artifact.md.
var expectedArtifactManifestSerialization = []byte(`{
"mediaType": "application/vnd.oci.artifact.manifest.v1+json",
"artifactType": "application/vnd.example.sbom.v1",
"blobs": [
{
"mediaType": "application/gzip",
"size": 123,
"digest": "sha256:87923725d74f4bfb94c9e86d64170f7521aad8221a5de834851470ca142da630"
}
],
"subject": {
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 1234,
"digest": "sha256:cc06a2839488b8bd2a2b99dcdc03d5cfd818eed72ad08ef3cc197aac64c0d0a0"
},
"annotations": {
"org.example.sbom.format": "json",
"org.opencontainers.artifact.created": "2022-01-01T14:42:55Z"
}
}`)

func makeTestManifest() Manifest {
return Manifest{
MediaType: v1.MediaTypeArtifactManifest,
ArtifactType: "application/vnd.example.sbom.v1",
Blobs: []distribution.Descriptor{
{
MediaType: "application/gzip",
Size: 123,
Digest: "sha256:87923725d74f4bfb94c9e86d64170f7521aad8221a5de834851470ca142da630",
},
},
Subject: distribution.Descriptor{
MediaType: v1.MediaTypeImageManifest,
Size: 1234,
Digest: "sha256:cc06a2839488b8bd2a2b99dcdc03d5cfd818eed72ad08ef3cc197aac64c0d0a0",
},
Annotations: map[string]string{
"org.opencontainers.artifact.created": "2022-01-01T14:42:55Z",
"org.example.sbom.format": "json"},
}
}
func TestArtifactManifest(t *testing.T) {
testManifest := makeTestManifest()

// Test FromStruct()
deserialized, err := FromStruct(testManifest)
if err != nil {
t.Fatalf("error creating DeserializedManifest: %v", err)
}

// Test DeserializedManifest.Payload()
mediaType, canonical, _ := deserialized.Payload()
if mediaType != v1.MediaTypeArtifactManifest {
t.Fatalf("unexpected media type: %s", mediaType)
}

// Validate DeserializedManifest.canonical
p, err := json.MarshalIndent(&testManifest, "", " ")
if err != nil {
t.Fatalf("error marshaling manifest: %v", err)
}
if !bytes.Equal(p, canonical) {
t.Fatalf("manifest bytes not equal: %q != %q", string(canonical), string(p))
}
// Check that canonical field matches expected value.
if !bytes.Equal(expectedArtifactManifestSerialization, canonical) {
t.Fatalf("manifest bytes not equal: %q != %q", string(canonical), string(expectedArtifactManifestSerialization))
}

// Validate DeserializedManifest.Manifest
var unmarshalled DeserializedManifest
if err := json.Unmarshal(deserialized.canonical, &unmarshalled); err != nil {
t.Fatalf("error unmarshaling manifest: %v", err)
}
if !reflect.DeepEqual(&unmarshalled, deserialized) {
t.Fatalf("manifests are different after unmarshaling: %v != %v", unmarshalled, *deserialized)
}

// Test DeserializedManifest.References()
references := deserialized.References()
if len(references) != 2 {
t.Fatalf("unexpected number of references: %d", len(references))
}
}