Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions ocm-kit/helmvalues/helmvalues_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -479,3 +479,78 @@ func TestGetRenderingInput(t *testing.T) {
t.Errorf("OCIResources[rel] = %#v, want %#v", gotRel, wantRel)
}
}

// TestGetRenderingInput_RelativeOciReference tests correct handling for
// mirrored components: after `ocm transfer`, resources carry a
// relativeOciReference access. Before this was handled they were silently
// dropped from OCIResources, so templates rendered an empty repository and tag
// and the resulting workload got an unusable image reference.
func TestGetRenderingInput_RelativeOciReference(t *testing.T) {
relRes := descriptor.Resource{
ElementMeta: descriptor.ElementMeta{
ObjectMeta: descriptor.ObjectMeta{Name: "nginx-image"},
},
Access: &runtime.Raw{
Type: runtime.Type{Name: "relativeOciReference", Version: "v1"},
Data: []byte(`{"type":"relativeOciReference/v1","reference":"my-components/linuxserver/nginx:1.28.3"}`),
},
}

desc := mkDescriptor("acme.org/app", "1.0.0", relRes)

input, err := GetRenderingInput(desc, "127.0.0.1:5000/my-components")
if err != nil {
t.Fatalf("GetRenderingInput() error = %v", err)
}

got, ok := input.OCIResources["nginx-image"]
if !ok {
t.Fatalf("OCIResources missing key %q — relativeOciReference resource was dropped", "nginx-image")
}

want := ImageReference{
Host: "127.0.0.1:5000",
Repository: "my-components/linuxserver/nginx",
Tag: "1.28.3",
}
if got != want {
t.Errorf("OCIResources[nginx-image] = %#v, want %#v", got, want)
}
}

// TestRenderRelativeOciReference renders the template, asserting the
// output is a fully qualified image
func TestRenderRelativeOciReference(t *testing.T) {
relRes := descriptor.Resource{
ElementMeta: descriptor.ElementMeta{
ObjectMeta: descriptor.ObjectMeta{Name: "nginx-image"},
},
Access: &runtime.Raw{
Type: runtime.Type{Name: "relativeOciReference", Version: "v1"},
Data: []byte(`{"type":"relativeOciReference/v1","reference":"my-components/linuxserver/nginx:1.28.3"}`),
},
}

input, err := GetRenderingInput(mkDescriptor("acme.org/app", "1.0.0", relRes), "127.0.0.1:5000/my-components")
if err != nil {
t.Fatalf("GetRenderingInput() error = %v", err)
}

tmpl := &HelmValuesTemplate{
ResourceName: "values",
TemplateContent: `{{- $nginx := index .OCIResources "nginx-image" }}
image:
repository: {{ $nginx.Host }}/{{ $nginx.Repository }}
tag: {{ $nginx.Tag }}`,
}

out, err := Render(tmpl, input, WithYAMLValidation())
if err != nil {
t.Fatalf("Render() error = %v", err)
}

const want = "\nimage:\n repository: 127.0.0.1:5000/my-components/linuxserver/nginx\n tag: 1.28.3"
if out != want {
t.Errorf("Render() =\n%q\nwant\n%q", out, want)
}
}
75 changes: 67 additions & 8 deletions ocm-kit/helmvalues/ociref.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,24 +87,46 @@ func isLocalBlobTypeName(name string) bool {
}
}

// relativeOCIReferenceType is the access type name for an artifact stored in the
// same OCI registry as the component version referencing it.
const relativeOCIReferenceType = "relativeOciReference"

// isRelativeOCIReferenceTypeName reports whether a runtime type name identifies
// the registry-relative OCI access.
func isRelativeOCIReferenceTypeName(name string) bool {
return name == relativeOCIReferenceType
}

// relativeOCIReferenceAccess mirrors the relativeOciReference access spec
// payload. Reference is an OCI repository name plus version, and carries no
// registry host.
type relativeOCIReferenceAccess struct {
Reference string `json:"reference"`
}

