Skip to content

Commit 86f5f7a

Browse files
authored
Merge pull request #159 from jcogilvie/engine-render-add-functions
fix(render): annotate functions when reusing an existing network
2 parents b1483cb + ad83077 commit 86f5f7a

3 files changed

Lines changed: 160 additions & 11 deletions

File tree

cmd/crossplane/render/engine.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,18 @@ type Engine interface {
3939

4040
// Setup performs engine-specific pre-render preparation, such as
4141
// creating Docker networks and annotating functions so their containers
42-
// can reach the render engine. It may mutate fns. The returned cleanup
43-
// function must be called when rendering is done.
42+
// can reach the render engine. It may mutate fns.
43+
//
44+
// Setup may be called more than once on the same engine to integrate
45+
// additional functions into an environment created by a prior call.
46+
// Only the call that creates a new environment returns a real cleanup;
47+
// calls that integrate fns into an environment that already exists
48+
// (because a prior Setup call established it, or because the engine was
49+
// pre-configured to use an externally-managed environment) return a
50+
// no-op cleanup, as do calls on engines with nothing to clean up. The
51+
// real cleanup, if any, must be called when rendering is done; callers
52+
// can safely defer every returned cleanup in LIFO order without
53+
// coordinating which one is real.
4454
Setup(ctx context.Context, fns []pkgv1.Function) (cleanup func(), err error)
4555

4656
// Render executes the render request and returns the response.

cmd/crossplane/render/engine_docker.go

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,20 +77,26 @@ func (e *dockerRenderEngine) CheckContextSupport() error {
7777
return nil
7878
}
7979

80-
// Setup creates a temporary Docker network, records its name so the render
81-
// container joins it, and annotates the supplied functions so their
82-
// containers also join it. The returned cleanup function removes the
83-
// network.
80+
// Setup wires the supplied functions into the Docker network the render
81+
// container joins. On the first call with an unset e.network, it creates a
82+
// temporary network, records its name, annotates fns to join it, and returns
83+
// a cleanup that removes the network. On any subsequent call — or on the
84+
// first call when e.network was already set via --crossplane-docker-network —
85+
// it only annotates fns with the existing network and returns a no-op
86+
// cleanup, leaving network ownership to whoever created it. fns whose
87+
// runtime-docker-network annotation is already set are left untouched.
8488
func (e *dockerRenderEngine) Setup(ctx context.Context, fns []pkgv1.Function) (func(), error) {
85-
var networkID, networkName string
86-
8789
if e.network != "" {
88-
// e.network was pre-configured, we don't own the network, so there is nothing to clean up.
90+
// Either the user pre-configured the network via the --crossplane-docker-network flag
91+
// (we don't own it), or a prior Setup call on this engine already created one (the
92+
// real cleanup belongs to that first call). In both cases we still need to annotate
93+
// the supplied fns so their containers join the network — injectNetworkAnnotation
94+
// won't overwrite an annotation a fn already carries.
95+
injectNetworkAnnotation(fns, e.network)
8996
return func() {}, nil
9097
}
9198

92-
var err error
93-
networkID, networkName, err = createRenderNetwork(ctx)
99+
networkID, networkName, err := createRenderNetwork(ctx)
94100
if err != nil {
95101
return func() {}, errors.Wrap(err, "cannot create Docker network for rendering")
96102
}

cmd/crossplane/render/engine_docker_test.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import (
2828

2929
"github.com/crossplane/crossplane-runtime/v2/pkg/logging"
3030

31+
pkgv1 "github.com/crossplane/crossplane/apis/v2/pkg/v1"
32+
3133
"github.com/crossplane/cli/v2/internal/docker"
3234
renderv1alpha1 "github.com/crossplane/cli/v2/proto/render/v1alpha1"
3335
)
@@ -212,6 +214,137 @@ func TestDockerRenderEngineRender(t *testing.T) {
212214
}
213215
}
214216

217+
func TestDockerRenderEngineSetup(t *testing.T) {
218+
// These cases all exercise the early-return branch of Setup — where
219+
// e.network is already non-empty, either because NewEngineFromFlags
220+
// populated it from --crossplane-docker-network or because a prior Setup
221+
// call on the same engine stored its created network there. The branch
222+
// must annotate the supplied functions so their containers join the
223+
// network, never create a second network, and always return a no-op
224+
// cleanup. The create-new-network branch is not covered here because it
225+
// depends on a live Docker daemon; the broader render command tests
226+
// exercise it integration-style.
227+
//
228+
// The MultiBatchAnnotatesAdditionalFunctions case simulates the
229+
// in-process multi-composition use case from crossplane/cli#96: a
230+
// downstream tool (crossplane-diff) calls Setup once per Composition it
231+
// encounters. Pre-seeding e.network stands in for the prior Setup call
232+
// that would have created the network, keeping the test hermetic.
233+
const presetNetwork = "preset-net"
234+
235+
// batch holds a single Setup invocation: the fns to pass in and the
236+
// expected fn state after Setup returns. Multi-call cases provide more
237+
// than one batch; the runner invokes Setup once per batch in order.
238+
type batch struct {
239+
fns []pkgv1.Function
240+
wantFns []pkgv1.Function
241+
}
242+
243+
cases := map[string]struct {
244+
reason string
245+
engine *dockerRenderEngine
246+
batches []batch
247+
}{
248+
"AnnotatesFunctionsWhenNetworkPreset": {
249+
reason: "When e.network is set, Setup must inject the network annotation on every fn that does not already carry one, so that crossplane-diff-style multi-batch callers can re-Setup to add new fns to the same network.",
250+
engine: &dockerRenderEngine{network: presetNetwork, log: logging.NewNopLogger()},
251+
batches: []batch{{
252+
fns: []pkgv1.Function{
253+
functionWithAnnotations(nil),
254+
functionWithAnnotations(nil),
255+
},
256+
wantFns: []pkgv1.Function{
257+
functionWithAnnotations(map[string]string{AnnotationKeyRuntimeDockerNetwork: presetNetwork}),
258+
functionWithAnnotations(map[string]string{AnnotationKeyRuntimeDockerNetwork: presetNetwork}),
259+
},
260+
}},
261+
},
262+
"PreservesUserSetFunctionAnnotation": {
263+
reason: "If a fn already carries a runtime-docker-network annotation, Setup must not overwrite it. This preserves the don't-overwrite contract from PR #65 for users who pin their fns to a specific network.",
264+
engine: &dockerRenderEngine{network: presetNetwork, log: logging.NewNopLogger()},
265+
batches: []batch{{
266+
fns: []pkgv1.Function{
267+
functionWithAnnotations(map[string]string{AnnotationKeyRuntimeDockerNetwork: "user-pinned-net"}),
268+
},
269+
wantFns: []pkgv1.Function{
270+
functionWithAnnotations(map[string]string{AnnotationKeyRuntimeDockerNetwork: "user-pinned-net"}),
271+
},
272+
}},
273+
},
274+
"NilFunctionsList": {
275+
reason: "A nil fns slice is a boundary case: Setup must succeed without panicking on the nil-map guard inside injectNetworkAnnotation.",
276+
engine: &dockerRenderEngine{network: presetNetwork, log: logging.NewNopLogger()},
277+
batches: []batch{{
278+
fns: nil,
279+
wantFns: nil,
280+
}},
281+
},
282+
"EmptyFunctionsList": {
283+
reason: "An empty (non-nil) fns slice is the other boundary: Setup must succeed, the slice stays empty, and no spurious entries appear.",
284+
engine: &dockerRenderEngine{network: presetNetwork, log: logging.NewNopLogger()},
285+
batches: []batch{{
286+
fns: []pkgv1.Function{},
287+
wantFns: []pkgv1.Function{},
288+
}},
289+
},
290+
"MultiBatchAnnotatesAdditionalFunctions": {
291+
reason: "Two consecutive Setup calls on the same engine — the crossplane/cli#96 in-process multi-composition pattern — must annotate every batch with the same network. Each call returns a no-op cleanup that is safe to defer in LIFO order.",
292+
engine: &dockerRenderEngine{network: presetNetwork, log: logging.NewNopLogger()},
293+
batches: []batch{
294+
{
295+
fns: []pkgv1.Function{
296+
functionWithAnnotations(nil),
297+
functionWithAnnotations(nil),
298+
},
299+
wantFns: []pkgv1.Function{
300+
functionWithAnnotations(map[string]string{AnnotationKeyRuntimeDockerNetwork: presetNetwork}),
301+
functionWithAnnotations(map[string]string{AnnotationKeyRuntimeDockerNetwork: presetNetwork}),
302+
},
303+
},
304+
{
305+
fns: []pkgv1.Function{
306+
functionWithAnnotations(nil),
307+
},
308+
wantFns: []pkgv1.Function{
309+
functionWithAnnotations(map[string]string{AnnotationKeyRuntimeDockerNetwork: presetNetwork}),
310+
},
311+
},
312+
},
313+
},
314+
}
315+
316+
for name, tc := range cases {
317+
t.Run(name, func(t *testing.T) {
318+
cleanups := make([]func(), 0, len(tc.batches))
319+
for i, b := range tc.batches {
320+
cleanup, err := tc.engine.Setup(context.Background(), b.fns)
321+
if err != nil {
322+
t.Fatalf("\n%s\nSetup(batch %d): unexpected error: %v", tc.reason, i, err)
323+
}
324+
if cleanup == nil {
325+
t.Fatalf("\n%s\nSetup(batch %d): cleanup is nil, want non-nil", tc.reason, i)
326+
}
327+
cleanups = append(cleanups, cleanup)
328+
329+
if diff := cmp.Diff(b.wantFns, b.fns); diff != "" {
330+
t.Errorf("\n%s\nSetup(batch %d): fns -want, +got:\n%s", tc.reason, i, diff)
331+
}
332+
}
333+
334+
// Defer-LIFO: all cleanups in this test are no-ops (we pre-seed
335+
// e.network, so no call took the create-network branch). Calling
336+
// them must not panic.
337+
for i := len(cleanups) - 1; i >= 0; i-- {
338+
cleanups[i]()
339+
}
340+
341+
if tc.engine.network != presetNetwork {
342+
t.Errorf("\n%s\nSetup(...): e.network mutated from %q to %q (early-return branch must not change it)", tc.reason, presetNetwork, tc.engine.network)
343+
}
344+
})
345+
}
346+
}
347+
215348
// nonExitError is a stand-in for non-*ContainerExitError failures (e.g. image
216349
// pull errors) returned by docker.RunContainer.
217350
type nonExitError struct{ msg string }

0 commit comments

Comments
 (0)