Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ef0875e
feat(diff): restore the XR-only diff path
jcogilvie Nov 2, 2026
8f1d2d8
feat(simulate): classify composite resources and claims for xr simulate
jcogilvie Nov 2, 2026
c4e0dc0
feat(xr): add xr simulate
jcogilvie Nov 2, 2026
c3c77f3
test(xr): restore the xr simulate integration tests
jcogilvie Nov 2, 2026
cf0e562
test(xr): restore the XR-only integration harness code
jcogilvie Nov 3, 2026
10b3a25
test(simulate): show composite resource runs ignore the project file
jcogilvie Nov 2, 2026
fd45af6
docs(xr): say xr simulate -f takes a file or directory
jcogilvie Nov 2, 2026
80f48a2
docs(simulate): cross-reference all three simulate commands
jcogilvie Nov 2, 2026
6f39964
fix(simulate): say why xr simulate rejects an input no XRD defines
jcogilvie Nov 2, 2026
c0dc566
fix(simulate): reject a kind: List at load in every simulate command
jcogilvie Nov 2, 2026
0b71dbc
test(simulate): restore renderXRReport coverage in TestSimulateRender…
jcogilvie Nov 2, 2026
2be0bfb
docs(simulate): point every simulate help at resource simulate
jcogilvie Nov 2, 2026
8d99792
docs(simulate): make the output help and design doc true for xr simulate
jcogilvie Nov 2, 2026
0618d88
docs(simulate): fix stale integration harness and testdata README com…
jcogilvie Nov 2, 2026
4d13413
test(simulate): make the reject symmetry check real and test diffComp…
jcogilvie Nov 2, 2026
f0a099c
test(simulate): fold the project-file image config test into the buil…
jcogilvie Nov 2, 2026
106327e
refactor(xr): validate -f inputs in xr simulate and take a comma-sepa…
jcogilvie Nov 6, 2026
0209b02
fix(simulate): create the package client only when planning
jcogilvie Nov 6, 2026
840facb
docs(xr): lead xr simulate's help with the permissions it needs
jcogilvie Nov 7, 2026
80be2bd
fix(simulate): match each assertion expectation to a distinct result
jcogilvie Nov 7, 2026
a7b1611
fix(simulate): check Before for deleted fields; verify storage version
jcogilvie Nov 7, 2026
008d87f
fix(diff): name the namespace in XR section headers and count only XR…
jcogilvie Nov 7, 2026
fd3f780
fix(simulate): default input namespaces to the kubeconfig context's
jcogilvie Nov 7, 2026
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
4 changes: 3 additions & 1 deletion cmd/crossplane/composition/help/simulate.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ one `--context` names.
`-f` (`--input`) names a file, or a directory of YAML files, of Compositions to
simulate, and is required. Give a comma-separated list, or repeat it, for more; `-f -` reads standard input.
Every resource in them must be a Composition; to simulate a project, use
`crossplane project simulate`.
`crossplane project simulate`, to simulate composite resources or claims, use
`crossplane xr simulate`, and to simulate managed resources directly, use
`crossplane resource simulate`.
Functions come from the cluster. `--cache-dir` sets where provider package
contents are cached.

4 changes: 3 additions & 1 deletion cmd/crossplane/project/help/simulate.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ The `project simulate` command previews how applying a project's Compositions
would change the composite resources in a cluster. It renders each affected
composite resource and shows the difference from what's in the cluster. It
doesn't change the cluster. To simulate Composition files against a cluster
without a project, use `crossplane composition simulate`.
without a project, use `crossplane composition simulate`; to simulate composite
resource or claim files, use `crossplane xr simulate`; to simulate managed
resources directly, use `crossplane resource simulate`.

> **Important:** Although it doesn't change the cluster, this command needs
> `create` and `patch` permission on the resources it renders. It checks each
Expand Down
95 changes: 87 additions & 8 deletions cmd/crossplane/project/integration_plan_output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,24 @@ import (
"github.com/crossplane/cli/v2/internal/testutils/integration"
)

