From 592e5f7dce73059a0df5d88f88b94527d0d6bba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Fern=C3=A1ndez?= <7312236+fernandezcuesta@users.noreply.github.com> Date: Thu, 9 Jul 2026 18:30:13 +0200 Subject: [PATCH 1/3] feat: add customizable grpc max size MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jesús Fernández <7312236+fernandezcuesta@users.noreply.github.com> --- README.md | 10 ++ cmd/diff/cmd_utils.go | 29 ++++++ cmd/diff/cmd_utils_test.go | 49 ++++++++++ cmd/diff/diffprocessor/diff_processor.go | 6 ++ cmd/diff/diffprocessor/function_provider.go | 97 +++++++++++++++++++ .../diffprocessor/function_provider_test.go | 94 ++++++++++++++++++ cmd/diff/diffprocessor/processor_config.go | 14 +++ .../diffprocessor/processor_config_test.go | 16 +++ cmd/diff/main.go | 11 ++- design/design-doc-cli-diff.md | 8 ++ 10 files changed, 329 insertions(+), 5 deletions(-) create mode 100644 cmd/diff/cmd_utils_test.go diff --git a/README.md b/README.md index 940b96dd..72397e3a 100644 --- a/README.md +++ b/README.md @@ -166,10 +166,16 @@ Flags: --eventual-state Show eventual state after all reconciliation cycles complete. Useful with function-sequencer which hides later stage resources until earlier stages become Ready. + --max-recv-message-size=0 Max gRPC message size (MB) for render function + containers. 0 leaves the function default (4MB). Falls + back to the CROSSPLANE_DIFF_MAX_RECV_MESSAGE_SIZE env var + when unset. ``` **Note**: XR namespaces are read directly from the YAML files being diffed, not from command-line flags. +**Large composites**: `crossplane render` starts functions with the function-sdk-go default 4MB gRPC receive limit and does not apply the cluster's DeploymentRuntimeConfig. Very large XRs (many/large observed resources) can exceed this and fail with `ResourceExhausted: received message larger than max`. Set `--max-recv-message-size` (or `CROSSPLANE_DIFF_MAX_RECV_MESSAGE_SIZE`) to raise it; crossplane-diff injects the value as the `MAX_RECV_MESSAGE_SIZE` container env var. This only takes effect on functions whose image reads that variable. + **Ignored Paths**: By default, `metadata.annotations[kubectl.kubernetes.io/last-applied-configuration]` is always ignored. Additional paths can be specified with `--ignore-paths`. This is useful for filtering out metadata added by tools like ArgoCD (e.g., tracking IDs, sync waves) that shouldn't affect diff results. #### `comp` - Diff Composition Impact @@ -210,6 +216,10 @@ Flags: --eventual-state Show eventual state after all reconciliation cycles complete. Useful with function-sequencer which hides later stage resources until earlier stages become Ready. + --max-recv-message-size=0 Max gRPC message size (MB) for render function + containers. 0 leaves the function default (4MB). Falls + back to the CROSSPLANE_DIFF_MAX_RECV_MESSAGE_SIZE env var + when unset. --resource=STRING,... Limit impact analysis to specific composites in [namespace/]name format. Repeatable or comma-separated. Bare name means cluster-scoped. Mutually exclusive with diff --git a/cmd/diff/cmd_utils.go b/cmd/diff/cmd_utils.go index d4b4d5b0..39a4dc9b 100644 --- a/cmd/diff/cmd_utils.go +++ b/cmd/diff/cmd_utils.go @@ -18,6 +18,8 @@ package main import ( "context" + "os" + "strconv" "time" dp "github.com/crossplane-contrib/crossplane-diff/cmd/diff/diffprocessor" @@ -30,6 +32,29 @@ import ( "github.com/crossplane/crossplane-runtime/v2/pkg/logging" ) +// envMaxRecvMessageSize is crossplane-diff's own process env var providing the +// fallback value for --max-recv-message-size (MB). Named after the existing +// CROSSPLANE_DIFF_DOCKER_NETWORK convention. This is distinct from the +// dp.EnvMaxRecvMessageSize var that gets injected INTO function containers. +const envMaxRecvMessageSize = "CROSSPLANE_DIFF_MAX_RECV_MESSAGE_SIZE" + +// resolveMaxRecvMessageSize returns the max gRPC recv size (MB) to inject into +// function containers: the flag when >0, else the CROSSPLANE_DIFF_MAX_RECV_MESSAGE_SIZE +// env var when it parses to a positive integer, else 0 (inject nothing). +func resolveMaxRecvMessageSize(flag int) int { + if flag > 0 { + return flag + } + + if v := os.Getenv(envMaxRecvMessageSize); v != "" { + if n, err := strconv.Atoi(v); err == nil && n > 0 { + return n + } + } + + return 0 +} + // initializeAppContext initializes the application context with timeout and error handling. func initializeAppContext(timeout time.Duration, appCtx *AppContext, log logging.Logger) (context.Context, context.CancelFunc, error) { ctx, cancel := context.WithTimeout(context.Background(), timeout) @@ -88,6 +113,10 @@ func defaultProcessorOptions(fields CommonCmdFields) []dp.ProcessorOption { opts = append(opts, dp.WithFunctionRegistryOverride(fields.FunctionRegistryOverride)) } + if sz := resolveMaxRecvMessageSize(fields.MaxRecvMessageSize); sz > 0 { + opts = append(opts, dp.WithMaxRecvMessageSize(sz)) + } + if fields.CrossplaneRenderBinary != "" { opts = append(opts, dp.WithCrossplaneRenderBinary(fields.CrossplaneRenderBinary)) } diff --git a/cmd/diff/cmd_utils_test.go b/cmd/diff/cmd_utils_test.go new file mode 100644 index 00000000..205bb55b --- /dev/null +++ b/cmd/diff/cmd_utils_test.go @@ -0,0 +1,49 @@ +/* +Copyright 2025 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import "testing" + +func TestResolveMaxRecvMessageSize(t *testing.T) { + tests := []struct { + name string + flag int + env string // "" means env unset + want int + }{ + {name: "flag set wins over env", flag: 16, env: "8", want: 16}, + {name: "env fallback when flag unset", flag: 0, env: "8", want: 8}, + {name: "neither set returns zero", flag: 0, env: "", want: 0}, + {name: "non-integer env ignored", flag: 0, env: "notanint", want: 0}, + {name: "zero env ignored", flag: 0, env: "0", want: 0}, + {name: "negative env ignored", flag: 0, env: "-5", want: 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.env == "" { + t.Setenv(envMaxRecvMessageSize, "") + } else { + t.Setenv(envMaxRecvMessageSize, tt.env) + } + + if got := resolveMaxRecvMessageSize(tt.flag); got != tt.want { + t.Errorf("resolveMaxRecvMessageSize(%d) with env %q = %d, want %d", tt.flag, tt.env, got, tt.want) + } + }) + } +} diff --git a/cmd/diff/diffprocessor/diff_processor.go b/cmd/diff/diffprocessor/diff_processor.go index 2786dfcf..dc21962b 100644 --- a/cmd/diff/diffprocessor/diff_processor.go +++ b/cmd/diff/diffprocessor/diff_processor.go @@ -8,6 +8,7 @@ import ( "os" "slices" "sort" + "strconv" "strings" "time" @@ -129,6 +130,11 @@ func NewDiffProcessor(k8cs k8.Clients, xpcs xp.Clients, opts ...ProcessorOption) functionProvider = NewRegistryOverrideFunctionProvider(functionProvider, config.FunctionRegistryOverride, config.Logger) } + if config.MaxRecvMessageSize > 0 { + functionProvider = NewEnvInjectingFunctionProvider(functionProvider, + map[string]string{EnvMaxRecvMessageSize: strconv.Itoa(config.MaxRecvMessageSize)}, config.Logger) + } + processor := &DefaultDiffProcessor{ compClient: xpcs.Composition, credentialClient: xpcs.Credential, diff --git a/cmd/diff/diffprocessor/function_provider.go b/cmd/diff/diffprocessor/function_provider.go index d521c233..1af03846 100644 --- a/cmd/diff/diffprocessor/function_provider.go +++ b/cmd/diff/diffprocessor/function_provider.go @@ -357,6 +357,103 @@ func (p *RegistryOverrideFunctionProvider) Cleanup(ctx context.Context) error { return p.inner.Cleanup(ctx) } +// EnvMaxRecvMessageSize is the container env var crossplane-diff injects to +// raise a function's gRPC max receive size. It is intentionally unprefixed and +// function-agnostic, mirroring the SDK-wide TLS_SERVER_CERTS_DIR convention +// shared by crossplane composition functions: functions that bind it honor it, +// functions that don't simply ignore it. +const EnvMaxRecvMessageSize = "MAX_RECV_MESSAGE_SIZE" + +// annKeyRuntimeDockerEnv mirrors crossplane/cli render's +// AnnotationKeyRuntimeEnvironmentVariables: a comma-separated list of key=value +// pairs set as env on the function's render container. +const annKeyRuntimeDockerEnv = "render.crossplane.io/runtime-docker-env" + +// EnvInjectingFunctionProvider wraps another FunctionProvider and upserts a set +// of env pairs into each function's runtime-docker-env annotation before +// returning them. Used to raise the function gRPC max-recv-message-size so large +// XRs don't trip the function-sdk-go 4MB default under `crossplane render` +// (which, unlike the in-cluster runtime, does not apply DeploymentRuntimeConfig). +type EnvInjectingFunctionProvider struct { + inner FunctionProvider + envs map[string]string + logger logging.Logger +} + +// NewEnvInjectingFunctionProvider wraps inner, upserting the given env pairs +// into each returned function's runtime-docker-env annotation. +func NewEnvInjectingFunctionProvider(inner FunctionProvider, envs map[string]string, logger logging.Logger) FunctionProvider { + return &EnvInjectingFunctionProvider{ + inner: inner, + envs: envs, + logger: logger, + } +} + +// GetFunctionsForComposition delegates to the wrapped provider and upserts the +// configured env pairs into each function's runtime-docker-env annotation. The +// upsert is idempotent so repeated calls over a caching inner provider don't +// duplicate pairs. +func (p *EnvInjectingFunctionProvider) GetFunctionsForComposition(comp *apiextensionsv1.Composition) ([]pkgv1.Function, error) { + fns, err := p.inner.GetFunctionsForComposition(comp) + if err != nil { + return nil, err + } + + for i := range fns { + if fns[i].Annotations == nil { + fns[i].Annotations = make(map[string]string) + } + + cur := fns[i].Annotations[annKeyRuntimeDockerEnv] + for k, v := range p.envs { + cur = upsertEnvPair(cur, k, v) + } + + fns[i].Annotations[annKeyRuntimeDockerEnv] = cur + + p.logger.Debug("Injected function runtime env", + "function", fns[i].GetName(), + annKeyRuntimeDockerEnv, cur) + } + + return fns, nil +} + +// Cleanup delegates to the wrapped provider. +func (p *EnvInjectingFunctionProvider) Cleanup(ctx context.Context) error { + return p.inner.Cleanup(ctx) +} + +// upsertEnvPair sets key=val in a comma-separated "k=v,k=v" list, replacing an +// existing entry for key (idempotent) or appending a new one. Blank segments +// are dropped. +func upsertEnvPair(cur, key, val string) string { + pairs := make([]string, 0, 4) + replaced := false + + for seg := range strings.SplitSeq(cur, ",") { + if seg == "" { + continue + } + + if k, _, ok := strings.Cut(seg, "="); ok && k == key { + pairs = append(pairs, key+"="+val) + replaced = true + + continue + } + + pairs = append(pairs, seg) + } + + if !replaced { + pairs = append(pairs, key+"="+val) + } + + return strings.Join(pairs, ",") +} + // 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. diff --git a/cmd/diff/diffprocessor/function_provider_test.go b/cmd/diff/diffprocessor/function_provider_test.go index bc142960..2e894bb0 100644 --- a/cmd/diff/diffprocessor/function_provider_test.go +++ b/cmd/diff/diffprocessor/function_provider_test.go @@ -610,6 +610,100 @@ func TestRegistryOverrideFunctionProvider(t *testing.T) { } } +func TestUpsertEnvPair(t *testing.T) { + tests := []struct { + name string + cur string + key string + val string + want string + }{ + {name: "empty", cur: "", key: "A", val: "1", want: "A=1"}, + {name: "append to other key", cur: "FOO=bar", key: "A", val: "1", want: "FOO=bar,A=1"}, + {name: "replace same key", cur: "A=1", key: "A", val: "2", want: "A=2"}, + {name: "replace same key among others", cur: "FOO=bar,A=1,BAZ=qux", key: "A", val: "2", want: "FOO=bar,A=2,BAZ=qux"}, + {name: "ignores blank segments", cur: "FOO=bar,", key: "A", val: "1", want: "FOO=bar,A=1"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := upsertEnvPair(tt.cur, tt.key, tt.val); got != tt.want { + t.Errorf("upsertEnvPair(%q, %q, %q) = %q, want %q", tt.cur, tt.key, tt.val, got, tt.want) + } + }) + } +} + +// TestEnvInjectingFunctionProvider verifies the decorator injects the env pair +// into every function's runtime-docker-env annotation, appends to any existing +// value, and is idempotent across repeated calls (which matters when the inner +// provider caches and returns the same Function values). +func TestEnvInjectingFunctionProvider(t *testing.T) { + const ( + envKey = EnvMaxRecvMessageSize + annKey = "render.crossplane.io/runtime-docker-env" + ) + + newInner := func(anns map[string]string) FunctionProvider { + fn := pkgv1.Function{ObjectMeta: metav1.ObjectMeta{Name: "function-go-templating"}} + if anns != nil { + fn.Annotations = anns + } + + fnClient := tu.NewMockFunctionClient(). + WithSuccessfulFunctionsFetch([]pkgv1.Function{fn}). + Build() + // CachedFunctionProvider caches and returns the same values on repeat + // calls, so it exercises idempotency of the decorator. + return NewCachedFunctionProvider(fnClient, tu.TestLogger(t, false)) + } + + comp := &apiextensionsv1.Composition{ObjectMeta: metav1.ObjectMeta{Name: "test-composition"}} + envs := map[string]string{envKey: "16"} + + t.Run("injects when annotation absent", func(t *testing.T) { + p := NewEnvInjectingFunctionProvider(newInner(nil), envs, tu.TestLogger(t, false)) + + fns, err := p.GetFunctionsForComposition(comp) + if err != nil { + t.Fatalf("GetFunctionsForComposition() error = %v", err) + } + + if got := fns[0].Annotations[annKey]; got != envKey+"=16" { + t.Errorf("annotation = %q, want %q", got, envKey+"=16") + } + }) + + t.Run("appends to existing runtime-docker-env", func(t *testing.T) { + p := NewEnvInjectingFunctionProvider(newInner(map[string]string{annKey: "FOO=bar"}), envs, tu.TestLogger(t, false)) + + fns, err := p.GetFunctionsForComposition(comp) + if err != nil { + t.Fatalf("GetFunctionsForComposition() error = %v", err) + } + + if got := fns[0].Annotations[annKey]; got != "FOO=bar,"+envKey+"=16" { + t.Errorf("annotation = %q, want %q", got, "FOO=bar,"+envKey+"=16") + } + }) + + t.Run("idempotent across repeated calls", func(t *testing.T) { + p := NewEnvInjectingFunctionProvider(newInner(nil), envs, tu.TestLogger(t, false)) + if _, err := p.GetFunctionsForComposition(comp); err != nil { + t.Fatalf("first call error = %v", err) + } + + fns, err := p.GetFunctionsForComposition(comp) + if err != nil { + t.Fatalf("second call error = %v", err) + } + + if got := fns[0].Annotations[annKey]; got != envKey+"=16" { + t.Errorf("annotation after repeat = %q, want single pair %q", got, envKey+"=16") + } + }) +} + 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 86cf01db..34334ca4 100644 --- a/cmd/diff/diffprocessor/processor_config.go +++ b/cmd/diff/diffprocessor/processor_config.go @@ -48,6 +48,12 @@ type ProcessorConfig struct { // FunctionRegistryOverride overrides the registry in all function package refs. FunctionRegistryOverride string + // MaxRecvMessageSize is the max gRPC message size (MB) for render function + // containers. Zero leaves the function's own default (function-sdk-go uses + // 4MB). When >0 it is injected as the FUNCTION_GO_TEMPLATING_MAX_RECV_MESSAGE_SIZE + // container env var so large XRs don't trip the default limit under render. + MaxRecvMessageSize int + // Stdout is the writer for diff output (defaults to os.Stdout) Stdout io.Writer @@ -175,6 +181,14 @@ func WithFunctionRegistryOverride(registry string) ProcessorOption { } } +// WithMaxRecvMessageSize sets the max gRPC message size (MB) injected into +// render function containers. Zero leaves the function default. +func WithMaxRecvMessageSize(mb int) ProcessorOption { + return func(config *ProcessorConfig) { + config.MaxRecvMessageSize = mb + } +} + // WithStdout sets the writer for diff output. func WithStdout(w io.Writer) ProcessorOption { return func(config *ProcessorConfig) { diff --git a/cmd/diff/diffprocessor/processor_config_test.go b/cmd/diff/diffprocessor/processor_config_test.go index cfc5b9c3..6a797438 100644 --- a/cmd/diff/diffprocessor/processor_config_test.go +++ b/cmd/diff/diffprocessor/processor_config_test.go @@ -229,3 +229,19 @@ func TestWithStdoutStderr(t *testing.T) { t.Errorf("Expected config.Stderr to be the injected buffer, got: %v", config.Stderr) } } + +// TestWithMaxRecvMessageSize verifies the option sets the field and that the +// zero value is preserved when the option is not applied. +func TestWithMaxRecvMessageSize(t *testing.T) { + def := ProcessorConfig{} + if def.MaxRecvMessageSize != 0 { + t.Errorf("Expected default MaxRecvMessageSize to be 0, got: %d", def.MaxRecvMessageSize) + } + + config := ProcessorConfig{} + WithMaxRecvMessageSize(16)(&config) + + if config.MaxRecvMessageSize != 16 { + t.Errorf("Expected config.MaxRecvMessageSize to be 16, got: %d", config.MaxRecvMessageSize) + } +} diff --git a/cmd/diff/main.go b/cmd/diff/main.go index 14ca1629..7c6b86ba 100644 --- a/cmd/diff/main.go +++ b/cmd/diff/main.go @@ -95,16 +95,17 @@ func (f *FunctionCredentials) Decode(ctx *kong.DecodeContext) error { 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"` + 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 for requirements resolution or eventual-state simulation. Increase for complex pipelines that need more cycles to converge." name:"max-iterations"` + MaxNestedDepth int `default:"10" help:"Maximum depth for nested XR recursion." name:"max-nested-depth"` + MaxIterations int `default:"20" help:"Maximum render iterations for requirements resolution or eventual-state simulation. 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"` + 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"` - EventualState bool `default:"false" help:"Show eventual state after all reconciliation cycles complete (useful with function-sequencer)." name:"eventual-state"` + EventualState bool `default:"false" help:"Show eventual state after all reconciliation cycles complete (useful with function-sequencer)." name:"eventual-state"` + MaxRecvMessageSize int `default:"0" help:"Max gRPC message size (MB) for render function containers. 0 leaves the function default (4MB). Falls back to the CROSSPLANE_DIFF_MAX_RECV_MESSAGE_SIZE env var when unset." name:"max-recv-message-size"` // CrossplaneRenderBinary is a hidden test-only override that points the // render engine at a local `crossplane` binary. Production users leave diff --git a/design/design-doc-cli-diff.md b/design/design-doc-cli-diff.md index 7d1625a2..a04acbe4 100644 --- a/design/design-doc-cli-diff.md +++ b/design/design-doc-cli-diff.md @@ -496,6 +496,10 @@ The `ProcessorConfig` structure provides configuration options: - `IgnorePaths`: Field paths to suppress from diffs (e.g., status fields known to be reconciler-set). - `FunctionCredentials`: Image-pull credentials for private function registries. - `FunctionRegistryOverride`: Rewrites function image references to a mirror. +- `MaxRecvMessageSize`: Max gRPC message size (MB) injected into render function containers as the + `MAX_RECV_MESSAGE_SIZE` env var (`--max-recv-message-size`, falling back to `CROSSPLANE_DIFF_MAX_RECV_MESSAGE_SIZE`). + Zero leaves the function-sdk-go 4MB default. Needed because `crossplane render` ignores the cluster + DeploymentRuntimeConfig, so large XRs can otherwise exceed the default limit. - `CrossplaneRenderBinary`: Optional path to an external `crossplane render` binary (otherwise the in-process render package is used). - `Stdout`, `Stderr`: Output sinks (writers are no longer threaded through method calls). @@ -963,6 +967,10 @@ crossplane-diff xr --max-nested-depth 3 xr.yaml # Show steady-state diff for compositions that need multiple reconciliation cycles crossplane-diff xr --eventual-state xr.yaml + +# Raise the function gRPC receive limit for very large XRs (MB); injected as the +# MAX_RECV_MESSAGE_SIZE container env var. Also settable via CROSSPLANE_DIFF_MAX_RECV_MESSAGE_SIZE. +crossplane-diff xr --max-recv-message-size 16 xr.yaml ``` `comp` examples: From f9ebff4be1be8c94443a4a4a4d6037ba34707400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Fern=C3=A1ndez?= <7312236+fernandezcuesta@users.noreply.github.com> Date: Fri, 10 Jul 2026 08:12:59 +0200 Subject: [PATCH 2/3] chore: envvar as kong directive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jesús Fernández <7312236+fernandezcuesta@users.noreply.github.com> --- cmd/diff/main.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/diff/main.go b/cmd/diff/main.go index 7c6b86ba..92beaf5d 100644 --- a/cmd/diff/main.go +++ b/cmd/diff/main.go @@ -95,17 +95,17 @@ func (f *FunctionCredentials) Decode(ctx *kong.DecodeContext) error { 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"` + 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 for requirements resolution or eventual-state simulation. Increase for complex pipelines that need more cycles to converge." name:"max-iterations"` + MaxNestedDepth int `default:"10" help:"Maximum depth for nested XR recursion." name:"max-nested-depth"` + MaxIterations int `default:"20" help:"Maximum render iterations for requirements resolution or eventual-state simulation. 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"` + 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"` - EventualState bool `default:"false" help:"Show eventual state after all reconciliation cycles complete (useful with function-sequencer)." name:"eventual-state"` - MaxRecvMessageSize int `default:"0" help:"Max gRPC message size (MB) for render function containers. 0 leaves the function default (4MB). Falls back to the CROSSPLANE_DIFF_MAX_RECV_MESSAGE_SIZE env var when unset." name:"max-recv-message-size"` + EventualState bool `default:"false" help:"Show eventual state after all reconciliation cycles complete (useful with function-sequencer)." name:"eventual-state"` + MaxRecvMessageSize int `env:"CROSSPLANE_DIFF_MAX_RECV_MESSAGE_SIZE" help:"Max gRPC message size (MB) for render function containers (4MB if undefined)." name:"max-recv-message-size"` // CrossplaneRenderBinary is a hidden test-only override that points the // render engine at a local `crossplane` binary. Production users leave From ced79e2fc3648fbbcda869c9cfe55c763b30d1bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Fern=C3=A1ndez?= <7312236+fernandezcuesta@users.noreply.github.com> Date: Fri, 10 Jul 2026 09:35:15 +0200 Subject: [PATCH 3/3] chore: cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jesús Fernández <7312236+fernandezcuesta@users.noreply.github.com> --- README.md | 12 ++--- cmd/diff/cmd_utils.go | 29 +---------- cmd/diff/cmd_utils_test.go | 49 ------------------- .../diffprocessor/function_provider_test.go | 21 +++----- cmd/diff/diffprocessor/processor_config.go | 6 +-- design/design-doc-cli-diff.md | 8 --- 6 files changed, 17 insertions(+), 108 deletions(-) delete mode 100644 cmd/diff/cmd_utils_test.go diff --git a/README.md b/README.md index 72397e3a..80059f1c 100644 --- a/README.md +++ b/README.md @@ -166,10 +166,8 @@ Flags: --eventual-state Show eventual state after all reconciliation cycles complete. Useful with function-sequencer which hides later stage resources until earlier stages become Ready. - --max-recv-message-size=0 Max gRPC message size (MB) for render function - containers. 0 leaves the function default (4MB). Falls - back to the CROSSPLANE_DIFF_MAX_RECV_MESSAGE_SIZE env var - when unset. + --max-recv-message-size=INT Max gRPC message size (MB) for render function + containers (4MB if undefined) ($CROSSPLANE_DIFF_MAX_RECV_MESSAGE_SIZE). ``` **Note**: XR namespaces are read directly from the YAML files being diffed, not from command-line flags. @@ -216,10 +214,8 @@ Flags: --eventual-state Show eventual state after all reconciliation cycles complete. Useful with function-sequencer which hides later stage resources until earlier stages become Ready. - --max-recv-message-size=0 Max gRPC message size (MB) for render function - containers. 0 leaves the function default (4MB). Falls - back to the CROSSPLANE_DIFF_MAX_RECV_MESSAGE_SIZE env var - when unset. + --max-recv-message-size=INT Max gRPC message size (MB) for render function + containers (4MB if undefined) ($CROSSPLANE_DIFF_MAX_RECV_MESSAGE_SIZE). --resource=STRING,... Limit impact analysis to specific composites in [namespace/]name format. Repeatable or comma-separated. Bare name means cluster-scoped. Mutually exclusive with diff --git a/cmd/diff/cmd_utils.go b/cmd/diff/cmd_utils.go index 39a4dc9b..bfa40166 100644 --- a/cmd/diff/cmd_utils.go +++ b/cmd/diff/cmd_utils.go @@ -18,8 +18,6 @@ package main import ( "context" - "os" - "strconv" "time" dp "github.com/crossplane-contrib/crossplane-diff/cmd/diff/diffprocessor" @@ -32,29 +30,6 @@ import ( "github.com/crossplane/crossplane-runtime/v2/pkg/logging" ) -// envMaxRecvMessageSize is crossplane-diff's own process env var providing the -// fallback value for --max-recv-message-size (MB). Named after the existing -// CROSSPLANE_DIFF_DOCKER_NETWORK convention. This is distinct from the -// dp.EnvMaxRecvMessageSize var that gets injected INTO function containers. -const envMaxRecvMessageSize = "CROSSPLANE_DIFF_MAX_RECV_MESSAGE_SIZE" - -// resolveMaxRecvMessageSize returns the max gRPC recv size (MB) to inject into -// function containers: the flag when >0, else the CROSSPLANE_DIFF_MAX_RECV_MESSAGE_SIZE -// env var when it parses to a positive integer, else 0 (inject nothing). -func resolveMaxRecvMessageSize(flag int) int { - if flag > 0 { - return flag - } - - if v := os.Getenv(envMaxRecvMessageSize); v != "" { - if n, err := strconv.Atoi(v); err == nil && n > 0 { - return n - } - } - - return 0 -} - // initializeAppContext initializes the application context with timeout and error handling. func initializeAppContext(timeout time.Duration, appCtx *AppContext, log logging.Logger) (context.Context, context.CancelFunc, error) { ctx, cancel := context.WithTimeout(context.Background(), timeout) @@ -113,8 +88,8 @@ func defaultProcessorOptions(fields CommonCmdFields) []dp.ProcessorOption { opts = append(opts, dp.WithFunctionRegistryOverride(fields.FunctionRegistryOverride)) } - if sz := resolveMaxRecvMessageSize(fields.MaxRecvMessageSize); sz > 0 { - opts = append(opts, dp.WithMaxRecvMessageSize(sz)) + if fields.MaxRecvMessageSize > 0 { + opts = append(opts, dp.WithMaxRecvMessageSize(fields.MaxRecvMessageSize)) } if fields.CrossplaneRenderBinary != "" { diff --git a/cmd/diff/cmd_utils_test.go b/cmd/diff/cmd_utils_test.go deleted file mode 100644 index 205bb55b..00000000 --- a/cmd/diff/cmd_utils_test.go +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright 2025 The Crossplane Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package main - -import "testing" - -func TestResolveMaxRecvMessageSize(t *testing.T) { - tests := []struct { - name string - flag int - env string // "" means env unset - want int - }{ - {name: "flag set wins over env", flag: 16, env: "8", want: 16}, - {name: "env fallback when flag unset", flag: 0, env: "8", want: 8}, - {name: "neither set returns zero", flag: 0, env: "", want: 0}, - {name: "non-integer env ignored", flag: 0, env: "notanint", want: 0}, - {name: "zero env ignored", flag: 0, env: "0", want: 0}, - {name: "negative env ignored", flag: 0, env: "-5", want: 0}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.env == "" { - t.Setenv(envMaxRecvMessageSize, "") - } else { - t.Setenv(envMaxRecvMessageSize, tt.env) - } - - if got := resolveMaxRecvMessageSize(tt.flag); got != tt.want { - t.Errorf("resolveMaxRecvMessageSize(%d) with env %q = %d, want %d", tt.flag, tt.env, got, tt.want) - } - }) - } -} diff --git a/cmd/diff/diffprocessor/function_provider_test.go b/cmd/diff/diffprocessor/function_provider_test.go index 2e894bb0..0c5f3f4b 100644 --- a/cmd/diff/diffprocessor/function_provider_test.go +++ b/cmd/diff/diffprocessor/function_provider_test.go @@ -639,11 +639,6 @@ func TestUpsertEnvPair(t *testing.T) { // value, and is idempotent across repeated calls (which matters when the inner // provider caches and returns the same Function values). func TestEnvInjectingFunctionProvider(t *testing.T) { - const ( - envKey = EnvMaxRecvMessageSize - annKey = "render.crossplane.io/runtime-docker-env" - ) - newInner := func(anns map[string]string) FunctionProvider { fn := pkgv1.Function{ObjectMeta: metav1.ObjectMeta{Name: "function-go-templating"}} if anns != nil { @@ -659,7 +654,7 @@ func TestEnvInjectingFunctionProvider(t *testing.T) { } comp := &apiextensionsv1.Composition{ObjectMeta: metav1.ObjectMeta{Name: "test-composition"}} - envs := map[string]string{envKey: "16"} + envs := map[string]string{EnvMaxRecvMessageSize: "16"} t.Run("injects when annotation absent", func(t *testing.T) { p := NewEnvInjectingFunctionProvider(newInner(nil), envs, tu.TestLogger(t, false)) @@ -669,21 +664,21 @@ func TestEnvInjectingFunctionProvider(t *testing.T) { t.Fatalf("GetFunctionsForComposition() error = %v", err) } - if got := fns[0].Annotations[annKey]; got != envKey+"=16" { - t.Errorf("annotation = %q, want %q", got, envKey+"=16") + if got := fns[0].Annotations[annKeyRuntimeDockerEnv]; got != EnvMaxRecvMessageSize+"=16" { + t.Errorf("annotation = %q, want %q", got, EnvMaxRecvMessageSize+"=16") } }) t.Run("appends to existing runtime-docker-env", func(t *testing.T) { - p := NewEnvInjectingFunctionProvider(newInner(map[string]string{annKey: "FOO=bar"}), envs, tu.TestLogger(t, false)) + p := NewEnvInjectingFunctionProvider(newInner(map[string]string{annKeyRuntimeDockerEnv: "FOO=bar"}), envs, tu.TestLogger(t, false)) fns, err := p.GetFunctionsForComposition(comp) if err != nil { t.Fatalf("GetFunctionsForComposition() error = %v", err) } - if got := fns[0].Annotations[annKey]; got != "FOO=bar,"+envKey+"=16" { - t.Errorf("annotation = %q, want %q", got, "FOO=bar,"+envKey+"=16") + if got := fns[0].Annotations[annKeyRuntimeDockerEnv]; got != "FOO=bar,"+EnvMaxRecvMessageSize+"=16" { + t.Errorf("annotation = %q, want %q", got, "FOO=bar,"+EnvMaxRecvMessageSize+"=16") } }) @@ -698,8 +693,8 @@ func TestEnvInjectingFunctionProvider(t *testing.T) { t.Fatalf("second call error = %v", err) } - if got := fns[0].Annotations[annKey]; got != envKey+"=16" { - t.Errorf("annotation after repeat = %q, want single pair %q", got, envKey+"=16") + if got := fns[0].Annotations[annKeyRuntimeDockerEnv]; got != EnvMaxRecvMessageSize+"=16" { + t.Errorf("annotation after repeat = %q, want single pair %q", got, EnvMaxRecvMessageSize+"=16") } }) } diff --git a/cmd/diff/diffprocessor/processor_config.go b/cmd/diff/diffprocessor/processor_config.go index 34334ca4..9fb69e01 100644 --- a/cmd/diff/diffprocessor/processor_config.go +++ b/cmd/diff/diffprocessor/processor_config.go @@ -50,8 +50,8 @@ type ProcessorConfig struct { // MaxRecvMessageSize is the max gRPC message size (MB) for render function // containers. Zero leaves the function's own default (function-sdk-go uses - // 4MB). When >0 it is injected as the FUNCTION_GO_TEMPLATING_MAX_RECV_MESSAGE_SIZE - // container env var so large XRs don't trip the default limit under render. + // 4MB). When >0 it is injected as the appropriate container env var, + // so large XRs don't trip the default limit under render. MaxRecvMessageSize int // Stdout is the writer for diff output (defaults to os.Stdout) @@ -182,7 +182,7 @@ func WithFunctionRegistryOverride(registry string) ProcessorOption { } // WithMaxRecvMessageSize sets the max gRPC message size (MB) injected into -// render function containers. Zero leaves the function default. +// render function containers. func WithMaxRecvMessageSize(mb int) ProcessorOption { return func(config *ProcessorConfig) { config.MaxRecvMessageSize = mb diff --git a/design/design-doc-cli-diff.md b/design/design-doc-cli-diff.md index a04acbe4..7d1625a2 100644 --- a/design/design-doc-cli-diff.md +++ b/design/design-doc-cli-diff.md @@ -496,10 +496,6 @@ The `ProcessorConfig` structure provides configuration options: - `IgnorePaths`: Field paths to suppress from diffs (e.g., status fields known to be reconciler-set). - `FunctionCredentials`: Image-pull credentials for private function registries. - `FunctionRegistryOverride`: Rewrites function image references to a mirror. -- `MaxRecvMessageSize`: Max gRPC message size (MB) injected into render function containers as the - `MAX_RECV_MESSAGE_SIZE` env var (`--max-recv-message-size`, falling back to `CROSSPLANE_DIFF_MAX_RECV_MESSAGE_SIZE`). - Zero leaves the function-sdk-go 4MB default. Needed because `crossplane render` ignores the cluster - DeploymentRuntimeConfig, so large XRs can otherwise exceed the default limit. - `CrossplaneRenderBinary`: Optional path to an external `crossplane render` binary (otherwise the in-process render package is used). - `Stdout`, `Stderr`: Output sinks (writers are no longer threaded through method calls). @@ -967,10 +963,6 @@ crossplane-diff xr --max-nested-depth 3 xr.yaml # Show steady-state diff for compositions that need multiple reconciliation cycles crossplane-diff xr --eventual-state xr.yaml - -# Raise the function gRPC receive limit for very large XRs (MB); injected as the -# MAX_RECV_MESSAGE_SIZE container env var. Also settable via CROSSPLANE_DIFF_MAX_RECV_MESSAGE_SIZE. -crossplane-diff xr --max-recv-message-size 16 xr.yaml ``` `comp` examples: