Skip to content

Commit

Permalink
feat!: implement artifact manifest type (#29)
Browse files Browse the repository at this point in the history
Implemented ArtifactManifest and related structs and functions. Unit
tests included.

Part 2 of #21 

Signed-off-by: wangxiaoxuan273 <[email protected]>
  • Loading branch information
wangxiaoxuan273 authored Oct 27, 2022
1 parent f71877f commit 0fce2c9
Show file tree
Hide file tree
Showing 2 changed files with 216 additions and 0 deletions.
117 changes: 117 additions & 0 deletions manifest/ociartifact/manifest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
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 m.Subject != nil {
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/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))
}
}

0 comments on commit 0fce2c9

Please sign in to comment.