Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
abb0bf3
feat(diff): restore the XR-only diff path
jcogilvie Nov 2, 2026
f3c6909
feat(simulate): classify composite resources and claims for xr simulate
jcogilvie Nov 2, 2026
dc6a3b8
feat(xr): add xr simulate
jcogilvie Nov 2, 2026
b8c3ba5
test(xr): restore the xr simulate integration tests
jcogilvie Nov 2, 2026
d7dfb94
test(xr): restore the XR-only integration harness code
jcogilvie Nov 3, 2026
93fe72c
test(simulate): show composite resource runs ignore the project file
jcogilvie Nov 2, 2026
5e007b2
docs(xr): say xr simulate -f takes a file or directory
jcogilvie Nov 2, 2026
88554fb
docs(simulate): cross-reference all three simulate commands
jcogilvie Nov 2, 2026
faff654
fix(simulate): say why xr simulate rejects an input no XRD defines
jcogilvie Nov 2, 2026
7be7366
fix(simulate): reject a kind: List at load in every simulate command
jcogilvie Nov 2, 2026
569c397
test(simulate): restore renderXRReport coverage in TestSimulateRender…
jcogilvie Nov 2, 2026
f560a4f
docs(simulate): point every simulate help at resource simulate
jcogilvie Nov 2, 2026
04b7229
docs(simulate): make the output help and design doc true for xr simulate
jcogilvie Nov 2, 2026
a1ad8af
docs(simulate): fix stale integration harness and testdata README com…
jcogilvie Nov 2, 2026
954188b
test(simulate): make the reject symmetry check real and test diffComp…
jcogilvie Nov 2, 2026
dbc418f
test(simulate): fold the project-file image config test into the buil…
jcogilvie Nov 2, 2026
29a3c90
refactor(xr): validate -f inputs in xr simulate and take a comma-sepa…
jcogilvie Nov 6, 2026
1dbbe40
fix(simulate): create the package client only when planning
jcogilvie Nov 6, 2026
69fa087
docs(xr): lead xr simulate's help with the permissions it needs
jcogilvie Nov 7, 2026
6837428
fix(simulate): match each assertion expectation to a distinct result
jcogilvie Nov 7, 2026
12ec3d6
fix(simulate): check Before for deleted fields; verify storage version
jcogilvie Nov 7, 2026
53abf84
fix(diff): name the namespace in XR section headers and count only XR…
jcogilvie Nov 7, 2026
7626bfb
fix(simulate): default input namespaces to the kubeconfig context's
jcogilvie Nov 7, 2026
f9636e2
refactor(xr): own xr simulate's input wording
jcogilvie Nov 8, 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