Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit ebb2819

Browse files
committedFeb 21, 2024··
Add ec.oci.image_manifest rego function
This commit adds a new custom rego function, `ec.oci.image_manifest`. This function retrieves the Image Manifest from an OCI registry. (It does not download the image, just its manifest.) The main use case this is trying to achieve is validating image references that may occur in the SLSA Provenance attestation of an image being validated. For example, the SLSA Provenance may contain a link to the corresponding source container image. This function allows policy rules to be created to verify that such references actually exists. Ref: EC-235 Signed-off-by: Luiz Carvalho <lucarval@redhat.com>
1 parent 379e101 commit ebb2819

File tree

7 files changed

+1582
-4
lines changed

7 files changed

+1582
-4
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package manifest
2+
3+
import rego.v1
4+
5+
# METADATA
6+
# custom:
7+
# short_name: match
8+
deny contains result if {
9+
manifest := ec.oci.image_manifest(input.image.ref)
10+
not manifest_matches(manifest)
11+
12+
result := {
13+
"code": "manifest.match",
14+
"msg": json.marshal(manifest),
15+
}
16+
}
17+
18+
manifest_matches(manifest) if {
19+
manifest.annotations["org.opencontainers.image.base.name"] != ""
20+
manifest.mediaType == "application/vnd.docker.distribution.manifest.v2+json"
21+
manifest.schemaVersion == 2
22+
non_empty_descriptor(manifest.config, "application/vnd.docker.container.image.v1+json")
23+
count(manifest.layers) == 2
24+
non_empty_descriptor(manifest.layers[0], "application/vnd.docker.image.rootfs.diff.tar.gzip")
25+
non_empty_descriptor(manifest.layers[1], "application/vnd.docker.image.rootfs.diff.tar.gzip")
26+
}
27+
28+
non_empty_descriptor(descriptor, media_type) if {
29+
descriptor.annotations == {}
30+
descriptor.artifactType == ""
31+
descriptor.data == ""
32+
count(descriptor.digest) == count("sha256:deb8b4cce42d6aa1be200e1c175c519433c116cd8f52f43548623ce2d6782366")
33+
descriptor.mediaType == media_type
34+
descriptor.size > 0
35+
descriptor.urls == []
36+
}

‎features/__snapshots__/validate_image.snap

+77
Original file line numberDiff line numberDiff line change
@@ -3344,3 +3344,80 @@ time="${TIMESTAMP}" level=error msg="Parsing PURL \"this-is-not-a-valid-purl\" f
33443344
Error: success criteria not met
33453345

33463346
---
3347+
3348+
[fetch OCI image manifest:stdout - 1]
3349+
{
3350+
"success": true,
3351+
"components": [
3352+
{
3353+
"name": "Unnamed",
3354+
"containerImage": "${REGISTRY}/acceptance/oci-image-manifest@sha256:${REGISTRY_acceptance/oci-image-manifest:latest_DIGEST}",
3355+
"source": {},
3356+
"successes": [
3357+
{
3358+
"msg": "Pass",
3359+
"metadata": {
3360+
"code": "builtin.attestation.signature_check"
3361+
}
3362+
},
3363+
{
3364+
"msg": "Pass",
3365+
"metadata": {
3366+
"code": "builtin.attestation.syntax_check"
3367+
}
3368+
},
3369+
{
3370+
"msg": "Pass",
3371+
"metadata": {
3372+
"code": "builtin.image.signature_check"
3373+
}
3374+
},
3375+
{
3376+
"msg": "Pass",
3377+
"metadata": {
3378+
"code": "manifest.match"
3379+
}
3380+
}
3381+
],
3382+
"success": true,
3383+
"signatures": [
3384+
{
3385+
"keyid": "",
3386+
"sig": "${IMAGE_SIGNATURE_acceptance/oci-image-manifest}"
3387+
}
3388+
],
3389+
"attestations": [
3390+
{
3391+
"type": "https://in-toto.io/Statement/v0.1",
3392+
"predicateType": "https://slsa.dev/provenance/v0.2",
3393+
"predicateBuildType": "https://tekton.dev/attestations/chains/pipelinerun@v2",
3394+
"signatures": [
3395+
{
3396+
"keyid": "",
3397+
"sig": "${ATTESTATION_SIGNATURE_acceptance/oci-image-manifest}"
3398+
}
3399+
]
3400+
}
3401+
]
3402+
}
3403+
],
3404+
"key": "${known_PUBLIC_KEY_JSON}",
3405+
"policy": {
3406+
"sources": [
3407+
{
3408+
"policy": [
3409+
"git::https://${GITHOST}/git/oci-image-manifest-policy"
3410+
]
3411+
}
3412+
],
3413+
"rekorUrl": "${REKOR}",
3414+
"publicKey": "${known_PUBLIC_KEY}"
3415+
},
3416+
"ec-version": "${EC_VERSION}",
3417+
"effective-time": "${TIMESTAMP}"
3418+
}
3419+
---
3420+
3421+
[fetch OCI image manifest:stderr - 1]
3422+
3423+
---

