Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 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
3912113
test(simulate): move the shared integration harness and testdata to i…
jcogilvie Nov 6, 2026
15fc396
test(composition): own the composition integration suite
jcogilvie Nov 6, 2026
c622f16
test(xr): own the xr integration suite
jcogilvie Nov 6, 2026
ab093a8
build(nix): run integration packages one at a time
jcogilvie Nov 6, 2026
5ad7bcc
refactor(simulate): rename ProjectResult to Result
jcogilvie Nov 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
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.

23 changes: 23 additions & 0 deletions cmd/crossplane/composition/integration_main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//go:build integration

package composition_test

import (
"fmt"
"os"
"testing"

"github.com/crossplane/cli/v2/internal/testutils/integration"
)

// TestMain runs before all tests.
func TestMain(m *testing.M) {
// Kill envtest servers that an earlier run left behind when it died abnormally.
integration.ReapOrphanedEnvtest(func(format string, args ...any) { fmt.Fprintf(os.Stderr, format+"\n", args...) })

// Run all tests
exitCode := m.Run()

// Exit with the test suite's exit code
os.Exit(exitCode)
}

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions cmd/crossplane/composition/testdata/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# composition simulate integration tests

`integration_test.go` runs `TestCompDiffIntegration`: crossplane-diff's composition cases, run as
`crossplane composition simulate -f <Composition files>` against an envtest cluster. A case keeps its
crossplane-diff args; `simharness`'s `RunIntegrationTest` translates them
into the command's flags and runs it. Fixtures only this suite uses are in
`testdata/comp`; those shared with another suite are in
`internal/testutils/integration/testdata/comp` (and `crds`).

The skips, how to run the suite and its coverage are in
`cmd/crossplane/project/testdata/README.md`. Run only this suite with
`go test -tags integration -run TestCompDiffIntegration ./cmd/crossplane/composition/`.
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
2 changes: 1 addition & 1 deletion cmd/crossplane/project/integration_main_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//go:build integration

package project
package project_test

import (
"fmt"
Expand Down
166 changes: 123 additions & 43 deletions cmd/crossplane/project/integration_plan_output_test.go

Large diffs are not rendered by default.

72 changes: 24 additions & 48 deletions cmd/crossplane/project/integration_project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,49 +16,25 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

package project
package project_test

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"
"github.com/crossplane/cli/v2/internal/testutils/integration/simharness"
)

