Skip to content

Commit 0cbe218

Browse files
bupdTerryHowe
andauthored
Add URI Scheme Stripping Support (#1091)
- Fixes #1090 ## Summary This PR adds support for stripping URI schemes from reference strings in ParseReference to improve cross-tool compatibility with Helm, Argo, Kustomize, and other tools that prefix references with schemes. ## Changes Made - Strip `oci://`, `http://`, `https://` schemes before parsing references - Add documentation demonstrating usage with `oci://` scheme - Add new test cases covering all schemes, forms, and edge cases --------- Signed-off-by: bupd <bupdprasanth@gmail.com> Co-authored-by: Terry Howe <terrylhowe@gmail.com>
1 parent 2cbf56d commit 0cbe218

File tree

3 files changed

+210
-0
lines changed

3 files changed

+210
-0
lines changed

registry/example_reference_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,22 @@ func ExampleParseReference_digest() {
4545
// Repository: oras-project/oras-go
4646
// Digest: sha256:601d05a48832e7946dab8f49b14953549bebf42e42f4d7973b1a5a287d77ab76
4747
}
48+
49+
// ExampleParseReference_withScheme demonstrates parsing a reference with
50+
// a URI scheme and printing its components.
51+
func ExampleParseReference_withScheme() {
52+
rawRef := "oci://ghcr.io/oras-project/oras-go:v3.0.0"
53+
ref, err := registry.ParseReference(rawRef)
54+
if err != nil {
55+
panic(err)
56+
}
57+
58+
fmt.Println("Registry:", ref.Registry)
59+
fmt.Println("Repository:", ref.Repository)
60+
fmt.Println("Tag:", ref.Reference)
61+
62+
// Output:
63+
// Registry: ghcr.io
64+
// Repository: oras-project/oras-go
65+
// Tag: v3.0.0
66+
}

registry/reference.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,19 @@ type Reference struct {
7171
// Note: An "image" is an "artifact", however, an "artifact" is not necessarily
7272
// an "image".
7373
//
74+
// ## URI Schemes
75+
//
76+
// ParseReference automatically strips the following URI schemes if present:
77+
// - oci:// (used by Helm, Argo, Kustomize)
78+
// - http:// (HTTP registry endpoints)
79+
// - https:// (HTTPS registry endpoints)
80+
//
81+
// Schemes must be lowercase and at the start of the string. Examples:
82+
// - "oci://ghcr.io/repo:tag" → parses as "ghcr.io/repo:tag"
83+
// - "https://registry.example.com/repo" → parses as "registry.example.com/repo"
84+
//
85+
// ## Specification
86+
//
7487
// The token `artifact` is composed of other tokens, and those in turn are
7588
// composed of others. This definition recursivity requires a notation capable
7689
// of recursion, thus the following two forms have been adopted:
@@ -117,6 +130,11 @@ type Reference struct {
117130
// Note: In the case of Valid Form B, TAG is dropped without any validation or
118131
// further consideration.
119132
func ParseReference(artifact string) (Reference, error) {
133+
// Strip URI schemes if present
134+
artifact = strings.TrimPrefix(artifact, "oci://")
135+
artifact = strings.TrimPrefix(artifact, "http://")
136+
artifact = strings.TrimPrefix(artifact, "https://")
137+
120138
registry, path := splitRegistry(artifact)
121139
if path == "" {
122140
return Reference{}, fmt.Errorf("%w: missing registry or repository", errdef.ErrInvalidReference)

registry/reference_test.go

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,3 +350,176 @@ func TestReference_String(t *testing.T) {
350350
})
351351
}
352352
}
353+
354+
func TestParseReferenceWithSchemes(t *testing.T) {
355+
tests := []struct {
356+
name string
357+
input string
358+
want Reference
359+
wantErr bool
360+
}{
361+
// oci:// scheme tests
362+
{
363+
name: "oci scheme with digest (valid form A)",
364+
input: fmt.Sprintf("oci://localhost/hello-world@%s", ValidDigest),
365+
want: Reference{
366+
Registry: "localhost",
367+
Repository: "hello-world",
368+
Reference: ValidDigest,
369+
},
370+
wantErr: false,
371+
},
372+
{
373+
name: "oci scheme with tag (valid form C)",
374+
input: "oci://registry.example.com/hello-world:v1",
375+
want: Reference{
376+
Registry: "registry.example.com",
377+
Repository: "hello-world",
378+
Reference: "v1",
379+
},
380+
wantErr: false,
381+
},
382+
{
383+
name: "oci scheme with tag and digest (valid form B)",
384+
input: fmt.Sprintf("oci://registry.example.com/hello-world:v2@%s", ValidDigest),
385+
want: Reference{
386+
Registry: "registry.example.com",
387+
Repository: "hello-world",
388+
Reference: ValidDigest,
389+
},
390+
wantErr: false,
391+
},
392+
{
393+
name: "oci scheme basic reference (valid form D)",
394+
input: "oci://127.0.0.1:5000/hello-world",
395+
want: Reference{
396+
Registry: "127.0.0.1:5000",
397+
Repository: "hello-world",
398+
},
399+
wantErr: false,
400+
},
401+
{
402+
name: "oci scheme with IPv6 registry",
403+
input: "oci://[::1]:5000/hello-world:latest",
404+
want: Reference{
405+
Registry: "[::1]:5000",
406+
Repository: "hello-world",
407+
Reference: "latest",
408+
},
409+
wantErr: false,
410+
},
411+
{
412+
name: "oci scheme with multi-level repository",
413+
input: "oci://ghcr.io/oras-project/oras-go:v3.0.0",
414+
want: Reference{
415+
Registry: "ghcr.io",
416+
Repository: "oras-project/oras-go",
417+
Reference: "v3.0.0",
418+
},
419+
wantErr: false,
420+
},
421+
422+
// http:// scheme tests
423+
{
424+
name: "http scheme with digest",
425+
input: fmt.Sprintf("http://localhost/hello-world@%s", ValidDigest),
426+
want: Reference{
427+
Registry: "localhost",
428+
Repository: "hello-world",
429+
Reference: ValidDigest,
430+
},
431+
wantErr: false,
432+
},
433+
{
434+
name: "http scheme with tag",
435+
input: "http://registry.example.com:8080/hello-world:v1",
436+
want: Reference{
437+
Registry: "registry.example.com:8080",
438+
Repository: "hello-world",
439+
Reference: "v1",
440+
},
441+
wantErr: false,
442+
},
443+
444+
// https:// scheme tests
445+
{
446+
name: "https scheme with digest",
447+
input: fmt.Sprintf("https://localhost/hello-world@%s", ValidDigest),
448+
want: Reference{
449+
Registry: "localhost",
450+
Repository: "hello-world",
451+
Reference: ValidDigest,
452+
},
453+
wantErr: false,
454+
},
455+
{
456+
name: "https scheme with tag",
457+
input: "https://registry.example.com/hello-world:v1",
458+
want: Reference{
459+
Registry: "registry.example.com",
460+
Repository: "hello-world",
461+
Reference: "v1",
462+
},
463+
wantErr: false,
464+
},
465+
466+
// Backward compatibility - no scheme
467+
{
468+
name: "no scheme (backward compatibility)",
469+
input: "localhost/hello-world:v1",
470+
want: Reference{
471+
Registry: "localhost",
472+
Repository: "hello-world",
473+
Reference: "v1",
474+
},
475+
wantErr: false,
476+
},
477+
478+
// Edge cases - invalid inputs should still fail validation
479+
{
480+
name: "oci scheme but missing repository",
481+
input: "oci://localhost",
482+
wantErr: true,
483+
},
484+
{
485+
name: "oci scheme but invalid registry",
486+
input: "oci://invalid registry/repo",
487+
wantErr: true,
488+
},
489+
{
490+
name: "oci scheme but invalid repository name",
491+
input: "oci://localhost/UPPERCASE",
492+
wantErr: true,
493+
},
494+
{
495+
name: "scheme in wrong position (middle)",
496+
input: "localhost/oci://hello-world",
497+
wantErr: true,
498+
},
499+
500+
// Case sensitivity - schemes should be lowercase
501+
{
502+
name: "uppercase OCI scheme not stripped",
503+
input: "OCI://localhost/hello-world",
504+
wantErr: true, // "OCI:" will be parsed as invalid registry
505+
},
506+
{
507+
name: "mixed case scheme not stripped",
508+
input: "Oci://localhost/hello-world",
509+
wantErr: true,
510+
},
511+
}
512+
513+
for _, tt := range tests {
514+
t.Run(tt.name, func(t *testing.T) {
515+
got, err := ParseReference(tt.input)
516+
if (err != nil) != tt.wantErr {
517+
t.Errorf("ParseReference() error = %v, wantErr %v", err, tt.wantErr)
518+
return
519+
}
520+
if !tt.wantErr && !reflect.DeepEqual(got, tt.want) {
521+
t.Errorf("ParseReference() = %v, want %v", got, tt.want)
522+
}
523+
})
524+
}
525+
}

0 commit comments

Comments
 (0)