‎features/validate_image.feature

+25
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,31 @@ Feature: evaluate enterprise contract
926926
Then the exit status should be 0
927927
Then the output should match the snapshot
928928

929+
Scenario: fetch OCI image manifest
930+
Given a key pair named "known"
931+
Given an image named "acceptance/oci-image-manifest"
932+
Given a valid image signature of "acceptance/oci-image-manifest" image signed by the "known" key
933+
Given a valid Rekor entry for image signature of "acceptance/oci-image-manifest"
934+
Given a valid attestation of "acceptance/oci-image-manifest" signed by the "known" key
935+
Given a valid Rekor entry for attestation of "acceptance/oci-image-manifest"
936+
Given a git repository named "oci-image-manifest-policy" with
937+
| main.rego | examples/oci_image_manifest.rego |
938+
Given policy configuration named "ec-policy" with specification
939+
"""
940+
{
941+
"sources": [
942+
{
943+
"policy": [
944+
"git::https://${GITHOST}/git/oci-image-manifest-policy"
945+
]
946+
}
947+
]
948+
}
949+
"""
950+
When ec command is run with "validate image --image ${REGISTRY}/acceptance/oci-image-manifest --policy acceptance/ec-policy --public-key ${known_PUBLIC_KEY} --rekor-url ${REKOR} --show-successes"
951+
Then the exit status should be 0
952+
Then the output should match the snapshot
953+
929954
Scenario: tracing and debug logging
930955
Given a key pair named "trace_debug"
931956
And an image named "acceptance/trace-debug"

‎internal/evaluator/__snapshots__/rego_test.snap

+869
Large diffs are not rendered by default.

‎internal/evaluator/documentation/__snapshots__/documentation_test.snap

+209-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,214 @@ nondeterministic: true
1717
---
1818

