From b5704e19814c69d9c70af6c21c13c447c4c8a2dc Mon Sep 17 00:00:00 2001 From: Stephen Cahill Date: Tue, 5 May 2026 13:18:42 -0400 Subject: [PATCH 1/4] feat(diffprocessor): add --function-registry flag to override function image registry Introduces a CLI flag that rewrites the registry portion of every function package ref before functions are pulled, via a RegistryOverrideFunctionProvider that wraps the default provider. Refs: CLOUDR-1645 Signed-off-by: Stephen Cahill --- cmd/diff/cmd_utils.go | 4 + cmd/diff/diffprocessor/diff_processor.go | 5 +- cmd/diff/diffprocessor/function_provider.go | 69 +++++++++++ .../diffprocessor/function_provider_test.go | 111 ++++++++++++++++++ cmd/diff/diffprocessor/processor_config.go | 10 ++ cmd/diff/main.go | 1 + 6 files changed, 199 insertions(+), 1 deletion(-) diff --git a/cmd/diff/cmd_utils.go b/cmd/diff/cmd_utils.go index b2524f6..fa3306a 100644 --- a/cmd/diff/cmd_utils.go +++ b/cmd/diff/cmd_utils.go @@ -90,6 +90,10 @@ func defaultProcessorOptions(fields CommonCmdFields, namespace string) []dp.Proc opts = append(opts, dp.WithFunctionCredentials(fields.FunctionCredentials.Secrets)) } + if fields.FunctionRegistry != "" { + opts = append(opts, dp.WithFunctionRegistry(fields.FunctionRegistry)) + } + return opts } diff --git a/cmd/diff/diffprocessor/diff_processor.go b/cmd/diff/diffprocessor/diff_processor.go index 76f9aca..e21e669 100644 --- a/cmd/diff/diffprocessor/diff_processor.go +++ b/cmd/diff/diffprocessor/diff_processor.go @@ -115,7 +115,10 @@ func NewDiffProcessor(k8cs k8.Clients, xpcs xp.Clients, opts ...ProcessorOption) requirementsProvider := config.Factories.RequirementsProvider(k8cs.Resource, xpcs.Environment, config.RenderFunc, config.Logger) diffCalculator := config.Factories.DiffCalculator(k8cs.Apply, xpcs.ResourceTree, resourceManager, config.Logger, diffOpts) diffRenderer := config.Factories.DiffRenderer(config.Logger, diffOpts) - functionProvider := config.Factories.FunctionProvider(xpcs.Function, config.Logger) + var functionProvider FunctionProvider = config.Factories.FunctionProvider(xpcs.Function, config.Logger) + if config.FunctionRegistry != "" { + functionProvider = NewRegistryOverrideFunctionProvider(functionProvider, config.FunctionRegistry, config.Logger) + } processor := &DefaultDiffProcessor{ compClient: xpcs.Composition, diff --git a/cmd/diff/diffprocessor/function_provider.go b/cmd/diff/diffprocessor/function_provider.go index 9b8a580..7c5f21f 100644 --- a/cmd/diff/diffprocessor/function_provider.go +++ b/cmd/diff/diffprocessor/function_provider.go @@ -302,6 +302,75 @@ func generateContainerName(pkg, instanceID string) string { return containerName } +// RegistryOverrideFunctionProvider wraps another FunctionProvider and replaces +// the registry in each function's package reference before returning them. +type RegistryOverrideFunctionProvider struct { + inner FunctionProvider + registry string + logger logging.Logger +} + +// NewRegistryOverrideFunctionProvider wraps inner, replacing the registry +// portion of every function package ref with the given registry. +func NewRegistryOverrideFunctionProvider(inner FunctionProvider, registry string, logger logging.Logger) FunctionProvider { + return &RegistryOverrideFunctionProvider{ + inner: inner, + registry: registry, + logger: logger, + } +} + +func (p *RegistryOverrideFunctionProvider) GetFunctionsForComposition(comp *apiextensionsv1.Composition) ([]pkgv1.Function, error) { + fns, err := p.inner.GetFunctionsForComposition(comp) + if err != nil { + return nil, err + } + + for i := range fns { + orig := fns[i].Spec.Package + + replaced := replaceRegistry(orig, p.registry) + if replaced != orig { + p.logger.Debug("Overriding function registry", + "function", fns[i].GetName(), + "from", orig, + "to", replaced) + fns[i].Spec.Package = replaced + } + } + + return fns, nil +} + +func (p *RegistryOverrideFunctionProvider) Cleanup(ctx context.Context) error { + return p.inner.Cleanup(ctx) +} + +// replaceRegistry replaces the registry portion of an OCI package reference, +// preserving the repository path, tag, and/or digest. A trailing slash on +// newRegistry is trimmed. +// +// The first path component is treated as a registry host only when it follows +// the standard OCI rule: it contains a '.' or ':', or it is exactly +// "localhost". Otherwise the ref has no explicit registry (e.g. +// "crossplane-contrib/function-auto-ready:v1.0.0") and newRegistry is prepended +// to the full ref instead of replacing the first path segment. +func replaceRegistry(pkg, newRegistry string) string { + newRegistry = strings.TrimRight(newRegistry, "/") + + idx := strings.Index(pkg, "/") + if idx < 0 { + return newRegistry + "/" + pkg + } + + first := pkg[:idx] + if !strings.ContainsAny(first, ".:") && first != "localhost" { + return newRegistry + "/" + pkg + } + + return newRegistry + pkg[idx:] +} + // truncateContainerName shortens a container name to fit within maxContainerNameLength // while maintaining uniqueness via a hash suffix derived from the original package name. func truncateContainerName(name, pkg, instanceID string) string { diff --git a/cmd/diff/diffprocessor/function_provider_test.go b/cmd/diff/diffprocessor/function_provider_test.go index 6df9826..89b6800 100644 --- a/cmd/diff/diffprocessor/function_provider_test.go +++ b/cmd/diff/diffprocessor/function_provider_test.go @@ -499,6 +499,117 @@ func TestGenerateContainerName_TruncationStability(t *testing.T) { } } +func TestReplaceRegistry(t *testing.T) { + tests := map[string]struct { + pkg string + newRegistry string + want string + }{ + "StandardPackage": { + pkg: "xpkg.crossplane.io/crossplane-contrib/function-go-templating:v0.11.0", + newRegistry: "registry.example.com", + want: "registry.example.com/crossplane-contrib/function-go-templating:v0.11.0", + }, + "DifferentRegistry": { + pkg: "ghcr.io/crossplane/function-auto-ready:v1.2.3", + newRegistry: "registry.example.com", + want: "registry.example.com/crossplane/function-auto-ready:v1.2.3", + }, + "SHA256Digest": { + pkg: "old.registry.io/org/func@sha256:abc123", + newRegistry: "new.registry.io", + want: "new.registry.io/org/func@sha256:abc123", + }, + "NoExplicitRegistry": { + // First segment lacks '.', ':' or "localhost", so it is treated as + // part of the path (per OCI rules), and newRegistry is prepended. + pkg: "crossplane-contrib/function-auto-ready:v1.0.0", + newRegistry: "new.registry.io", + want: "new.registry.io/crossplane-contrib/function-auto-ready:v1.0.0", + }, + "NewRegistryWithTrailingSlash": { + pkg: "xpkg.crossplane.io/foo/bar:v1", + newRegistry: "new.registry.io/", + want: "new.registry.io/foo/bar:v1", + }, + "LocalhostWithPort": { + pkg: "localhost:5000/foo/bar:v1", + newRegistry: "new.registry.io", + want: "new.registry.io/foo/bar:v1", + }, + "NoSlash": { + pkg: "bare-image:v1", + newRegistry: "new.registry.io", + want: "new.registry.io/bare-image:v1", + }, + "DeepPath": { + pkg: "registry.io/a/b/c/func:v1", + newRegistry: "other.io", + want: "other.io/a/b/c/func:v1", + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + got := replaceRegistry(tt.pkg, tt.newRegistry) + if got != tt.want { + t.Errorf("replaceRegistry(%q, %q) = %q, want %q", tt.pkg, tt.newRegistry, got, tt.want) + } + }) + } +} + +func TestRegistryOverrideFunctionProvider(t *testing.T) { + functions := []pkgv1.Function{ + { + ObjectMeta: metav1.ObjectMeta{Name: "function-go-templating"}, + Spec: pkgv1.FunctionSpec{ + PackageSpec: pkgv1.PackageSpec{ + Package: "original.registry.io/crossplane-contrib/function-go-templating:v0.11.0", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "function-auto-ready"}, + Spec: pkgv1.FunctionSpec{ + PackageSpec: pkgv1.PackageSpec{ + Package: "other.registry.io/crossplane-contrib/function-auto-ready:v1.0.0", + }, + }, + }, + } + + fnClient := tu.NewMockFunctionClient(). + WithSuccessfulFunctionsFetch(functions). + Build() + + logger := tu.TestLogger(t, false) + inner := NewDefaultFunctionProvider(fnClient, logger) + + provider := NewRegistryOverrideFunctionProvider(inner, "registry.example.com", logger) + + comp := &apiextensionsv1.Composition{ + ObjectMeta: metav1.ObjectMeta{Name: "test-composition"}, + } + + fns, err := provider.GetFunctionsForComposition(comp) + if err != nil { + t.Fatalf("GetFunctionsForComposition() error = %v", err) + } + + if len(fns) != 2 { + t.Fatalf("GetFunctionsForComposition() returned %d functions, want 2", len(fns)) + } + + if fns[0].Spec.Package != "registry.example.com/crossplane-contrib/function-go-templating:v0.11.0" { + t.Errorf("function[0].Spec.Package = %q, want overridden registry", fns[0].Spec.Package) + } + + if fns[1].Spec.Package != "registry.example.com/crossplane-contrib/function-auto-ready:v1.0.0" { + t.Errorf("function[1].Spec.Package = %q, want overridden registry", fns[1].Spec.Package) + } +} + func TestGenerateContainerName(t *testing.T) { const testInstanceID = "test1234" diff --git a/cmd/diff/diffprocessor/processor_config.go b/cmd/diff/diffprocessor/processor_config.go index c75796d..609d03d 100644 --- a/cmd/diff/diffprocessor/processor_config.go +++ b/cmd/diff/diffprocessor/processor_config.go @@ -49,6 +49,9 @@ type ProcessorConfig struct { // FunctionCredentials holds Secret credentials to pass to Functions during rendering FunctionCredentials []corev1.Secret + // FunctionRegistry overrides the registry in all function package refs. + FunctionRegistry string + // Stdout is the writer for diff output (defaults to os.Stdout) Stdout io.Writer @@ -169,6 +172,13 @@ func WithFunctionCredentials(creds []corev1.Secret) ProcessorOption { } } +// WithFunctionRegistry overrides the registry in all function package refs. +func WithFunctionRegistry(registry string) ProcessorOption { + return func(config *ProcessorConfig) { + config.FunctionRegistry = registry + } +} + // WithStdout sets the writer for diff output. func WithStdout(w io.Writer) ProcessorOption { return func(config *ProcessorConfig) { diff --git a/cmd/diff/main.go b/cmd/diff/main.go index 4a72913..eea1fd7 100644 --- a/cmd/diff/main.go +++ b/cmd/diff/main.go @@ -103,6 +103,7 @@ type CommonCmdFields struct { Timeout time.Duration `default:"1m" help:"How long to run before timing out."` IgnorePaths []string `help:"Paths to ignore in diffs (e.g., 'metadata.annotations[argocd.argoproj.io/tracking-id]')." name:"ignore-paths"` FunctionCredentials FunctionCredentials `help:"A YAML file or directory of YAML files specifying Secret credentials to pass to Functions." name:"function-credentials" placeholder:"PATH"` + FunctionRegistry string `help:"Override the registry for all function images (e.g., 'my-company.registry.io')." name:"function-registry"` } // GetKubeContext implements ContextProvider. From 15f8d77a0c778d66fbf43bd2037754aaea317b59 Mon Sep 17 00:00:00 2001 From: Stephen Cahill Date: Tue, 5 May 2026 13:27:17 -0400 Subject: [PATCH 2/4] chore(lint): satisfy revive on registry-override function provider - Drop redundant explicit type from functionProvider declaration; use a type conversion at the call site to keep the variable typed as the FunctionProvider interface. - Add doc comments on RegistryOverrideFunctionProvider.GetFunctionsForComposition and .Cleanup. Signed-off-by: Stephen Cahill --- cmd/diff/diffprocessor/diff_processor.go | 3 ++- cmd/diff/diffprocessor/function_provider.go | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/cmd/diff/diffprocessor/diff_processor.go b/cmd/diff/diffprocessor/diff_processor.go index e21e669..8c6217b 100644 --- a/cmd/diff/diffprocessor/diff_processor.go +++ b/cmd/diff/diffprocessor/diff_processor.go @@ -115,7 +115,8 @@ func NewDiffProcessor(k8cs k8.Clients, xpcs xp.Clients, opts ...ProcessorOption) requirementsProvider := config.Factories.RequirementsProvider(k8cs.Resource, xpcs.Environment, config.RenderFunc, config.Logger) diffCalculator := config.Factories.DiffCalculator(k8cs.Apply, xpcs.ResourceTree, resourceManager, config.Logger, diffOpts) diffRenderer := config.Factories.DiffRenderer(config.Logger, diffOpts) - var functionProvider FunctionProvider = config.Factories.FunctionProvider(xpcs.Function, config.Logger) + + functionProvider := config.Factories.FunctionProvider(xpcs.Function, config.Logger) if config.FunctionRegistry != "" { functionProvider = NewRegistryOverrideFunctionProvider(functionProvider, config.FunctionRegistry, config.Logger) } diff --git a/cmd/diff/diffprocessor/function_provider.go b/cmd/diff/diffprocessor/function_provider.go index 7c5f21f..94e4774 100644 --- a/cmd/diff/diffprocessor/function_provider.go +++ b/cmd/diff/diffprocessor/function_provider.go @@ -320,6 +320,8 @@ func NewRegistryOverrideFunctionProvider(inner FunctionProvider, registry string } } +// GetFunctionsForComposition delegates to the wrapped provider and rewrites +// the registry portion of each returned function's package ref. func (p *RegistryOverrideFunctionProvider) GetFunctionsForComposition(comp *apiextensionsv1.Composition) ([]pkgv1.Function, error) { fns, err := p.inner.GetFunctionsForComposition(comp) if err != nil { @@ -342,6 +344,7 @@ func (p *RegistryOverrideFunctionProvider) GetFunctionsForComposition(comp *apie return fns, nil } +// Cleanup delegates to the wrapped provider. func (p *RegistryOverrideFunctionProvider) Cleanup(ctx context.Context) error { return p.inner.Cleanup(ctx) } From e0eb74f4b971268ff550b81303be7ed867c08d9b Mon Sep 17 00:00:00 2001 From: Stephen Cahill Date: Tue, 5 May 2026 13:32:50 -0400 Subject: [PATCH 3/4] refactor(diff): rename --function-registry to --function-registry-override The flag always rewrites the registry portion of every function package ref; "override" reads more accurately than the flag setting "the" function registry, and aligns with the existing RegistryOverrideFunctionProvider type name. Renames: - --function-registry -> --function-registry-override - CommonCmdFields.FunctionRegistry -> .FunctionRegistryOverride - ProcessorConfig.FunctionRegistry -> .FunctionRegistryOverride - WithFunctionRegistry -> WithFunctionRegistryOverride Refs: CLOUDR-1645 Signed-off-by: Stephen Cahill --- cmd/diff/cmd_utils.go | 4 ++-- cmd/diff/diffprocessor/diff_processor.go | 4 ++-- cmd/diff/diffprocessor/processor_config.go | 10 +++++----- cmd/diff/main.go | 20 ++++++++++---------- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/cmd/diff/cmd_utils.go b/cmd/diff/cmd_utils.go index fa3306a..702c1cf 100644 --- a/cmd/diff/cmd_utils.go +++ b/cmd/diff/cmd_utils.go @@ -90,8 +90,8 @@ func defaultProcessorOptions(fields CommonCmdFields, namespace string) []dp.Proc opts = append(opts, dp.WithFunctionCredentials(fields.FunctionCredentials.Secrets)) } - if fields.FunctionRegistry != "" { - opts = append(opts, dp.WithFunctionRegistry(fields.FunctionRegistry)) + if fields.FunctionRegistryOverride != "" { + opts = append(opts, dp.WithFunctionRegistryOverride(fields.FunctionRegistryOverride)) } return opts diff --git a/cmd/diff/diffprocessor/diff_processor.go b/cmd/diff/diffprocessor/diff_processor.go index 8c6217b..de61e54 100644 --- a/cmd/diff/diffprocessor/diff_processor.go +++ b/cmd/diff/diffprocessor/diff_processor.go @@ -117,8 +117,8 @@ func NewDiffProcessor(k8cs k8.Clients, xpcs xp.Clients, opts ...ProcessorOption) diffRenderer := config.Factories.DiffRenderer(config.Logger, diffOpts) functionProvider := config.Factories.FunctionProvider(xpcs.Function, config.Logger) - if config.FunctionRegistry != "" { - functionProvider = NewRegistryOverrideFunctionProvider(functionProvider, config.FunctionRegistry, config.Logger) + if config.FunctionRegistryOverride != "" { + functionProvider = NewRegistryOverrideFunctionProvider(functionProvider, config.FunctionRegistryOverride, config.Logger) } processor := &DefaultDiffProcessor{ diff --git a/cmd/diff/diffprocessor/processor_config.go b/cmd/diff/diffprocessor/processor_config.go index 609d03d..be0bb56 100644 --- a/cmd/diff/diffprocessor/processor_config.go +++ b/cmd/diff/diffprocessor/processor_config.go @@ -49,8 +49,8 @@ type ProcessorConfig struct { // FunctionCredentials holds Secret credentials to pass to Functions during rendering FunctionCredentials []corev1.Secret - // FunctionRegistry overrides the registry in all function package refs. - FunctionRegistry string + // FunctionRegistryOverride overrides the registry in all function package refs. + FunctionRegistryOverride string // Stdout is the writer for diff output (defaults to os.Stdout) Stdout io.Writer @@ -172,10 +172,10 @@ func WithFunctionCredentials(creds []corev1.Secret) ProcessorOption { } } -// WithFunctionRegistry overrides the registry in all function package refs. -func WithFunctionRegistry(registry string) ProcessorOption { +// WithFunctionRegistryOverride overrides the registry in all function package refs. +func WithFunctionRegistryOverride(registry string) ProcessorOption { return func(config *ProcessorConfig) { - config.FunctionRegistry = registry + config.FunctionRegistryOverride = registry } } diff --git a/cmd/diff/main.go b/cmd/diff/main.go index eea1fd7..6a95670 100644 --- a/cmd/diff/main.go +++ b/cmd/diff/main.go @@ -94,16 +94,16 @@ func (f *FunctionCredentials) Decode(ctx *kong.DecodeContext) error { // after flag parsing completes. type CommonCmdFields struct { // Configuration options - Context KubeContext `help:"Kubernetes context to use (defaults to current context)." name:"context"` - Output string `default:"diff" enum:"diff,json,yaml" help:"Output format (diff, json, or yaml)." name:"output" short:"o"` - NoColor bool `help:"Disable colorized output." name:"no-color"` - Compact bool `help:"Show compact diffs with minimal context." name:"compact"` - MaxNestedDepth int `default:"10" help:"Maximum depth for nested XR recursion." name:"max-nested-depth"` - MaxIterations int `default:"20" help:"Maximum render iterations. Increase for complex pipelines that need more cycles to converge." name:"max-iterations"` - Timeout time.Duration `default:"1m" help:"How long to run before timing out."` - IgnorePaths []string `help:"Paths to ignore in diffs (e.g., 'metadata.annotations[argocd.argoproj.io/tracking-id]')." name:"ignore-paths"` - FunctionCredentials FunctionCredentials `help:"A YAML file or directory of YAML files specifying Secret credentials to pass to Functions." name:"function-credentials" placeholder:"PATH"` - FunctionRegistry string `help:"Override the registry for all function images (e.g., 'my-company.registry.io')." name:"function-registry"` + Context KubeContext `help:"Kubernetes context to use (defaults to current context)." name:"context"` + Output string `default:"diff" enum:"diff,json,yaml" help:"Output format (diff, json, or yaml)." name:"output" short:"o"` + NoColor bool `help:"Disable colorized output." name:"no-color"` + Compact bool `help:"Show compact diffs with minimal context." name:"compact"` + MaxNestedDepth int `default:"10" help:"Maximum depth for nested XR recursion." name:"max-nested-depth"` + MaxIterations int `default:"20" help:"Maximum render iterations. Increase for complex pipelines that need more cycles to converge." name:"max-iterations"` + Timeout time.Duration `default:"1m" help:"How long to run before timing out."` + IgnorePaths []string `help:"Paths to ignore in diffs (e.g., 'metadata.annotations[argocd.argoproj.io/tracking-id]')." name:"ignore-paths"` + FunctionCredentials FunctionCredentials `help:"A YAML file or directory of YAML files specifying Secret credentials to pass to Functions." name:"function-credentials" placeholder:"PATH"` + FunctionRegistryOverride string `help:"Override the registry for all function images (e.g., 'my-company.registry.io')." name:"function-registry-override"` } // GetKubeContext implements ContextProvider. From d265494e9b88d5320c9315b43a2cce5c882f78c2 Mon Sep 17 00:00:00 2001 From: Stephen Cahill Date: Tue, 5 May 2026 13:36:40 -0400 Subject: [PATCH 4/4] docs(readme): document --function-registry-override flag Adds the new flag to the xr and comp flag tables and a "Function Registry Override" section parallel to the existing "Function Credentials" section, covering when to use the flag and how it rewrites package refs. Refs: CLOUDR-1645 Signed-off-by: Stephen Cahill --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 924e12a..f98d288 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,11 @@ Flags: --function-credentials=PATH Path to YAML file or directory containing Secret resources to pass as function credentials. Overrides auto-fetched credentials from cluster. + --function-registry-override=STRING + Override the registry for all function images + (e.g., 'my-company.registry.io'). Useful when + pulling functions from a mirror or private + registry. --eventual-state Show eventual state after all reconciliation cycles complete. Useful with function-sequencer which hides later stage resources until earlier stages become Ready. @@ -186,6 +191,11 @@ Flags: --function-credentials=PATH Path to YAML file or directory containing Secret resources to pass as function credentials. Overrides auto-fetched credentials from cluster. + --function-registry-override=STRING + Override the registry for all function images + (e.g., 'my-company.registry.io'). Useful when + pulling functions from a mirror or private + registry. ``` **Note**: The `diff` subcommand is deprecated. Use `xr` instead.