Skip to content
Merged
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
437 changes: 52 additions & 385 deletions cmd/crossplane/render/runtime_docker.go

Large diffs are not rendered by default.

98 changes: 19 additions & 79 deletions cmd/crossplane/render/runtime_docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package render

import (
"context"
"io"
"net"
"strconv"
"testing"
Expand All @@ -36,69 +35,10 @@ import (
"github.com/crossplane/crossplane-runtime/v2/pkg/logging"

pkgv1 "github.com/crossplane/crossplane/apis/v2/pkg/v1"
)

type mockPullClient struct {
MockPullImage func(_ context.Context, ref string, options client.ImagePullOptions) (client.ImagePullResponse, error)
}

func (m *mockPullClient) ImagePull(ctx context.Context, ref string, options client.ImagePullOptions) (client.ImagePullResponse, error) {
return m.MockPullImage(ctx, ref, options)
}

var _ pullClient = &mockPullClient{}

type mockContainerClient 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)
}

func (m *mockContainerClient) ImagePull(ctx context.Context, ref string, options client.ImagePullOptions) (client.ImagePullResponse, error) {
return m.MockImagePull(ctx, ref, options)
}

func (m *mockContainerClient) ContainerInspect(ctx context.Context, containerID string, options client.ContainerInspectOptions) (client.ContainerInspectResult, error) {
return m.MockContainerInspect(ctx, containerID, options)
}

func (m *mockContainerClient) ContainerCreate(ctx context.Context, options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
return m.MockContainerCreate(ctx, options)
}

func (m *mockContainerClient) ContainerStart(ctx context.Context, containerID string, options client.ContainerStartOptions) (client.ContainerStartResult, error) {
return m.MockContainerStart(ctx, containerID, options)
}

func (m *mockContainerClient) ContainerStop(ctx context.Context, containerID string, options client.ContainerStopOptions) (client.ContainerStopResult, error) {
return m.MockContainerStop(ctx, containerID, options)
}

func (m *mockContainerClient) ContainerRemove(ctx context.Context, containerID string, options client.ContainerRemoveOptions) (client.ContainerRemoveResult, error) {
return m.MockContainerRemove(ctx, containerID, options)
}

var _ containerClient = &mockContainerClient{}

// imagePullDone is a MockImagePull that reports an image pull that has
// already completed.
func imagePullDone(context.Context, string, client.ImagePullOptions) (client.ImagePullResponse, error) {
return pullDoneResponse{}, nil
}

// pullDoneResponse is an ImagePullResponse whose body is empty. Only the
// io.ReadCloser methods PullImage uses are implemented; the embedded
// interface is nil.
type pullDoneResponse struct {
client.ImagePullResponse
}

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

func (pullDoneResponse) Close() error { return nil }
"github.com/crossplane/cli/v2/internal/containerruntime"
"github.com/crossplane/cli/v2/internal/containerruntime/fake"
)

