Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions cmd/crossplane/render/docker_fake_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
Copyright 2026 The Crossplane Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package render

import (
"context"
"io"

"github.com/moby/moby/client"
)

// fakeDockerCall records a single call received by a fake Docker client.
type fakeDockerCall struct {
// Method is the Docker client method name, e.g. "ContainerStop".
Method string
// Ref is the call's identifying argument: a container ID or name, a
// network ID or name, or an image reference. It is empty for
// ContainerCreate, which names its container through Options.
Ref string
// Options is the options struct the method was called with.
Options any
}

// fakeContainerClient is a containerClient that records every call it
// receives in Calls. Each method delegates to its Mock function when set, and
// otherwise succeeds with a zero result (ImagePull returns an empty, already
// complete pull).
type fakeContainerClient struct {
MockImagePull func(ctx context.Context, ref string, options client.ImagePullOptions) (client.ImagePullResponse, error)
MockContainerInspect func(ctx context.Context, containerID string, options client.ContainerInspectOptions) (client.ContainerInspectResult, error)
MockContainerCreate func(ctx context.Context, options client.ContainerCreateOptions) (client.ContainerCreateResult, error)
MockContainerStart func(ctx context.Context, containerID string, options client.ContainerStartOptions) (client.ContainerStartResult, error)
MockContainerStop func(ctx context.Context, containerID string, options client.ContainerStopOptions) (client.ContainerStopResult, error)
MockContainerRemove func(ctx context.Context, containerID string, options client.ContainerRemoveOptions) (client.ContainerRemoveResult, error)

Calls []fakeDockerCall
}

var _ containerClient = &fakeContainerClient{}

func (f *fakeContainerClient) ImagePull(ctx context.Context, ref string, options client.ImagePullOptions) (client.ImagePullResponse, error) {
f.Calls = append(f.Calls, fakeDockerCall{Method: "ImagePull", Ref: ref, Options: options})
if f.MockImagePull != nil {
return f.MockImagePull(ctx, ref, options)
}
return fakeImagePullResponse{}, nil
}

func (f *fakeContainerClient) ContainerInspect(ctx context.Context, containerID string, options client.ContainerInspectOptions) (client.ContainerInspectResult, error) {
f.Calls = append(f.Calls, fakeDockerCall{Method: "ContainerInspect", Ref: containerID, Options: options})
if f.MockContainerInspect != nil {
return f.MockContainerInspect(ctx, containerID, options)
}
return client.ContainerInspectResult{}, nil
}

func (f *fakeContainerClient) ContainerCreate(ctx context.Context, options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
f.Calls = append(f.Calls, fakeDockerCall{Method: "ContainerCreate", Options: options})
if f.MockContainerCreate != nil {
return f.MockContainerCreate(ctx, options)
}
return client.ContainerCreateResult{}, nil
}

func (f *fakeContainerClient) ContainerStart(ctx context.Context, containerID string, options client.ContainerStartOptions) (client.ContainerStartResult, error) {
f.Calls = append(f.Calls, fakeDockerCall{Method: "ContainerStart", Ref: containerID, Options: options})
if f.MockContainerStart != nil {
return f.MockContainerStart(ctx, containerID, options)
}
return client.ContainerStartResult{}, nil
}

func (f *fakeContainerClient) ContainerStop(ctx context.Context, containerID string, options client.ContainerStopOptions) (client.ContainerStopResult, error) {
f.Calls = append(f.Calls, fakeDockerCall{Method: "ContainerStop", Ref: containerID, Options: options})
if f.MockContainerStop != nil {
return f.MockContainerStop(ctx, containerID, options)
}
return client.ContainerStopResult{}, nil
}

func (f *fakeContainerClient) ContainerRemove(ctx context.Context, containerID string, options client.ContainerRemoveOptions) (client.ContainerRemoveResult, error) {
f.Calls = append(f.Calls, fakeDockerCall{Method: "ContainerRemove", Ref: containerID, Options: options})
if f.MockContainerRemove != nil {
return f.MockContainerRemove(ctx, containerID, options)
}
return client.ContainerRemoveResult{}, nil
}

// fakeImagePullResponse is an ImagePullResponse whose body is empty, i.e. a
// pull that has already completed. Only the io.ReadCloser methods PullImage
// uses are implemented; the embedded interface is nil.
type fakeImagePullResponse struct {
client.ImagePullResponse
}

func (fakeImagePullResponse) Read([]byte) (int, error) { return 0, io.EOF }

func (fakeImagePullResponse) Close() error { return nil }

// fakeNetworkClient is a networkClient that records every call it receives in
// Calls. Each method delegates to its Mock function when set, and otherwise
// succeeds with a zero result.
type fakeNetworkClient struct {
MockNetworkCreate func(ctx context.Context, name string, options client.NetworkCreateOptions) (client.NetworkCreateResult, error)
MockNetworkRemove func(ctx context.Context, networkID string, options client.NetworkRemoveOptions) (client.NetworkRemoveResult, error)

Calls []fakeDockerCall
}

var _ networkClient = &fakeNetworkClient{}

func (f *fakeNetworkClient) NetworkCreate(ctx context.Context, name string, options client.NetworkCreateOptions) (client.NetworkCreateResult, error) {
f.Calls = append(f.Calls, fakeDockerCall{Method: "NetworkCreate", Ref: name, Options: options})
if f.MockNetworkCreate != nil {
return f.MockNetworkCreate(ctx, name, options)
}
return client.NetworkCreateResult{}, nil
}

func (f *fakeNetworkClient) NetworkRemove(ctx context.Context, networkID string, options client.NetworkRemoveOptions) (client.NetworkRemoveResult, error) {
f.Calls = append(f.Calls, fakeDockerCall{Method: "NetworkRemove", Ref: networkID, Options: options})
if f.MockNetworkRemove != nil {
return f.MockNetworkRemove(ctx, networkID, options)
}
return client.NetworkRemoveResult{}, nil
}
21 changes: 19 additions & 2 deletions cmd/crossplane/render/engine_docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ type dockerRenderEngine struct {
// (exit-3 partial output, *docker.ContainerExitError vs non-exit errors)
// without a real Docker daemon.
runner containerRunner

// networks creates and removes the temporary Docker network Setup owns.
// Production callers leave it nil and Setup builds a real client from the
// environment only when it needs to create a network. Tests substitute a
// fake to exercise the create-network branch without a real Docker
// daemon.
networks networkClient
}

func (e *dockerRenderEngine) CheckContextSupport() error {
Expand Down Expand Up @@ -95,7 +102,16 @@ func (e *dockerRenderEngine) Setup(ctx context.Context, fns []pkgv1.Function) (f
return func() {}, nil
}

networkID, networkName, err := createRenderNetwork(ctx)
cli := e.networks
if cli == nil {
c, err := newNetworkClient()
if err != nil {
return func() {}, errors.Wrap(err, "cannot create Docker network for rendering")
}
cli = c
}

networkID, networkName, err := createRenderNetwork(ctx, cli)
if err != nil {
return func() {}, errors.Wrap(err, "cannot create Docker network for rendering")
}
Expand All @@ -104,7 +120,7 @@ func (e *dockerRenderEngine) Setup(ctx context.Context, fns []pkgv1.Function) (f
injectNetworkAnnotation(fns, networkName)

cleanup := func() { //nolint:contextcheck // Detached context for cleanup.
_ = removeRenderNetwork(context.Background(), networkID)
_ = removeRenderNetwork(context.Background(), cli, networkID)
}

return cleanup, nil
Expand Down Expand Up @@ -140,6 +156,7 @@ func (e *dockerRenderEngine) Render(ctx context.Context, req *renderv1alpha1.Ren
// Let the container access any functions running in development mode on
// the host.
docker.RunWithExtraHosts([]string{"host.docker.internal:host-gateway"}),
docker.RunWithLabels(managedLabels()),
}
if e.network != "" {
opts = append(opts, docker.RunWithNetworkName(e.network))
Expand Down
47 changes: 44 additions & 3 deletions cmd/crossplane/render/engine_docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/moby/moby/client"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/known/structpb"
Expand Down Expand Up @@ -222,9 +223,8 @@ func TestDockerRenderEngineSetup(t *testing.T) {
// call on the same engine stored its created network there. The branch
// must annotate the supplied functions so their containers join the
// network, never create a second network, and always return a no-op
// cleanup. The create-new-network branch is not covered here because it
// depends on a live Docker daemon; the broader render command tests
// exercise it integration-style.
// cleanup. The create-new-network branch is covered separately by
// TestDockerRenderEngineSetupCreatesNetwork.
//
// The MultiBatchAnnotatesAdditionalFunctions case simulates the
// in-process multi-composition use case from crossplane/cli#96: a
Expand Down Expand Up @@ -346,6 +346,47 @@ func TestDockerRenderEngineSetup(t *testing.T) {
}
}

func TestDockerRenderEngineSetupCreatesNetwork(t *testing.T) {
// When e.network is unset, Setup must create a temporary network through
// the engine's network client, record its name, annotate the supplied
// functions to join it, and return a cleanup that removes it through the
// same client.
cli := &fakeNetworkClient{
MockNetworkCreate: func(_ context.Context, _ string, _ client.NetworkCreateOptions) (client.NetworkCreateResult, error) {
return client.NetworkCreateResult{ID: "network-id"}, nil
},
}
e := &dockerRenderEngine{log: logging.NewNopLogger(), networks: cli}
fns := []pkgv1.Function{functionWithAnnotations(nil)}

cleanup, err := e.Setup(t.Context(), fns)
if err != nil {
t.Fatalf("Setup(...): unexpected error: %v", err)
}
if len(cli.Calls) != 1 || cli.Calls[0].Method != "NetworkCreate" {
t.Fatalf("Setup(...): calls %+v, want a single NetworkCreate", cli.Calls)
}

created := cli.Calls[0].Ref
if e.network != created {
t.Errorf("Setup(...): e.network = %q, want the created network %q", e.network, created)
}
wantFns := []pkgv1.Function{functionWithAnnotations(map[string]string{AnnotationKeyRuntimeDockerNetwork: created})}
if diff := cmp.Diff(wantFns, fns); diff != "" {
t.Errorf("Setup(...): fns -want, +got:\n%s", diff)
}

cleanup()

wantCalls := []fakeDockerCall{
{Method: "NetworkCreate", Ref: created, Options: client.NetworkCreateOptions{Driver: "bridge", Labels: map[string]string{LabelKeyManagedBy: LabelValueManagedByCrossplane}}},
{Method: "NetworkRemove", Ref: "network-id", Options: client.NetworkRemoveOptions{}},
}
if diff := cmp.Diff(wantCalls, cli.Calls); diff != "" {
t.Errorf("Setup(...) cleanup: -want calls, +got calls:\n%s", diff)
}
}

// nonExitError is a stand-in for non-*ContainerExitError failures (e.g. image
// pull errors) returned by docker.RunContainer.
type nonExitError struct{ msg string }
Expand Down
32 changes: 21 additions & 11 deletions cmd/crossplane/render/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,33 @@ func (f *EngineFlags) SetDefaultCrossplaneDockerNetwork(fns []pkgv1.Function) {
}
}

// 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.
func createRenderNetwork(ctx context.Context) (string, string, error) {
// networkClient is the subset of the Docker client the render engine uses to
// manage its temporary Docker network.
type networkClient interface {
NetworkCreate(ctx context.Context, name string, options client.NetworkCreateOptions) (client.NetworkCreateResult, error)
NetworkRemove(ctx context.Context, networkID string, options client.NetworkRemoveOptions) (client.NetworkRemoveResult, error)
}

var _ networkClient = (*client.Client)(nil)

// newNetworkClient returns a real Docker client built from the environment.
func newNetworkClient() (networkClient, error) {
cli, err := docker.NewClient()
if err != nil {
return "", "", errors.Wrap(err, "cannot create Docker client")
return nil, errors.Wrap(err, "cannot create Docker client")
}
return cli, nil
}

// 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.
func createRenderNetwork(ctx context.Context, cli networkClient) (string, string, error) {
name := fmt.Sprintf("crossplane-render-%s", rand.String(8))

resp, err := cli.NetworkCreate(ctx, name, client.NetworkCreateOptions{
Driver: "bridge",
Labels: managedLabels(),
})
if err != nil {
return "", "", errors.Wrapf(err, "cannot create Docker network %q", name)
Expand All @@ -68,11 +82,7 @@ func createRenderNetwork(ctx context.Context) (string, string, error) {
}

// removeRenderNetwork removes a temporary Docker network.
func removeRenderNetwork(ctx context.Context, networkID string) error {
cli, err := docker.NewClient()
if err != nil {
return errors.Wrap(err, "cannot create Docker client")
}
_, err = cli.NetworkRemove(ctx, networkID, client.NetworkRemoveOptions{})
func removeRenderNetwork(ctx context.Context, cli networkClient, networkID string) error {
_, err := cli.NetworkRemove(ctx, networkID, client.NetworkRemoveOptions{})
return errors.Wrap(err, "cannot remove Docker network")
}
Loading
Loading