// 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 Expand Up @@ -114,18 +90,18 @@ func TestProjectSimulateIntegration(t *testing.T) {
"PlainProject": {
reason: "A project with no dependencies or embedded functions simulates its Composition against cluster functions, like the same Composition in Composition mode.",
setupFiles: []string{
"testdata/comp/resources/xrd.yaml",
"testdata/comp/resources/original-composition.yaml",
"testdata/comp/resources/functions.yaml",
"../../../internal/testutils/integration/testdata/comp/resources/xrd.yaml",
"../../../internal/testutils/integration/testdata/comp/resources/original-composition.yaml",
"../../../internal/testutils/integration/testdata/comp/resources/functions.yaml",
},
crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{
integration.Composes(
"testdata/comp/resources/existing-xr-1.yaml",
"testdata/comp/resources/existing-downstream-1.yaml",
"../../../internal/testutils/integration/testdata/comp/resources/existing-xr-1.yaml",
"../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-1.yaml",
),
integration.Composes(
"testdata/comp/resources/existing-xr-2.yaml",
"testdata/comp/resources/existing-downstream-2.yaml",
"../../../internal/testutils/integration/testdata/comp/resources/existing-xr-2.yaml",
"../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-2.yaml",
),
},
project: "plain",
Expand All @@ -151,14 +127,14 @@ func TestProjectSimulateIntegration(t *testing.T) {
"ProviderPlanJoinsChanges": {
reason: "A provider plan routed for a composed resource joins that resource's changes.",
setupFiles: []string{
"testdata/comp/resources/xrd.yaml",
"testdata/comp/resources/original-composition.yaml",
"testdata/comp/resources/functions.yaml",
"../../../internal/testutils/integration/testdata/comp/resources/xrd.yaml",
"../../../internal/testutils/integration/testdata/comp/resources/original-composition.yaml",
"../../../internal/testutils/integration/testdata/comp/resources/functions.yaml",
},
crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{
integration.Composes(
"testdata/comp/resources/existing-xr-1.yaml",
"testdata/comp/resources/existing-downstream-1.yaml",
"../../../internal/testutils/integration/testdata/comp/resources/existing-xr-1.yaml",
"../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-1.yaml",
),
},
project: "providerdep",
Expand All @@ -182,15 +158,15 @@ func TestProjectSimulateIntegration(t *testing.T) {
"EmbeddedFunctionOverlay": {
reason: "A Composition step naming a function embedded in the project runs the project's function, not a same-named cluster Function, which would fail without step input.",
setupFiles: []string{
"testdata/comp/resources/xrd.yaml",
"testdata/comp/resources/original-composition.yaml",
"testdata/comp/resources/functions.yaml",
"../../../internal/testutils/integration/testdata/comp/resources/xrd.yaml",
"../../../internal/testutils/integration/testdata/comp/resources/original-composition.yaml",
"../../../internal/testutils/integration/testdata/comp/resources/functions.yaml",
"testdata/project/embedded/cluster-function.yaml",
},
crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{
integration.Composes(
"testdata/comp/resources/existing-xr-1.yaml",
"testdata/comp/resources/existing-downstream-1.yaml",
"../../../internal/testutils/integration/testdata/comp/resources/existing-xr-1.yaml",
"../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-1.yaml",
),
},
project: "embedded",
Expand All @@ -217,13 +193,13 @@ func TestProjectSimulateIntegration(t *testing.T) {
}

cfg := integration.Start(t, integration.Setup{
CRDDirs: []string{"testdata/comp/crds"},
CRDDirs: []string{"../../../internal/testutils/integration/testdata/comp/crds"},
SetupFiles: tc.setupFiles,
CrossplaneManagedResources: tc.crossplaneManagedResources,
}).Config

argv := []string{
"project", "simulate", "--timeout=" + defaultTimeout.String(),
"project", "simulate", "--timeout=" + simharness.DefaultTimeout.String(),
"-f", copyProjectFixture(t, tc.project),
"--cache-dir=" + t.TempDir(),
}
Expand All @@ -234,17 +210,17 @@ func TestProjectSimulateIntegration(t *testing.T) {
argv = append(argv, "-o", "json")

var stdout, stderr bytes.Buffer
code, err := runSimulateArgvWith(t, cfg, withRenderBinary(argv, localCrossplaneBinary(t)), &simulate.Hooks{Router: tc.router}, &stdout, &stderr)
code, err := simharness.RunSimulateArgvWith(t, cfg, simharness.WithRenderBinary(argv, simharness.LocalCrossplaneBinary(t)), &simulate.Hooks{Router: tc.router}, &stdout, &stderr)
if err != nil {
t.Fatalf("\n%s\nsimulate: unexpected error: %v\nstderr:\n%s", tc.reason, err, stderr.String())
}
if code != tc.expectedExitCode {
t.Errorf("\n%s\nsimulate: want exit code %d, got %d", tc.reason, tc.expectedExitCode, code)
}

var res simulate.ProjectResult
var res simulate.Result
if err := json.Unmarshal(stdout.Bytes(), &res); err != nil {
t.Fatalf("\n%s\nsimulate -o json: output is not a simulate.ProjectResult: %v\n%s", tc.reason, err, stdout.String())
t.Fatalf("\n%s\nsimulate -o json: output is not a simulate.Result: %v\n%s", tc.reason, err, stdout.String())
}
if diff := cmp.Diff(tc.expectedResult, summarize(res)); diff != "" {
t.Errorf("\n%s\nsimulate -o json: -want, +got:\n%s", tc.reason, diff)
Expand Down
Loading
Loading