forked from distribution/distribution
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat!: implement artifact manifest type (#29)
Implemented ArtifactManifest and related structs and functions. Unit tests included. Part 2 of #21 Signed-off-by: wangxiaoxuan273 <[email protected]>
- Loading branch information
1 parent
f71877f
commit 0fce2c9
Showing
2 changed files
with
216 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
} |