Skip to content

Commit e9225f9

Browse files
authored
Merge pull request #2568 from dheerajodha/EC-1312
improve OCI ref Parsing and Digest Resolution
2 parents d8c2e8b + 1f27bbf commit e9225f9

File tree

3 files changed

+259
-46
lines changed

3 files changed

+259
-46
lines changed

internal/rego/oci/__snapshots__/oci_test.snap

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2215,3 +2215,81 @@
22152215
]
22162216
}
22172217
---
2218+
2219+
[TestOCIDescriptorManifest/tag-based_URI - 1]
2220+
{
2221+
"type": "object",
2222+
"value": [
2223+
[
2224+
{
2225+
"type": "string",
2226+
"value": "annotations"
2227+
},
2228+
{
2229+
"type": "object",
2230+
"value": []
2231+
}
2232+
],
2233+
[
2234+
{
2235+
"type": "string",
2236+
"value": "artifactType"
2237+
},
2238+
{
2239+
"type": "string",
2240+
"value": ""
2241+
}
2242+
],
2243+
[
2244+
{
2245+
"type": "string",
2246+
"value": "data"
2247+
},
2248+
{
2249+
"type": "string",
2250+
"value": ""
2251+
}
2252+
],
2253+
[
2254+
{
2255+
"type": "string",
2256+
"value": "digest"
2257+
},
2258+
{
2259+
"type": "string",
2260+
"value": "sha256:4e388ab32b10dc8dbc7e28144f552830adc74787c1e2c0824032078a79f227fb"
2261+
}
2262+
],
2263+
[
2264+
{
2265+
"type": "string",
2266+
"value": "mediaType"
2267+
},
2268+
{
2269+
"type": "string",
2270+
"value": "application/vnd.oci.image.manifest.v1+json"
2271+
}
2272+
],
2273+
[
2274+
{
2275+
"type": "string",
2276+
"value": "size"
2277+
},
2278+
{
2279+
"type": "number",
2280+
"value": 123
2281+
}
2282+
],
2283+
[
2284+
{
2285+
"type": "string",
2286+
"value": "urls"
2287+
},
2288+
{
2289+
"type": "array",
2290+
"value": []
2291+
}
2292+
]
2293+
]
2294+
}
2295+
---

internal/rego/oci/oci.go

Lines changed: 38 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ import (
2424
"bytes"
2525
"crypto/sha256"
2626
"encoding/json"
27+
"errors"
2728
"fmt"
2829
"io"
29-
"strings"
3030

3131
"github.com/google/go-containerregistry/pkg/name"
3232
v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -38,7 +38,6 @@ import (
3838
"k8s.io/client-go/util/retry"
3939

4040
"github.com/conforma/cli/internal/fetchers/oci/files"
41-
"github.com/conforma/cli/internal/image"
4241
"github.com/conforma/cli/internal/utils/oci"
4342
)
4443

@@ -388,22 +387,13 @@ func ociDescriptor(bctx rego.BuiltinContext, a *ast.Term) (*ast.Term, error) {
388387

389388
client := oci.NewClient(bctx.Context)
390389

391-
uri, err := resolveIfNeeded(client, string(uriValue))
390+
uri, ref, err := resolveIfNeeded(client, string(uriValue))
392391
if err != nil {
393392
logger.WithField("action", "resolveIfNeeded").Error(err)
394393
return nil, nil
395394
}
396395
logger = logger.WithField("ref", uri)
397396

398-
ref, err := name.NewDigest(uri)
399-
if err != nil {
400-
logger.WithFields(log.Fields{
401-
"action": "new digest",
402-
"error": err,
403-
}).Error("failed to create new digest")
404-
return nil, nil
405-
}
406-
407397
descriptor, err := client.Head(ref)
408398
if err != nil {
409399
logger.WithFields(log.Fields{
@@ -430,22 +420,13 @@ func ociImageManifest(bctx rego.BuiltinContext, a *ast.Term) (*ast.Term, error)
430420

431421
client := oci.NewClient(bctx.Context)
432422

433-
uri, err := resolveIfNeeded(client, string(uriValue))
423+
uri, ref, err := resolveIfNeeded(client, string(uriValue))
434424
if err != nil {
435425
logger.WithField("action", "resolveIfNeeded").Error(err)
436426
return nil, nil
437427
}
438428
logger = logger.WithField("ref", uri)
439429

440-
ref, err := name.NewDigest(uri)
441-
if err != nil {
442-
logger.WithFields(log.Fields{
443-
"action": "new digest",
444-
"error": err,
445-
}).Error("failed to create new digest")
446-
return nil, nil
447-
}
448-
449430
var image v1.Image
450431
err = retry.OnError(retry.DefaultRetry, func(_ error) bool { return true }, func() error {
451432
image, err = client.Image(ref)
@@ -575,22 +556,13 @@ func ociImageIndex(bctx rego.BuiltinContext, a *ast.Term) (*ast.Term, error) {
575556

576557
client := oci.NewClient(bctx.Context)
577558

578-
uri, err := resolveIfNeeded(client, string(uriValue))
559+
uri, ref, err := resolveIfNeeded(client, string(uriValue))
579560
if err != nil {
580561
logger.WithField("action", "resolveIfNeeded").Error(err)
581562
return nil, nil
582563
}
583564
logger = logger.WithField("ref", uri)
584565

585-
ref, err := name.NewDigest(uri)
586-
if err != nil {
587-
logger.WithFields(log.Fields{
588-
"action": "new digest",
589-
"error": err,
590-
}).Error("failed to create new digest")
591-
return nil, nil
592-
}
593-
594566
imageIndex, err := client.Index(ref)
595567
if err != nil {
596568
logger.WithFields(log.Fields{
@@ -686,23 +658,43 @@ func newAnnotationsTerm(annotations map[string]string) *ast.Term {
686658
return ast.ObjectTerm(annotationTerms...)
687659
}
688660

689-
func resolveIfNeeded(client oci.Client, uri string) (string, error) {
690-
if !strings.Contains(uri, "@") {
691-
original := uri
692-
ref, err := image.NewImageReference(uri)
693-
if err != nil {
694-
return "", fmt.Errorf("unable to parse reference: %w", err)
695-
}
661+
func resolveIfNeeded(client oci.Client, uri string) (string, name.Reference, error) {
662+
ref, err := parseReference(uri)
663+
if err != nil {
664+
return "", nil, fmt.Errorf("unable to parse reference: %w", err)
665+
}
696666

697-
digest, err := client.ResolveDigest(ref.Ref())
698-
if err != nil {
699-
return "", fmt.Errorf("unable to resolve digest: %w", err)
700-
}
701-
uri = fmt.Sprintf("%s@%s", uri, digest)
667+
// If it's already a digest reference, return as is
668+
if _, ok := ref.(name.Digest); ok {
669+
return uri, ref, nil
670+
}
671+
672+
// For tag references, resolve to digest
673+
digest, err := client.ResolveDigest(ref)
674+
if err != nil {
675+
return "", nil, fmt.Errorf("unable to resolve digest: %w", err)
676+
}
702677

703-
log.Debugf("resolved image reference %q to %q", original, uri)
678+
resolved := fmt.Sprintf("%s@%s", uri, digest)
679+
log.Debugf("resolved image reference %q to %q", uri, resolved)
680+
return resolved, ref, nil
681+
}
682+
683+
func parseReference(uri string) (name.Reference, error) {
684+
// Try to parse as digest first, if that fails with ErrBadName, try as tag
685+
ref, err := name.NewDigest(uri)
686+
if err != nil {
687+
if errors.Is(err, &name.ErrBadName{}) {
688+
tag, err := name.NewTag(uri)
689+
if err != nil {
690+
return nil, fmt.Errorf("invalid reference format: %w", err)
691+
}
692+
return tag, nil
693+
} else {
694+
return nil, fmt.Errorf("invalid digest format: %w", err)
695+
}
704696
}
705-
return uri, nil
697+
return ref, nil
706698
}
707699

708700
func init() {

internal/rego/oci/oci_test.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,12 +184,42 @@ func TestOCIDescriptorManifest(t *testing.T) {
184184
},
185185
},
186186
},
187+
{
188+
name: "tag-based URI with error",
189+
ref: ast.StringTerm("registry.local/spam:latest"),
190+
resolvedDigest: "sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b",
191+
headErr: errors.New("tag error"),
192+
wantErr: true,
193+
},
187194
{
188195
name: "resolve error",
189196
ref: ast.StringTerm("registry.local/spam:latest"),
190197
resolveErr: errors.New("kaboom!"),
191198
wantErr: true,
192199
},
200+
{
201+
name: "unsupported digest algorithm",
202+
ref: ast.StringTerm("registry.local/spam@sha512:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b"),
203+
resolvedDigest: "sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b",
204+
wantErr: true,
205+
},
206+
{
207+
name: "malformed digest with extra @",
208+
ref: ast.StringTerm("registry.local/spam@@sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b"),
209+
resolvedDigest: "sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b",
210+
wantErr: true,
211+
},
212+
{
213+
name: "invalid tag after digest fallback",
214+
ref: ast.StringTerm("registry.local/spam:!nv@lid"),
215+
resolvedDigest: "sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b",
216+
wantErr: true,
217+
},
218+
{
219+
name: "invalid digest format",
220+
ref: ast.StringTerm("registry.local/spam@sha256:invalid-digest-format"),
221+
wantErr: true,
222+
},
193223
}
194224

195225
for _, c := range cases {
@@ -464,6 +494,7 @@ func TestOCIImageManifest(t *testing.T) {
464494
})
465495
}
466496
}
497+
467498
func TestOCIImageFiles(t *testing.T) {
468499

469500
image, err := crane.Image(map[string][]byte{
@@ -747,3 +778,115 @@ func TestFunctionsRegistered(t *testing.T) {
747778
})
748779
}
749780
}
781+
782+
func TestParseReference(t *testing.T) {
783+
cases := []struct {
784+
name string
785+
uri string
786+
wantErr bool
787+
}{
788+
{
789+
name: "valid digest",
790+
uri: "registry.local/spam@sha256:4bbf56a3a9231f752d3b9c174637975f0f83ed2b15e65799837c571e4ef3374b",
791+
},
792+
{
793+
name: "valid tag",
794+
uri: "registry.local/spam:latest",
795+
},
796+
{
797+
name: "invalid digest format",
798+
uri: "registry.local/spam@sha256:invalid",
799+
wantErr: true,
800+
},
801+
{
802+
name: "invalid tag format",
803+
uri: "registry.local/spam:!nv@lid",
804+
wantErr: true,
805+
},
806+
{
807+
name: "trailing @",
808+
uri: "registry.local/spam@",
809+
wantErr: true,
810+
},
811+
{
812+
name: "multiple @",
813+
uri: "registry.local/spam@@sha256:abc123",
814+
wantErr: true,
815+
},
816+
{
817+
name: "empty string",
818+
uri: "",
819+
wantErr: true,
820+
},
821+
}
822+
823+
for _, c := range cases {
824+
t.Run(c.name, func(t *testing.T) {
825+
ref, err := parseReference(c.uri)
826+
if c.wantErr {
827+
require.Error(t, err)
828+
require.Nil(t, ref)
829+
} else {
830+
require.NoError(t, err)
831+
require.NotNil(t, ref)
832+
}
833+
})
834+
}
835+
}
836+
837+
func TestResolveIfNeeded(t *testing.T) {
838+
cases := []struct {
839+
name string
840+
uri string
841+
resolvedDigest string
842+
resolveErr error
843+
wantErr bool
844+
}{
845+
{
846+
name: "digest reference unchanged",
847+
uri: "registry.local/spam@sha256:4bbf56a3a9231f752d3b9c174637975f0f83ed2b15e65799837c571e4ef3374b",
848+
},
849+
{
850+
name: "tag reference resolved",
851+
uri: "registry.local/spam:latest",
852+
resolvedDigest: "sha256:4bbf56a3a9231f752d3b9c174637975f0f83ed2b15e65799837c571e4ef3374b",
853+
},
854+
{
855+
name: "resolve error",
856+
uri: "registry.local/spam:latest",
857+
resolveErr: errors.New("resolve error"),
858+
wantErr: true,
859+
},
860+
{
861+
name: "invalid reference",
862+
uri: "registry.local/spam@",
863+
wantErr: true,
864+
},
865+
}
866+
867+
for _, c := range cases {
868+
t.Run(c.name, func(t *testing.T) {
869+
client := fake.FakeClient{}
870+
if c.resolveErr != nil {
871+
client.On("ResolveDigest", mock.Anything).Return("", c.resolveErr)
872+
} else if c.resolvedDigest != "" {
873+
client.On("ResolveDigest", mock.Anything).Return(c.resolvedDigest, nil)
874+
}
875+
876+
uri, ref, err := resolveIfNeeded(&client, c.uri)
877+
if c.wantErr {
878+
require.Error(t, err)
879+
require.Empty(t, uri)
880+
require.Nil(t, ref)
881+
} else {
882+
require.NoError(t, err)
883+
require.NotNil(t, ref)
884+
if c.resolvedDigest != "" {
885+
require.Contains(t, uri, c.resolvedDigest)
886+
} else {
887+
require.Equal(t, c.uri, uri)
888+
}
889+
}
890+
})
891+
}
892+
}

0 commit comments

Comments
 (0)