// createContainerReturns returns a MockContainerCreate that creates a
// container with the supplied ID.
Expand Down Expand Up @@ -356,7 +296,7 @@ func TestRuntimeDockerStart(t *testing.T) {

type args struct {
pullPolicy DockerPullPolicy
cli *mockContainerClient
cli *fake.Client
}
type want struct {
rctx RuntimeContext
Expand All @@ -372,7 +312,7 @@ func TestRuntimeDockerStart(t *testing.T) {
reason: "Start should create, start, and inspect the Function's container through the injected client, without pulling an image that is present.",
args: args{
pullPolicy: AnnotationValueRuntimeDockerPullPolicyIfNotPresent,
cli: &mockContainerClient{
cli: &fake.Client{
MockContainerCreate: createContainerReturns(containerID),
MockContainerStart: startContainer(containerID),
MockContainerInspect: inspectContainerOnNetwork(containerID, containerName, dockerNetwork),
Expand All @@ -386,12 +326,12 @@ func TestRuntimeDockerStart(t *testing.T) {
reason: "Start should pull the Function's image through the injected client when the pull policy is Always.",
args: args{
pullPolicy: AnnotationValueRuntimeDockerPullPolicyAlways,
cli: &mockContainerClient{
cli: &fake.Client{
MockImagePull: func(ctx context.Context, ref string, options client.ImagePullOptions) (client.ImagePullResponse, error) {
if diff := cmp.Diff(image, ref); diff != "" {
return nil, errors.Errorf("ImagePull(...): -want ref, +got ref:\n%s", diff)
}
return imagePullDone(ctx, ref, options)
return fake.ImagePullDone(ctx, ref, options)
},
MockContainerCreate: createContainerReturns(containerID),
MockContainerStart: startContainer(containerID),
Expand All @@ -406,7 +346,7 @@ func TestRuntimeDockerStart(t *testing.T) {
reason: "Start should return an error, without creating a container, when the injected client cannot pull the image.",
args: args{
pullPolicy: AnnotationValueRuntimeDockerPullPolicyAlways,
cli: &mockContainerClient{
cli: &fake.Client{
MockImagePull: func(context.Context, string, client.ImagePullOptions) (client.ImagePullResponse, error) {
return nil, errBoom
},
Expand All @@ -420,7 +360,7 @@ func TestRuntimeDockerStart(t *testing.T) {
reason: "Start should return an error when the injected client cannot start the container.",
args: args{
pullPolicy: AnnotationValueRuntimeDockerPullPolicyIfNotPresent,
cli: &mockContainerClient{
cli: &fake.Client{
MockContainerCreate: createContainerReturns(containerID),
MockContainerStart: func(context.Context, string, client.ContainerStartOptions) (client.ContainerStartResult, error) {
return client.ContainerStartResult{}, errBoom
Expand Down Expand Up @@ -577,7 +517,7 @@ func TestRuntimeDockerStop(t *testing.T) {

for name, tc := range cases {
t.Run(name, func(t *testing.T) {
cli := &mockContainerClient{
cli := &fake.Client{
MockContainerCreate: createContainerReturns(containerID),
MockContainerStart: startContainer(containerID),
MockContainerInspect: inspectContainerOnNetwork(containerID, containerName, dockerNetwork),
Expand All @@ -594,8 +534,8 @@ func TestRuntimeDockerStop(t *testing.T) {
}
// The stop request must have its own deadline, so a stop
// that hangs can't stall cleanup.
if deadline, ok := ctx.Deadline(); !ok || time.Until(deadline) > containerStopTimeout {
t.Errorf("\n%s\nContainerStop(...): want a deadline within %s, got deadline %v (set: %t)", tc.reason, containerStopTimeout, deadline, ok)
if deadline, ok := ctx.Deadline(); !ok || time.Until(deadline) > containerruntime.StopTimeout {
t.Errorf("\n%s\nContainerStop(...): want a deadline within %s, got deadline %v (set: %t)", tc.reason, containerruntime.StopTimeout, deadline, ok)
}
return stop(ctx, id, options)
}
Expand All @@ -610,8 +550,8 @@ func TestRuntimeDockerStop(t *testing.T) {
}
// The remove request must have its own deadline, so a stop
// that hangs can't leave it without time to run.
if deadline, ok := ctx.Deadline(); !ok || time.Until(deadline) > containerRemoveTimeout || ctx.Err() != nil {
t.Errorf("\n%s\nContainerRemove(...): want a live context with a deadline within %s, got deadline %v (set: %t), err %v", tc.reason, containerRemoveTimeout, deadline, ok, ctx.Err())
if deadline, ok := ctx.Deadline(); !ok || time.Until(deadline) > containerruntime.RemoveTimeout || ctx.Err() != nil {
t.Errorf("\n%s\nContainerRemove(...): want a live context with a deadline within %s, got deadline %v (set: %t), err %v", tc.reason, containerruntime.RemoveTimeout, deadline, ok, ctx.Err())
}
return remove(ctx, id, options)
}
Expand Down Expand Up @@ -673,7 +613,7 @@ func TestRuntimeDockerStartLabels(t *testing.T) {
type args struct {
cleanup DockerCleanup
session string
cli *mockContainerClient
cli *fake.Client
}
type want struct {
err error
Expand All @@ -688,7 +628,7 @@ func TestRuntimeDockerStartLabels(t *testing.T) {
reason: "Function containers should be marked as managed by render and record the Stop policy.",
args: args{
cleanup: AnnotationValueRuntimeDockerCleanupStop,
cli: &mockContainerClient{
cli: &fake.Client{
MockContainerCreate: createLabeledContainer(map[string]string{
LabelKeyManagedBy: LabelValueManagedByCrossplane,
LabelKeySession: sessionID(),
Expand All @@ -703,7 +643,7 @@ func TestRuntimeDockerStartLabels(t *testing.T) {
reason: "Function containers should be marked as managed by render and record the Remove policy.",
args: args{
cleanup: AnnotationValueRuntimeDockerCleanupRemove,
cli: &mockContainerClient{
cli: &fake.Client{
MockContainerCreate: createLabeledContainer(map[string]string{
LabelKeyManagedBy: LabelValueManagedByCrossplane,
LabelKeySession: sessionID(),
Expand All @@ -719,7 +659,7 @@ func TestRuntimeDockerStartLabels(t *testing.T) {
args: args{
cleanup: AnnotationValueRuntimeDockerCleanupRemove,
session: "my-session",
cli: &mockContainerClient{
cli: &fake.Client{
MockContainerCreate: createLabeledContainer(map[string]string{
LabelKeyManagedBy: LabelValueManagedByCrossplane,
LabelKeySession: "my-session",
Expand All @@ -734,7 +674,7 @@ func TestRuntimeDockerStartLabels(t *testing.T) {
reason: "Function containers should record the Orphan policy so sweeps can skip them.",
args: args{
cleanup: AnnotationValueRuntimeDockerCleanupOrphan,
cli: &mockContainerClient{
cli: &fake.Client{
MockContainerCreate: createLabeledContainer(map[string]string{
LabelKeyManagedBy: LabelValueManagedByCrossplane,
LabelKeySession: sessionID(),
Expand Down
2 changes: 2 additions & 0 deletions cmd/crossplane/resource/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ limitations under the License.
package resource

import (
"github.com/crossplane/cli/v2/cmd/crossplane/simulate"
"github.com/crossplane/cli/v2/cmd/crossplane/trace"
"github.com/crossplane/cli/v2/cmd/crossplane/validate"
)

// Cmd contains commands for working with Crossplane resources.
type Cmd struct {
Simulate simulate.Cmd `cmd:"" help:"Preview what applying managed resources would do." maturity:"alpha"`
Trace trace.Cmd `cmd:"" help:"Trace a Crossplane resource for troubleshooting."`
Validate validate.Cmd `cmd:"" help:"Validate Crossplane resources."`
}
69 changes: 69 additions & 0 deletions cmd/crossplane/simulate/help/simulate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
This command previews what applying one or more managed resources would do,
without applying anything: which resources it would create, which fields it
would change, and, most importantly, which changes the provider could only
apply by destroying and recreating the external resource.

For each input resource the CLI fetches the live managed resource from the
cluster, if one exists, and asks the provider itself for the verdict: it runs
the provider's package image locally as a diff server (a stateless,
credential-free gRPC service upjet-based providers serve) and sends it the
desired and live state. The provider computes its own diff, so the preview
includes replacement detection, values known only after apply, and redacted
sensitive values. A resource with no live counterpart plans as a create.

Which providers run as diff servers comes from the current kubeconfig context
by default: the CLI follows each resource's kind to the provider that
installed it on the cluster, so the preview matches the provider versions
actually serving those resources. --providers overrides that discovery with
a file of Provider manifests (like render takes Function manifests), for
example to preview how a provider version that isn't installed yet would
treat existing resources. The manifests also carry per-provider runtime
annotations (`simulate.crossplane.io/runtime-docker-publish-address` and
`simulate.crossplane.io/runtime-docker-target`) for Docker setups where the
published port isn't reachable on the address the daemon reports.

Not every provider serves diffs. A provider package declares the capability
in its metadata, and a resource whose provider serves no diff server (or
whose diff server can't compute a diff for it) still appears in the preview
with an approximate client-side comparison, marked [?], rather than failing
the run or dropping out of the preview.

Nothing simulate does writes to the cluster or the cloud: it reads the
cluster with your kubeconfig and RBAC, and the diff servers run locally with
no credentials and no cluster access.

Diff server startup takes seconds per provider image. In an
edit-simulate-edit loop, annotate a provider manifest with
`simulate.crossplane.io/runtime-docker-cleanup: Orphan` (like render's
per-function cleanup annotation) to leave its container running so the next
run reuses it. The containers have names like `crossplane-diff-server-DIGEST`
and carry `simulate.crossplane.io/managed-by` and session labels; any later
run without the annotation cleans them up, or remove them with docker rm.

## Examples

Preview what applying resources.yaml would do:

```shell
crossplane resource simulate -f resources.yaml
```

Preview against specific provider versions instead of the installed ones,
from a checked-in file of Provider manifests:

```shell
crossplane resource simulate -f resources.yaml --providers providers.yaml
```

Keep diff servers running so later runs start faster, via a provider manifest
annotated with `simulate.crossplane.io/runtime-docker-cleanup: Orphan`:

```shell
crossplane resource simulate -f resources.yaml --providers providers.yaml
```

Emit machine-readable results for CI gating:

```shell
crossplane resource simulate -f resources.yaml -o json
```
Loading
Loading