From 301db1f865341df1a2ef90880a6d1d973b7e718e Mon Sep 17 00:00:00 2001 From: Nikita Z Date: Thu, 4 Jun 2026 09:51:11 +0200 Subject: [PATCH 1/7] 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 ea08736..d9bed4a 100644 --- a/cmd/crossplane/render/render.go +++ b/cmd/crossplane/render/render.go @@ -202,7 +202,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 b35b6bc737df8bb844e28d0b884d818c29cc516c Mon Sep 17 00:00:00 2001 From: Nikita Z Date: Thu, 4 Jun 2026 09:51:11 +0200 Subject: [PATCH 2/7] chore: update function comment Signed-off-by: Nikita Z --- cmd/crossplane/render/render.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/crossplane/render/render.go b/cmd/crossplane/render/render.go index d9bed4a..4e7cb7a 100644 --- a/cmd/crossplane/render/render.go +++ b/cmd/crossplane/render/render.go @@ -195,7 +195,7 @@ func SetComposedResourceMetadata(cd resource.Object, xr resource.LegacyComposite return errors.Wrapf(meta.AddControllerReference(cd, or), "cannot set composite resource %q as controller ref of composed resource", xr.GetName()) } -// 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 dd291ffcec8837760a8d26a3a363f9f3aa2b18d9 Mon Sep 17 00:00:00 2001 From: Nikita Z Date: Thu, 4 Jun 2026 09:51:11 +0200 Subject: [PATCH 3/7] 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 | 18 ++++++++++++++++-- 4 files changed, 46 insertions(+), 12 deletions(-) diff --git a/cmd/crossplane/render/engine.go b/cmd/crossplane/render/engine.go index 22aeea6..0b2a3dc 100644 --- a/cmd/crossplane/render/engine.go +++ b/cmd/crossplane/render/engine.go @@ -53,17 +53,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 0b4e3b1..8841837 100644 --- a/cmd/crossplane/render/engine_docker.go +++ b/cmd/crossplane/render/engine_docker.go @@ -61,13 +61,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 36bb60b..5e228fa 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 5f4a317..4e1a8e0 100644 --- a/cmd/crossplane/render/xr/cmd.go +++ b/cmd/crossplane/render/xr/cmd.go @@ -22,6 +22,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "time" "dario.cat/mergo" @@ -90,7 +91,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. @@ -222,7 +223,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 abb26bdfdc95e03cd05c5cb04e7bbfdac0d915b1 Mon Sep 17 00:00:00 2001 From: Nikita Z Date: Thu, 4 Jun 2026 09:51:11 +0200 Subject: [PATCH 4/7] 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 f17ec0c..6a9e16e 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 d57cc14..cf40315 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 01ce8f55bb14ca9e1a43b0870fa12c9587b29467 Mon Sep 17 00:00:00 2001 From: Nikita Z Date: Thu, 4 Jun 2026 11:59:39 +0200 Subject: [PATCH 5/7] test: add tests for injectNetworkAnnotation Signed-off-by: Nikita Z --- cmd/crossplane/render/render_test.go | 65 ++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/cmd/crossplane/render/render_test.go b/cmd/crossplane/render/render_test.go index fd7e47c..2052a27 100644 --- a/cmd/crossplane/render/render_test.go +++ b/cmd/crossplane/render/render_test.go @@ -28,6 +28,8 @@ import ( "github.com/crossplane/crossplane-runtime/v2/pkg/resource/unstructured/composed" ucomposite "github.com/crossplane/crossplane-runtime/v2/pkg/resource/unstructured/composite" "github.com/crossplane/crossplane-runtime/v2/pkg/resource/unstructured/reference" + + pkgv1 "github.com/crossplane/crossplane/apis/v2/pkg/v1" ) func TestGetSecret(t *testing.T) { @@ -231,6 +233,69 @@ func TestSetComposedResourceMetadata(t *testing.T) { } } +func TestInjectNetworkAnnotation(t *testing.T) { + cases := map[string]struct { + reason string + fns []pkgv1.Function + networkName string + want []pkgv1.Function + }{ + "SetsAnnotationWhenAbsent": { + reason: "The network annotation should be set when the function has no existing annotation.", + networkName: "render-network", + fns: []pkgv1.Function{ + {ObjectMeta: metav1.ObjectMeta{Name: "fn-a"}}, + }, + want: []pkgv1.Function{ + {ObjectMeta: metav1.ObjectMeta{Name: "fn-a", Annotations: map[string]string{ + AnnotationKeyRuntimeDockerNetwork: "render-network", + }}}, + }, + }, + "DoesNotOverwriteExistingAnnotation": { + reason: "The network annotation should not be overwritten when the function already has one set.", + networkName: "render-network", + fns: []pkgv1.Function{ + {ObjectMeta: metav1.ObjectMeta{Name: "fn-a", Annotations: map[string]string{ + AnnotationKeyRuntimeDockerNetwork: "my-custom-network", + }}}, + }, + want: []pkgv1.Function{ + {ObjectMeta: metav1.ObjectMeta{Name: "fn-a", Annotations: map[string]string{ + AnnotationKeyRuntimeDockerNetwork: "my-custom-network", + }}}, + }, + }, + "MixedFunctions": { + reason: "The annotation should only be set on functions that don't already have it.", + networkName: "render-network", + fns: []pkgv1.Function{ + {ObjectMeta: metav1.ObjectMeta{Name: "fn-a"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "fn-b", Annotations: map[string]string{ + AnnotationKeyRuntimeDockerNetwork: "my-custom-network", + }}}, + }, + want: []pkgv1.Function{ + {ObjectMeta: metav1.ObjectMeta{Name: "fn-a", Annotations: map[string]string{ + AnnotationKeyRuntimeDockerNetwork: "render-network", + }}}, + {ObjectMeta: metav1.ObjectMeta{Name: "fn-b", Annotations: map[string]string{ + AnnotationKeyRuntimeDockerNetwork: "my-custom-network", + }}}, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + injectNetworkAnnotation(tc.fns, tc.networkName) + if diff := cmp.Diff(tc.want, tc.fns); diff != "" { + t.Errorf("\n%s\ninjectNetworkAnnotation(...): -want +got:\n%s", tc.reason, diff) + } + }) + } +} + func MustStructJSON(j string) *structpb.Struct { s := &structpb.Struct{} if err := protojson.Unmarshal([]byte(j), s); err != nil { From 988e3d2d1447ab895c75fbd2c71caae19e8263f7 Mon Sep 17 00:00:00 2001 From: Nikita Z Date: Thu, 4 Jun 2026 12:18:39 +0200 Subject: [PATCH 6/7] 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 0b2a3dc..cd6370b 100644 --- a/cmd/crossplane/render/engine.go +++ b/cmd/crossplane/render/engine.go @@ -53,12 +53,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 8841837..be680b9 100644 --- a/cmd/crossplane/render/engine_docker.go +++ b/cmd/crossplane/render/engine_docker.go @@ -38,8 +38,7 @@ import ( 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 5e228fa..b566295 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 4e1a8e0..5641615 100644 --- a/cmd/crossplane/render/xr/cmd.go +++ b/cmd/crossplane/render/xr/cmd.go @@ -223,7 +223,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 669f0388e2cc12813d2f3f6ccac9e923aee751f3 Mon Sep 17 00:00:00 2001 From: Nikita Z Date: Thu, 4 Jun 2026 12:33:12 +0200 Subject: [PATCH 7/7] 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 be680b9..bbc5cbf 100644 --- a/cmd/crossplane/render/engine_docker.go +++ b/cmd/crossplane/render/engine_docker.go @@ -71,13 +71,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