Skip to content
Merged
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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions cmd/diff/cmd_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ func defaultProcessorOptions(fields CommonCmdFields, namespace string) []dp.Proc
opts = append(opts, dp.WithFunctionCredentials(fields.FunctionCredentials.Secrets))
}

if fields.FunctionRegistryOverride != "" {
opts = append(opts, dp.WithFunctionRegistryOverride(fields.FunctionRegistryOverride))
}

return opts
}

Expand Down
4 changes: 4 additions & 0 deletions cmd/diff/diffprocessor/diff_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,11 @@ 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)
if config.FunctionRegistryOverride != "" {
functionProvider = NewRegistryOverrideFunctionProvider(functionProvider, config.FunctionRegistryOverride, config.Logger)
}

processor := &DefaultDiffProcessor{
compClient: xpcs.Composition,
Expand Down
72 changes: 72 additions & 0 deletions cmd/diff/diffprocessor/function_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,78 @@ 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,
}
}

// 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 {
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
}

// Cleanup delegates to the wrapped provider.
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 {
Expand Down
111 changes: 111 additions & 0 deletions cmd/diff/diffprocessor/function_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
}
Comment thread
cahillsf marked this conversation as resolved.

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"

Expand Down
10 changes: 10 additions & 0 deletions cmd/diff/diffprocessor/processor_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ type ProcessorConfig struct {
// FunctionCredentials holds Secret credentials to pass to Functions during rendering
FunctionCredentials []corev1.Secret

// 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

Expand Down Expand Up @@ -169,6 +172,13 @@ func WithFunctionCredentials(creds []corev1.Secret) ProcessorOption {
}
}

// WithFunctionRegistryOverride overrides the registry in all function package refs.
func WithFunctionRegistryOverride(registry string) ProcessorOption {
return func(config *ProcessorConfig) {
config.FunctionRegistryOverride = registry
}
}

// WithStdout sets the writer for diff output.
func WithStdout(w io.Writer) ProcessorOption {
return func(config *ProcessorConfig) {
Expand Down
19 changes: 10 additions & 9 deletions cmd/diff/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +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"`
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.
Expand Down
Loading