// ResourceOCIReference returns the absolute OCI image reference for a resource
// backed by OCI content. ok is false when the resource has no resolvable OCI
// reference: a non-OCI access, or a component-local blob that exists only by
// digest with no repository path to build from.
//
// A resource's access is either an OCIImage, which carries an absolute image
// reference directly, or a LocalBlob for component-local content. A LocalBlob
// resolves via its optional GlobalAccess (an absolute OCIImage or OCIImageLayer
// reference) or, failing that, its ReferenceName — a repository-relative path
// carrying no registry host.
// A resource's access is one of three forms:
//
// - OCIImage, which carries an absolute image reference directly.
// - LocalBlob, for component-local content. It resolves via its optional
// GlobalAccess (an absolute OCIImage or OCIImageLayer reference) or, failing
// that, its ReferenceName — a path relative to the repository base URL.
// - relativeOciReference, for an artifact in the same registry as the
// component version. Its Reference already includes the namespace, so it is
// relative to the registry HOST rather than the repository base URL. This is
// the form `ocm transfer` produces, so it is what mirrored components carry.
//
// The access may be a concrete typed value or, when read back from a repository,
// an un-decoded *runtime.Raw; both forms are handled.
//
// repoBaseURL is the "<host>/<namespace>" the repository was opened with (e.g.
// "127.0.0.1:5000/my-components"). A host-less ReferenceName is prefixed with it
// to form a full absolute reference; references that already carry a registry
// host are returned unchanged.
// "127.0.0.1:5000/my-components"), and supplies whichever part of the prefix the
// access form needs: the whole base URL for a LocalBlob ReferenceName, only the
// registry host for a relativeOciReference.
func ResourceOCIReference(res descriptor.Resource, repoBaseURL string) (ref string, ok bool, err error) {
switch a := res.Access.(type) {
case *ociaccessv1.OCIImage:
Expand Down Expand Up @@ -138,6 +160,12 @@ func ResourceOCIReference(res descriptor.Resource, repoBaseURL string) (ref stri
return "", false, fmt.Errorf("failed to decode LocalBlob access: %w", err)
}
return localBlobReference(&lb, repoBaseURL)
case isRelativeOCIReferenceTypeName(a.Name):
var rel relativeOCIReferenceAccess
if err := json.Unmarshal(a.Data, &rel); err != nil {
return "", false, fmt.Errorf("failed to decode relativeOciReference access: %w", err)
}
return registryRelativeReference(rel.Reference, repoBaseURL)
default:
return "", false, nil
}
Expand Down Expand Up @@ -220,6 +248,37 @@ func absoluteReference(candidate, repoBaseURL string) string {
return repoBaseURL + "/" + candidate
}

// registryRelativeReference reconstructs a FULL, host-qualified OCI reference
// from a relativeOciReference.
//
// Unlike absoluteReference this applies no isRegistryHost heuristic. The access
// type is host-less by definition and the heuristic would misfire on the common case of
// a namespace whose first segment contains a dot
func registryRelativeReference(reference, repoBaseURL string) (ref string, ok bool, err error) {
if reference == "" {
return "", false, nil
}
host := registryHostOf(repoBaseURL)
if host == "" {
return reference, true, nil
}

return host + "/" + reference, true, nil
}

// registryHostOf extracts the registry host from a repository base URL, which
// may carry a scheme and a namespace path (e.g. "https://127.0.0.1:5000/ns/sub"
// yields "127.0.0.1:5000"). It returns "" for an empty input.
func registryHostOf(repoBaseURL string) string {
rest := repoBaseURL
if _, after, found := strings.Cut(rest, "://"); found {
rest = after
}
host, _, _ := strings.Cut(rest, "/")

return host
}

// isRegistryHost reports whether a leading reference segment is a registry host
// rather than a repository namespace segment, using the standard OCI/Docker
// heuristic: a host contains a "." or a ":" (port), or is exactly "localhost".
Expand Down
102 changes: 102 additions & 0 deletions ocm-kit/helmvalues/ociref_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,3 +257,105 @@ func TestResourceOCIReference_LocalBlobReferenceNameAlreadyAbsolute(t *testing.T
require.True(t, ok)
require.Equal(t, "ghcr.io/acme/app:v1", ref)
}

// TestResourceOCIReference_RelativeOciReference covers the access type that
// `ocm transfer` produces: an artifact stored in the same registry as the
// component version. Its reference already contains the namespace, so only the
// registry host is prefixed
func TestResourceOCIReference_RelativeOciReference(t *testing.T) {
raw := &runtime.Raw{
Type: runtime.Type{Name: "relativeOciReference", Version: "v1"},
Data: []byte(`{"type":"relativeOciReference/v1","reference":"my-components/linuxserver/nginx:1.28.3"}`),
}

ref, ok, err := ResourceOCIReference(descriptor.Resource{Access: raw}, "127.0.0.1:5000/my-components")
require.NoError(t, err)
require.True(t, ok)
require.Equal(t, "127.0.0.1:5000/my-components/linuxserver/nginx:1.28.3", ref)
}

// TestResourceOCIReference_RelativeOciReferenceWithDigest ensures a
// digest-pinned reference survives intact.
func TestResourceOCIReference_RelativeOciReferenceWithDigest(t *testing.T) {
raw := &runtime.Raw{
Type: runtime.Type{Name: "relativeOciReference", Version: "v1"},
Data: []byte(`{"type":"relativeOciReference/v1","reference":"my-components/linuxserver/nginx:1.28.3@sha256:eca40c5bede627b5557701bde4b7588a98c07f644221b804b0a9350298400042"}`),
}

ref, ok, err := ResourceOCIReference(descriptor.Resource{Access: raw}, "127.0.0.1:5000/my-components")
require.NoError(t, err)
require.True(t, ok)
require.Equal(t,
"127.0.0.1:5000/my-components/linuxserver/nginx:1.28.3@sha256:eca40c5bede627b5557701bde4b7588a98c07f644221b804b0a9350298400042",
ref)
}

// TestResourceOCIReference_RelativeOciReferenceDottedNamespace guards the reason
// this path does not reuse absoluteReference's isRegistryHost heuristic; a
// namespace whose first segment contains a dot must still be prefixed, not
// mistaken for a registry host.
func TestResourceOCIReference_RelativeOciReferenceDottedNamespace(t *testing.T) {
raw := &runtime.Raw{
Type: runtime.Type{Name: "relativeOciReference", Version: "v1"},
Data: []byte(`{"type":"relativeOciReference/v1","reference":"my.company/demo/chart:0.1.0"}`),
}

ref, ok, err := ResourceOCIReference(descriptor.Resource{Access: raw}, "127.0.0.1:5000")
require.NoError(t, err)
require.True(t, ok)
require.Equal(t, "127.0.0.1:5000/my.company/demo/chart:0.1.0", ref)
}

// TestResourceOCIReference_RelativeOciReferenceSchemedBaseURL ensures a base URL
// carrying a scheme contributes only its host.
func TestResourceOCIReference_RelativeOciReferenceSchemedBaseURL(t *testing.T) {
raw := &runtime.Raw{
Type: runtime.Type{Name: "relativeOciReference", Version: "v1"},
Data: []byte(`{"type":"relativeOciReference/v1","reference":"my-components/acme/app:v1"}`),
}

ref, ok, err := ResourceOCIReference(descriptor.Resource{Access: raw}, "https://127.0.0.1:5000/my-components")
require.NoError(t, err)
require.True(t, ok)
require.Equal(t, "127.0.0.1:5000/my-components/acme/app:v1", ref)
}

// TestResourceOCIReference_RelativeOciReferenceNoBaseURL leaves the reference
// untouched when there is no repository base URL to draw a host from.
func TestResourceOCIReference_RelativeOciReferenceNoBaseURL(t *testing.T) {
raw := &runtime.Raw{
Type: runtime.Type{Name: "relativeOciReference", Version: "v1"},
Data: []byte(`{"type":"relativeOciReference/v1","reference":"my-components/acme/app:v1"}`),
}

ref, ok, err := ResourceOCIReference(descriptor.Resource{Access: raw}, "")
require.NoError(t, err)
require.True(t, ok)
require.Equal(t, "my-components/acme/app:v1", ref)
}

// TestResourceOCIReference_RelativeOciReferenceEmpty treats an empty reference as
// unresolvable rather than emitting a bare host.
func TestResourceOCIReference_RelativeOciReferenceEmpty(t *testing.T) {
raw := &runtime.Raw{
Type: runtime.Type{Name: "relativeOciReference", Version: "v1"},
Data: []byte(`{"type":"relativeOciReference/v1","reference":""}`),
}

_, ok, err := ResourceOCIReference(descriptor.Resource{Access: raw}, "127.0.0.1:5000/my-components")
require.NoError(t, err)
require.False(t, ok)
}

// TestResourceOCIReference_RelativeOciReferenceMalformed surfaces a decode
// failure rather than silently skipping the resource.
func TestResourceOCIReference_RelativeOciReferenceMalformed(t *testing.T) {
raw := &runtime.Raw{
Type: runtime.Type{Name: "relativeOciReference", Version: "v1"},
Data: []byte(`{"reference":`),
}

_, ok, err := ResourceOCIReference(descriptor.Resource{Access: raw}, "127.0.0.1:5000/my-components")
require.Error(t, err)
require.False(t, ok)
}