// TestSimulatePlanOutput runs project simulate's -o json simulate.ProjectResult
// against the plain project fixture, which the diff cases cannot
// express. Each case passes simulate args directly.
// TestSimulatePlanOutput runs simulate's own output formats, -o plan and
// -o json's simulate.ProjectResult, for xr, composition and the plain project
// fixture, which crossplane-diff's cases cannot express. Each case passes
// simulate args directly rather than crossplane-diff's.
func TestSimulatePlanOutput(t *testing.T) {
t.Parallel()

xrCase := integration.Setup{
CRDDirs: []string{"testdata/diff/crds"},
SetupFiles: []string{
"testdata/diff/resources/xrd.yaml",
"testdata/diff/resources/composition.yaml",
"testdata/diff/resources/functions.yaml",
},
CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{
integration.Composes("testdata/diff/resources/existing-xr.yaml", "testdata/diff/resources/existing-downstream-resource.yaml"),
},
}
compCase := integration.Setup{
CRDDirs: []string{"testdata/comp/crds"},
SetupFiles: []string{
Expand All @@ -60,19 +72,79 @@ func TestSimulatePlanOutput(t *testing.T) {
another.Name = "another-resource"
comp := plan.ResourceRef{APIVersion: "apiextensions.crossplane.io/v1", Kind: "Composition", Name: "xnopresources.diff.example.org"}

xrArgs := []string{"-f", "testdata/diff/modified-xr.yaml"}
compArgs := []string{"-f", "testdata/comp/updated-composition.yaml", "--namespace=default"}

cases := map[string]struct {
reason string
// args follow the plain project's -f, or with compArgv set, `composition simulate`.
// args follow the plain project's -f, or `composition simulate`'s with compArgv, or `xr simulate`'s with tt.
args []string
// compArgv runs `composition simulate` with args instead.
compArgv bool
// tt, when set, runs `xr simulate` with args against its cluster
// instead of compCase's.
tt *integration.Setup
// wantText is the -o plan output; empty for a json case.
wantText string
// wantResult is the -o json output, reduced by summarize.
wantResult *planShape
}{
"XRPlanText": {
reason: "-o plan, the default, prints an XR's updates grouped under it, then the summary and notes.",
tt: &xrCase,
args: xrArgs,
wantText: `XR: XNopResource.ns.diff.example.org default/test-resource
[~] XDownstreamResource.ns.nop.example.org default/test-resource (update)
spec:
forProvider:
~ configData: "existing-value" -> "modified-value"

[~] XNopResource.ns.diff.example.org default/test-resource (update)
spec:
~ coolField: "existing-value" -> "modified-value"

Plan: 0 to add, 2 to change, 0 to destroy.

Note: Provider plan not run: no provider diff server planned these resources.
`,
},
"XRCreatePlanText": {
reason: "-o plan prints a dotted annotation or label key, such as crossplane.io/composite, whole as one level.",
tt: &integration.Setup{CRDDirs: xrCase.CRDDirs, SetupFiles: xrCase.SetupFiles},
args: []string{"-f", "testdata/diff/new-xr.yaml"},
wantText: `XR: XNopResource.ns.diff.example.org default/test-resource
[+] XDownstreamResource.ns.nop.example.org default/test-resource (create)
metadata:
annotations:
+ crossplane.io/composition-resource-name: "nop-resource"
labels:
+ crossplane.io/composite: "test-resource"
spec:
forProvider:
+ configData: "new-value"

[+] XNopResource.ns.diff.example.org default/test-resource (create)
spec:
+ coolField: "new-value"

Plan: 2 to add, 0 to change, 0 to destroy.

Note: Provider plan not run: no provider diff server planned these resources.
`,
},
"XRPlanJSON": {
reason: "-o json emits simulate.ProjectResult: the XR as a changed composite, with both its plans grouped under it.",
tt: &xrCase,
args: append([]string{"-o", "json"}, xrArgs...),
wantResult: &planShape{
Summary: plan.Summary{Update: 2},
Composites: []simulate.CompositeResult{{Ref: xnop, Status: simulate.CompositeChanged}},
Plans: []planEntry{
{Kind: "XDownstreamResource", Name: "test-resource", Action: plan.ActionUpdate, Composite: "test-resource"},
{Kind: "XNopResource", Name: "test-resource", Action: plan.ActionUpdate, Composite: "test-resource"},
},
},
},
"CompPlanText": {
reason: "-o plan prints the Composition's plan with its revision impact, then each affected XR's plans under it.",
compArgv: true,
Expand Down Expand Up @@ -170,14 +242,21 @@ Note: Provider plan not run: no provider diff server planned these resources.
t.Skip(reason)
}

cfg := integration.Start(t, compCase).Config
setup := compCase
if tc.tt != nil {
setup = *tc.tt
}
cfg := integration.Start(t, setup).Config

var stdout, stderr bytes.Buffer
argv := append(plainProjectArgv(t), tc.args...)
if tc.compArgv {
switch {
case tc.compArgv:
argv = append([]string{"composition", "simulate", "--timeout=" + defaultTimeout.String()}, tc.args...)
case tc.tt != nil:
argv = append([]string{"xr", "simulate", "--timeout=" + defaultTimeout.String()}, tc.args...)
}
code, err := runSimulateArgv(t, cfg, argv, localCrossplaneBinary(t), &stdout, &stderr)
code, err := runSimulateArgv(t, cfg, argv, localCrossplaneBinary(t), nil, &stdout, &stderr)
if err != nil {
t.Fatalf("\n%s\nsimulate: unexpected error: %v\nstderr:\n%s", tc.reason, err, stderr.String())
}
Expand Down Expand Up @@ -428,7 +507,7 @@ func TestSimulateNameRejected(t *testing.T) {

var stdout, stderr bytes.Buffer
argv := append(plainProjectArgv(t), tc.args...)
code, err := runSimulateArgv(t, cfg, argv, localCrossplaneBinary(t), &stdout, &stderr)
code, err := runSimulateArgv(t, cfg, argv, localCrossplaneBinary(t), nil, &stdout, &stderr)
if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
t.Fatalf("\n%s\nsimulate: want error containing %q, got %v (exit %d)", tc.reason, tc.wantErr, err, code)
}
Expand Down
25 changes: 0 additions & 25 deletions cmd/crossplane/project/integration_project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,45 +20,20 @@ package project

import (
"bytes"
"context"
"encoding/json"
"os"
"path/filepath"
"testing"

diffv1alpha1 "github.com/crossplane/upjet/v2/proto/diff/v1alpha1"
"github.com/google/go-cmp/cmp"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/structpb"
"k8s.io/apimachinery/pkg/runtime/schema"

dp "github.com/crossplane/cli/v2/internal/diff/diffprocessor"
"github.com/crossplane/cli/v2/internal/plan"
"github.com/crossplane/cli/v2/internal/simulate"
"github.com/crossplane/cli/v2/internal/testutils/integration"
)

// fakeProviderRouter routes the composed XDownstreamResource kind, and only
// it, to a fake diff server that answers with fn.
type fakeProviderRouter struct {
fn func(*diffv1alpha1.PlanRequest) (*diffv1alpha1.PlanResponse, error)
}

func (f fakeProviderRouter) Route(gk schema.GroupKind) (diffv1alpha1.PlanServiceClient, bool) {
if gk.Kind != "XDownstreamResource" {
return nil, false
}
return f, true
}

func (f fakeProviderRouter) Plan(_ context.Context, in *diffv1alpha1.PlanRequest, _ ...grpc.CallOption) (*diffv1alpha1.PlanResponse, error) {
return f.fn(in)
}

func strValue(s string) *diffv1alpha1.FieldValue {
return &diffv1alpha1.FieldValue{Kind: &diffv1alpha1.FieldValue_Value{Value: structpb.NewStringValue(s)}}
}

// projectTestCase is a `project simulate -f <project file>` run against a
// fixture project, exercising how a project varies rather than the XR and
// Composition logic the other tables cover.
Expand Down
Loading
Loading