1919
[TestWriteBuiltinsToYAML - 2]
20+
decl:
21+
args:
22+
- description: OCI image reference
23+
name: ref
24+
type: string
25+
result:
26+
description: the Image Manifest object
27+
name: object
28+
static:
29+
- key: annotations
30+
value:
31+
dynamic:
32+
key:
33+
type: string
34+
value:
35+
type: string
36+
type: object
37+
- key: config
38+
value:
39+
static:
40+
- key: annotations
41+
value:
42+
dynamic:
43+
key:
44+
type: string
45+
value:
46+
type: string
47+
type: object
48+
- key: artifactType
49+
value:
50+
type: string
51+
- key: data
52+
value:
53+
type: string
54+
- key: digest
55+
value:
56+
type: string
57+
- key: mediaType
58+
value:
59+
type: string
60+
- key: platform
61+
value:
62+
static:
63+
- key: architecture
64+
value:
65+
type: string
66+
- key: features
67+
value:
68+
static:
69+
- type: string
70+
type: array
71+
- key: os
72+
value:
73+
type: string
74+
- key: os.features
75+
value:
76+
static:
77+
- type: string
78+
type: array
79+
- key: os.version
80+
value:
81+
type: string
82+
- key: variant
83+
value:
84+
type: string
85+
type: object
86+
- key: size
87+
value:
88+
type: number
89+
- key: urls
90+
value:
91+
static:
92+
- type: string
93+
type: array
94+
type: object
95+
- key: layers
96+
value:
97+
static:
98+
- static:
99+
- key: annotations
100+
value:
101+
dynamic:
102+
key:
103+
type: string
104+
value:
105+
type: string
106+
type: object
107+
- key: artifactType
108+
value:
109+
type: string
110+
- key: data
111+
value:
112+
type: string
113+
- key: digest
114+
value:
115+
type: string
116+
- key: mediaType
117+
value:
118+
type: string
119+
- key: platform
120+
value:
121+
static:
122+
- key: architecture
123+
value:
124+
type: string
125+
- key: features
126+
value:
127+
static:
128+
- type: string
129+
type: array
130+
- key: os
131+
value:
132+
type: string
133+
- key: os.features
134+
value:
135+
static:
136+
- type: string
137+
type: array
138+
- key: os.version
139+
value:
140+
type: string
141+
- key: variant
142+
value:
143+
type: string
144+
type: object
145+
- key: size
146+
value:
147+
type: number
148+
- key: urls
149+
value:
150+
static:
151+
- type: string
152+
type: array
153+
type: object
154+
type: array
155+
- key: mediaType
156+
value:
157+
type: string
158+
- key: schemaVersion
159+
value:
160+
type: number
161+
- key: subject
162+
value:
163+
static:
164+
- key: annotations
165+
value:
166+
dynamic:
167+
key:
168+
type: string
169+
value:
170+
type: string
171+
type: object
172+
- key: artifactType
173+
value:
174+
type: string
175+
- key: data
176+
value:
177+
type: string
178+
- key: digest
179+
value:
180+
type: string
181+
- key: mediaType
182+
value:
183+
type: string
184+
- key: platform
185+
value:
186+
static:
187+
- key: architecture
188+
value:
189+
type: string
190+
- key: features
191+
value:
192+
static:
193+
- type: string
194+
type: array
195+
- key: os
196+
value:
197+
type: string
198+
- key: os.features
199+
value:
200+
static:
201+
- type: string
202+
type: array
203+
- key: os.version
204+
value:
205+
type: string
206+
- key: variant
207+
value:
208+
type: string
209+
type: object
210+
- key: size
211+
value:
212+
type: number
213+
- key: urls
214+
value:
215+
static:
216+
- type: string
217+
type: array
218+
type: object
219+
type: object
220+
type: function
221+
description: Fetch an Image Manifest from an OCI registry.
222+
name: ec.oci.image_manifest
223+
nondeterministic: true
224+
225+
---
226+
227+
[TestWriteBuiltinsToYAML - 3]
20228
decl:
21229
args:
22230
- description: the PURL
@@ -32,7 +240,7 @@ name: ec.purl.is_valid
32240

33241
---
34242

35-
[TestWriteBuiltinsToYAML - 3]
243+
[TestWriteBuiltinsToYAML - 4]
36244
decl:
37245
args:
38246
- description: the PURL

‎internal/evaluator/rego.go

+190-3
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@
2323
import (
2424
"bytes"
2525
"crypto/sha256"
26+
"encoding/json"
2627
"fmt"
2728
"io"
2829

2930
"github.com/google/go-containerregistry/pkg/authn"
3031
"github.com/google/go-containerregistry/pkg/name"
32+
v1 "github.com/google/go-containerregistry/pkg/v1"
3133
"github.com/google/go-containerregistry/pkg/v1/remote"
3234
"github.com/open-policy-agent/opa/ast"
3335
"github.com/open-policy-agent/opa/rego"
@@ -38,9 +40,12 @@
3840
"github.com/enterprise-contract/ec-cli/internal/fetchers/oci"
3941
)
4042

41-
const ociBlobName = "ec.oci.blob"
42-
const purlIsValidName = "ec.purl.is_valid"
43-
const purlParseName = "ec.purl.parse"
43+
const (
44+
ociBlobName = "ec.oci.blob"
45+
ociImageManifestName = "ec.oci.image_manifest"
46+
purlIsValidName = "ec.purl.is_valid"
47+
purlParseName = "ec.purl.parse"
48+
)
4449

4550
func registerOCIBlob() {
4651
decl := rego.Function{
@@ -70,6 +75,79 @@
7075
})
7176
}
7277

