Skip to content

render: Engine.Setup not idempotent — no clean way to add functions to an existing render network #96

@jcogilvie

Description

@jcogilvie

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions