Skip to content
Open
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
1 change: 1 addition & 0 deletions cmd/crossplane/render/engine_docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,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
1 change: 1 addition & 0 deletions cmd/crossplane/render/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ func createRenderNetwork(ctx context.Context, cli networkClient) (string, string

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 Down
4 changes: 2 additions & 2 deletions cmd/crossplane/render/network_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,13 @@ const renderNetworkPrefix = "crossplane-render-"

// createRenderNetworkReturns returns a MockNetworkCreate that returns the
// supplied network ID and error. It returns an error instead when it is not
// asked to create a render bridge network.
// asked to create a render bridge network labeled as managed by render.
func createRenderNetworkReturns(id string, err error) func(context.Context, string, client.NetworkCreateOptions) (client.NetworkCreateResult, error) {
return func(_ context.Context, name string, options client.NetworkCreateOptions) (client.NetworkCreateResult, error) {
if !strings.HasPrefix(name, renderNetworkPrefix) {
return client.NetworkCreateResult{}, errors.Errorf("NetworkCreate(...): name %q does not have prefix %q", name, renderNetworkPrefix)
}
if diff := cmp.Diff(client.NetworkCreateOptions{Driver: "bridge"}, options); diff != "" {
if diff := cmp.Diff(client.NetworkCreateOptions{Driver: "bridge", Labels: map[string]string{LabelKeyManagedBy: LabelValueManagedByCrossplane}}, options); diff != "" {
return client.NetworkCreateResult{}, errors.Errorf("NetworkCreate(...): -want options, +got options:\n%s", diff)
}
return client.NetworkCreateResult{ID: id}, err
Expand Down
34 changes: 34 additions & 0 deletions cmd/crossplane/render/runtime_docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,36 @@ const (
AnnotationKeyRuntimeDockerNetwork = "render.crossplane.io/runtime-docker-network"
)

// Labels that render applies to the Docker resources it creates, so that
// leftovers (e.g. after a crash or with an Orphan cleanup policy) can be
// identified and swept.
const (
// LabelKeyManagedBy is applied to every Docker container and network
// created by render. Its value is always LabelValueManagedByCrossplane.
LabelKeyManagedBy = "render.crossplane.io/managed-by"

// LabelValueManagedByCrossplane is the value of LabelKeyManagedBy.
LabelValueManagedByCrossplane = "crossplane"

// LabelKeyCleanup is applied to Function containers and records the
// effective DockerCleanup policy the container was created with.
LabelKeyCleanup = "render.crossplane.io/cleanup"
)

// managedLabels returns the labels applied to every Docker resource render
// creates.
func managedLabels() map[string]string {
return map[string]string{LabelKeyManagedBy: LabelValueManagedByCrossplane}
}

// functionContainerLabels returns the labels applied to a Function container
// created with the supplied cleanup policy.
func functionContainerLabels(cleanup DockerCleanup) map[string]string {
l := managedLabels()
l[LabelKeyCleanup] = string(cleanup)
return l
}

// DockerCleanup specifies what Docker should do with a Function container after
// it has been run.
type DockerCleanup string
Expand Down Expand Up @@ -323,6 +353,10 @@ func (r *RuntimeDocker) createContainer(ctx context.Context, cli containerClient
Cmd: []string{"--insecure"},
ExposedPorts: network.PortSet{port: struct{}{}},
Env: r.Env,
// Labels are only set at creation time. A pre-existing named container
// that is reused is not relabeled, since Docker can't change labels on
// an existing container.
Labels: functionContainerLabels(r.Cleanup),
}
hcfg := &container.HostConfig{}
var ncfg *network.NetworkingConfig
Expand Down
100 changes: 100 additions & 0 deletions cmd/crossplane/render/runtime_docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -647,3 +647,103 @@ func TestRuntimeDockerStop(t *testing.T) {
})
}
}

func TestRuntimeDockerStartLabels(t *testing.T) {
const (
containerID = "container-id"
containerName = "fn-container"
dockerNetwork = "render-net"
)

// createLabeledContainer returns a MockContainerCreate that creates a
// container, or returns an error when the container config doesn't carry
// the supplied labels.
createLabeledContainer := func(labels map[string]string) func(context.Context, client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
return func(_ context.Context, options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
if options.Config == nil {
return client.ContainerCreateResult{}, errors.New("ContainerCreate(...): called without a container config")
}
if diff := cmp.Diff(labels, options.Config.Labels); diff != "" {
return client.ContainerCreateResult{}, errors.Errorf("ContainerCreate(...): -want labels, +got labels:\n%s", diff)
}
return client.ContainerCreateResult{ID: containerID}, nil
}
}

type args struct {
cleanup DockerCleanup
cli *mockContainerClient
}
type want struct {
err error
}

cases := map[string]struct {
reason string
args args
want want
}{
"Stop": {
reason: "Function containers should be marked as managed by render and record the Stop policy.",
args: args{
cleanup: AnnotationValueRuntimeDockerCleanupStop,
cli: &mockContainerClient{
MockContainerCreate: createLabeledContainer(map[string]string{
LabelKeyManagedBy: LabelValueManagedByCrossplane,
LabelKeyCleanup: string(AnnotationValueRuntimeDockerCleanupStop),
}),
MockContainerStart: startContainer(containerID),
MockContainerInspect: inspectContainerOnNetwork(containerID, containerName, dockerNetwork),
},
},
},
"Remove": {
reason: "Function containers should be marked as managed by render and record the Remove policy.",
args: args{
cleanup: AnnotationValueRuntimeDockerCleanupRemove,
cli: &mockContainerClient{
MockContainerCreate: createLabeledContainer(map[string]string{
LabelKeyManagedBy: LabelValueManagedByCrossplane,
LabelKeyCleanup: string(AnnotationValueRuntimeDockerCleanupRemove),
}),
MockContainerStart: startContainer(containerID),
MockContainerInspect: inspectContainerOnNetwork(containerID, containerName, dockerNetwork),
},
},
},
"Orphan": {
reason: "Function containers should record the Orphan policy so sweeps can skip them.",
args: args{
cleanup: AnnotationValueRuntimeDockerCleanupOrphan,
cli: &mockContainerClient{
MockContainerCreate: createLabeledContainer(map[string]string{
LabelKeyManagedBy: LabelValueManagedByCrossplane,
LabelKeyCleanup: string(AnnotationValueRuntimeDockerCleanupOrphan),
}),
MockContainerStart: startContainer(containerID),
MockContainerInspect: inspectContainerOnNetwork(containerID, containerName, dockerNetwork),
},
},
},
}

for name, tc := range cases {
t.Run(name, func(t *testing.T) {
r := &RuntimeDocker{
Image: "xpkg.crossplane.io/crossplane-contrib/function-dummy:v0.1.0",
Cleanup: tc.args.cleanup,
PullPolicy: AnnotationValueRuntimeDockerPullPolicyIfNotPresent,
Keychain: authn.NewMultiKeychain(),
Network: dockerNetwork,
log: logging.NewNopLogger(),
dockerClient: tc.args.cli,
}

_, err := r.Start(t.Context())

if diff := cmp.Diff(tc.want.err, err, cmpopts.EquateErrors()); diff != "" {
t.Errorf("\n%s\nStart(...): -want error, +got error:\n%s", tc.reason, diff)
}
})
}
}
11 changes: 11 additions & 0 deletions internal/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"encoding/json"
"fmt"
"io"
"maps"
"path"
"path/filepath"
"slices"
Expand Down Expand Up @@ -377,6 +378,16 @@ func RunWithExtraHosts(hosts []string) RunContainerOption {
}
}

// RunWithLabels adds labels to the container.
func RunWithLabels(labels map[string]string) RunContainerOption {
return func(cfg *runContainerConfig) {
if cfg.containerConfig.Labels == nil {
cfg.containerConfig.Labels = map[string]string{}
}
maps.Copy(cfg.containerConfig.Labels, labels)
}
}

// RunWithBindMount adds a bind mount to the container.
func RunWithBindMount(hostPath, containerPath string) RunContainerOption {
return func(cfg *runContainerConfig) {
Expand Down
31 changes: 31 additions & 0 deletions internal/docker/docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"

"github.com/crossplane/crossplane-runtime/v2/pkg/errors"
Expand Down Expand Up @@ -136,3 +137,33 @@ func TestStartAndAttach(t *testing.T) {
})
}
}

func TestRunWithLabels(t *testing.T) {
cases := map[string]struct {
reason string
existing map[string]string
labels map[string]string
want map[string]string
}{
"NilLabels": {
reason: "Labels should be set when the container config has none.",
labels: map[string]string{"a": "1"},
want: map[string]string{"a": "1"},
},
"MergeLabels": {
reason: "Labels should be merged into existing labels.",
existing: map[string]string{"a": "1"},
labels: map[string]string{"b": "2"},
want: map[string]string{"a": "1", "b": "2"},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
cfg := &runContainerConfig{containerConfig: &container.Config{Labels: tc.existing}}
RunWithLabels(tc.labels)(cfg)
if diff := cmp.Diff(tc.want, cfg.containerConfig.Labels); diff != "" {
t.Errorf("\n%s\nRunWithLabels(...): -want, +got:\n%s", tc.reason, diff)
}
})
}
}
Loading