78+
func registerOCIImageManifest() {
79+
platform := types.NewObject(
80+
[]*types.StaticProperty{
81+
{Key: "architecture", Value: types.S},
82+
{Key: "os", Value: types.S},
83+
{Key: "os.version", Value: types.S},
84+
{Key: "os.features", Value: types.NewArray([]types.Type{types.S}, nil)},
85+
{Key: "variant", Value: types.S},
86+
{Key: "features", Value: types.NewArray([]types.Type{types.S}, nil)},
87+
},
88+
nil,
89+
)
90+
91+
// annotations represents the map[string]string rego type
92+
annotations := types.NewObject(nil, types.NewDynamicProperty(types.S, types.S))
93+
94+
descriptor := types.NewObject(
95+
[]*types.StaticProperty{
96+
{Key: "mediaType", Value: types.S},
97+
{Key: "size", Value: types.N},
98+
{Key: "digest", Value: types.S},
99+
{Key: "data", Value: types.S},
100+
{Key: "urls", Value: types.NewArray(
101+
[]types.Type{types.S}, nil,
102+
)},
103+
{Key: "annotations", Value: annotations},
104+
{Key: "platform", Value: platform},
105+
{Key: "artifactType", Value: types.S},
106+
},
107+
nil,
108+
)
109+
110+
decl := rego.Function{
111+
Name: ociImageManifestName,
112+
Decl: types.NewFunction(
113+
types.Args(
114+
types.Named("ref", types.S).Description("OCI image reference"),
115+
),
116+
types.Named("object", types.NewObject(
117+
[]*types.StaticProperty{
118+
// Specifying the properties like this ensure the compiler catches typos when
119+
// evaluating rego functions.
120+
{Key: "schemaVersion", Value: types.N},
121+
{Key: "mediaType", Value: types.S},
122+
{Key: "config", Value: descriptor},
123+
{Key: "layers", Value: types.NewArray(
124+
[]types.Type{descriptor}, nil,
125+
)},
126+
{Key: "annotations", Value: annotations},
127+
{Key: "subject", Value: descriptor},
128+
},
129+
nil,
130+
)).Description("the Image Manifest object"),
131+
),
132+
// As per the documentation, enable memoization to ensure function evaluation is
133+
// deterministic. But also mark it as non-deterministic because it does rely on external
134+
// entities, i.e. OCI registry. https://www.openpolicyagent.org/docs/latest/extensions/
135+
Memoize: true,
136+
Nondeterministic: true,
137+
}
138+
139+
rego.RegisterBuiltin1(&decl, ociImageManifest)
140+
// Due to https://github.com/open-policy-agent/opa/issues/6449, we cannot set a description for
141+
// the custom function through the call above. As a workaround we re-register the function with
142+
// a declaration that does include the description.
143+
ast.RegisterBuiltin(&ast.Builtin{
144+
Name: decl.Name,
145+
Description: "Fetch an Image Manifest from an OCI registry.",
146+
Decl: decl.Decl,
147+
Nondeterministic: decl.Nondeterministic,
148+
})
149+
}
150+
73151
func registerPURLIsValid() {
74152
decl := rego.Function{
75153
Name: purlIsValidName,
@@ -204,6 +282,114 @@
204282
return ast.StringTerm(blob.String()), nil
205283
}
206284

285+
func ociImageManifest(bctx rego.BuiltinContext, a *ast.Term) (*ast.Term, error) {
286+
log := log.WithField("rfunc", ociImageManifestName)
287+
uri, ok := a.Value.(ast.String)
288+
if !ok {
289+
return nil, nil
290+
}
291+
292+
ref, err := name.NewDigest(string(uri))
293+
if err != nil {
294+
log.Errorf("new digest: %s", err)
295+
return nil, nil
296+
}
297+
298+
opts := []remote.Option{
299+
remote.WithTransport(remote.DefaultTransport),
300+
remote.WithContext(bctx.Context),
301+
remote.WithAuthFromKeychain(authn.DefaultKeychain),
302+
}
303+
304+
image, err := oci.NewClient(bctx.Context).Image(ref, opts...)
305+
if err != nil {
306+
log.Errorf("fetch image: %s", err)
307+
return nil, nil
308+
}
309+
310+
manifest, err := image.Manifest()
311+
if err != nil {
312+
log.Errorf("fetch manifest: %s", err)
313+
return nil, nil
314+
}
315+
316+
if manifest == nil {
317+
log.Error("manifest is nil")
318+
return nil, nil
319+
}
320+
321+
layers := []*ast.Term{}
322+
for _, layer := range manifest.Layers {
323+
layers = append(layers, newDescriptorTerm(layer))
324+
}
325+
326+
manifestTerms := [][2]*ast.Term{
327+
ast.Item(ast.StringTerm("schemaVersion"), ast.NumberTerm(json.Number(fmt.Sprintf("%d", manifest.SchemaVersion)))),
328+
ast.Item(ast.StringTerm("mediaType"), ast.StringTerm(string(manifest.MediaType))),
329+
ast.Item(ast.StringTerm("config"), newDescriptorTerm(manifest.Config)),
330+
ast.Item(ast.StringTerm("layers"), ast.ArrayTerm(layers...)),
331+
ast.Item(ast.StringTerm("annotations"), newAnnotationsTerm(manifest.Annotations)),
332+
}
333+
334+
if s := manifest.Subject; s != nil {
335+
manifestTerms = append(manifestTerms, ast.Item(ast.StringTerm("subject"), newDescriptorTerm(*s)))
336+
}
337+
338+
return ast.ObjectTerm(manifestTerms...), nil
339+
}
340+
341+
func newPlatformTerm(p v1.Platform) *ast.Term {
342+
osFeatures := []*ast.Term{}
343+
for _, f := range p.OSFeatures {
344+
osFeatures = append(osFeatures, ast.StringTerm(f))
345+
}
346+
347+
features := []*ast.Term{}
348+
for _, f := range p.Features {
349+
features = append(features, ast.StringTerm(f))
350+
}
351+
352+
return ast.ObjectTerm(
353+
ast.Item(ast.StringTerm("architecture"), ast.StringTerm(p.Architecture)),
354+
ast.Item(ast.StringTerm("os"), ast.StringTerm(p.OS)),
355+
ast.Item(ast.StringTerm("os.version"), ast.StringTerm(p.OSVersion)),
356+
ast.Item(ast.StringTerm("os.features"), ast.ArrayTerm(osFeatures...)),
357+
ast.Item(ast.StringTerm("variant"), ast.StringTerm(p.Variant)),
358+
ast.Item(ast.StringTerm("features"), ast.ArrayTerm(features...)),
359+
)
360+
}
361+
362+
func newDescriptorTerm(d v1.Descriptor) *ast.Term {
363+
urls := []*ast.Term{}
364+
for _, url := range d.URLs {
365+
urls = append(urls, ast.StringTerm(url))
366+
}
367+
368+
dTerms := [][2]*ast.Term{
369+
ast.Item(ast.StringTerm("mediaType"), ast.StringTerm(string(d.MediaType))),
370+
ast.Item(ast.StringTerm("size"), ast.NumberTerm(json.Number(fmt.Sprintf("%d", d.Size)))),
371+
ast.Item(ast.StringTerm("digest"), ast.StringTerm(d.Digest.String())),
372+
ast.Item(ast.StringTerm("data"), ast.StringTerm(string(d.Data))),
373+
ast.Item(ast.StringTerm("urls"), ast.ArrayTerm(urls...)),
374+
ast.Item(ast.StringTerm("annotations"), newAnnotationsTerm(d.Annotations)),
375+
ast.Item(ast.StringTerm("artifactType"), ast.StringTerm(d.ArtifactType)),
376+
}
377+
378+
if d.Platform != nil {
379+
dTerms = append(dTerms, ast.Item(ast.StringTerm("platform"), newPlatformTerm(*d.Platform)))
380+
}
381+
382+
return ast.ObjectTerm(dTerms...)
383+
}
384+
385+
func newAnnotationsTerm(annotations map[string]string) *ast.Term {
386+
annotationTerms := [][2]*ast.Term{}
387+
for key, value := range annotations {
388+
annotationTerms = append(annotationTerms, ast.Item(ast.StringTerm(key), ast.StringTerm(value)))
389+
}
390+
return ast.ObjectTerm(annotationTerms...)
391+
}
392+
207393
func purlIsValid(bctx rego.BuiltinContext, a *ast.Term) (*ast.Term, error) {
208394
uri, ok := a.Value.(ast.String)
209395
if !ok {
@@ -248,6 +434,7 @@
248434

249435
func init() {
250436
registerOCIBlob()
437+
registerOCIImageManifest()
251438
registerPURLIsValid()
252439
registerPURLParse()
253440
}

‎internal/evaluator/rego_test.go

+176
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ import (
2323
"errors"
2424
"testing"
2525

26+
"github.com/gkampitakis/go-snaps/snaps"
27+
v1 "github.com/google/go-containerregistry/pkg/v1"
28+
v1fake "github.com/google/go-containerregistry/pkg/v1/fake"
2629
"github.com/google/go-containerregistry/pkg/v1/static"
2730
"github.com/google/go-containerregistry/pkg/v1/types"
2831
"github.com/open-policy-agent/opa/ast"
@@ -106,6 +109,178 @@ func TestOCIBlob(t *testing.T) {
106109
}
107110
}
108111

112+
func TestOCIImageManifest(t *testing.T) {
113+
cases := []struct {
114+
name string
115+
ref *ast.Term
116+
manifest *v1.Manifest
117+
imageErr error
118+
manifestErr error
119+
wantErr bool
120+
}{
121+
{
122+
name: "complete image manifest",
123+
ref: ast.StringTerm("registry.local/spam:latest@sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b"),
124+
manifest: &v1.Manifest{
125+
SchemaVersion: 2,
126+
MediaType: types.OCIManifestSchema1,
127+
Config: v1.Descriptor{
128+
MediaType: types.OCIConfigJSON,
129+
Size: 123,
130+
Digest: v1.Hash{
131+
Algorithm: "sha256",
132+
Hex: "4e388ab32b10dc8dbc7e28144f552830adc74787c1e2c0824032078a79f227fb",
133+
},
134+
Data: []byte(`{"data": "config"}`),
135+
URLs: []string{"https://config-1.local/spam", "https://config-2.local/spam"},
136+
Annotations: map[string]string{
137+
"config.annotation.1": "config.annotation.value.1",
138+
"config.annotation.2": "config.annotation.value.2",
139+
},
140+
Platform: &v1.Platform{
141+
Architecture: "arch",
142+
OS: "os",
143+
OSVersion: "os-version",
144+
OSFeatures: []string{"os-feature-1", "os-feature-2"},
145+
Variant: "variant",
146+
Features: []string{"feature-1", "feature-2"},
147+
},
148+
ArtifactType: "artifact-type",
149+
},
150+
Layers: []v1.Descriptor{
151+
{
152+
MediaType: types.OCILayer,
153+
Size: 9999,
154+
Digest: v1.Hash{
155+
Algorithm: "sha256",
156+
Hex: "325392e8dd2826a53a9a35b7a7f8d71683cd27ebc2c73fee85dab673bc909b67",
157+
},
158+
Data: []byte(`{"data": "layer"}`),
159+
URLs: []string{"https://layer-1.local/spam", "https://layer-2.local/spam"},
160+
Annotations: map[string]string{
161+
"layer.annotation.1": "layer.annotation.value.1",
162+
"layer.annotation.2": "layer.annotation.value.2",
163+
},
164+
Platform: &v1.Platform{
165+
Architecture: "arch",
166+
OS: "os",
167+
OSVersion: "os-version",
168+
OSFeatures: []string{"os-feature-1", "os-feature-2"},
169+
Variant: "variant",
170+
Features: []string{"feature-1", "feature-2"},
171+
},
172+
ArtifactType: "artifact-type",
173+
},
174+
},
175+
Annotations: map[string]string{
176+
"manifest.annotation.1": "config.annotation.value.1",
177+
"manifest.annotation.2": "config.annotation.value.2",
178+
},
179+
Subject: &v1.Descriptor{
180+
MediaType: types.OCIManifestSchema1,
181+
Size: 8888,
182+
Digest: v1.Hash{
183+
Algorithm: "sha256",
184+
Hex: "d9298a10d1b0735837dc4bd85dac641b0f3cef27a47e5d53a54f2f3f5b2fcffa",
185+
},
186+
Data: []byte(`{"data": "subject"}`),
187+
URLs: []string{"https://subject-1.local/spam", "https://subject-2.local/spam"},
188+
Annotations: map[string]string{
189+
"subject.annotation.1": "subject.annotation.value.1",
190+
"subject.annotation.2": "subject.annotation.value.2",
191+
},
192+
Platform: &v1.Platform{
193+
Architecture: "arch",
194+
OS: "os",
195+
OSVersion: "os-version",
196+
OSFeatures: []string{"os-feature-1", "os-feature-2"},
197+
Variant: "variant",
198+
Features: []string{"feature-1", "feature-2"},
199+
},
200+
ArtifactType: "artifact-type",
201+
},
202+
},
203+
},
204+
{
205+
name: "minimal image manifest",
206+
ref: ast.StringTerm("registry.local/spam:latest@sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b"),
207+
manifest: &v1.Manifest{
208+
SchemaVersion: 2,
209+
MediaType: types.OCIManifestSchema1,
210+
Config: v1.Descriptor{
211+
MediaType: types.OCIConfigJSON,
212+
Size: 123,
213+
Digest: v1.Hash{
214+
Algorithm: "sha256",
215+
Hex: "4e388ab32b10dc8dbc7e28144f552830adc74787c1e2c0824032078a79f227fb",
216+
},
217+
},
218+
Layers: []v1.Descriptor{
219+
{
220+
MediaType: types.OCILayer,
221+
Size: 9999,
222+
Digest: v1.Hash{
223+
Algorithm: "sha256",
224+
Hex: "325392e8dd2826a53a9a35b7a7f8d71683cd27ebc2c73fee85dab673bc909b67",
225+
},
226+
},
227+
},
228+
},
229+
},
230+
{
231+
name: "missing digest",
232+
ref: ast.StringTerm("registry.local/spam:latest"),
233+
wantErr: true,
234+
},
235+
{
236+
name: "bad image ref",
237+
ref: ast.StringTerm("......registry.local/spam:latest@sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b"),
238+
wantErr: true,
239+
},
240+
{
241+
name: "invalid ref type",
242+
ref: ast.IntNumberTerm(42),
243+
wantErr: true,
244+
},
245+
{
246+
name: "image error",
247+
ref: ast.StringTerm("registry.local/spam:latest@sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b"),
248+
manifestErr: errors.New("kaboom!"),
249+
wantErr: true,
250+
},
251+
{
252+
name: "nil manifest",
253+
ref: ast.StringTerm("registry.local/spam:latest@sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b"),
254+
manifest: nil,
255+
wantErr: true,
256+
},
257+
}
258+
259+
for _, c := range cases {
260+
t.Run(c.name, func(t *testing.T) {
261+
client := fake.FakeClient{}
262+
if c.imageErr != nil {
263+
client.On("Image", mock.Anything, mock.Anything).Return(nil, c.imageErr)
264+
} else {
265+
imageManifest := v1fake.FakeImage{}
266+
imageManifest.ManifestReturns(c.manifest, c.manifestErr)
267+
client.On("Image", mock.Anything, mock.Anything).Return(&imageManifest, nil)
268+
}
269+
ctx := oci.WithClient(context.Background(), &client)
270+
bctx := rego.BuiltinContext{Context: ctx}
271+
272+
got, err := ociImageManifest(bctx, c.ref)
273+
require.NoError(t, err)
274+
if c.wantErr {
275+
require.Nil(t, got)
276+
} else {
277+
require.NotNil(t, got)
278+
snaps.MatchJSON(t, got)
279+
}
280+
})
281+
}
282+
}
283+
109284
func TestPURLIsValid(t *testing.T) {
110285
cases := []struct {
111286
name string
@@ -185,6 +360,7 @@ func TestPURLParse(t *testing.T) {
185360
func TestFunctionsRegistered(t *testing.T) {
186361
names := []string{
187362
ociBlobName,
363+
ociImageManifestName,
188364
purlIsValidName,
189365
purlParseName,
190366
}

0 commit comments

Comments
 (0)
Please sign in to comment.