What problem are you facing?
I'm building a downstream tool (crossplane-contrib/crossplane-diff) that uses crossplane internal render programmatically against many resources in a single process. When the resources resolve to different Composition objects with overlapping-but-not-identical function pipelines (a common shape — e.g. diffing a GitOps directory), I need to add the new compositions' functions to the same Docker network the engine created on the first call.
render.Engine.Setup (specifically dockerRenderEngine.Setup) makes this awkward. It creates a fresh Docker network on every call, with no way in the API to add function containers to a previously-created network:
- Calling
Setup a second time creates a second network, leaks the first one, and strands containers from the first batch.
- Skipping
Setup for the second batch leaves the new functions un-annotated with the existing network — their containers default to the host's default Docker bridge and are unreachable from the render container.
This forces downstream tools to either accept the multi-composition-in-one-process limitation or build a workaround that couples to the engine's internal state. crossplane-diff currently does the latter, in PR #326: we call Setup once on the first render, capture the network name back off the first batch's render.AnnotationKeyRuntimeDockerNetwork annotation, and on subsequent renders manually apply that annotation to functions we haven't seen yet (mimicking the unexported injectNetworkAnnotation) before passing them to StartFunctionRuntimes.
The workaround works but reaches into internal state via an annotation side-channel. We've filed an internal issue to delete it once a clean upstream API lands (crossplane-contrib/crossplane-diff#338).
Relationship to #75
#75 describes two adjacent problems on the same code surface (Engine.Setup's docker network behavior): (1) cross-invocation function-container reuse breaks because the network is recreated each run, and (2) running crossplane render inside a container has no way to override the network the engine creates. This issue is a third user-visible problem on that same surface — intra-process multi-composition. The three are related but not duplicates: distinct symptoms, shared root cause. Notably, Option A below would also fix #75 problem 1 (a caller could re-Setup with the same network name to add functions to it across runs).
How could Crossplane help solve your problem?
Either of the following would let downstream tools render multiple compositions from a single engine instance without state-coupling. Happy to put up the PR once we've agreed on the shape.
Option A — Make Setup idempotent
When dockerRenderEngine.Setup is called with e.network != "" (i.e. the engine was already initialized via a prior Setup call), skip network creation and just call injectNetworkAnnotation(fns, e.network), returning a no-op cleanup. Combined with the existing don't-overwrite proposal in #65, this is safe — pre-annotated functions are unaffected.
- Pros: smallest change, no interface delta, naturally backward-compatible. Existing single-batch callers (
xr / op render commands) work unchanged. Also closes #75 problem 1 with no further work.
- Cons:
Setup does double duty (initialize + grow). Cleanup ownership is mildly subtle — only the first call returns a real cleanup, and a caller that discards subsequent cleanups must remember to keep the first one.
Option B — Add Engine.AnnotateFunctions(fns)
type Engine interface {
CheckContextSupport() error
Setup(ctx context.Context, fns []pkgv1.Function) (cleanup func(), err error)
AnnotateFunctions(fns []pkgv1.Function) // NEW: integrate fns with engine env
Render(ctx context.Context, req *renderv1alpha1.RenderRequest) (*renderv1alpha1.RenderResponse, error)
}
localRenderEngine.AnnotateFunctions is a no-op; dockerRenderEngine.AnnotateFunctions calls injectNetworkAnnotation(fns, e.network). Setup stays single-call.
- Pros: cleaner separation. "Initialize the environment" and "add more functions to it" are distinct verbs.
- Cons: interface change. Anyone implementing
Engine (effectively just the two upstream engines + MockEngine) needs to add the method.
Interaction with #65
#65 introduces a network parameter to NewEngineFromFlags that pre-supplies the network and skips both creation and annotation in Setup. That's the natural fix for #75 problem 2 (DinD / devcontainer) but doesn't address ours: we want the engine to create the network on first call AND annotate later batches with it. Either Option A or Option B would compose cleanly with #65.
🤖 Generated with Claude Code
What problem are you facing?
I'm building a downstream tool (crossplane-contrib/crossplane-diff) that uses
crossplane internal renderprogrammatically against many resources in a single process. When the resources resolve to differentCompositionobjects with overlapping-but-not-identical function pipelines (a common shape — e.g. diffing a GitOps directory), I need to add the new compositions' functions to the same Docker network the engine created on the first call.render.Engine.Setup(specificallydockerRenderEngine.Setup) makes this awkward. It creates a fresh Docker network on every call, with no way in the API to add function containers to a previously-created network:Setupa second time creates a second network, leaks the first one, and strands containers from the first batch.Setupfor the second batch leaves the new functions un-annotated with the existing network — their containers default to the host's default Docker bridge and are unreachable from the render container.This forces downstream tools to either accept the multi-composition-in-one-process limitation or build a workaround that couples to the engine's internal state. crossplane-diff currently does the latter, in PR #326: we call
Setuponce on the first render, capture the network name back off the first batch'srender.AnnotationKeyRuntimeDockerNetworkannotation, and on subsequent renders manually apply that annotation to functions we haven't seen yet (mimicking the unexportedinjectNetworkAnnotation) before passing them toStartFunctionRuntimes.The workaround works but reaches into internal state via an annotation side-channel. We've filed an internal issue to delete it once a clean upstream API lands (crossplane-contrib/crossplane-diff#338).
Relationship to #75
#75 describes two adjacent problems on the same code surface (
Engine.Setup's docker network behavior): (1) cross-invocation function-container reuse breaks because the network is recreated each run, and (2) runningcrossplane renderinside a container has no way to override the network the engine creates. This issue is a third user-visible problem on that same surface — intra-process multi-composition. The three are related but not duplicates: distinct symptoms, shared root cause. Notably, Option A below would also fix #75 problem 1 (a caller could re-Setup with the same network name to add functions to it across runs).How could Crossplane help solve your problem?
Either of the following would let downstream tools render multiple compositions from a single engine instance without state-coupling. Happy to put up the PR once we've agreed on the shape.
Option A — Make
SetupidempotentWhen
dockerRenderEngine.Setupis called withe.network != ""(i.e. the engine was already initialized via a priorSetupcall), skip network creation and just callinjectNetworkAnnotation(fns, e.network), returning a no-op cleanup. Combined with the existing don't-overwrite proposal in #65, this is safe — pre-annotated functions are unaffected.xr/op rendercommands) work unchanged. Also closes #75 problem 1 with no further work.Setupdoes double duty (initialize + grow). Cleanup ownership is mildly subtle — only the first call returns a real cleanup, and a caller that discards subsequent cleanups must remember to keep the first one.Option B — Add
Engine.AnnotateFunctions(fns)localRenderEngine.AnnotateFunctionsis a no-op;dockerRenderEngine.AnnotateFunctionscallsinjectNetworkAnnotation(fns, e.network).Setupstays single-call.Engine(effectively just the two upstream engines +MockEngine) needs to add the method.Interaction with #65
#65 introduces a
networkparameter toNewEngineFromFlagsthat pre-supplies the network and skips both creation and annotation in Setup. That's the natural fix for #75 problem 2 (DinD / devcontainer) but doesn't address ours: we want the engine to create the network on first call AND annotate later batches with it. Either Option A or Option B would compose cleanly with #65.🤖 Generated with Claude Code