From 843d2bb37582e4ccf3b4f2c72c2137f2cf2ad92f Mon Sep 17 00:00:00 2001 From: Lorenzo Felletti Date: Fri, 16 Jan 2026 14:15:38 +0000 Subject: [PATCH 1/8] refactor: update RemoteModule to add Ref, deprecate old fields for OCI reference --- api/krm.go | 6 +- api/remote_module.go | 86 ++++++++++++++++++++++++++-- pkg/cuestomize/model/oci_provider.go | 14 ++++- 3 files changed, 100 insertions(+), 6 deletions(-) diff --git a/api/krm.go b/api/krm.go index 63162f1..2e5a466 100644 --- a/api/krm.go +++ b/api/krm.go @@ -78,7 +78,11 @@ func (i *KRMInput) GetRemoteClient(items []*kyaml.RNode) (*auth.Client, error) { } } - return registryauth.ConfigureClient(i.RemoteModule.Registry, secret) + registry, err := i.RemoteModule.GetRegistry() + if err != nil { + return nil, fmt.Errorf("failed to get registry: %w", err) + } + return registryauth.ConfigureClient(registry, secret) } // ItemMatchReference checks if the given item matches the provided selector. diff --git a/api/remote_module.go b/api/remote_module.go index 387295c..b95acc6 100644 --- a/api/remote_module.go +++ b/api/remote_module.go @@ -1,13 +1,91 @@ package api -import "sigs.k8s.io/kustomize/api/types" +import ( + "fmt" + "strings" + + "sigs.k8s.io/kustomize/api/types" +) // RemoteModule defines the structure to describe a remote CUE module to fetch from an OCI registry. type RemoteModule struct { - Registry string `yaml:"registry" json:"registry"` - Repo string `yaml:"repo" json:"repo"` - Tag string `yaml:"tag" json:"tag"` + // Ref is the full OCI reference in the format: registry/repo:tag + // Example: ghcr.io/workday/my-module:v1.0.0 + // If Ref is specified, it takes precedence over Registry, Repo, and Tag. + Ref string `yaml:"ref,omitempty" json:"ref,omitempty"` + + // Registry is the OCI registry hosting the module. + // + // Deprecated: Use Ref instead. Registry will be removed in a future version. + Registry string `yaml:"registry,omitempty" json:"registry,omitempty"` + // Repo is the repository path within the OCI registry. + // + // Deprecated: Use Ref instead. Repo will be removed in a future version. + Repo string `yaml:"repo,omitempty" json:"repo,omitempty"` + // Tag is the version tag of the module. + // + // Deprecated: Use Ref instead. Tag will be removed in a future version. + Tag string `yaml:"tag,omitempty" json:"tag,omitempty"` Auth *types.Selector `yaml:"auth,omitempty" json:"auth,omitempty"` PlainHTTP bool `yaml:"plainHTTP,omitempty" json:"plainHTTP,omitempty"` } + +// GetRegistry returns the registry, preferring Ref over the deprecated Registry field. +func (r *RemoteModule) GetRegistry() (string, error) { + if r.Ref != "" { + return parseRegistry(r.Ref) + } + return r.Registry, nil +} + +// GetRepo returns the repository, preferring Ref over the deprecated Repo field. +func (r *RemoteModule) GetRepo() (string, error) { + if r.Ref != "" { + return parseRepo(r.Ref) + } + return r.Repo, nil +} + +// GetTag returns the tag, preferring Ref over the deprecated Tag field. +func (r *RemoteModule) GetTag() (string, error) { + if r.Ref != "" { + return parseTag(r.Ref) + } + return r.Tag, nil +} + +// parseRegistry extracts the registry from a full OCI reference. +// Example: "ghcr.io/workday/my-module:v1.0.0" -> "ghcr.io" +func parseRegistry(ref string) (string, error) { + // Remove tag if present + ref = strings.Split(ref, ":")[0] + parts := strings.Split(ref, "/") + if len(parts) < 2 { + return "", fmt.Errorf("invalid OCI reference format: %s (expected format: registry/repo:tag)", ref) + } + return parts[0], nil +} + +// parseRepo extracts the repository path from a full OCI reference. +// Example: "ghcr.io/workday/my-module:v1.0.0" -> "workday/my-module" +func parseRepo(ref string) (string, error) { + // Remove tag if present + ref = strings.Split(ref, ":")[0] + parts := strings.Split(ref, "/") + if len(parts) < 2 { + return "", fmt.Errorf("invalid OCI reference format: %s (expected format: registry/repo:tag)", ref) + } + return strings.Join(parts[1:], "/"), nil +} + +// parseTag extracts the tag from a full OCI reference. +// Example: "ghcr.io/workday/my-module:v1.0.0" -> "v1.0.0" +// If no tag is specified, returns "latest". +func parseTag(ref string) (string, error) { + parts := strings.Split(ref, ":") + if len(parts) < 2 { + return "latest", nil + } + return parts[len(parts)-1], nil +} diff --git a/pkg/cuestomize/model/oci_provider.go b/pkg/cuestomize/model/oci_provider.go index 80346b2..df0cd5c 100644 --- a/pkg/cuestomize/model/oci_provider.go +++ b/pkg/cuestomize/model/oci_provider.go @@ -75,8 +75,20 @@ func NewOCIModelProviderFromConfigAndItems(config *api.KRMInput, items []*kyaml. if err != nil { return nil, fmt.Errorf("failed to configure remote client: %w", err) } + registry, err := config.RemoteModule.GetRegistry() + if err != nil { + return nil, fmt.Errorf("failed to get registry: %w", err) + } + repo, err := config.RemoteModule.GetRepo() + if err != nil { + return nil, fmt.Errorf("failed to get repo: %w", err) + } + tag, err := config.RemoteModule.GetTag() + if err != nil { + return nil, fmt.Errorf("failed to get tag: %w", err) + } return New( - WithRemote(config.RemoteModule.Registry, config.RemoteModule.Repo, config.RemoteModule.Tag), + WithRemote(registry, repo, tag), WithPlainHTTP(config.RemoteModule.PlainHTTP), WithClient(client), ) From 417c97104df0547fb35aa8ed3782a95c593b4b50 Mon Sep 17 00:00:00 2001 From: Lorenzo Felletti Date: Fri, 16 Jan 2026 14:18:41 +0000 Subject: [PATCH 2/8] refactor: streamline OCI reference parsing in RemoteModule methods --- api/remote_module.go | 58 +++++++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/api/remote_module.go b/api/remote_module.go index b95acc6..3c3c2ef 100644 --- a/api/remote_module.go +++ b/api/remote_module.go @@ -34,7 +34,8 @@ type RemoteModule struct { // GetRegistry returns the registry, preferring Ref over the deprecated Registry field. func (r *RemoteModule) GetRegistry() (string, error) { if r.Ref != "" { - return parseRegistry(r.Ref) + registry, _, _, err := parseOCIRef(r.Ref) + return registry, err } return r.Registry, nil } @@ -42,7 +43,8 @@ func (r *RemoteModule) GetRegistry() (string, error) { // GetRepo returns the repository, preferring Ref over the deprecated Repo field. func (r *RemoteModule) GetRepo() (string, error) { if r.Ref != "" { - return parseRepo(r.Ref) + _, repo, _, err := parseOCIRef(r.Ref) + return repo, err } return r.Repo, nil } @@ -50,42 +52,38 @@ func (r *RemoteModule) GetRepo() (string, error) { // GetTag returns the tag, preferring Ref over the deprecated Tag field. func (r *RemoteModule) GetTag() (string, error) { if r.Ref != "" { - return parseTag(r.Ref) + _, _, tag, err := parseOCIRef(r.Ref) + return tag, err } return r.Tag, nil } -// parseRegistry extracts the registry from a full OCI reference. -// Example: "ghcr.io/workday/my-module:v1.0.0" -> "ghcr.io" -func parseRegistry(ref string) (string, error) { - // Remove tag if present - ref = strings.Split(ref, ":")[0] - parts := strings.Split(ref, "/") - if len(parts) < 2 { - return "", fmt.Errorf("invalid OCI reference format: %s (expected format: registry/repo:tag)", ref) +// parseOCIRef parses a full OCI reference into its components. +// Example: "ghcr.io/workday/my-module:v1.0.0" -> ("ghcr.io", "workday/my-module", "v1.0.0", nil) +// If no tag is specified, returns "latest" as the tag. +func parseOCIRef(ref string) (registry, repo, tag string, err error) { + // Split by colon to separate tag + tagParts := strings.Split(ref, ":") + if len(tagParts) > 2 { + return "", "", "", fmt.Errorf("invalid OCI reference format: %s (multiple colons found)", ref) } - return parts[0], nil -} -// parseRepo extracts the repository path from a full OCI reference. -// Example: "ghcr.io/workday/my-module:v1.0.0" -> "workday/my-module" -func parseRepo(ref string) (string, error) { - // Remove tag if present - ref = strings.Split(ref, ":")[0] - parts := strings.Split(ref, "/") - if len(parts) < 2 { - return "", fmt.Errorf("invalid OCI reference format: %s (expected format: registry/repo:tag)", ref) + // Get tag or default to "latest" + if len(tagParts) == 2 { + tag = tagParts[1] + } else { + tag = "latest" } - return strings.Join(parts[1:], "/"), nil -} -// parseTag extracts the tag from a full OCI reference. -// Example: "ghcr.io/workday/my-module:v1.0.0" -> "v1.0.0" -// If no tag is specified, returns "latest". -func parseTag(ref string) (string, error) { - parts := strings.Split(ref, ":") + // Split by slash to separate registry and repo + pathWithRegistry := tagParts[0] + parts := strings.Split(pathWithRegistry, "/") if len(parts) < 2 { - return "latest", nil + return "", "", "", fmt.Errorf("invalid OCI reference format: %s (expected format: registry/repo:tag)", ref) } - return parts[len(parts)-1], nil + + registry = parts[0] + repo = strings.Join(parts[1:], "/") + + return registry, repo, tag, nil } From e53fb09d58b38804c3054f1956da6c1bd9e19654 Mon Sep 17 00:00:00 2001 From: Lorenzo Felletti Date: Fri, 16 Jan 2026 14:20:40 +0000 Subject: [PATCH 3/8] chore: bump version to 0.3.2 --- semver | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/semver b/semver index a2268e2..9fc80f9 100644 --- a/semver +++ b/semver @@ -1 +1 @@ -0.3.1 \ No newline at end of file +0.3.2 \ No newline at end of file From b90fcf5c44720011f9b79a9ec9c49079c41153f7 Mon Sep 17 00:00:00 2001 From: Lorenzo Felletti Date: Fri, 16 Jan 2026 15:08:24 +0000 Subject: [PATCH 4/8] refactor: replace deprecated fields with registry.Reference in RemoteModule and OCIModelProvider --- api/remote_module.go | 46 +---- api/remote_module_test.go | 243 +++++++++++++++++++++++++++ pkg/cuestomize/model/oci_provider.go | 43 +++-- 3 files changed, 275 insertions(+), 57 deletions(-) create mode 100644 api/remote_module_test.go diff --git a/api/remote_module.go b/api/remote_module.go index 3c3c2ef..7513ec9 100644 --- a/api/remote_module.go +++ b/api/remote_module.go @@ -1,9 +1,7 @@ package api import ( - "fmt" - "strings" - + "oras.land/oras-go/v2/registry" "sigs.k8s.io/kustomize/api/types" ) @@ -34,8 +32,8 @@ type RemoteModule struct { // GetRegistry returns the registry, preferring Ref over the deprecated Registry field. func (r *RemoteModule) GetRegistry() (string, error) { if r.Ref != "" { - registry, _, _, err := parseOCIRef(r.Ref) - return registry, err + ref, err := registry.ParseReference(r.Ref) + return ref.Registry, err } return r.Registry, nil } @@ -43,8 +41,8 @@ func (r *RemoteModule) GetRegistry() (string, error) { // GetRepo returns the repository, preferring Ref over the deprecated Repo field. func (r *RemoteModule) GetRepo() (string, error) { if r.Ref != "" { - _, repo, _, err := parseOCIRef(r.Ref) - return repo, err + ref, err := registry.ParseReference(r.Ref) + return ref.Repository, err } return r.Repo, nil } @@ -52,38 +50,8 @@ func (r *RemoteModule) GetRepo() (string, error) { // GetTag returns the tag, preferring Ref over the deprecated Tag field. func (r *RemoteModule) GetTag() (string, error) { if r.Ref != "" { - _, _, tag, err := parseOCIRef(r.Ref) - return tag, err + ref, err := registry.ParseReference(r.Ref) + return ref.Reference, err } return r.Tag, nil } - -// parseOCIRef parses a full OCI reference into its components. -// Example: "ghcr.io/workday/my-module:v1.0.0" -> ("ghcr.io", "workday/my-module", "v1.0.0", nil) -// If no tag is specified, returns "latest" as the tag. -func parseOCIRef(ref string) (registry, repo, tag string, err error) { - // Split by colon to separate tag - tagParts := strings.Split(ref, ":") - if len(tagParts) > 2 { - return "", "", "", fmt.Errorf("invalid OCI reference format: %s (multiple colons found)", ref) - } - - // Get tag or default to "latest" - if len(tagParts) == 2 { - tag = tagParts[1] - } else { - tag = "latest" - } - - // Split by slash to separate registry and repo - pathWithRegistry := tagParts[0] - parts := strings.Split(pathWithRegistry, "/") - if len(parts) < 2 { - return "", "", "", fmt.Errorf("invalid OCI reference format: %s (expected format: registry/repo:tag)", ref) - } - - registry = parts[0] - repo = strings.Join(parts[1:], "/") - - return registry, repo, tag, nil -} diff --git a/api/remote_module_test.go b/api/remote_module_test.go new file mode 100644 index 0000000..f053b7d --- /dev/null +++ b/api/remote_module_test.go @@ -0,0 +1,243 @@ +package api + +import ( + "testing" +) + +func TestRemoteModule_GetRegistry(t *testing.T) { + tests := []struct { + name string + module RemoteModule + want string + wantErr bool + }{ + { + name: "uses ref when present", + module: RemoteModule{ + Ref: "ghcr.io/workday/module:v1.0.0", + Registry: "old-registry.io", + }, + want: "ghcr.io", + wantErr: false, + }, + { + name: "falls back to registry field when ref is empty", + module: RemoteModule{ + Registry: "docker.io", + }, + want: "docker.io", + wantErr: false, + }, + { + name: "returns error for invalid ref", + module: RemoteModule{ + Ref: "invalid", + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.module.GetRegistry() + if (err != nil) != tt.wantErr { + t.Errorf("GetRegistry() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && got != tt.want { + t.Errorf("GetRegistry() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestRemoteModule_GetRepo(t *testing.T) { + tests := []struct { + name string + module RemoteModule + want string + wantErr bool + }{ + { + name: "uses ref when present", + module: RemoteModule{ + Ref: "ghcr.io/workday/my-module:v1.0.0", + Repo: "old-repo", + }, + want: "workday/my-module", + wantErr: false, + }, + { + name: "falls back to repo field when ref is empty", + module: RemoteModule{ + Repo: "library/nginx", + }, + want: "library/nginx", + wantErr: false, + }, + { + name: "handles nested repo paths", + module: RemoteModule{ + Ref: "registry.io/org/team/project:v2.0.0", + }, + want: "org/team/project", + wantErr: false, + }, + { + name: "returns error for invalid ref", + module: RemoteModule{ + Ref: "invalid", + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.module.GetRepo() + if (err != nil) != tt.wantErr { + t.Errorf("GetRepo() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && got != tt.want { + t.Errorf("GetRepo() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestRemoteModule_GetTag(t *testing.T) { + tests := []struct { + name string + module RemoteModule + want string + wantErr bool + }{ + { + name: "uses ref when present", + module: RemoteModule{ + Ref: "ghcr.io/workday/module:v1.0.0", + Tag: "old-tag", + }, + want: "v1.0.0", + wantErr: false, + }, + { + name: "falls back to tag field when ref is empty", + module: RemoteModule{ + Tag: "v2.0.0", + }, + want: "v2.0.0", + wantErr: false, + }, + { + name: "defaults to latest when no tag in ref", + module: RemoteModule{ + Ref: "ghcr.io/workday/module", + }, + want: "latest", + wantErr: false, + }, + { + name: "handles sha256 tags", + module: RemoteModule{ + Ref: "docker.io/nginx:sha256-abc123", + }, + want: "sha256-abc123", + wantErr: false, + }, + { + name: "returns error for invalid ref", + module: RemoteModule{ + Ref: "invalid", + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.module.GetTag() + if (err != nil) != tt.wantErr { + t.Errorf("GetTag() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && got != tt.want { + t.Errorf("GetTag() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestRemoteModule_BackwardsCompatibility(t *testing.T) { + t.Run("deprecated fields still work", func(t *testing.T) { + module := RemoteModule{ + Registry: "ghcr.io", + Repo: "workday/module", + Tag: "v1.0.0", + } + + registry, err := module.GetRegistry() + if err != nil { + t.Errorf("GetRegistry() unexpected error = %v", err) + } + if registry != "ghcr.io" { + t.Errorf("GetRegistry() = %v, want ghcr.io", registry) + } + + repo, err := module.GetRepo() + if err != nil { + t.Errorf("GetRepo() unexpected error = %v", err) + } + if repo != "workday/module" { + t.Errorf("GetRepo() = %v, want workday/module", repo) + } + + tag, err := module.GetTag() + if err != nil { + t.Errorf("GetTag() unexpected error = %v", err) + } + if tag != "v1.0.0" { + t.Errorf("GetTag() = %v, want v1.0.0", tag) + } + }) + + t.Run("ref takes precedence over deprecated fields", func(t *testing.T) { + module := RemoteModule{ + Ref: "new-registry.io/new-repo:v2.0.0", + Registry: "old-registry.io", + Repo: "old-repo", + Tag: "v1.0.0", + } + + registry, _ := module.GetRegistry() + if registry != "new-registry.io" { + t.Errorf("GetRegistry() = %v, want new-registry.io (ref should take precedence)", registry) + } + + repo, _ := module.GetRepo() + if repo != "new-repo" { + t.Errorf("GetRepo() = %v, want new-repo (ref should take precedence)", repo) + } + + tag, _ := module.GetTag() + if tag != "v2.0.0" { + t.Errorf("GetTag() = %v, want v2.0.0 (ref should take precedence)", tag) + } + }) +} + +// Helper function to check if a string contains a substring +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(substr) == 0 || + (len(s) > 0 && len(substr) > 0 && stringContains(s, substr))) +} + +func stringContains(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} diff --git a/pkg/cuestomize/model/oci_provider.go b/pkg/cuestomize/model/oci_provider.go index df0cd5c..fdc576c 100644 --- a/pkg/cuestomize/model/oci_provider.go +++ b/pkg/cuestomize/model/oci_provider.go @@ -9,6 +9,7 @@ import ( "github.com/Workday/cuestomize/api" "github.com/Workday/cuestomize/pkg/oci/fetcher" "github.com/go-logr/logr" + "oras.land/oras-go/v2/registry" "oras.land/oras-go/v2/registry/remote/auth" kyaml "sigs.k8s.io/kustomize/kyaml/yaml" ) @@ -18,20 +19,26 @@ type OCIOption func(*ociModelProviderOptions) // ociModelProviderOptions holds configuration options for OCIModelProvider. type ociModelProviderOptions struct { - Registry string - Repo string - Tag string + Reference registry.Reference PlainHTTP bool Client *auth.Client WorkingDir string } -// WithRemote configures the OCI remote to fetch the CUE model from. -func WithRemote(registry, repo, tag string) OCIOption { +// WithRemote configures the OCI remote to fetch the CUE model from an OCI registry. +// +// Panics if the provided registry, repo, and tag do not form a valid OCI reference. +func WithRemote(reg, repo, tag string) OCIOption { + reference := fmt.Sprintf("%s/%s", reg, repo) + if tag != "" { + reference = fmt.Sprintf("%s:%s", reference, tag) + } + ref, err := registry.ParseReference(reference) + if err != nil { + panic(fmt.Sprintf("invalid reference: %v", err)) + } return func(opts *ociModelProviderOptions) { - opts.Registry = registry - opts.Repo = repo - opts.Tag = tag + opts.Reference = ref } } @@ -58,9 +65,7 @@ func WithClient(client *auth.Client) OCIOption { // OCIModelProvider is a model provider that fetches the CUE model from an OCI registry. type OCIModelProvider struct { - registry string - repo string - tag string + reference registry.Reference plainHTTP bool workingDir string client *auth.Client @@ -114,9 +119,7 @@ func New(opts ...OCIOption) (*OCIModelProvider, error) { } return &OCIModelProvider{ - registry: options.Registry, - repo: options.Repo, - tag: options.Tag, + reference: options.Reference, plainHTTP: options.PlainHTTP, workingDir: options.WorkingDir, client: options.Client, @@ -130,8 +133,12 @@ func (p *OCIModelProvider) Path() string { // Get fetches the CUE model from the OCI registry and stores it in the working directory. func (p *OCIModelProvider) Get(ctx context.Context) error { + registry := p.reference.Registry + repo := p.reference.Repository + tag := p.reference.Reference + log := logr.FromContextOrDiscard(ctx).V(4).WithValues( - "registry", p.registry, "repo", p.repo, "tag", p.tag, "workingDir", p.workingDir, + "registry", registry, "repo", repo, "tag", tag, "workingDir", p.workingDir, ) log.Info("fetching from OCI registry", "plainHTTP", p.plainHTTP) @@ -140,9 +147,9 @@ func (p *OCIModelProvider) Get(ctx context.Context) error { ctx, p.client, p.workingDir, - p.registry, - p.repo, - p.tag, + registry, + repo, + tag, p.plainHTTP, ) if err != nil { From 08b95db66a70770cf3ab8a920d4346888dc154aa Mon Sep 17 00:00:00 2001 From: Lorenzo Felletti Date: Fri, 16 Jan 2026 15:10:16 +0000 Subject: [PATCH 5/8] refactor: rename WithRemote to WithRemoteParts for clarity in OCI configuration --- pkg/cuestomize/model/oci_provider.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pkg/cuestomize/model/oci_provider.go b/pkg/cuestomize/model/oci_provider.go index fdc576c..8da18be 100644 --- a/pkg/cuestomize/model/oci_provider.go +++ b/pkg/cuestomize/model/oci_provider.go @@ -25,10 +25,10 @@ type ociModelProviderOptions struct { WorkingDir string } -// WithRemote configures the OCI remote to fetch the CUE model from an OCI registry. +// WithRemoteParts configures the OCI remote to fetch the CUE model from an OCI registry. // // Panics if the provided registry, repo, and tag do not form a valid OCI reference. -func WithRemote(reg, repo, tag string) OCIOption { +func WithRemoteParts(reg, repo, tag string) OCIOption { reference := fmt.Sprintf("%s/%s", reg, repo) if tag != "" { reference = fmt.Sprintf("%s:%s", reference, tag) @@ -42,6 +42,12 @@ func WithRemote(reg, repo, tag string) OCIOption { } } +func WithRemote(ref registry.Reference) OCIOption { + return func(opts *ociModelProviderOptions) { + opts.Reference = ref + } +} + // WithPlainHTTP configures whether to use plain HTTP when fetching from the OCI registry. func WithPlainHTTP(plainHTTP bool) OCIOption { return func(opts *ociModelProviderOptions) { @@ -93,7 +99,7 @@ func NewOCIModelProviderFromConfigAndItems(config *api.KRMInput, items []*kyaml. return nil, fmt.Errorf("failed to get tag: %w", err) } return New( - WithRemote(registry, repo, tag), + WithRemoteParts(registry, repo, tag), WithPlainHTTP(config.RemoteModule.PlainHTTP), WithClient(client), ) From 39edcd78ffb721be1fd8d6619d9b4ed495962c4b Mon Sep 17 00:00:00 2001 From: Lorenzo Felletti Date: Fri, 16 Jan 2026 15:16:09 +0000 Subject: [PATCH 6/8] refactor: update FetchFromOCIRegistry to use registry.Reference for improved clarity and consistency --- pkg/cuestomize/model/oci_provider.go | 10 ++-------- pkg/oci/fetcher/fetcher.go | 12 +++++++----- pkg/oci/fetcher/fetcher_integration_test.go | 7 ++++++- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/pkg/cuestomize/model/oci_provider.go b/pkg/cuestomize/model/oci_provider.go index 8da18be..5dd645f 100644 --- a/pkg/cuestomize/model/oci_provider.go +++ b/pkg/cuestomize/model/oci_provider.go @@ -139,12 +139,8 @@ func (p *OCIModelProvider) Path() string { // Get fetches the CUE model from the OCI registry and stores it in the working directory. func (p *OCIModelProvider) Get(ctx context.Context) error { - registry := p.reference.Registry - repo := p.reference.Repository - tag := p.reference.Reference - log := logr.FromContextOrDiscard(ctx).V(4).WithValues( - "registry", registry, "repo", repo, "tag", tag, "workingDir", p.workingDir, + "registry", p.reference.Registry, "repo", p.reference.Repository, "tag", p.reference.Reference, "workingDir", p.workingDir, ) log.Info("fetching from OCI registry", "plainHTTP", p.plainHTTP) @@ -153,9 +149,7 @@ func (p *OCIModelProvider) Get(ctx context.Context) error { ctx, p.client, p.workingDir, - registry, - repo, - tag, + p.reference, p.plainHTTP, ) if err != nil { diff --git a/pkg/oci/fetcher/fetcher.go b/pkg/oci/fetcher/fetcher.go index 87b0b42..0e25e0c 100644 --- a/pkg/oci/fetcher/fetcher.go +++ b/pkg/oci/fetcher/fetcher.go @@ -8,11 +8,12 @@ import ( "github.com/go-logr/logr" "oras.land/oras-go/v2" "oras.land/oras-go/v2/content/file" + "oras.land/oras-go/v2/registry" "oras.land/oras-go/v2/registry/remote" ) // FetchFromOCIRegistry fetches an artifact from an OCI registry and stores it in the specified working directory. -func FetchFromOCIRegistry(ctx context.Context, client remote.Client, workingDir, reg, repo, tag string, plainHTTP bool) error { +func FetchFromOCIRegistry(ctx context.Context, client remote.Client, workingDir string, ref registry.Reference, plainHTTP bool) error { log := logr.FromContextOrDiscard(ctx).V(4) fs, err := file.New(workingDir) @@ -20,7 +21,7 @@ func FetchFromOCIRegistry(ctx context.Context, client remote.Client, workingDir, return fmt.Errorf("failed to create file store: %w", err) } - repository, err := remote.NewRepository(reg + "/" + repo) + repository, err := remote.NewRepository(ref.String()) if err != nil { return err } @@ -29,14 +30,15 @@ func FetchFromOCIRegistry(ctx context.Context, client remote.Client, workingDir, } repository.PlainHTTP = plainHTTP - desc, err := oras.Copy(ctx, repository, tag, fs, tag, oras.DefaultCopyOptions) + desc, err := oras.Copy(ctx, repository, ref.Reference, fs, ref.Reference, oras.DefaultCopyOptions) if err != nil { return err } log.Info("fetched artifact from OCI registry", - "reg", reg, - "repo", repo, + "reg", ref.Registry, + "repo", ref.Repository, + "tag", ref.Reference, "workingDir", workingDir, "digest", desc.Digest.String(), "mediaType", desc.MediaType, diff --git a/pkg/oci/fetcher/fetcher_integration_test.go b/pkg/oci/fetcher/fetcher_integration_test.go index 7ad65cb..3b010be 100644 --- a/pkg/oci/fetcher/fetcher_integration_test.go +++ b/pkg/oci/fetcher/fetcher_integration_test.go @@ -1,12 +1,14 @@ package fetcher import ( + "fmt" "os" "path/filepath" "testing" "github.com/Workday/cuestomize/internal/pkg/testhelpers" "github.com/stretchr/testify/require" + "oras.land/oras-go/v2/registry" "oras.land/oras-go/v2/registry/remote" "oras.land/oras-go/v2/registry/remote/auth" ) @@ -76,8 +78,11 @@ func Test_FetchFromRegistry(t *testing.T) { // push testdata/sample-module to the registry _ = testhelpers.PushDirectoryToOCIRegistryT(t, tc.registryHost+"/"+tc.repo+":"+tc.tag, tc.testdataDir, tc.artifactType, tc.tag, tc.client, tc.plainHTTP) + ref, err := registry.ParseReference(fmt.Sprintf("%s/%s:%s", tc.registryHost, tc.repo, tc.tag)) + require.NoError(t, err, "failed to parse OCI reference") + // Fetch the module from the registry - err := FetchFromOCIRegistry(ctx, tc.client, tempDir, tc.registryHost, tc.repo, tc.tag, tc.plainHTTP) + err = FetchFromOCIRegistry(ctx, tc.client, tempDir, ref, tc.plainHTTP) if !tc.shouldError { require.NoError(t, err, "failed to fetch module from OCI registry") // verify that tempDir contains the expected files From 8d221941a7c95eaaa6376067bc6d0b460276debd Mon Sep 17 00:00:00 2001 From: Lorenzo Felletti Date: Fri, 16 Jan 2026 15:24:16 +0000 Subject: [PATCH 7/8] refactor: update RemoteModule methods to use GetReference for improved clarity and consistency --- api/krm.go | 6 +++--- api/remote_module.go | 30 ++++++++-------------------- api/remote_module_test.go | 8 -------- dagger.json | 2 +- pkg/cuestomize/model/oci_provider.go | 16 +++++---------- 5 files changed, 17 insertions(+), 45 deletions(-) diff --git a/api/krm.go b/api/krm.go index 2e5a466..39992b4 100644 --- a/api/krm.go +++ b/api/krm.go @@ -78,11 +78,11 @@ func (i *KRMInput) GetRemoteClient(items []*kyaml.RNode) (*auth.Client, error) { } } - registry, err := i.RemoteModule.GetRegistry() + reference, err := i.RemoteModule.GetReference() if err != nil { - return nil, fmt.Errorf("failed to get registry: %w", err) + return nil, fmt.Errorf("failed to get reference: %w", err) } - return registryauth.ConfigureClient(registry, secret) + return registryauth.ConfigureClient(reference.Registry, secret) } // ItemMatchReference checks if the given item matches the provided selector. diff --git a/api/remote_module.go b/api/remote_module.go index 7513ec9..a7543d7 100644 --- a/api/remote_module.go +++ b/api/remote_module.go @@ -1,6 +1,8 @@ package api import ( + "fmt" + "oras.land/oras-go/v2/registry" "sigs.k8s.io/kustomize/api/types" ) @@ -29,29 +31,13 @@ type RemoteModule struct { PlainHTTP bool `yaml:"plainHTTP,omitempty" json:"plainHTTP,omitempty"` } -// GetRegistry returns the registry, preferring Ref over the deprecated Registry field. -func (r *RemoteModule) GetRegistry() (string, error) { - if r.Ref != "" { - ref, err := registry.ParseReference(r.Ref) - return ref.Registry, err - } - return r.Registry, nil -} - -// GetRepo returns the repository, preferring Ref over the deprecated Repo field. -func (r *RemoteModule) GetRepo() (string, error) { +func (r *RemoteModule) GetReference() (registry.Reference, error) { if r.Ref != "" { - ref, err := registry.ParseReference(r.Ref) - return ref.Repository, err + return registry.ParseReference(r.Ref) } - return r.Repo, nil -} - -// GetTag returns the tag, preferring Ref over the deprecated Tag field. -func (r *RemoteModule) GetTag() (string, error) { - if r.Ref != "" { - ref, err := registry.ParseReference(r.Ref) - return ref.Reference, err + referenceStr := fmt.Sprintf("%s/%s", r.Registry, r.Repo) + if r.Tag != "" { + referenceStr = fmt.Sprintf("%s:%s", referenceStr, r.Tag) } - return r.Tag, nil + return registry.ParseReference(referenceStr) } diff --git a/api/remote_module_test.go b/api/remote_module_test.go index f053b7d..b91fa94 100644 --- a/api/remote_module_test.go +++ b/api/remote_module_test.go @@ -130,14 +130,6 @@ func TestRemoteModule_GetTag(t *testing.T) { want: "v2.0.0", wantErr: false, }, - { - name: "defaults to latest when no tag in ref", - module: RemoteModule{ - Ref: "ghcr.io/workday/module", - }, - want: "latest", - wantErr: false, - }, { name: "handles sha256 tags", module: RemoteModule{ diff --git a/dagger.json b/dagger.json index 4867f02..2603482 100644 --- a/dagger.json +++ b/dagger.json @@ -1,6 +1,6 @@ { "name": "cuestomize", - "engineVersion": "v0.19.4", + "engineVersion": "v0.19.10", "sdk": { "source": "go" }, diff --git a/pkg/cuestomize/model/oci_provider.go b/pkg/cuestomize/model/oci_provider.go index 5dd645f..5a58c15 100644 --- a/pkg/cuestomize/model/oci_provider.go +++ b/pkg/cuestomize/model/oci_provider.go @@ -86,20 +86,14 @@ func NewOCIModelProviderFromConfigAndItems(config *api.KRMInput, items []*kyaml. if err != nil { return nil, fmt.Errorf("failed to configure remote client: %w", err) } - registry, err := config.RemoteModule.GetRegistry() - if err != nil { - return nil, fmt.Errorf("failed to get registry: %w", err) - } - repo, err := config.RemoteModule.GetRepo() - if err != nil { - return nil, fmt.Errorf("failed to get repo: %w", err) - } - tag, err := config.RemoteModule.GetTag() + + reference, err := config.RemoteModule.GetReference() if err != nil { - return nil, fmt.Errorf("failed to get tag: %w", err) + return nil, fmt.Errorf("failed to get reference: %w", err) } + return New( - WithRemoteParts(registry, repo, tag), + WithRemote(reference), WithPlainHTTP(config.RemoteModule.PlainHTTP), WithClient(client), ) From a387a0a0d7c5f7b61796cc7042a606c96201a1b9 Mon Sep 17 00:00:00 2001 From: Lorenzo Felletti Date: Fri, 16 Jan 2026 15:30:54 +0000 Subject: [PATCH 8/8] test: fix tests --- api/remote_module_test.go | 221 ++++++++++++-------------------------- 1 file changed, 68 insertions(+), 153 deletions(-) diff --git a/api/remote_module_test.go b/api/remote_module_test.go index b91fa94..cbf61dc 100644 --- a/api/remote_module_test.go +++ b/api/remote_module_test.go @@ -2,141 +2,95 @@ package api import ( "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestRemoteModule_GetRegistry(t *testing.T) { +func TestRemoteModule_GetReference(t *testing.T) { tests := []struct { - name string - module RemoteModule - want string - wantErr bool + name string + module RemoteModule + wantRegistry string + wantRepo string + wantRef string + wantErr bool }{ { name: "uses ref when present", module: RemoteModule{ Ref: "ghcr.io/workday/module:v1.0.0", Registry: "old-registry.io", + Repo: "old-repo", + Tag: "old-tag", }, - want: "ghcr.io", - wantErr: false, + wantRegistry: "ghcr.io", + wantRepo: "workday/module", + wantRef: "v1.0.0", + wantErr: false, }, { - name: "falls back to registry field when ref is empty", + name: "constructs from separate fields when ref is empty", module: RemoteModule{ Registry: "docker.io", + Repo: "library/nginx", + Tag: "latest", }, - want: "docker.io", - wantErr: false, - }, - { - name: "returns error for invalid ref", - module: RemoteModule{ - Ref: "invalid", - }, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := tt.module.GetRegistry() - if (err != nil) != tt.wantErr { - t.Errorf("GetRegistry() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !tt.wantErr && got != tt.want { - t.Errorf("GetRegistry() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestRemoteModule_GetRepo(t *testing.T) { - tests := []struct { - name string - module RemoteModule - want string - wantErr bool - }{ - { - name: "uses ref when present", - module: RemoteModule{ - Ref: "ghcr.io/workday/my-module:v1.0.0", - Repo: "old-repo", - }, - want: "workday/my-module", - wantErr: false, - }, - { - name: "falls back to repo field when ref is empty", - module: RemoteModule{ - Repo: "library/nginx", - }, - want: "library/nginx", - wantErr: false, + wantRegistry: "docker.io", + wantRepo: "library/nginx", + wantRef: "latest", + wantErr: false, }, { name: "handles nested repo paths", module: RemoteModule{ Ref: "registry.io/org/team/project:v2.0.0", }, - want: "org/team/project", - wantErr: false, + wantRegistry: "registry.io", + wantRepo: "org/team/project", + wantRef: "v2.0.0", + wantErr: false, }, { - name: "returns error for invalid ref", + name: "handles registry with port", module: RemoteModule{ - Ref: "invalid", + Ref: "registry:5000/sample-module:latest", }, - wantErr: true, + wantRegistry: "registry:5000", + wantRepo: "sample-module", + wantRef: "latest", + wantErr: false, }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := tt.module.GetRepo() - if (err != nil) != tt.wantErr { - t.Errorf("GetRepo() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !tt.wantErr && got != tt.want { - t.Errorf("GetRepo() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestRemoteModule_GetTag(t *testing.T) { - tests := []struct { - name string - module RemoteModule - want string - wantErr bool - }{ { - name: "uses ref when present", + name: "handles localhost registry", module: RemoteModule{ - Ref: "ghcr.io/workday/module:v1.0.0", - Tag: "old-tag", + Ref: "localhost:5000/my-module:v1.0.0", }, - want: "v1.0.0", - wantErr: false, + wantRegistry: "localhost:5000", + wantRepo: "my-module", + wantRef: "v1.0.0", + wantErr: false, }, { - name: "falls back to tag field when ref is empty", + name: "constructs without tag defaults to empty", module: RemoteModule{ - Tag: "v2.0.0", + Registry: "ghcr.io", + Repo: "workday/module", }, - want: "v2.0.0", - wantErr: false, + wantRegistry: "ghcr.io", + wantRepo: "workday/module", + wantRef: "", + wantErr: false, }, { name: "handles sha256 tags", module: RemoteModule{ Ref: "docker.io/nginx:sha256-abc123", }, - want: "sha256-abc123", - wantErr: false, + wantRegistry: "docker.io", + wantRepo: "nginx", + wantRef: "sha256-abc123", + wantErr: false, }, { name: "returns error for invalid ref", @@ -149,13 +103,14 @@ func TestRemoteModule_GetTag(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := tt.module.GetTag() - if (err != nil) != tt.wantErr { - t.Errorf("GetTag() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !tt.wantErr && got != tt.want { - t.Errorf("GetTag() = %v, want %v", got, tt.want) + ref, err := tt.module.GetReference() + + if tt.wantErr { + require.Error(t, err) + } else { + assert.Equal(t, tt.wantRegistry, ref.Registry) + assert.Equal(t, tt.wantRepo, ref.Repository) + assert.Equal(t, tt.wantRef, ref.Reference) } }) } @@ -169,29 +124,12 @@ func TestRemoteModule_BackwardsCompatibility(t *testing.T) { Tag: "v1.0.0", } - registry, err := module.GetRegistry() - if err != nil { - t.Errorf("GetRegistry() unexpected error = %v", err) - } - if registry != "ghcr.io" { - t.Errorf("GetRegistry() = %v, want ghcr.io", registry) - } + ref, err := module.GetReference() + require.NoError(t, err, "GetReference() unexpected error = %v", err) - repo, err := module.GetRepo() - if err != nil { - t.Errorf("GetRepo() unexpected error = %v", err) - } - if repo != "workday/module" { - t.Errorf("GetRepo() = %v, want workday/module", repo) - } - - tag, err := module.GetTag() - if err != nil { - t.Errorf("GetTag() unexpected error = %v", err) - } - if tag != "v1.0.0" { - t.Errorf("GetTag() = %v, want v1.0.0", tag) - } + assert.Equal(t, "ghcr.io", ref.Registry) + assert.Equal(t, "workday/module", ref.Repository) + assert.Equal(t, "v1.0.0", ref.Reference) }) t.Run("ref takes precedence over deprecated fields", func(t *testing.T) { @@ -202,34 +140,11 @@ func TestRemoteModule_BackwardsCompatibility(t *testing.T) { Tag: "v1.0.0", } - registry, _ := module.GetRegistry() - if registry != "new-registry.io" { - t.Errorf("GetRegistry() = %v, want new-registry.io (ref should take precedence)", registry) - } - - repo, _ := module.GetRepo() - if repo != "new-repo" { - t.Errorf("GetRepo() = %v, want new-repo (ref should take precedence)", repo) - } + ref, err := module.GetReference() + require.NoError(t, err) - tag, _ := module.GetTag() - if tag != "v2.0.0" { - t.Errorf("GetTag() = %v, want v2.0.0 (ref should take precedence)", tag) - } + assert.Equal(t, "new-registry.io", ref.Registry) + assert.Equal(t, "new-repo", ref.Repository) + assert.Equal(t, "v2.0.0", ref.Reference) }) } - -// Helper function to check if a string contains a substring -func contains(s, substr string) bool { - return len(s) >= len(substr) && (s == substr || len(substr) == 0 || - (len(s) > 0 && len(substr) > 0 && stringContains(s, substr))) -} - -func stringContains(s, substr string) bool { - for i := 0; i <= len(s)-len(substr); i++ { - if s[i:i+len(substr)] == substr { - return true - } - } - return false -}