From 215fa6ac2179aa1332ea7e8086915625917a2183 Mon Sep 17 00:00:00 2001 From: Nikita Z Date: Thu, 4 Jun 2026 09:51:11 +0200 Subject: [PATCH 01/18] fix(render): do not override function docker-network annotation Signed-off-by: Nikita Z --- cmd/crossplane/render/render.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cmd/crossplane/render/render.go b/cmd/crossplane/render/render.go index 5cc373f9..25b0be8e 100644 --- a/cmd/crossplane/render/render.go +++ b/cmd/crossplane/render/render.go @@ -158,7 +158,11 @@ func injectNetworkAnnotation(fns []pkgv1.Function, networkName string) { if fns[i].Annotations == nil { fns[i].Annotations = make(map[string]string) } - fns[i].Annotations[AnnotationKeyRuntimeDockerNetwork] = networkName + + _, ok := fns[i].Annotations[AnnotationKeyRuntimeDockerNetwork] + if !ok { + fns[i].Annotations[AnnotationKeyRuntimeDockerNetwork] = networkName + } } } From 550bd8df2be6a5694f791c3cad0608065910ef93 Mon Sep 17 00:00:00 2001 From: Nikita Z Date: Thu, 4 Jun 2026 09:51:11 +0200 Subject: [PATCH 02/18] fix: if AnnotationKeyRuntimeDockerNetwork is set, start crossplane container in it Signed-off-by: Nikita Z --- cmd/crossplane/render/engine.go | 5 +++-- cmd/crossplane/render/engine_docker.go | 17 +++++++++++------ cmd/crossplane/render/op/cmd.go | 18 ++++++++++++++++-- cmd/crossplane/render/xr/cmd.go | 17 +++++++++++++++-- 4 files changed, 45 insertions(+), 12 deletions(-) diff --git a/cmd/crossplane/render/engine.go b/cmd/crossplane/render/engine.go index 1e08ce45..aa801fa4 100644 --- a/cmd/crossplane/render/engine.go +++ b/cmd/crossplane/render/engine.go @@ -61,17 +61,18 @@ type EngineFlags struct { CrossplaneVersion string `help:"Version of the Crossplane image to use for rendering. Defaults to the latest stable version." placeholder:"VERSION" xor:"crossplane-selector"` CrossplaneImage string `help:"Override the full Crossplane Docker image reference for rendering." placeholder:"IMAGE" xor:"crossplane-selector"` CrossplaneBinary string `help:"Path to a local crossplane binary to use instead of Docker." placeholder:"PATH" type:"existingfile" xor:"crossplane-selector"` + Network string `help:"The network containers should connect to"` } // NewEngineFromFlags creates an Engine from the flag configuration. If a binary // path is set, it returns a local engine. Otherwise it returns a Docker engine // using the resolved image reference. -func NewEngineFromFlags(f *EngineFlags, log logging.Logger) Engine { +func NewEngineFromFlags(f *EngineFlags, network string, log logging.Logger) Engine { if f.CrossplaneBinary != "" { return &localRenderEngine{BinaryPath: f.CrossplaneBinary} } - return &dockerRenderEngine{image: crossplaneImageFromFlags(f), log: log} + return &dockerRenderEngine{image: crossplaneImageFromFlags(f), network: network, log: log} } func crossplaneImageFromFlags(f *EngineFlags) string { diff --git a/cmd/crossplane/render/engine_docker.go b/cmd/crossplane/render/engine_docker.go index b67cf54b..9bde1fb8 100644 --- a/cmd/crossplane/render/engine_docker.go +++ b/cmd/crossplane/render/engine_docker.go @@ -83,13 +83,18 @@ func (e *dockerRenderEngine) CheckContextSupport() error { // containers also join it. The returned cleanup function removes the // network. func (e *dockerRenderEngine) Setup(ctx context.Context, fns []pkgv1.Function) (func(), error) { - networkID, networkName, err := createRenderNetwork(ctx) - if err != nil { - return func() {}, errors.Wrap(err, "cannot create Docker network for rendering") - } + var networkID, networkName string - e.network = networkName - injectNetworkAnnotation(fns, networkName) + if e.network == "" { + var err error + networkID, networkName, err = createRenderNetwork(ctx) + if err != nil { + return func() {}, errors.Wrap(err, "cannot create Docker network for rendering") + } + e.network = networkName + + injectNetworkAnnotation(fns, networkName) + } cleanup := func() { //nolint:contextcheck // Detached context for cleanup. _ = removeRenderNetwork(context.Background(), networkID) diff --git a/cmd/crossplane/render/op/cmd.go b/cmd/crossplane/render/op/cmd.go index 04e6bc93..120cc79a 100644 --- a/cmd/crossplane/render/op/cmd.go +++ b/cmd/crossplane/render/op/cmd.go @@ -22,6 +22,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "time" "github.com/alecthomas/kong" @@ -84,7 +85,7 @@ type Cmd struct { fs afero.Fs // newEngine constructs the render Engine. - newEngine func(*render.EngineFlags, logging.Logger) render.Engine + newEngine func(*render.EngineFlags, string, logging.Logger) render.Engine } // Help prints out the help for the alpha render op command. @@ -167,7 +168,20 @@ func (c *Cmd) Run(k *kong.Context, log logging.Logger, sp terminal.SpinnerPrinte } } - engine := c.newEngine(&c.EngineFlags, log) + network := "" + for _, annotation := range c.FunctionAnnotations { + parts := strings.SplitN(annotation, "=", 2) + if len(parts) != 2 { + return errors.Errorf("invalid function annotation format %q, expected key=value", annotation) + } + key, value := parts[0], parts[1] + if key == render.AnnotationKeyRuntimeDockerNetwork { + network = value + break + } + } + + engine := c.newEngine(&c.EngineFlags, network, log) seedCtx := len(c.ContextValues) > 0 || len(c.ContextFiles) > 0 captureCtx := c.IncludeContext diff --git a/cmd/crossplane/render/xr/cmd.go b/cmd/crossplane/render/xr/cmd.go index b7543a6c..58020f4c 100644 --- a/cmd/crossplane/render/xr/cmd.go +++ b/cmd/crossplane/render/xr/cmd.go @@ -93,7 +93,7 @@ type Cmd struct { fs afero.Fs // newEngine constructs the render Engine. - newEngine func(*render.EngineFlags, logging.Logger) render.Engine + newEngine func(*render.EngineFlags, string, logging.Logger) render.Engine } // Help prints out the help for the render command. @@ -220,7 +220,20 @@ func (c *Cmd) Run(k *kong.Context, log logging.Logger, sp terminal.SpinnerPrinte } } - engine := c.newEngine(&c.EngineFlags, log) + network := "" + for _, annotation := range c.FunctionAnnotations { + parts := strings.SplitN(annotation, "=", 2) + if len(parts) != 2 { + return errors.Errorf("invalid function annotation format %q, expected key=value", annotation) + } + key, value := parts[0], parts[1] + if key == render.AnnotationKeyRuntimeDockerNetwork { + network = value + break + } + } + + engine := c.newEngine(&c.EngineFlags, network, log) seedCtx := len(c.ContextValues) > 0 || len(c.ContextFiles) > 0 captureCtx := c.IncludeContext From bfdae166f858f6f96d9763b75fc463a4d68bac19 Mon Sep 17 00:00:00 2001 From: Nikita Z Date: Thu, 4 Jun 2026 09:51:11 +0200 Subject: [PATCH 03/18] test: update newEngineFunc signature Signed-off-by: Nikita Z --- cmd/crossplane/render/op/cmd_test.go | 4 ++-- cmd/crossplane/render/xr/cmd_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/crossplane/render/op/cmd_test.go b/cmd/crossplane/render/op/cmd_test.go index f17ec0c7..6a9e16e4 100644 --- a/cmd/crossplane/render/op/cmd_test.go +++ b/cmd/crossplane/render/op/cmd_test.go @@ -63,8 +63,8 @@ var includeFunctionResultsOutput string //go:embed testdata/cmd/output/include-full-operation.yaml var includeFullOperationOutput string -func newEngineFunc(engine render.Engine) func(*render.EngineFlags, logging.Logger) render.Engine { - return func(*render.EngineFlags, logging.Logger) render.Engine { +func newEngineFunc(engine render.Engine) func(*render.EngineFlags, string, logging.Logger) render.Engine { + return func(*render.EngineFlags, string, logging.Logger) render.Engine { return engine } } diff --git a/cmd/crossplane/render/xr/cmd_test.go b/cmd/crossplane/render/xr/cmd_test.go index d57cc14a..cf403156 100644 --- a/cmd/crossplane/render/xr/cmd_test.go +++ b/cmd/crossplane/render/xr/cmd_test.go @@ -78,8 +78,8 @@ var includeFunctionResultsOutput string //go:embed testdata/cmd/output/include-full-xr.yaml var includeFullXROutput string -func newEngineFunc(engine render.Engine) func(*render.EngineFlags, logging.Logger) render.Engine { - return func(*render.EngineFlags, logging.Logger) render.Engine { +func newEngineFunc(engine render.Engine) func(*render.EngineFlags, string, logging.Logger) render.Engine { + return func(*render.EngineFlags, string, logging.Logger) render.Engine { return engine } } From 312ecbea0c32f86e4a823746e7cda3dcf1cfb5fe Mon Sep 17 00:00:00 2001 From: Nikita Z Date: Thu, 4 Jun 2026 12:18:39 +0200 Subject: [PATCH 04/18] fix: remove network from engineflage, use dockerRenderEngine Signed-off-by: Nikita Z --- cmd/crossplane/render/engine.go | 5 +++-- cmd/crossplane/render/engine_docker.go | 3 +-- cmd/crossplane/render/op/cmd.go | 2 +- cmd/crossplane/render/xr/cmd.go | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/crossplane/render/engine.go b/cmd/crossplane/render/engine.go index aa801fa4..333fb791 100644 --- a/cmd/crossplane/render/engine.go +++ b/cmd/crossplane/render/engine.go @@ -61,12 +61,13 @@ type EngineFlags struct { CrossplaneVersion string `help:"Version of the Crossplane image to use for rendering. Defaults to the latest stable version." placeholder:"VERSION" xor:"crossplane-selector"` CrossplaneImage string `help:"Override the full Crossplane Docker image reference for rendering." placeholder:"IMAGE" xor:"crossplane-selector"` CrossplaneBinary string `help:"Path to a local crossplane binary to use instead of Docker." placeholder:"PATH" type:"existingfile" xor:"crossplane-selector"` - Network string `help:"The network containers should connect to"` } // NewEngineFromFlags creates an Engine from the flag configuration. If a binary // path is set, it returns a local engine. Otherwise it returns a Docker engine -// using the resolved image reference. +// using the resolved image reference. The network parameter sets the Docker +// network the render container should join; it is derived from function +// annotations (AnnotationKeyRuntimeDockerNetwork) by the caller. func NewEngineFromFlags(f *EngineFlags, network string, log logging.Logger) Engine { if f.CrossplaneBinary != "" { return &localRenderEngine{BinaryPath: f.CrossplaneBinary} diff --git a/cmd/crossplane/render/engine_docker.go b/cmd/crossplane/render/engine_docker.go index 9bde1fb8..83d5059f 100644 --- a/cmd/crossplane/render/engine_docker.go +++ b/cmd/crossplane/render/engine_docker.go @@ -53,8 +53,7 @@ func (realContainerRunner) Run(ctx context.Context, img string, opts ...docker.R type dockerRenderEngine struct { // image is the Crossplane Docker image reference. image string - // network is the Docker network to connect the container to. When set, - // the container joins this network so it can reach function containers. + // network is the Docker network to connect the container to. network string log logging.Logger diff --git a/cmd/crossplane/render/op/cmd.go b/cmd/crossplane/render/op/cmd.go index 120cc79a..ee709140 100644 --- a/cmd/crossplane/render/op/cmd.go +++ b/cmd/crossplane/render/op/cmd.go @@ -168,7 +168,7 @@ func (c *Cmd) Run(k *kong.Context, log logging.Logger, sp terminal.SpinnerPrinte } } - network := "" + var network string for _, annotation := range c.FunctionAnnotations { parts := strings.SplitN(annotation, "=", 2) if len(parts) != 2 { diff --git a/cmd/crossplane/render/xr/cmd.go b/cmd/crossplane/render/xr/cmd.go index 58020f4c..11d63bfe 100644 --- a/cmd/crossplane/render/xr/cmd.go +++ b/cmd/crossplane/render/xr/cmd.go @@ -220,7 +220,7 @@ func (c *Cmd) Run(k *kong.Context, log logging.Logger, sp terminal.SpinnerPrinte } } - network := "" + var network string for _, annotation := range c.FunctionAnnotations { parts := strings.SplitN(annotation, "=", 2) if len(parts) != 2 { From 7164a4a23d994de26f566a4f70eae6d10fe041ea Mon Sep 17 00:00:00 2001 From: Nikita Z Date: Thu, 4 Jun 2026 12:33:12 +0200 Subject: [PATCH 05/18] fix: move cleanup to the empty network branch Signed-off-by: Nikita Z --- cmd/crossplane/render/engine_docker.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/cmd/crossplane/render/engine_docker.go b/cmd/crossplane/render/engine_docker.go index 83d5059f..63ccb3db 100644 --- a/cmd/crossplane/render/engine_docker.go +++ b/cmd/crossplane/render/engine_docker.go @@ -93,13 +93,17 @@ func (e *dockerRenderEngine) Setup(ctx context.Context, fns []pkgv1.Function) (f e.network = networkName injectNetworkAnnotation(fns, networkName) - } - cleanup := func() { //nolint:contextcheck // Detached context for cleanup. - _ = removeRenderNetwork(context.Background(), networkID) + cleanup := func() { //nolint:contextcheck // Detached context for cleanup. + _ = removeRenderNetwork(context.Background(), networkID) + } + + return cleanup, nil } - return cleanup, nil + // e.network was pre-configured by the caller (e.g. from a function + // annotation). We don't own the network, so there is nothing to clean up. + return func() {}, nil } // Render marshals the request, runs it through a Docker container, and returns From acf5682b943719e7a80947c285697780897f404e Mon Sep 17 00:00:00 2001 From: Nikita Z Date: Tue, 9 Jun 2026 13:43:23 +0200 Subject: [PATCH 06/18] refactor: move common annotation logic to render/annotation.go Signed-off-by: Nikita Z --- cmd/crossplane/render/annotation.go | 23 +++++++++++++++++++++++ cmd/crossplane/render/op/cmd.go | 14 +++----------- cmd/crossplane/render/xr/cmd.go | 13 +++---------- 3 files changed, 29 insertions(+), 21 deletions(-) create mode 100644 cmd/crossplane/render/annotation.go diff --git a/cmd/crossplane/render/annotation.go b/cmd/crossplane/render/annotation.go new file mode 100644 index 00000000..e596e7c6 --- /dev/null +++ b/cmd/crossplane/render/annotation.go @@ -0,0 +1,23 @@ +package render + +import "strings" + +type Annotations map[string]string + +// NewAnnotationsFromStrings parses an array of strings in the format "key=value" into a map. +// Silently skips strings in incorrect format. +func NewAnnotationsFromStrings(annotations []string) Annotations { + result := make(Annotations, 0) + for _, annotation := range annotations { + parts := strings.SplitN(annotation, "=", 2) + + if len(parts) != 2 { + continue + } + + key, value := parts[0], parts[1] + result[key] = value + } + + return result +} diff --git a/cmd/crossplane/render/op/cmd.go b/cmd/crossplane/render/op/cmd.go index ee709140..032a9dff 100644 --- a/cmd/crossplane/render/op/cmd.go +++ b/cmd/crossplane/render/op/cmd.go @@ -22,7 +22,6 @@ import ( "fmt" "os" "path/filepath" - "strings" "time" "github.com/alecthomas/kong" @@ -169,16 +168,9 @@ func (c *Cmd) Run(k *kong.Context, log logging.Logger, sp terminal.SpinnerPrinte } var network string - for _, annotation := range c.FunctionAnnotations { - parts := strings.SplitN(annotation, "=", 2) - if len(parts) != 2 { - return errors.Errorf("invalid function annotation format %q, expected key=value", annotation) - } - key, value := parts[0], parts[1] - if key == render.AnnotationKeyRuntimeDockerNetwork { - network = value - break - } + annotations := render.NewAnnotationsFromStrings(c.FunctionAnnotations) + if value, ok := annotations[render.AnnotationKeyRuntimeDockerNetwork]; ok { + network = value } engine := c.newEngine(&c.EngineFlags, network, log) diff --git a/cmd/crossplane/render/xr/cmd.go b/cmd/crossplane/render/xr/cmd.go index 11d63bfe..ed13504c 100644 --- a/cmd/crossplane/render/xr/cmd.go +++ b/cmd/crossplane/render/xr/cmd.go @@ -221,16 +221,9 @@ func (c *Cmd) Run(k *kong.Context, log logging.Logger, sp terminal.SpinnerPrinte } var network string - for _, annotation := range c.FunctionAnnotations { - parts := strings.SplitN(annotation, "=", 2) - if len(parts) != 2 { - return errors.Errorf("invalid function annotation format %q, expected key=value", annotation) - } - key, value := parts[0], parts[1] - if key == render.AnnotationKeyRuntimeDockerNetwork { - network = value - break - } + annotations := render.NewAnnotationsFromStrings(c.FunctionAnnotations) + if value, ok := annotations[render.AnnotationKeyRuntimeDockerNetwork]; ok { + network = value } engine := c.newEngine(&c.EngineFlags, network, log) From 26d75b432974087218654026489214d9e07a2780 Mon Sep 17 00:00:00 2001 From: Nikita Z Date: Wed, 10 Jun 2026 13:38:41 +0200 Subject: [PATCH 07/18] chore: specify that injectNetworkAnnotation does not overwrite annotations in comment Signed-off-by: Nikita Z --- cmd/crossplane/render/render.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/crossplane/render/render.go b/cmd/crossplane/render/render.go index 25b0be8e..90d406ea 100644 --- a/cmd/crossplane/render/render.go +++ b/cmd/crossplane/render/render.go @@ -151,7 +151,8 @@ func RewriteAddressesForDocker(fns []*renderv1alpha1.FunctionInput) []*renderv1a return fns } -// injectNetworkAnnotation sets the Docker network annotation on all functions +// injectNetworkAnnotation sets the Docker network annotation +// on all functions without existing runtime-docker-network annotation // so their containers join the specified network. func injectNetworkAnnotation(fns []pkgv1.Function, networkName string) { for i := range fns { From ef449617880f230fb91d92a46c4abc6e8a3b607e Mon Sep 17 00:00:00 2001 From: Nikita Z Date: Thu, 11 Jun 2026 11:43:59 +0200 Subject: [PATCH 08/18] refactor(dockerRenderEngine/setup): return early if e.network is set Signed-off-by: Nikita Z --- cmd/crossplane/render/engine_docker.go | 29 +++++++++++++------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/cmd/crossplane/render/engine_docker.go b/cmd/crossplane/render/engine_docker.go index 63ccb3db..7815f138 100644 --- a/cmd/crossplane/render/engine_docker.go +++ b/cmd/crossplane/render/engine_docker.go @@ -84,26 +84,25 @@ func (e *dockerRenderEngine) CheckContextSupport() error { func (e *dockerRenderEngine) Setup(ctx context.Context, fns []pkgv1.Function) (func(), error) { var networkID, networkName string - if e.network == "" { - var err error - networkID, networkName, err = createRenderNetwork(ctx) - if err != nil { - return func() {}, errors.Wrap(err, "cannot create Docker network for rendering") - } - e.network = networkName + if e.network != "" { + // e.network was pre-configured, we don't own the network, so there is nothing to clean up. + return func() {}, nil + } - injectNetworkAnnotation(fns, networkName) + var err error + networkID, networkName, err = createRenderNetwork(ctx) + if err != nil { + return func() {}, errors.Wrap(err, "cannot create Docker network for rendering") + } + e.network = networkName - cleanup := func() { //nolint:contextcheck // Detached context for cleanup. - _ = removeRenderNetwork(context.Background(), networkID) - } + injectNetworkAnnotation(fns, networkName) - return cleanup, nil + cleanup := func() { //nolint:contextcheck // Detached context for cleanup. + _ = removeRenderNetwork(context.Background(), networkID) } - // e.network was pre-configured by the caller (e.g. from a function - // annotation). We don't own the network, so there is nothing to clean up. - return func() {}, nil + return cleanup, nil } // Render marshals the request, runs it through a Docker container, and returns From 64e25348f24d5fb9021f5caf3f23d2a023c71ee9 Mon Sep 17 00:00:00 2001 From: Nikita Z Date: Thu, 11 Jun 2026 13:41:26 +0200 Subject: [PATCH 09/18] refactor(render/dockerengine): add CrossplaneDockerNetwork to engineflags if empty, default to the first docker-network annotation in the provided functions. If provided, the docker-network annotation in the FunctionAnnotations cli flag takes presedence Signed-off-by: Nikita Z --- cmd/crossplane/render/engine.go | 11 ++++++----- cmd/crossplane/render/op/cmd.go | 25 ++++++++++++++++++------- cmd/crossplane/render/op/cmd_test.go | 4 ++-- cmd/crossplane/render/xr/cmd.go | 24 ++++++++++++++++++------ cmd/crossplane/render/xr/cmd_test.go | 4 ++-- 5 files changed, 46 insertions(+), 22 deletions(-) diff --git a/cmd/crossplane/render/engine.go b/cmd/crossplane/render/engine.go index 333fb791..0c70adda 100644 --- a/cmd/crossplane/render/engine.go +++ b/cmd/crossplane/render/engine.go @@ -58,9 +58,10 @@ type Engine interface { // EngineFlags contains flags for configuring the render engine. It is embedded // by render command structs to provide shared engine configuration. type EngineFlags struct { - CrossplaneVersion string `help:"Version of the Crossplane image to use for rendering. Defaults to the latest stable version." placeholder:"VERSION" xor:"crossplane-selector"` - CrossplaneImage string `help:"Override the full Crossplane Docker image reference for rendering." placeholder:"IMAGE" xor:"crossplane-selector"` - CrossplaneBinary string `help:"Path to a local crossplane binary to use instead of Docker." placeholder:"PATH" type:"existingfile" xor:"crossplane-selector"` + CrossplaneVersion string `help:"Version of the Crossplane image to use for rendering. Defaults to the latest stable version." placeholder:"VERSION" xor:"crossplane-selector"` + CrossplaneImage string `help:"Override the full Crossplane Docker image reference for rendering." placeholder:"IMAGE" xor:"crossplane-selector"` + CrossplaneBinary string `help:"Path to a local crossplane binary to use instead of Docker." placeholder:"PATH" type:"existingfile" xor:"crossplane-selector,crossplane-docker"` + CrossplaneDockerNetwork string `help:"The docker network to start the crossplane container in" xor:"crossplane-docker"` } // NewEngineFromFlags creates an Engine from the flag configuration. If a binary @@ -68,12 +69,12 @@ type EngineFlags struct { // using the resolved image reference. The network parameter sets the Docker // network the render container should join; it is derived from function // annotations (AnnotationKeyRuntimeDockerNetwork) by the caller. -func NewEngineFromFlags(f *EngineFlags, network string, log logging.Logger) Engine { +func NewEngineFromFlags(f *EngineFlags, log logging.Logger) Engine { if f.CrossplaneBinary != "" { return &localRenderEngine{BinaryPath: f.CrossplaneBinary} } - return &dockerRenderEngine{image: crossplaneImageFromFlags(f), network: network, log: log} + return &dockerRenderEngine{image: crossplaneImageFromFlags(f), network: f.CrossplaneDockerNetwork, log: log} } func crossplaneImageFromFlags(f *EngineFlags) string { diff --git a/cmd/crossplane/render/op/cmd.go b/cmd/crossplane/render/op/cmd.go index 032a9dff..83f5e981 100644 --- a/cmd/crossplane/render/op/cmd.go +++ b/cmd/crossplane/render/op/cmd.go @@ -84,7 +84,7 @@ type Cmd struct { fs afero.Fs // newEngine constructs the render Engine. - newEngine func(*render.EngineFlags, string, logging.Logger) render.Engine + newEngine func(*render.EngineFlags, logging.Logger) render.Engine } // Help prints out the help for the alpha render op command. @@ -167,13 +167,25 @@ func (c *Cmd) Run(k *kong.Context, log logging.Logger, sp terminal.SpinnerPrinte } } - var network string - annotations := render.NewAnnotationsFromStrings(c.FunctionAnnotations) - if value, ok := annotations[render.AnnotationKeyRuntimeDockerNetwork]; ok { - network = value + if c.EngineFlags.CrossplaneDockerNetwork == "" { + // Default to the first docker-network annotation in the provided functions + for _, fn := range fns { + if value, ok := fn.Annotations[render.AnnotationKeyRuntimeDockerNetwork]; ok { + c.CrossplaneDockerNetwork = value + break + } + } + + // Overwrite with docker-network annotation from function-annotations cli flag if set + if len(c.FunctionAnnotations) > 0 { + annotations := render.NewAnnotationsFromStrings(c.FunctionAnnotations) + if value, ok := annotations[render.AnnotationKeyRuntimeDockerNetwork]; ok { + c.EngineFlags.CrossplaneDockerNetwork = value + } + } } - engine := c.newEngine(&c.EngineFlags, network, log) + engine := c.newEngine(&c.EngineFlags, log) seedCtx := len(c.ContextValues) > 0 || len(c.ContextFiles) > 0 captureCtx := c.IncludeContext @@ -404,6 +416,5 @@ func (c *Cmd) loadFunctions(ctx context.Context, log logging.Logger, sp terminal }); err != nil { return nil, errors.Wrap(err, "cannot build embedded functions") } - return fns, nil } diff --git a/cmd/crossplane/render/op/cmd_test.go b/cmd/crossplane/render/op/cmd_test.go index 6a9e16e4..f17ec0c7 100644 --- a/cmd/crossplane/render/op/cmd_test.go +++ b/cmd/crossplane/render/op/cmd_test.go @@ -63,8 +63,8 @@ var includeFunctionResultsOutput string //go:embed testdata/cmd/output/include-full-operation.yaml var includeFullOperationOutput string -func newEngineFunc(engine render.Engine) func(*render.EngineFlags, string, logging.Logger) render.Engine { - return func(*render.EngineFlags, string, logging.Logger) render.Engine { +func newEngineFunc(engine render.Engine) func(*render.EngineFlags, logging.Logger) render.Engine { + return func(*render.EngineFlags, logging.Logger) render.Engine { return engine } } diff --git a/cmd/crossplane/render/xr/cmd.go b/cmd/crossplane/render/xr/cmd.go index ed13504c..6c5bb08d 100644 --- a/cmd/crossplane/render/xr/cmd.go +++ b/cmd/crossplane/render/xr/cmd.go @@ -93,7 +93,7 @@ type Cmd struct { fs afero.Fs // newEngine constructs the render Engine. - newEngine func(*render.EngineFlags, string, logging.Logger) render.Engine + newEngine func(*render.EngineFlags, logging.Logger) render.Engine } // Help prints out the help for the render command. @@ -220,13 +220,25 @@ func (c *Cmd) Run(k *kong.Context, log logging.Logger, sp terminal.SpinnerPrinte } } - var network string - annotations := render.NewAnnotationsFromStrings(c.FunctionAnnotations) - if value, ok := annotations[render.AnnotationKeyRuntimeDockerNetwork]; ok { - network = value + if c.EngineFlags.CrossplaneDockerNetwork == "" { + // Default to the first docker-network annotation in the provided functions + for _, fn := range fns { + if value, ok := fn.Annotations[render.AnnotationKeyRuntimeDockerNetwork]; ok { + c.CrossplaneDockerNetwork = value + break + } + } + + // Overwrite with docker-network annotation from function-annotations cli flag if set + if len(c.FunctionAnnotations) > 0 { + annotations := render.NewAnnotationsFromStrings(c.FunctionAnnotations) + if value, ok := annotations[render.AnnotationKeyRuntimeDockerNetwork]; ok { + c.EngineFlags.CrossplaneDockerNetwork = value + } + } } - engine := c.newEngine(&c.EngineFlags, network, log) + engine := c.newEngine(&c.EngineFlags, log) seedCtx := len(c.ContextValues) > 0 || len(c.ContextFiles) > 0 captureCtx := c.IncludeContext diff --git a/cmd/crossplane/render/xr/cmd_test.go b/cmd/crossplane/render/xr/cmd_test.go index cf403156..d57cc14a 100644 --- a/cmd/crossplane/render/xr/cmd_test.go +++ b/cmd/crossplane/render/xr/cmd_test.go @@ -78,8 +78,8 @@ var includeFunctionResultsOutput string //go:embed testdata/cmd/output/include-full-xr.yaml var includeFullXROutput string -func newEngineFunc(engine render.Engine) func(*render.EngineFlags, string, logging.Logger) render.Engine { - return func(*render.EngineFlags, string, logging.Logger) render.Engine { +func newEngineFunc(engine render.Engine) func(*render.EngineFlags, logging.Logger) render.Engine { + return func(*render.EngineFlags, logging.Logger) render.Engine { return engine } } From a8d6ea1d3ec9c129a9f8e387f87bc8bd5b178f56 Mon Sep 17 00:00:00 2001 From: Nikita Z Date: Thu, 11 Jun 2026 13:44:05 +0200 Subject: [PATCH 10/18] chore: change c.EngineFlags.CrossplaneDockerNetwork refs to c.CrossplaneDockerNetwork Signed-off-by: Nikita Z --- cmd/crossplane/render/op/cmd.go | 2 +- cmd/crossplane/render/xr/cmd.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/crossplane/render/op/cmd.go b/cmd/crossplane/render/op/cmd.go index 83f5e981..1ab57a46 100644 --- a/cmd/crossplane/render/op/cmd.go +++ b/cmd/crossplane/render/op/cmd.go @@ -180,7 +180,7 @@ func (c *Cmd) Run(k *kong.Context, log logging.Logger, sp terminal.SpinnerPrinte if len(c.FunctionAnnotations) > 0 { annotations := render.NewAnnotationsFromStrings(c.FunctionAnnotations) if value, ok := annotations[render.AnnotationKeyRuntimeDockerNetwork]; ok { - c.EngineFlags.CrossplaneDockerNetwork = value + c.CrossplaneDockerNetwork = value } } } diff --git a/cmd/crossplane/render/xr/cmd.go b/cmd/crossplane/render/xr/cmd.go index 6c5bb08d..348a6104 100644 --- a/cmd/crossplane/render/xr/cmd.go +++ b/cmd/crossplane/render/xr/cmd.go @@ -233,7 +233,7 @@ func (c *Cmd) Run(k *kong.Context, log logging.Logger, sp terminal.SpinnerPrinte if len(c.FunctionAnnotations) > 0 { annotations := render.NewAnnotationsFromStrings(c.FunctionAnnotations) if value, ok := annotations[render.AnnotationKeyRuntimeDockerNetwork]; ok { - c.EngineFlags.CrossplaneDockerNetwork = value + c.CrossplaneDockerNetwork = value } } } From bb038602752515c2a905ab07fce070eaa6be8ade Mon Sep 17 00:00:00 2001 From: Nikita Z Date: Thu, 11 Jun 2026 13:44:51 +0200 Subject: [PATCH 11/18] chore: add back newline Signed-off-by: Nikita Z --- cmd/crossplane/render/op/cmd.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/crossplane/render/op/cmd.go b/cmd/crossplane/render/op/cmd.go index 1ab57a46..4d507cd4 100644 --- a/cmd/crossplane/render/op/cmd.go +++ b/cmd/crossplane/render/op/cmd.go @@ -416,5 +416,6 @@ func (c *Cmd) loadFunctions(ctx context.Context, log logging.Logger, sp terminal }); err != nil { return nil, errors.Wrap(err, "cannot build embedded functions") } + return fns, nil } From c7c680aa3c7c7d509eeeca149c714cb2dbeb7cdd Mon Sep 17 00:00:00 2001 From: Nikita Z Date: Thu, 11 Jun 2026 14:03:47 +0200 Subject: [PATCH 12/18] chore: use c.EngineFlags.CrossplaneDockerNetwork to be explicit Signed-off-by: Nikita Z --- cmd/crossplane/render/engine.go | 4 +--- cmd/crossplane/render/op/cmd.go | 4 ++-- cmd/crossplane/render/xr/cmd.go | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/cmd/crossplane/render/engine.go b/cmd/crossplane/render/engine.go index 0c70adda..9bb55ac1 100644 --- a/cmd/crossplane/render/engine.go +++ b/cmd/crossplane/render/engine.go @@ -66,9 +66,7 @@ type EngineFlags struct { // NewEngineFromFlags creates an Engine from the flag configuration. If a binary // path is set, it returns a local engine. Otherwise it returns a Docker engine -// using the resolved image reference. The network parameter sets the Docker -// network the render container should join; it is derived from function -// annotations (AnnotationKeyRuntimeDockerNetwork) by the caller. +// using the resolved image reference. func NewEngineFromFlags(f *EngineFlags, log logging.Logger) Engine { if f.CrossplaneBinary != "" { return &localRenderEngine{BinaryPath: f.CrossplaneBinary} diff --git a/cmd/crossplane/render/op/cmd.go b/cmd/crossplane/render/op/cmd.go index 4d507cd4..a258b0af 100644 --- a/cmd/crossplane/render/op/cmd.go +++ b/cmd/crossplane/render/op/cmd.go @@ -171,7 +171,7 @@ func (c *Cmd) Run(k *kong.Context, log logging.Logger, sp terminal.SpinnerPrinte // Default to the first docker-network annotation in the provided functions for _, fn := range fns { if value, ok := fn.Annotations[render.AnnotationKeyRuntimeDockerNetwork]; ok { - c.CrossplaneDockerNetwork = value + c.EngineFlags.CrossplaneDockerNetwork = value break } } @@ -180,7 +180,7 @@ func (c *Cmd) Run(k *kong.Context, log logging.Logger, sp terminal.SpinnerPrinte if len(c.FunctionAnnotations) > 0 { annotations := render.NewAnnotationsFromStrings(c.FunctionAnnotations) if value, ok := annotations[render.AnnotationKeyRuntimeDockerNetwork]; ok { - c.CrossplaneDockerNetwork = value + c.EngineFlags.CrossplaneDockerNetwork = value } } } diff --git a/cmd/crossplane/render/xr/cmd.go b/cmd/crossplane/render/xr/cmd.go index 348a6104..dbd80abc 100644 --- a/cmd/crossplane/render/xr/cmd.go +++ b/cmd/crossplane/render/xr/cmd.go @@ -224,7 +224,7 @@ func (c *Cmd) Run(k *kong.Context, log logging.Logger, sp terminal.SpinnerPrinte // Default to the first docker-network annotation in the provided functions for _, fn := range fns { if value, ok := fn.Annotations[render.AnnotationKeyRuntimeDockerNetwork]; ok { - c.CrossplaneDockerNetwork = value + c.EngineFlags.CrossplaneDockerNetwork = value break } } @@ -233,7 +233,7 @@ func (c *Cmd) Run(k *kong.Context, log logging.Logger, sp terminal.SpinnerPrinte if len(c.FunctionAnnotations) > 0 { annotations := render.NewAnnotationsFromStrings(c.FunctionAnnotations) if value, ok := annotations[render.AnnotationKeyRuntimeDockerNetwork]; ok { - c.CrossplaneDockerNetwork = value + c.EngineFlags.CrossplaneDockerNetwork = value } } } From 1f04f51cda180ba3766a5f34e722ab4dd70e24d3 Mon Sep 17 00:00:00 2001 From: Nikita Z Date: Fri, 12 Jun 2026 08:49:00 +0200 Subject: [PATCH 13/18] chore: fix lints and format Signed-off-by: Nikita Z --- cmd/crossplane/render/annotation.go | 1 + cmd/crossplane/render/engine.go | 6 +++--- cmd/crossplane/render/op/cmd.go | 6 +++--- cmd/crossplane/render/xr/cmd.go | 6 +++--- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/cmd/crossplane/render/annotation.go b/cmd/crossplane/render/annotation.go index e596e7c6..f88a55a7 100644 --- a/cmd/crossplane/render/annotation.go +++ b/cmd/crossplane/render/annotation.go @@ -2,6 +2,7 @@ package render import "strings" +// Annotations is a type used to store annotations. type Annotations map[string]string // NewAnnotationsFromStrings parses an array of strings in the format "key=value" into a map. diff --git a/cmd/crossplane/render/engine.go b/cmd/crossplane/render/engine.go index 9bb55ac1..d8ebed0c 100644 --- a/cmd/crossplane/render/engine.go +++ b/cmd/crossplane/render/engine.go @@ -58,9 +58,9 @@ type Engine interface { // EngineFlags contains flags for configuring the render engine. It is embedded // by render command structs to provide shared engine configuration. type EngineFlags struct { - CrossplaneVersion string `help:"Version of the Crossplane image to use for rendering. Defaults to the latest stable version." placeholder:"VERSION" xor:"crossplane-selector"` - CrossplaneImage string `help:"Override the full Crossplane Docker image reference for rendering." placeholder:"IMAGE" xor:"crossplane-selector"` - CrossplaneBinary string `help:"Path to a local crossplane binary to use instead of Docker." placeholder:"PATH" type:"existingfile" xor:"crossplane-selector,crossplane-docker"` + CrossplaneVersion string `help:"Version of the Crossplane image to use for rendering. Defaults to the latest stable version." placeholder:"VERSION" xor:"crossplane-selector"` + CrossplaneImage string `help:"Override the full Crossplane Docker image reference for rendering." placeholder:"IMAGE" xor:"crossplane-selector"` + CrossplaneBinary string `help:"Path to a local crossplane binary to use instead of Docker." placeholder:"PATH" type:"existingfile" xor:"crossplane-selector,crossplane-docker"` CrossplaneDockerNetwork string `help:"The docker network to start the crossplane container in" xor:"crossplane-docker"` } diff --git a/cmd/crossplane/render/op/cmd.go b/cmd/crossplane/render/op/cmd.go index a258b0af..9cace63d 100644 --- a/cmd/crossplane/render/op/cmd.go +++ b/cmd/crossplane/render/op/cmd.go @@ -167,11 +167,11 @@ func (c *Cmd) Run(k *kong.Context, log logging.Logger, sp terminal.SpinnerPrinte } } - if c.EngineFlags.CrossplaneDockerNetwork == "" { + if c.CrossplaneDockerNetwork == "" { // Default to the first docker-network annotation in the provided functions for _, fn := range fns { if value, ok := fn.Annotations[render.AnnotationKeyRuntimeDockerNetwork]; ok { - c.EngineFlags.CrossplaneDockerNetwork = value + c.CrossplaneDockerNetwork = value break } } @@ -180,7 +180,7 @@ func (c *Cmd) Run(k *kong.Context, log logging.Logger, sp terminal.SpinnerPrinte if len(c.FunctionAnnotations) > 0 { annotations := render.NewAnnotationsFromStrings(c.FunctionAnnotations) if value, ok := annotations[render.AnnotationKeyRuntimeDockerNetwork]; ok { - c.EngineFlags.CrossplaneDockerNetwork = value + c.CrossplaneDockerNetwork = value } } } diff --git a/cmd/crossplane/render/xr/cmd.go b/cmd/crossplane/render/xr/cmd.go index dbd80abc..dc4d32cd 100644 --- a/cmd/crossplane/render/xr/cmd.go +++ b/cmd/crossplane/render/xr/cmd.go @@ -220,11 +220,11 @@ func (c *Cmd) Run(k *kong.Context, log logging.Logger, sp terminal.SpinnerPrinte } } - if c.EngineFlags.CrossplaneDockerNetwork == "" { + if c.CrossplaneDockerNetwork == "" { // Default to the first docker-network annotation in the provided functions for _, fn := range fns { if value, ok := fn.Annotations[render.AnnotationKeyRuntimeDockerNetwork]; ok { - c.EngineFlags.CrossplaneDockerNetwork = value + c.CrossplaneDockerNetwork = value break } } @@ -233,7 +233,7 @@ func (c *Cmd) Run(k *kong.Context, log logging.Logger, sp terminal.SpinnerPrinte if len(c.FunctionAnnotations) > 0 { annotations := render.NewAnnotationsFromStrings(c.FunctionAnnotations) if value, ok := annotations[render.AnnotationKeyRuntimeDockerNetwork]; ok { - c.EngineFlags.CrossplaneDockerNetwork = value + c.CrossplaneDockerNetwork = value } } } From 591793ce669f194a1a95ce5afd0f555767d92924 Mon Sep 17 00:00:00 2001 From: Nikita Z Date: Tue, 16 Jun 2026 12:08:11 +0200 Subject: [PATCH 14/18] refactor: factor out default network logic and add tests Move Crossplane Docker network defaulting into a shared render helper used by both XR and operation render commands. Add focused coverage for function annotation overrides and network defaulting precedence. Also test both render commands to make sure --function-annotations sets the render engine Docker network before the engine is created. Signed-off-by: Nikita Z --- cmd/crossplane/render/network.go | 17 +++++ cmd/crossplane/render/network_test.go | 76 ++++++++++++++++++++++ cmd/crossplane/render/op/cmd.go | 18 +----- cmd/crossplane/render/op/cmd_test.go | 23 +++++++ cmd/crossplane/render/render_test.go | 93 +++++++++++++++++++++++++++ cmd/crossplane/render/xr/cmd.go | 18 +----- cmd/crossplane/render/xr/cmd_test.go | 24 +++++++ 7 files changed, 235 insertions(+), 34 deletions(-) create mode 100644 cmd/crossplane/render/network_test.go create mode 100644 cmd/crossplane/render/render_test.go diff --git a/cmd/crossplane/render/network.go b/cmd/crossplane/render/network.go index 1772f6e3..25f63538 100644 --- a/cmd/crossplane/render/network.go +++ b/cmd/crossplane/render/network.go @@ -20,6 +20,7 @@ import ( "context" "fmt" + pkgv1 "github.com/crossplane/crossplane/apis/v2/pkg/v1" "github.com/docker/docker/api/types/network" "k8s.io/apimachinery/pkg/util/rand" @@ -28,6 +29,22 @@ import ( "github.com/crossplane/cli/v2/internal/docker" ) +// SetDefaultCrossplaneDockerNetwork defaults the Crossplane render engine's +// Docker network to the first function runtime Docker network annotation. +// If network is already set, return with no changes +func (f *EngineFlags) SetDefaultCrossplaneDockerNetwork(fns []pkgv1.Function) { + if f.CrossplaneDockerNetwork != "" { + return + } + + for _, fn := range fns { + if value, ok := fn.Annotations[AnnotationKeyRuntimeDockerNetwork]; ok { + f.CrossplaneDockerNetwork = value + return + } + } +} + // createRenderNetwork creates a temporary Docker bridge network for render. // Function containers and the Crossplane render container join this network so // they can reach each other. Returns the network ID and name. diff --git a/cmd/crossplane/render/network_test.go b/cmd/crossplane/render/network_test.go new file mode 100644 index 00000000..b770515d --- /dev/null +++ b/cmd/crossplane/render/network_test.go @@ -0,0 +1,76 @@ +package render + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + + pkgv1 "github.com/crossplane/crossplane/apis/v2/pkg/v1" +) + +func TestSetDefaultCrossplaneDockerNetwork(t *testing.T) { + type args struct { + flags EngineFlags + functions []pkgv1.Function + } + type want struct { + flags EngineFlags + } + + cases := map[string]struct { + reason string + args args + want want + }{ + "ExplicitNetworkIsPreserved": { + reason: "An explicit --crossplane-docker-network value should not be overwritten by function annotations.", + args: args{ + flags: EngineFlags{CrossplaneDockerNetwork: "explicit-network"}, + functions: []pkgv1.Function{ + functionWithAnnotations(map[string]string{AnnotationKeyRuntimeDockerNetwork: "function-network"}), + }, + }, + want: want{ + flags: EngineFlags{CrossplaneDockerNetwork: "explicit-network"}, + }, + }, + "FirstFunctionAnnotationIsUsed": { + reason: "When no explicit network is set, the render engine should join the first function runtime Docker network.", + args: args{ + functions: []pkgv1.Function{ + functionWithAnnotations(map[string]string{"example.org/other": "ignored"}), + functionWithAnnotations(map[string]string{AnnotationKeyRuntimeDockerNetwork: "first-network"}), + functionWithAnnotations(map[string]string{AnnotationKeyRuntimeDockerNetwork: "second-network"}), + }, + }, + want: want{ + flags: EngineFlags{CrossplaneDockerNetwork: "first-network"}, + }, + }, + "NoNetwork": { + reason: "The flags should remain unchanged when no function has a runtime Docker network annotation.", + args: args{ + functions: []pkgv1.Function{ + functionWithAnnotations(nil), + functionWithAnnotations(map[string]string{"example.org/other": "ignored"}), + }, + }, + want: want{}, + }, + "NoFunctions": { + reason: "Rendering with no functions should not create a Docker network.", + want: want{}, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + t.Log(tc.reason) + + tc.args.flags.SetDefaultCrossplaneDockerNetwork(tc.args.functions) + if diff := cmp.Diff(tc.want.flags, tc.args.flags); diff != "" { + t.Errorf("SetDefaultCrossplaneDockerNetwork(...), -want, +got:\n%s", diff) + } + }) + } +} diff --git a/cmd/crossplane/render/op/cmd.go b/cmd/crossplane/render/op/cmd.go index 9cace63d..032a1dcf 100644 --- a/cmd/crossplane/render/op/cmd.go +++ b/cmd/crossplane/render/op/cmd.go @@ -167,23 +167,7 @@ func (c *Cmd) Run(k *kong.Context, log logging.Logger, sp terminal.SpinnerPrinte } } - if c.CrossplaneDockerNetwork == "" { - // Default to the first docker-network annotation in the provided functions - for _, fn := range fns { - if value, ok := fn.Annotations[render.AnnotationKeyRuntimeDockerNetwork]; ok { - c.CrossplaneDockerNetwork = value - break - } - } - - // Overwrite with docker-network annotation from function-annotations cli flag if set - if len(c.FunctionAnnotations) > 0 { - annotations := render.NewAnnotationsFromStrings(c.FunctionAnnotations) - if value, ok := annotations[render.AnnotationKeyRuntimeDockerNetwork]; ok { - c.CrossplaneDockerNetwork = value - } - } - } + c.SetDefaultCrossplaneDockerNetwork(fns) engine := c.newEngine(&c.EngineFlags, log) diff --git a/cmd/crossplane/render/op/cmd_test.go b/cmd/crossplane/render/op/cmd_test.go index f17ec0c7..c82b94a4 100644 --- a/cmd/crossplane/render/op/cmd_test.go +++ b/cmd/crossplane/render/op/cmd_test.go @@ -259,6 +259,29 @@ func TestCmdRun(t *testing.T) { }, want: want{err: cmpopts.AnyError}, }, + "FunctionAnnotationNetworkDefaultsEngineNetwork": { + reason: "A runtime Docker network supplied by --function-annotations should default the Crossplane engine Docker network before the engine is created.", + args: args{ + cmd: Cmd{ + Operation: "operation.yaml", + Functions: "functions.yaml", + FunctionAnnotations: []string{render.AnnotationKeyRuntimeDockerNetwork + "=override-network"}, + Timeout: time.Minute, + fs: newTestFS(nil), + newEngine: func(flags *render.EngineFlags, _ logging.Logger) render.Engine { + if diff := cmp.Diff("override-network", flags.CrossplaneDockerNetwork); diff != "" { + t.Errorf("CrossplaneDockerNetwork: -want, +got:\n%s", diff) + } + return &render.MockEngine{ + MockSetup: func(_ context.Context, _ []pkgv1.Function) (func(), error) { + return func() {}, errors.New("setup blew up") + }, + } + }, + }, + }, + want: want{err: cmpopts.AnyError}, + }, "LoadFunctionCredentialsError": { reason: "Missing function credentials file should return a wrapped load error.", args: args{ diff --git a/cmd/crossplane/render/render_test.go b/cmd/crossplane/render/render_test.go new file mode 100644 index 00000000..6d87ae2d --- /dev/null +++ b/cmd/crossplane/render/render_test.go @@ -0,0 +1,93 @@ +package render + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + pkgv1 "github.com/crossplane/crossplane/apis/v2/pkg/v1" +) + +func TestOverrideFunctionAnnotations(t *testing.T) { + type args struct { + functions []pkgv1.Function + annotations []string + } + type want struct { + functions []pkgv1.Function + err error + } + + cases := map[string]struct { + reason string + args args + want want + }{ + "AnnotationsAreAppliedToAllFunctions": { + reason: "Function annotation flags are global overrides applied to every function before rendering.", + args: args{ + functions: []pkgv1.Function{ + functionWithAnnotations(map[string]string{"example.org/existing": "value"}), + functionWithAnnotations(nil), + }, + annotations: []string{"example.org/override=override-value"}, + }, + want: want{ + functions: []pkgv1.Function{ + functionWithAnnotations(map[string]string{"example.org/existing": "value", "example.org/override": "override-value"}), + functionWithAnnotations(map[string]string{"example.org/override": "override-value"}), + }, + }, + }, + "ExistingAnnotationIsOverridden": { + reason: "A function annotation flag should replace an existing annotation with the same key.", + args: args{ + functions: []pkgv1.Function{ + functionWithAnnotations(map[string]string{AnnotationKeyRuntimeDockerNetwork: "function-network"}), + }, + annotations: []string{AnnotationKeyRuntimeDockerNetwork + "=override-network"}, + }, + want: want{ + functions: []pkgv1.Function{ + functionWithAnnotations(map[string]string{AnnotationKeyRuntimeDockerNetwork: "override-network"}), + }, + }, + }, + "InvalidAnnotationReturnsError": { + reason: "Invalid function annotation flags should fail instead of being silently ignored.", + args: args{ + functions: []pkgv1.Function{ + functionWithAnnotations(map[string]string{AnnotationKeyRuntimeDockerNetwork: "function-network"}), + }, + annotations: []string{"malformed"}, + }, + want: want{ + functions: []pkgv1.Function{ + functionWithAnnotations(map[string]string{AnnotationKeyRuntimeDockerNetwork: "function-network"}), + }, + err: cmpopts.AnyError, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + t.Log(tc.reason) + + err := OverrideFunctionAnnotations(tc.args.functions, tc.args.annotations) + if diff := cmp.Diff(tc.want.err, err, cmpopts.EquateErrors()); diff != "" { + t.Errorf("OverrideFunctionAnnotations(...), -want, +got:\n%s", diff) + } + + if diff := cmp.Diff(tc.want.functions, tc.args.functions); diff != "" { + t.Errorf("OverrideFunctionAnnotations(...), -want, +got:\n%s", diff) + } + }) + } +} + +func functionWithAnnotations(annotations map[string]string) pkgv1.Function { + return pkgv1.Function{ObjectMeta: metav1.ObjectMeta{Annotations: annotations}} +} diff --git a/cmd/crossplane/render/xr/cmd.go b/cmd/crossplane/render/xr/cmd.go index dc4d32cd..71cb51d6 100644 --- a/cmd/crossplane/render/xr/cmd.go +++ b/cmd/crossplane/render/xr/cmd.go @@ -220,23 +220,7 @@ func (c *Cmd) Run(k *kong.Context, log logging.Logger, sp terminal.SpinnerPrinte } } - if c.CrossplaneDockerNetwork == "" { - // Default to the first docker-network annotation in the provided functions - for _, fn := range fns { - if value, ok := fn.Annotations[render.AnnotationKeyRuntimeDockerNetwork]; ok { - c.CrossplaneDockerNetwork = value - break - } - } - - // Overwrite with docker-network annotation from function-annotations cli flag if set - if len(c.FunctionAnnotations) > 0 { - annotations := render.NewAnnotationsFromStrings(c.FunctionAnnotations) - if value, ok := annotations[render.AnnotationKeyRuntimeDockerNetwork]; ok { - c.CrossplaneDockerNetwork = value - } - } - } + c.SetDefaultCrossplaneDockerNetwork(fns) engine := c.newEngine(&c.EngineFlags, log) diff --git a/cmd/crossplane/render/xr/cmd_test.go b/cmd/crossplane/render/xr/cmd_test.go index d57cc14a..f7e8cadb 100644 --- a/cmd/crossplane/render/xr/cmd_test.go +++ b/cmd/crossplane/render/xr/cmd_test.go @@ -299,6 +299,30 @@ func TestCmdRun(t *testing.T) { }, want: want{err: cmpopts.AnyError}, }, + "FunctionAnnotationNetworkDefaultsEngineNetwork": { + reason: "A runtime Docker network supplied by --function-annotations should default the Crossplane engine Docker network before the engine is created.", + args: args{ + cmd: Cmd{ + CompositeResource: "xr.yaml", + Composition: "composition.yaml", + Functions: "functions.yaml", + FunctionAnnotations: []string{render.AnnotationKeyRuntimeDockerNetwork + "=override-network"}, + Timeout: time.Minute, + fs: newTestFS(nil), + newEngine: func(flags *render.EngineFlags, _ logging.Logger) render.Engine { + if diff := cmp.Diff("override-network", flags.CrossplaneDockerNetwork); diff != "" { + t.Errorf("CrossplaneDockerNetwork: -want, +got:\n%s", diff) + } + return &render.MockEngine{ + MockSetup: func(_ context.Context, _ []pkgv1.Function) (func(), error) { + return func() {}, errors.New("setup blew up") + }, + } + }, + }, + }, + want: want{err: cmpopts.AnyError}, + }, "LoadXRDError": { reason: "Missing XRD file should return a wrapped load error.", args: args{ From fa0e2c4a6d61460295fb01ca1cad8a3e091a1c02 Mon Sep 17 00:00:00 2001 From: Nikita Z Date: Tue, 16 Jun 2026 12:13:34 +0200 Subject: [PATCH 15/18] test(setDefaultCrossplaneDockerNetwork): update test-case description Signed-off-by: Nikita Z --- cmd/crossplane/render/network_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/crossplane/render/network_test.go b/cmd/crossplane/render/network_test.go index b770515d..fde13f0f 100644 --- a/cmd/crossplane/render/network_test.go +++ b/cmd/crossplane/render/network_test.go @@ -57,8 +57,8 @@ func TestSetDefaultCrossplaneDockerNetwork(t *testing.T) { }, want: want{}, }, - "NoFunctions": { - reason: "Rendering with no functions should not create a Docker network.", + "NoFunctionsPreservesDefaultBehavior": { + reason: "No functions should leave CrossplaneDockerNetwork unset so engine setup can use its default temporary network behavior.", want: want{}, }, } From 37a6e901325151e9c37eacec11ed435c4582e6b7 Mon Sep 17 00:00:00 2001 From: Nikita Z <54776184+nkzk@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:18:23 +0200 Subject: [PATCH 16/18] Apply suggestion from @nkzk Signed-off-by: Nikita Z <54776184+nkzk@users.noreply.github.com> --- cmd/crossplane/render/network.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/crossplane/render/network.go b/cmd/crossplane/render/network.go index 25f63538..baf9d32f 100644 --- a/cmd/crossplane/render/network.go +++ b/cmd/crossplane/render/network.go @@ -31,7 +31,7 @@ import ( // SetDefaultCrossplaneDockerNetwork defaults the Crossplane render engine's // Docker network to the first function runtime Docker network annotation. -// If network is already set, return with no changes +// If network is already set, return with no changes. func (f *EngineFlags) SetDefaultCrossplaneDockerNetwork(fns []pkgv1.Function) { if f.CrossplaneDockerNetwork != "" { return From cf98778bea443dcd9705352d5d17f3c90cd66c00 Mon Sep 17 00:00:00 2001 From: Nikita Z Date: Wed, 17 Jun 2026 09:44:09 +0200 Subject: [PATCH 17/18] chore: run format with nix lint Signed-off-by: Nikita Z --- cmd/crossplane/render/network.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/crossplane/render/network.go b/cmd/crossplane/render/network.go index baf9d32f..9fd9abc0 100644 --- a/cmd/crossplane/render/network.go +++ b/cmd/crossplane/render/network.go @@ -20,12 +20,13 @@ import ( "context" "fmt" - pkgv1 "github.com/crossplane/crossplane/apis/v2/pkg/v1" "github.com/docker/docker/api/types/network" "k8s.io/apimachinery/pkg/util/rand" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" + pkgv1 "github.com/crossplane/crossplane/apis/v2/pkg/v1" + "github.com/crossplane/cli/v2/internal/docker" ) From a0f813bb6872ca668e434e29e334ad302bb95f0d Mon Sep 17 00:00:00 2001 From: Nikita Z <54776184+nkzk@users.noreply.github.com> Date: Wed, 17 Jun 2026 10:42:54 +0200 Subject: [PATCH 18/18] Update cmd/crossplane/render/network.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Nikita Z <54776184+nkzk@users.noreply.github.com> --- cmd/crossplane/render/network.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/crossplane/render/network.go b/cmd/crossplane/render/network.go index 9fd9abc0..a0413018 100644 --- a/cmd/crossplane/render/network.go +++ b/cmd/crossplane/render/network.go @@ -39,7 +39,7 @@ func (f *EngineFlags) SetDefaultCrossplaneDockerNetwork(fns []pkgv1.Function) { } for _, fn := range fns { - if value, ok := fn.Annotations[AnnotationKeyRuntimeDockerNetwork]; ok { + if value, ok := fn.Annotations[AnnotationKeyRuntimeDockerNetwork]; ok && value != "" { f.CrossplaneDockerNetwork = value return }