From 0ab4c163ba745848b6da0b45312de08ac6c6d9b3 Mon Sep 17 00:00:00 2001 From: Jonathan Ogilvie Date: Fri, 6 Nov 2026 18:02:42 -0500 Subject: [PATCH 1/6] test(simulate): move the shared integration harness and testdata to internal/testutils The simulate argv translation, case runner and fixtures shared by the composition, xr and project integration suites move to simharness and internal/testutils/integration/testdata so each command can own its suite. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Jonathan Ogilvie --- .../project/integration_main_test.go | 2 +- .../project/integration_plan_output_test.go | 77 +- .../project/integration_project_test.go | 43 +- .../project/integration_provider_plan_test.go | 21 +- cmd/crossplane/project/integration_test.go | 5965 ----------------- cmd/crossplane/project/testdata/README.md | 22 +- .../comp/api-version-updated-composition.yaml | 32 - ...vision-templating-updated-composition.yaml | 37 - .../testdata/comp/composition-no-changes.yaml | 33 - .../field-removal-updated-composition.yaml | 32 - .../mixed-status-updated-composition.yaml | 59 - .../testdata/comp/net-new-composition.yaml | 37 - .../api-version-original-composition.yaml | 32 - .../comp/resources/claim-composition.yaml | 32 - ...emplating-composition-kubectl-applied.yaml | 41 - ...claim-revision-templating-composition.yaml | 39 - .../testdata/comp/resources/claim-xrd.yaml | 27 - .../comp/resources/custom-namespace.yaml | 4 - .../resources/cycle/existing-cycle-xr.yaml | 12 - ...isting-api-version-downstream-v1beta1.yaml | 10 - .../resources/existing-api-version-xr.yaml | 11 - .../comp/resources/existing-claim-1-xr.yaml | 21 - .../comp/resources/existing-claim-1.yaml | 18 - .../comp/resources/existing-claim-2-xr.yaml | 21 - .../comp/resources/existing-claim-2.yaml | 18 - .../existing-claim-downstream-1.yaml | 10 - .../existing-claim-downstream-2.yaml | 10 - ...xisting-claim-revision-ref-downstream.yaml | 14 - ...vision-ref-kubectl-applied-downstream.yaml | 12 - ...claim-revision-ref-kubectl-applied-xr.yaml | 15 - ...ng-claim-revision-ref-kubectl-applied.yaml | 20 - .../existing-claim-revision-ref-xr.yaml | 15 - .../existing-claim-revision-ref.yaml | 20 - .../existing-conditional-gating-stage0.yaml | 12 - .../existing-conditional-gating-xr.yaml | 10 - .../existing-custom-ns-downstream.yaml | 11 - .../comp/resources/existing-custom-ns-xr.yaml | 10 - .../existing-downstream-deleting.yaml | 11 - .../existing-downstream-drifted.yaml | 14 - ...isting-downstream-pinned-revision-ref.yaml | 15 - .../resources/existing-downstream-v2.yaml | 13 - .../existing-downstream-with-argocd.yaml | 14 - .../existing-sequencer-gating-stage0.yaml | 12 - .../existing-sequencer-gating-xr.yaml | 10 - .../resources/existing-sha256-downstream.yaml | 11 - .../comp/resources/existing-sha256-xr.yaml | 10 - .../existing-unverified-cluster-xr.yaml | 11 - ...existing-xr-1-kubectl-applied-current.yaml | 13 - .../existing-xr-1-kubectl-applied.yaml | 13 - .../comp/resources/existing-xr-deleting.yaml | 14 - ...ing-xr-listing-uncontrolled-resources.yaml | 22 - .../existing-xr-pinned-revision-ref.yaml | 18 - .../resources/existing-xr-selector-match.yaml | 18 - .../existing-xr-selector-mismatch.yaml | 18 - .../comp/resources/existing-xr-v2.yaml | 12 - .../resources/existing-xr-with-argocd.yaml | 14 - .../field-removal/composition-with-field.yaml | 33 - .../field-removal/existing-downstream.yaml | 11 - .../resources/field-removal/existing-xr.yaml | 10 - .../comp/resources/functions-sha256.yaml | 17 - .../comp/resources/mixed-downstream-db-1.yaml | 11 - .../comp/resources/mixed-downstream-db-2.yaml | 11 - .../comp/resources/mixed-downstream-db-3.yaml | 12 - .../resources/mixed-downstream-network-1.yaml | 11 - .../resources/mixed-downstream-network-2.yaml | 11 - .../resources/mixed-downstream-network-3.yaml | 11 - .../resources/mixed-downstream-storage-1.yaml | 11 - .../resources/mixed-downstream-storage-2.yaml | 11 - .../resources/mixed-downstream-storage-3.yaml | 11 - .../resources/mixed-status-composition.yaml | 58 - .../testdata/comp/resources/mixed-xr-1.yaml | 10 - .../testdata/comp/resources/mixed-xr-2.yaml | 10 - .../testdata/comp/resources/mixed-xr-3.yaml | 10 - .../collision-composition-revision.yaml | 56 - .../collision-composition.yaml | 52 - .../namespace-collision/configmap-ns-a.yaml | 8 - .../namespace-collision/configmap-ns-b.yaml | 8 - .../existing-downstream-ns-a.yaml | 14 - .../existing-downstream-ns-b.yaml | 14 - .../namespace-collision/existing-xr-ns-a.yaml | 15 - .../namespace-collision/existing-xr-ns-b.yaml | 15 - .../namespace-collision/namespace-ns-a.yaml | 4 - .../namespace-collision/namespace-ns-b.yaml | 4 - .../nested-xr/existing-child-downstream.yaml | 16 - .../nested-xr/existing-nested-child-xr.yaml | 14 - .../existing-parent-direct-downstream.yaml | 12 - .../nested-xr/existing-parent-xr.yaml | 12 - .../nested-xr/parent-composition.yaml | 49 - .../comp/resources/nested-xr/parent-xrd.yaml | 50 - .../resources/original-composition-2.yaml | 33 - ...l-composition-kubectl-applied-current.yaml | 41 - .../original-composition-kubectl-applied.yaml | 40 - .../removal-test-original-composition.yaml | 45 - ...templating-composition-older-revision.yaml | 40 - .../comp/resources/sha256-composition.yaml | 33 - .../resources/shared-child-xrs-differing.yaml | 23 - .../comp/resources/status-downstream-1.yaml | 11 - .../comp/resources/status-downstream-2.yaml | 11 - .../status-indicator-composition.yaml | 33 - .../testdata/comp/resources/status-xr-1.yaml | 10 - .../testdata/comp/resources/status-xr-2.yaml | 10 - .../comp/resources/test-namespace.yaml | 4 - .../unverified-cluster-composition.yaml | 15 - ...vision-templating-updated-composition.yaml | 43 - .../status-indicator-updated-composition.yaml | 35 - .../comp/updated-claim-composition.yaml | 32 - .../comp/updated-collision-composition.yaml | 50 - .../comp/updated-composition-2-changed.yaml | 36 - .../testdata/comp/updated-composition-2.yaml | 33 - .../comp/updated-composition-labeled.yaml | 35 - ...pdated-conditional-gating-composition.yaml | 57 - .../comp/updated-cycle-composition.yaml | 35 - .../comp/updated-parent-composition.yaml | 50 - .../updated-sequencer-gating-composition.yaml | 53 - .../comp/updated-sha256-composition.yaml | 33 - .../updated-shared-child-composition.yaml | 36 - ...pdated-unverified-cluster-composition.yaml | 47 - .../existing-xr-revision-ref-omitted.yaml | 16 - .../diff/first-xr-with-composition-ref.yaml | 11 - .../project/testdata/diff/first-xr.yaml | 7 - .../testdata/diff/generated-name-xr.yaml | 7 - .../testdata/diff/invalid-schema-xr.yaml | 8 - ...ied-api-version-xr-rev2-changed-field.yaml | 13 - .../diff/modified-api-version-xr-rev2.yaml | 13 - .../testdata/diff/modified-claim-nested.yaml | 12 - ...ified-claim-omitting-defaulted-fields.yaml | 11 - .../project/testdata/diff/modified-claim.yaml | 10 - .../testdata/diff/modified-cluster-xr.yaml | 6 - .../testdata/diff/modified-cross-ns-xr.yaml | 8 - .../testdata/diff/modified-env-xr.yaml | 7 - ...dified-legacy-xr-manual-upgrade-to-v2.yaml | 11 - ...egacy-xr-manual-v1-omits-revision-ref.yaml | 12 - .../testdata/diff/modified-legacy-xr.yaml | 7 - .../diff/modified-manual-pinned-claim.yaml | 12 - .../testdata/diff/modified-nested-xr.yaml | 8 - .../testdata/diff/modified-xr-automatic.yaml | 11 - .../modified-xr-manual-upgrade-to-v2.yaml | 13 - ...ified-xr-manual-v1-omits-revision-ref.yaml | 16 - .../testdata/diff/modified-xr-manual-v1.yaml | 13 - .../diff/modified-xr-selector-bound.yaml | 13 - .../diff/modified-xr-selector-nomatch.yaml | 14 - .../diff/modified-xr-selector-stable.yaml | 14 - .../diff/modified-xr-switch-to-automatic.yaml | 11 - .../diff/modified-xr-with-external-dep.yaml | 8 - .../new-claim-with-claimref-composition.yaml | 10 - .../project/testdata/diff/new-claim.yaml | 11 - .../project/testdata/diff/new-cycle-xr.yaml | 8 - .../testdata/diff/new-manual-default-xr.yaml | 15 - .../testdata/diff/new-nested-child-xr.yaml | 9 - .../project/testdata/diff/new-nested-xr.yaml | 8 - ...arent-claim-omitting-defaulted-fields.yaml | 11 - .../diff/new-rendered-defaults-xrs.yaml | 16 - .../testdata/diff/new-xr-manual-no-ref.yaml | 11 - .../diff/new-xr-missing-namespace-second.yaml | 10 - .../diff/new-xr-missing-namespace.yaml | 13 - .../project/testdata/diff/new-xr-second.yaml | 7 - .../testdata/diff/new-xr-with-creds.yaml | 10 - .../new-xr-with-cross-ns-external-dep.yaml | 11 - .../resources/admission-policy-canary.yaml | 15 - .../api-version-composition-revision-v1.yaml | 34 - .../resources/api-version-composition.yaml | 34 - .../claim-composition-with-claimref.yaml | 35 - .../diff/resources/claim-composition.yaml | 31 - .../claim-nested/child-composition.yaml | 34 - .../claim-nested/child-definition.yaml | 29 - .../claim-nested/existing-child-xr.yaml | 11 - .../claim-nested/existing-claim.yaml | 18 - .../existing-managed-resource.yaml | 14 - .../claim-nested/existing-parent-xr.yaml | 24 - .../claim-nested/parent-composition.yaml | 31 - .../parent-defaults-composition.yaml | 35 - .../claim-nested/parent-definition.yaml | 35 - .../testdata/diff/resources/claim-xrd.yaml | 30 - .../composition-no-namespace-patch.yaml | 32 - .../composition-revision-preview.yaml | 38 - .../composition-revision-stable.yaml | 38 - .../resources/composition-revision-v1.yaml | 34 - .../diff/resources/composition-v2.yaml | 32 - .../composition-with-builtin-defaults.yaml | 61 - .../resources/composition-with-configmap.yaml | 48 - ...osition-with-crd-defaulted-downstream.yaml | 33 - .../resources/composition-with-defaults.yaml | 16 - .../composition-with-missing-function.yaml | 17 - ...n-with-non-storage-version-downstream.yaml | 33 - .../resources/composition-with-status.yaml | 41 - .../composition-with-unserved-k8s-group.yaml | 42 - .../diff/resources/conditional-gating-xr.yaml | 7 - .../credentials/cli-credentials.yaml | 9 - .../credentials/composition-with-creds.yaml | 39 - .../function-credentials-secret.yaml | 14 - .../resources/cross-namespace-configmap.yaml | 21 - .../cross-namespace-fn-composition.yaml | 72 - .../diff/resources/default-composition.yaml | 36 - .../diff/resources/env-composition.yaml | 46 - .../testdata/diff/resources/env-xrd.yaml | 22 - .../resources/environment-config-v1beta1.yaml | 8 - ...isting-api-version-downstream-v1beta1.yaml | 10 - .../existing-api-version-xr-rev1.yaml | 13 - ...-claim-defaulted-fields-other-manager.yaml | 10 - .../existing-claim-downstream-resource.yaml | 9 - .../diff/resources/existing-claim-xr.yaml | 21 - .../diff/resources/existing-claim.yaml | 18 - .../diff/resources/existing-cluster-xr.yaml | 10 - .../existing-cross-ns-downstream.yaml | 10 - .../diff/resources/existing-cross-ns-xr.yaml | 11 - ...xisting-defaulted-downstream-composed.yaml | 14 - ...ng-defaulted-downstream-other-manager.yaml | 10 - .../existing-downstream-automatic.yaml | 10 - .../existing-downstream-manual-v1.yaml | 10 - ...sting-downstream-resource-with-argocd.yaml | 15 - .../existing-downstream-selector-stable.yaml | 10 - ...existing-downstream-with-external-dep.yaml | 11 - ...isting-downstream-with-generated-name.yaml | 13 - .../existing-env-downstream-resource.yaml | 16 - .../diff/resources/existing-env-xr.yaml | 9 - .../resources/existing-generate-name-xr.yaml | 13 - .../existing-legacy-downstream-manual-v1.yaml | 9 - .../existing-legacy-xr-manual-v1.yaml | 11 - .../diff/resources/existing-legacy-xr.yaml | 9 - .../diff/resources/existing-namespace.yaml | 4 - .../resources/existing-removal-test-xr.yaml | 13 - ...xisting-sequencer-gating-stage0-ready.yaml | 19 - .../existing-sequencer-gating-stage0.yaml | 19 - ...xisting-sequencer-gating-stage1-ready.yaml | 18 - .../existing-sequencer-gating-xr.yaml | 13 - .../diff/resources/existing-sequencer-xr.yaml | 12 - .../existing-v2xrd-v1paths-downstream.yaml | 10 - .../resources/existing-v2xrd-v1paths-xr.yaml | 14 - .../diff/resources/existing-xr-automatic.yaml | 13 - .../diff/resources/existing-xr-deleting.yaml | 14 - ...ing-xr-listing-uncontrolled-resources.yaml | 23 - .../diff/resources/existing-xr-manual-v1.yaml | 13 - .../existing-xr-selector-stable.yaml | 16 - .../resources/existing-xr-with-argocd.yaml | 19 - ...isting-xr-with-defaults-other-manager.yaml | 9 - .../existing-xr-with-defaults-reconciled.yaml | 16 - ...-xr-with-external-dep-multistep-fatal.yaml | 14 - ...isting-xr-with-external-dep-templated.yaml | 13 - ...isting-xr-with-external-dep-unguarded.yaml | 14 - .../existing-xr-with-external-dep.yaml | 11 - .../resources/external-named-clusterrole.yaml | 8 - .../external-res-fn-composition.yaml | 64 - .../external-res-gotpl-composition.yaml | 46 - ...ernal-res-matchname-gotpl-composition.yaml | 46 - ...al-resource-configmap-other-namespace.yaml | 22 - .../external-resource-configmap.yaml | 21 - .../resources/generated-name-composition.yaml | 32 - .../legacy-composition-revision-v1.yaml | 33 - .../diff/resources/legacy-composition-v2.yaml | 31 - .../testdata/diff/resources/legacy-xrd.yaml | 49 - .../resources/manual-default/definitions.yaml | 96 - .../resources/modified-v2xrd-v1paths-xr.yaml | 13 - ...tep-fatal-after-extra-res-composition.yaml | 65 - .../resources/nested/child-composition.yaml | 33 - ...child-revision-templating-composition.yaml | 38 - .../diff/resources/nested/child-xrd.yaml | 55 - .../existing-child-xr-revision-ref.yaml | 18 - .../existing-child-xr-tier-other-manager.yaml | 9 - .../resources/nested/existing-child-xr.yaml | 13 - ...xisting-managed-resource-revision-ref.yaml | 15 - .../nested/existing-managed-resource.yaml | 11 - .../resources/nested/existing-parent-xr.yaml | 11 - .../resources/nested/parent-composition.yaml | 32 - .../diff/resources/nested/parent-xrd.yaml | 50 - .../resources/pinned-claim/definitions.yaml | 96 - .../existing-automatic-downstream.yaml | 10 - .../pinned-claim/existing-automatic-xr.yaml | 15 - .../pinned-claim/existing-automatic.yaml | 19 - ...existing-lagging-automatic-downstream.yaml | 10 - .../existing-lagging-automatic-xr.yaml | 16 - .../existing-manual-downstream.yaml | 10 - .../pinned-claim/existing-manual-xr.yaml | 17 - .../pinned-claim/existing-manual.yaml | 19 - .../resources/production-composition.yaml | 36 - .../resources/rejecting-admission-policy.yaml | 38 - .../removal-test-cluster-composition.yaml | 31 - ...val-test-cluster-downstream-resource1.yaml | 9 - ...removal-test-legacy-child-composition.yaml | 31 - .../removal-test-legacy-child-xrd.yaml | 24 - .../removal-test-legacy-composition.yaml | 31 - ...removal-test-legacy-nested-xr-managed.yaml | 11 - .../removal-test-legacy-nested-xr.yaml | 13 - ...st-legacycluster-downstream-resource1.yaml | 9 - .../removal-test-nested-xr-managed.yaml | 11 - .../resources/removal-test-nested-xr.yaml | 15 - .../rendered-defaults/definitions.yaml | 129 - .../selector-bound/existing-downstream.yaml | 11 - .../resources/selector-bound/existing-xr.yaml | 16 - .../diff/resources/selector-bound/setup.yaml | 74 - .../diff/resources/sequencer-composition.yaml | 53 - .../sequencer-stage0-downstream.yaml | 10 - .../sequencer-stage1-downstream.yaml | 10 - .../sequencer-with-envconfig-composition.yaml | 67 - .../sequencer-with-envconfig-xr.yaml | 10 - .../testdata/diff/resources/sequencer-xr.yaml | 7 - .../diff/resources/staging-composition.yaml | 36 - .../diff/resources/unestablished-xrd.yaml | 25 - ...uarded-external-res-gotpl-composition.yaml | 44 - .../v2-xrd-with-v1-paths-composition.yaml | 32 - .../diff/resources/v2-xrd-with-v1-paths.yaml | 101 - .../resources/xdownstreamenvresource-xrd.yaml | 31 - .../diff/resources/xrd-with-defaults.yaml | 79 - .../unchanged-automatic-pinned-claim.yaml | 12 - .../diff/unchanged-lagging-automatic-xr.yaml | 11 - .../diff/xr-with-ambiguous-selector.yaml | 11 - .../diff/xr-with-argocd-annotations.yaml | 13 - .../diff/xr-with-argocd-mixed-changes.yaml | 13 - .../diff/xr-with-composition-ref.yaml | 10 - .../diff/xr-with-composition-selector.yaml | 12 - .../diff/xr-with-missing-defaults.yaml | 9 - .../diff/xr-with-overridden-defaults.yaml | 19 - .../testutils/integration/simharness/cases.go | 397 ++ .../integration/simharness/harness.go | 56 +- .../comp/crds/xapimigrateresource-crd.yaml | 0 .../crds/xdownstreamresource-cluster-crd.yaml | 0 ...xdownstreamresource-legacycluster-crd.yaml | 0 .../comp/crds/xdownstreamresource-ns-crd.yaml | 0 .../comp/resources/existing-downstream-1.yaml | 0 .../comp/resources/existing-downstream-2.yaml | 0 .../resources/existing-downstream-manual.yaml | 0 .../existing-downstream-revision-ref.yaml | 0 .../comp/resources/existing-xr-1.yaml | 0 .../comp/resources/existing-xr-2.yaml | 0 .../comp/resources/existing-xr-manual.yaml | 0 .../resources/existing-xr-revision-ref.yaml | 0 .../testdata/comp/resources/functions.yaml | 0 .../comp/resources/original-composition.yaml | 0 ...ision-templating-composition-revision.yaml | 0 .../revision-templating-composition.yaml | 0 .../testdata/comp/resources/xrd.yaml | 0 .../testdata/comp/updated-composition.yaml | 0 ...clusternopresources.nop.crossplane.io.yaml | 0 .../diff/crds/xapimigrateresource-crd.yaml | 0 .../xdefaulteddownstreamresource-ns-crd.yaml | 0 .../crds/xdownstreamresource-cluster-crd.yaml | 0 ...xdownstreamresource-legacycluster-crd.yaml | 0 .../diff/crds/xdownstreamresource-ns-crd.yaml | 0 .../diff/crds/xstatusresource-ns-crd.yaml | 0 .../xversioneddownstreamresource-ns-crd.yaml | 0 .../testdata/diff/modified-xr.yaml | 0 .../integration}/testdata/diff/new-xr.yaml | 0 .../testdata/diff/resources/cluster-xrd.yaml | 0 .../testdata/diff/resources/composition.yaml | 0 .../conditional-gating-composition.yaml | 0 .../diff/resources/cycle/compositions.yaml | 0 .../testdata/diff/resources/cycle/xrds.yaml | 0 .../existing-downstream-resource.yaml | 0 .../existing-other-xr-downstream.yaml | 0 .../diff/resources/existing-other-xr.yaml | 0 .../testdata/diff/resources/existing-xr.yaml | 0 .../testdata/diff/resources/functions.yaml | 0 .../resources/removal-test-composition.yaml | 0 .../removal-test-ns-downstream-resource1.yaml | 0 .../removal-test-ns-downstream-resource2.yaml | 0 ...test-ns-downstream-without-controller.yaml | 0 .../sequencer-gating-composition.yaml | 0 .../resources/shared-child-composition.yaml | 0 .../testdata/diff/resources/xrd.yaml | 0 .../testdata/diff/shared-child-xrs.yaml | 0 .../testdata/diff/unmodified-xr.yaml | 0 360 files changed, 514 insertions(+), 12820 deletions(-) delete mode 100644 cmd/crossplane/project/integration_test.go delete mode 100644 cmd/crossplane/project/testdata/comp/api-version-updated-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/claim-revision-templating-updated-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/composition-no-changes.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/field-removal-updated-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/mixed-status-updated-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/net-new-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/api-version-original-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/claim-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/claim-revision-templating-composition-kubectl-applied.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/claim-revision-templating-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/claim-xrd.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/custom-namespace.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/cycle/existing-cycle-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-api-version-downstream-v1beta1.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-api-version-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-claim-1-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-claim-1.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-claim-2-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-claim-2.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-claim-downstream-1.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-claim-downstream-2.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-claim-revision-ref-downstream.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-claim-revision-ref-kubectl-applied-downstream.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-claim-revision-ref-kubectl-applied-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-claim-revision-ref-kubectl-applied.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-claim-revision-ref-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-claim-revision-ref.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-conditional-gating-stage0.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-conditional-gating-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-custom-ns-downstream.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-custom-ns-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-downstream-deleting.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-downstream-drifted.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-downstream-pinned-revision-ref.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-downstream-v2.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-downstream-with-argocd.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-sequencer-gating-stage0.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-sequencer-gating-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-sha256-downstream.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-sha256-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-unverified-cluster-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-xr-1-kubectl-applied-current.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-xr-1-kubectl-applied.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-xr-deleting.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-xr-listing-uncontrolled-resources.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-xr-pinned-revision-ref.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-xr-selector-match.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-xr-selector-mismatch.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-xr-v2.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/existing-xr-with-argocd.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/field-removal/composition-with-field.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/field-removal/existing-downstream.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/field-removal/existing-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/functions-sha256.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/mixed-downstream-db-1.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/mixed-downstream-db-2.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/mixed-downstream-db-3.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/mixed-downstream-network-1.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/mixed-downstream-network-2.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/mixed-downstream-network-3.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/mixed-downstream-storage-1.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/mixed-downstream-storage-2.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/mixed-downstream-storage-3.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/mixed-status-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/mixed-xr-1.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/mixed-xr-2.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/mixed-xr-3.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/namespace-collision/collision-composition-revision.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/namespace-collision/collision-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/namespace-collision/configmap-ns-a.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/namespace-collision/configmap-ns-b.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/namespace-collision/existing-downstream-ns-a.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/namespace-collision/existing-downstream-ns-b.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/namespace-collision/existing-xr-ns-a.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/namespace-collision/existing-xr-ns-b.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/namespace-collision/namespace-ns-a.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/namespace-collision/namespace-ns-b.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/nested-xr/existing-child-downstream.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/nested-xr/existing-nested-child-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/nested-xr/existing-parent-direct-downstream.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/nested-xr/existing-parent-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/nested-xr/parent-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/nested-xr/parent-xrd.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/original-composition-2.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/original-composition-kubectl-applied-current.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/original-composition-kubectl-applied.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/removal-test-original-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/revision-templating-composition-older-revision.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/sha256-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/shared-child-xrs-differing.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/status-downstream-1.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/status-downstream-2.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/status-indicator-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/status-xr-1.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/status-xr-2.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/test-namespace.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/resources/unverified-cluster-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/revision-templating-updated-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/status-indicator-updated-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/updated-claim-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/updated-collision-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/updated-composition-2-changed.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/updated-composition-2.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/updated-composition-labeled.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/updated-conditional-gating-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/updated-cycle-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/updated-parent-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/updated-sequencer-gating-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/updated-sha256-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/updated-shared-child-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/comp/updated-unverified-cluster-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/existing-xr-revision-ref-omitted.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/first-xr-with-composition-ref.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/first-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/generated-name-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/invalid-schema-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/modified-api-version-xr-rev2-changed-field.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/modified-api-version-xr-rev2.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/modified-claim-nested.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/modified-claim-omitting-defaulted-fields.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/modified-claim.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/modified-cluster-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/modified-cross-ns-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/modified-env-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/modified-legacy-xr-manual-upgrade-to-v2.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/modified-legacy-xr-manual-v1-omits-revision-ref.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/modified-legacy-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/modified-manual-pinned-claim.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/modified-nested-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/modified-xr-automatic.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/modified-xr-manual-upgrade-to-v2.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/modified-xr-manual-v1-omits-revision-ref.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/modified-xr-manual-v1.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/modified-xr-selector-bound.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/modified-xr-selector-nomatch.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/modified-xr-selector-stable.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/modified-xr-switch-to-automatic.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/modified-xr-with-external-dep.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/new-claim-with-claimref-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/new-claim.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/new-cycle-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/new-manual-default-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/new-nested-child-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/new-nested-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/new-parent-claim-omitting-defaulted-fields.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/new-rendered-defaults-xrs.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/new-xr-manual-no-ref.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/new-xr-missing-namespace-second.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/new-xr-missing-namespace.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/new-xr-second.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/new-xr-with-creds.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/new-xr-with-cross-ns-external-dep.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/admission-policy-canary.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/api-version-composition-revision-v1.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/api-version-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/claim-composition-with-claimref.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/claim-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/claim-nested/child-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/claim-nested/child-definition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/claim-nested/existing-child-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/claim-nested/existing-claim.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/claim-nested/existing-managed-resource.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/claim-nested/existing-parent-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/claim-nested/parent-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/claim-nested/parent-defaults-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/claim-nested/parent-definition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/claim-xrd.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/composition-no-namespace-patch.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/composition-revision-preview.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/composition-revision-stable.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/composition-revision-v1.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/composition-v2.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/composition-with-builtin-defaults.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/composition-with-configmap.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/composition-with-crd-defaulted-downstream.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/composition-with-defaults.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/composition-with-missing-function.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/composition-with-non-storage-version-downstream.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/composition-with-status.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/composition-with-unserved-k8s-group.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/conditional-gating-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/credentials/cli-credentials.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/credentials/composition-with-creds.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/credentials/function-credentials-secret.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/cross-namespace-configmap.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/cross-namespace-fn-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/default-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/env-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/env-xrd.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/environment-config-v1beta1.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-api-version-downstream-v1beta1.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-api-version-xr-rev1.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-claim-defaulted-fields-other-manager.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-claim-downstream-resource.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-claim-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-claim.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-cluster-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-cross-ns-downstream.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-cross-ns-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-defaulted-downstream-composed.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-defaulted-downstream-other-manager.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-downstream-automatic.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-downstream-manual-v1.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-downstream-resource-with-argocd.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-downstream-selector-stable.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-downstream-with-external-dep.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-downstream-with-generated-name.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-env-downstream-resource.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-env-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-generate-name-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-legacy-downstream-manual-v1.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-legacy-xr-manual-v1.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-legacy-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-namespace.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-removal-test-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-sequencer-gating-stage0-ready.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-sequencer-gating-stage0.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-sequencer-gating-stage1-ready.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-sequencer-gating-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-sequencer-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-v2xrd-v1paths-downstream.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-v2xrd-v1paths-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-xr-automatic.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-xr-deleting.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-xr-listing-uncontrolled-resources.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-xr-manual-v1.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-xr-selector-stable.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-xr-with-argocd.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-xr-with-defaults-other-manager.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-xr-with-defaults-reconciled.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-xr-with-external-dep-multistep-fatal.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-xr-with-external-dep-templated.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-xr-with-external-dep-unguarded.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/existing-xr-with-external-dep.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/external-named-clusterrole.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/external-res-fn-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/external-res-gotpl-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/external-res-matchname-gotpl-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/external-resource-configmap-other-namespace.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/external-resource-configmap.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/generated-name-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/legacy-composition-revision-v1.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/legacy-composition-v2.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/legacy-xrd.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/manual-default/definitions.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/modified-v2xrd-v1paths-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/multistep-fatal-after-extra-res-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/nested/child-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/nested/child-revision-templating-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/nested/child-xrd.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/nested/existing-child-xr-revision-ref.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/nested/existing-child-xr-tier-other-manager.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/nested/existing-child-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/nested/existing-managed-resource-revision-ref.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/nested/existing-managed-resource.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/nested/existing-parent-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/nested/parent-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/nested/parent-xrd.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/pinned-claim/definitions.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-automatic-downstream.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-automatic-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-automatic.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-lagging-automatic-downstream.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-lagging-automatic-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-manual-downstream.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-manual-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-manual.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/production-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/rejecting-admission-policy.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/removal-test-cluster-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/removal-test-cluster-downstream-resource1.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/removal-test-legacy-child-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/removal-test-legacy-child-xrd.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/removal-test-legacy-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/removal-test-legacy-nested-xr-managed.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/removal-test-legacy-nested-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/removal-test-legacycluster-downstream-resource1.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/removal-test-nested-xr-managed.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/removal-test-nested-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/rendered-defaults/definitions.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/selector-bound/existing-downstream.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/selector-bound/existing-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/selector-bound/setup.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/sequencer-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/sequencer-stage0-downstream.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/sequencer-stage1-downstream.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/sequencer-with-envconfig-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/sequencer-with-envconfig-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/sequencer-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/staging-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/unestablished-xrd.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/unguarded-external-res-gotpl-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/v2-xrd-with-v1-paths-composition.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/v2-xrd-with-v1-paths.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/xdownstreamenvresource-xrd.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/resources/xrd-with-defaults.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/unchanged-automatic-pinned-claim.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/unchanged-lagging-automatic-xr.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/xr-with-ambiguous-selector.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/xr-with-argocd-annotations.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/xr-with-argocd-mixed-changes.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/xr-with-composition-ref.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/xr-with-composition-selector.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/xr-with-missing-defaults.yaml delete mode 100644 cmd/crossplane/project/testdata/diff/xr-with-overridden-defaults.yaml create mode 100644 internal/testutils/integration/simharness/cases.go rename cmd/crossplane/project/integration_simulate_harness_test.go => internal/testutils/integration/simharness/harness.go (86%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/comp/crds/xapimigrateresource-crd.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/comp/crds/xdownstreamresource-cluster-crd.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/comp/crds/xdownstreamresource-legacycluster-crd.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/comp/crds/xdownstreamresource-ns-crd.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/comp/resources/existing-downstream-1.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/comp/resources/existing-downstream-2.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/comp/resources/existing-downstream-manual.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/comp/resources/existing-downstream-revision-ref.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/comp/resources/existing-xr-1.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/comp/resources/existing-xr-2.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/comp/resources/existing-xr-manual.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/comp/resources/existing-xr-revision-ref.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/comp/resources/functions.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/comp/resources/original-composition.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/comp/resources/revision-templating-composition-revision.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/comp/resources/revision-templating-composition.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/comp/resources/xrd.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/comp/updated-composition.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/crds/clusternopresources.nop.crossplane.io.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/crds/xapimigrateresource-crd.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/crds/xdefaulteddownstreamresource-ns-crd.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/crds/xdownstreamresource-cluster-crd.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/crds/xdownstreamresource-legacycluster-crd.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/crds/xdownstreamresource-ns-crd.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/crds/xstatusresource-ns-crd.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/crds/xversioneddownstreamresource-ns-crd.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/modified-xr.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/new-xr.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/resources/cluster-xrd.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/resources/composition.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/resources/conditional-gating-composition.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/resources/cycle/compositions.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/resources/cycle/xrds.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/resources/existing-downstream-resource.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/resources/existing-other-xr-downstream.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/resources/existing-other-xr.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/resources/existing-xr.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/resources/functions.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/resources/removal-test-composition.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/resources/removal-test-ns-downstream-resource1.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/resources/removal-test-ns-downstream-resource2.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/resources/removal-test-ns-downstream-without-controller.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/resources/sequencer-gating-composition.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/resources/shared-child-composition.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/resources/xrd.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/shared-child-xrs.yaml (100%) rename {cmd/crossplane/project => internal/testutils/integration}/testdata/diff/unmodified-xr.yaml (100%) diff --git a/cmd/crossplane/project/integration_main_test.go b/cmd/crossplane/project/integration_main_test.go index 3087fcde..23aba2e8 100644 --- a/cmd/crossplane/project/integration_main_test.go +++ b/cmd/crossplane/project/integration_main_test.go @@ -1,6 +1,6 @@ //go:build integration -package project +package project_test import ( "fmt" diff --git a/cmd/crossplane/project/integration_plan_output_test.go b/cmd/crossplane/project/integration_plan_output_test.go index 549416e2..08153a8a 100644 --- a/cmd/crossplane/project/integration_plan_output_test.go +++ b/cmd/crossplane/project/integration_plan_output_test.go @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package project +package project_test import ( "bytes" @@ -34,6 +34,7 @@ import ( "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" ) // TestSimulatePlanOutput runs simulate's own output formats, -o plan and @@ -44,26 +45,26 @@ func TestSimulatePlanOutput(t *testing.T) { t.Parallel() xrCase := integration.Setup{ - CRDDirs: []string{"testdata/diff/crds"}, + CRDDirs: []string{"../../../internal/testutils/integration/testdata/diff/crds"}, SetupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition.yaml", - "testdata/diff/resources/functions.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", }, CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes("testdata/diff/resources/existing-xr.yaml", "testdata/diff/resources/existing-downstream-resource.yaml"), + integration.Composes("../../../internal/testutils/integration/testdata/diff/resources/existing-xr.yaml", "../../../internal/testutils/integration/testdata/diff/resources/existing-downstream-resource.yaml"), }, } compCase := integration.Setup{ - CRDDirs: []string{"testdata/comp/crds"}, + CRDDirs: []string{"../../../internal/testutils/integration/testdata/comp/crds"}, 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"), - integration.Composes("testdata/comp/resources/existing-xr-2.yaml", "testdata/comp/resources/existing-downstream-2.yaml"), + integration.Composes("../../../internal/testutils/integration/testdata/comp/resources/existing-xr-1.yaml", "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-1.yaml"), + integration.Composes("../../../internal/testutils/integration/testdata/comp/resources/existing-xr-2.yaml", "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-2.yaml"), }, } @@ -72,8 +73,8 @@ 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"} + xrArgs := []string{"-f", "../../../internal/testutils/integration/testdata/diff/modified-xr.yaml"} + compArgs := []string{"-f", "../../../internal/testutils/integration/testdata/comp/updated-composition.yaml", "--namespace=default"} cases := map[string]struct { reason string @@ -111,7 +112,7 @@ 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"}, + args: []string{"-f", "../../../internal/testutils/integration/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: @@ -252,11 +253,11 @@ Note: Provider plan not run: no provider diff server planned these resources. argv := append(plainProjectArgv(t), tc.args...) switch { case tc.compArgv: - argv = append([]string{"composition", "simulate", "--timeout=" + defaultTimeout.String()}, tc.args...) + argv = append([]string{"composition", "simulate", "--timeout=" + simharness.DefaultTimeout.String()}, tc.args...) case tc.tt != nil: - argv = append([]string{"xr", "simulate", "--timeout=" + defaultTimeout.String()}, tc.args...) + argv = append([]string{"xr", "simulate", "--timeout=" + simharness.DefaultTimeout.String()}, tc.args...) } - code, err := runSimulateArgv(t, cfg, argv, localCrossplaneBinary(t), nil, &stdout, &stderr) + code, err := simharness.RunSimulateArgv(t, cfg, argv, simharness.LocalCrossplaneBinary(t), nil, &stdout, &stderr) if err != nil { t.Fatalf("\n%s\nsimulate: unexpected error: %v\nstderr:\n%s", tc.reason, err, stderr.String()) } @@ -327,7 +328,7 @@ func summarize(r simulate.ProjectResult) planShape { func integrationEnvGap() string { // integration.Start fails, rather than skips, a run without envtest's // binaries or Crossplane's CRDs. - if !dockerAvailable() { + if !simharness.DockerAvailable() { return "env: no Docker daemon for the function render engine" } return "" @@ -356,7 +357,7 @@ func TestSimulatePinnedChangedFunction(t *testing.T) { }, "PinnedXRWithUnchangedFunctionDeferred": { reason: "A Manual XR whose pinned revision calls no changed function stays deferred.", - composition: "testdata/comp/resources/original-composition.yaml", + composition: "../../../internal/testutils/integration/testdata/comp/resources/original-composition.yaml", wantDeferred: true, }, } @@ -370,17 +371,17 @@ func TestSimulatePinnedChangedFunction(t *testing.T) { } cluster := integration.Start(t, integration.Setup{ - CRDDirs: []string{"testdata/comp/crds"}, + CRDDirs: []string{"../../../internal/testutils/integration/testdata/comp/crds"}, SetupFiles: []string{ - "testdata/comp/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/xrd.yaml", tc.composition, - "testdata/comp/resources/functions.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-manual.yaml", - "testdata/comp/resources/existing-downstream-manual.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-manual.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-manual.yaml", ), }, }) @@ -395,14 +396,14 @@ func TestSimulatePinnedChangedFunction(t *testing.T) { } rev, _, _ := un.NestedString(xr.Object, "spec", "crossplane", "compositionRevisionRef", "name") - base := withRenderBinary([]string{ - "project", "simulate", "--timeout=" + defaultTimeout.String(), + base := simharness.WithRenderBinary([]string{ + "project", "simulate", "--timeout=" + simharness.DefaultTimeout.String(), "-f", copyProjectFixture(t, "embedded"), "--cache-dir=" + t.TempDir(), "--skip-plan", "-n", "default", - }, localCrossplaneBinary(t)) + }, simharness.LocalCrossplaneBinary(t)) var stdout, stderr bytes.Buffer - code, err := runSimulateArgvWith(t, cfg, append(slices.Clip(base), "-o", "json"), &simulate.Hooks{}, &stdout, &stderr) + code, err := simharness.RunSimulateArgvWith(t, cfg, append(slices.Clip(base), "-o", "json"), &simulate.Hooks{}, &stdout, &stderr) if err != nil { t.Fatalf("\n%s\nsimulate: unexpected error: %v\nstderr:\n%s", tc.reason, err, stderr.String()) } @@ -443,7 +444,7 @@ func TestSimulatePinnedChangedFunction(t *testing.T) { } var text bytes.Buffer - if _, err := runSimulateArgvWith(t, cfg, base, &simulate.Hooks{}, &text, &stderr); err != nil { + if _, err := simharness.RunSimulateArgvWith(t, cfg, base, &simulate.Hooks{}, &text, &stderr); err != nil { t.Fatalf("simulate -o plan: unexpected error: %v", err) } if os.Getenv("SIMULATE_DEMO") != "" { @@ -464,15 +465,15 @@ func TestSimulateNameRejected(t *testing.T) { t.Parallel() compCase := integration.Setup{ - CRDDirs: []string{"testdata/comp/crds"}, + CRDDirs: []string{"../../../internal/testutils/integration/testdata/comp/crds"}, 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"), - integration.Composes("testdata/comp/resources/existing-xr-2.yaml", "testdata/comp/resources/existing-downstream-2.yaml"), + integration.Composes("../../../internal/testutils/integration/testdata/comp/resources/existing-xr-1.yaml", "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-1.yaml"), + integration.Composes("../../../internal/testutils/integration/testdata/comp/resources/existing-xr-2.yaml", "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-2.yaml"), }, } cases := map[string]struct { @@ -507,7 +508,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), nil, &stdout, &stderr) + code, err := simharness.RunSimulateArgv(t, cfg, argv, simharness.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) } @@ -527,7 +528,7 @@ func plainProjectArgv(t *testing.T) []string { t.Helper() return []string{ - "project", "simulate", "--timeout=" + defaultTimeout.String(), + "project", "simulate", "--timeout=" + simharness.DefaultTimeout.String(), "-f", copyProjectFixture(t, "plain"), "--cache-dir=" + t.TempDir(), } diff --git a/cmd/crossplane/project/integration_project_test.go b/cmd/crossplane/project/integration_project_test.go index eaff9f2c..8148685b 100644 --- a/cmd/crossplane/project/integration_project_test.go +++ b/cmd/crossplane/project/integration_project_test.go @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package project +package project_test import ( "bytes" @@ -32,6 +32,7 @@ import ( "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" ) // projectTestCase is a `project simulate -f ` run against a @@ -89,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", @@ -126,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", @@ -157,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", @@ -192,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(), } @@ -209,7 +210,7 @@ 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()) } diff --git a/cmd/crossplane/project/integration_provider_plan_test.go b/cmd/crossplane/project/integration_provider_plan_test.go index c4a66d40..2d4918cf 100644 --- a/cmd/crossplane/project/integration_provider_plan_test.go +++ b/cmd/crossplane/project/integration_provider_plan_test.go @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package project +package project_test import ( "bytes" @@ -39,6 +39,7 @@ import ( dp "github.com/crossplane/cli/v2/internal/diff/diffprocessor" "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 @@ -70,14 +71,14 @@ func TestSimulateProviderPlan(t *testing.T) { t.Parallel() xrCase := integration.Setup{ - CRDDirs: []string{"testdata/diff/crds"}, + CRDDirs: []string{"../../../internal/testutils/integration/testdata/diff/crds"}, SetupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition.yaml", - "testdata/diff/resources/functions.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", }, CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes("testdata/diff/resources/existing-xr.yaml", "testdata/diff/resources/existing-downstream-resource.yaml"), + integration.Composes("../../../internal/testutils/integration/testdata/diff/resources/existing-xr.yaml", "../../../internal/testutils/integration/testdata/diff/resources/existing-downstream-resource.yaml"), }, } @@ -217,7 +218,7 @@ func TestSimulateProviderPlan(t *testing.T) { }, "Drift": { reason: "A provider-managed resource the cluster sees unchanged still shows the provider's change.", - xrFile: "testdata/diff/unmodified-xr.yaml", + xrFile: "../../../internal/testutils/integration/testdata/diff/unmodified-xr.yaml", args: []string{"-o", "plan"}, ownCRD: true, fn: update(&diffv1alpha1.FieldChange{Field: "spec.forProvider.region", Actual: strValue("us-east-1"), Planned: strValue("eu-west-1")}), @@ -245,12 +246,12 @@ func TestSimulateProviderPlan(t *testing.T) { xr := tc.xrFile if xr == "" { - xr = "testdata/diff/modified-xr.yaml" + xr = "../../../internal/testutils/integration/testdata/diff/modified-xr.yaml" } - argv := append([]string{"xr", "simulate", "--timeout=" + defaultTimeout.String(), "-f", xr}, tc.args...) + argv := append([]string{"xr", "simulate", "--timeout=" + simharness.DefaultTimeout.String(), "-f", xr}, tc.args...) var stdout, stderr bytes.Buffer - code, err := runSimulateArgv(t, cfg, argv, localCrossplaneBinary(t), fakeProviderRouter{fn: tc.fn}, &stdout, &stderr) + code, err := simharness.RunSimulateArgv(t, cfg, argv, simharness.LocalCrossplaneBinary(t), fakeProviderRouter{fn: tc.fn}, &stdout, &stderr) if code != tc.wantCode { t.Errorf("\n%s\nwant exit code %d, got %d (err %v)\nstdout:\n%s\nstderr:\n%s", tc.reason, tc.wantCode, code, err, stdout.String(), stderr.String()) } diff --git a/cmd/crossplane/project/integration_test.go b/cmd/crossplane/project/integration_test.go deleted file mode 100644 index a5ed562a..00000000 --- a/cmd/crossplane/project/integration_test.go +++ /dev/null @@ -1,5965 +0,0 @@ -//go:build integration - -package project - -import ( - "bytes" - "fmt" - "strconv" - "strings" - "testing" - "time" - - rbacv1 "k8s.io/api/rbac/v1" - "k8s.io/client-go/rest" - - dp "github.com/crossplane/cli/v2/internal/diff/diffprocessor" - tu "github.com/crossplane/cli/v2/internal/diff/testutils" - "github.com/crossplane/cli/v2/internal/simulate/simtest" - "github.com/crossplane/cli/v2/internal/testutils/integration" -) - -// longTimeout is the --timeout for cases with multiple iterations, such as -// eventual state. -const longTimeout = 180 * time.Second - -// DiffTestType represents the type of diff test to run. -type DiffTestType string - -const ( - XRDiffTest DiffTestType = "diff" - CompositionDiffTest DiffTestType = "comp" -) - -// IntegrationTestCase represents a common test case structure for both XR and composition diff tests. -type IntegrationTestCase struct { - reason string // Description of what this test validates - setupFiles []string - deleteAfterSetup []string // Files whose resources are deleted after setup; with a finalizer this leaves them Terminating - // crossplaneManagedResources are the XRs that exist in the cluster, with what they compose. Every - // composed resource is server-side applied under Crossplane's composed field manager and given a - // controller ownerReference to its XR's real UID, and each XR lists its composed resources in its - // resourceRefs, as Crossplane leaves them. Use integration.Composes() for an XR without nested XRs. Put a - // composed resource in setupFiles instead only when it deliberately has no controlling XR. - crossplaneManagedResources []integration.HierarchicalOwnershipRelation - // fieldManagerApplies are server-side applied, in order, after setupFiles and - // crossplaneManagedResources, each under its own - // field manager. Use it when a test depends on which manager owns a field: a plain Create - // records only the test client as owner, and the apiserver drops managedFields supplied on Create. - fieldManagerApplies []integration.FieldManagerApply - // unestablishedXRDs are XRD manifests applied after setupFiles but, unlike an XRD in setupFiles, given - // no generated CRDs: an XRD Crossplane has not established yet, so its XR and claim types are not served. - unestablishedXRDs []string - // rbacRules, when set, makes the case run the command twice: as the cluster admin, then as a user - // bound to a ClusterRole granting only these rules (plus the discovery and self-review access the - // apiserver's bootstrap RBAC grants every authenticated user). The two runs must print identical - // output, and the usual assertions apply to the second. - rbacRules []rbacv1.PolicyRule - inputFiles []string // Input files to diff (XR YAML files or Composition YAML files) - expectedOutput string - expectedError bool - expectedErrorContains string - expectedStderrContains []string // substrings that must appear on stderr (warnings, error lines) - expectedExitCode int // Expected exit code (0=success, 1=tool error, 2=schema validation, 3=diff detected) - noColor bool - namespace string // For composition tests (optional) - xrdAPIVersion integration.XRDAPIVersion // For XR tests (optional) - ignorePaths []string // Paths to ignore in diffs - functionCredentials string // Path to function credentials file (optional) - eventualState bool // Enable eventual state simulation for XR or composition tests (optional) - timeout time.Duration // Custom timeout for this test (0 = use default) - resources []string // For composition tests: --resource values; each entry passed as one --resource flag - resourcesCSV string // For composition tests: alternative single --resource=a,b style invocation - includeManual bool // For composition tests: pass --include-manual flag - analyzeUnchanged bool // For composition tests: pass the deprecated --analyze-unchanged flag - analyzeOn string // For composition tests: pass --analyze-on= (empty = rely on the default) - // dryRunOn passes --dry-run-on= (empty = rely on the default, which is "all"). - // The flag lives on CommonCmdFields, so it applies to both `xr` and `comp`. - dryRunOn string - // awaitAdmissionDenialFor names a manifest whose resources a correctly-installed - // ValidatingAdmissionPolicy must refuse. Before running the diff, the harness dry-run - // creates each of them until the apiserver rejects it. - // - // This is not belt-and-braces: the apiserver loads admission policies through an informer, - // so a diff issued immediately after the policy is created races the plugin and observes a - // cluster that accepts everything. Measured on envtest 1.32, the policy took ~600ms and four - // attempts to take effect — long enough that without this gate the test would pass or fail - // depending on machine load, and a "pass" would prove nothing. - awaitAdmissionDenialFor string - // assertDeterministicAcrossRuns runs the command twice against the same cluster and requires - // byte-identical stdout, then applies the usual assertions to the second run. Needed for - // server-generated identity: the apiserver runs names.Generator before the dry-run - // short-circuit, so a generateName addition whose identity was not restored would emit a - // different random name on each run — which a single-run test cannot see. - assertDeterministicAcrossRuns bool - skip bool - skipReason string - // JSON output support: set outputFormat to "json" to use structured assertions. - outputFormat string // "json" or "" (default=visual diff) - // expectedStructured is asserted against the JSON output in either mode. It is typed as the - // Expectation interface, which every builder level implements, so a chain can end where it ends. - expectedStructured simtest.Expectation -} - -// runIntegrationTest runs a single integration test case for both XR and composition diff commands. -func runIntegrationTest(t *testing.T, testType DiffTestType, tt IntegrationTestCase) { - t.Helper() - - t.Parallel() // Enable parallel test execution - - // Resolve a local crossplane binary if one is present. When non-empty, - // it's threaded into the kong arg slice below as - // --crossplane-render-binary= so each parallel subtest has its own - // copy with no shared process state. When empty, the diff command falls - // through to the docker render engine — slower but works wherever Docker - // does (including the Earthfile's go-test target's WITH DOCKER block). - crossplaneBin := localCrossplaneBinary(t) - - // Skip test if requested - if tt.skip { - t.Skip(tt.skipReason) - return - } - - // Use custom timeout if specified, otherwise use default - testTimeout := defaultTimeout - if tt.timeout > 0 { - testTimeout = tt.timeout - } - - // Set up the test files - testFiles := make([]string, 0, len(tt.inputFiles)) - - // Handle any additional input files - // Note: NewCompositeLoader handles both individual files and directories, - // so we can pass paths directly without special handling - testFiles = append(testFiles, tt.inputFiles...) - - // Capture stdout and stderr into separate buffers. The diff command - // writes structured output (JSON / YAML / text diff) to stdout and - // human-readable error lines to stderr; mixing them defeats JSON - // parsing for tests that drive the structured assertion path. - var stdout, stderr bytes.Buffer - - // Create command line args that match your pre-populated struct - args := []string{ - fmt.Sprintf("--timeout=%s", testTimeout.String()), - } - - // Only thread --crossplane-render-binary when a local binary exists; an - // empty value would still parse but waste cycles, and a missing flag - // lets the diff command pick the docker render engine cleanly. - if crossplaneBin != "" { - args = append(args, fmt.Sprintf("--crossplane-render-binary=%s", crossplaneBin)) - } - - // Add namespace if specified (for composition tests only) - if tt.namespace != "" && testType == CompositionDiffTest { - args = append(args, fmt.Sprintf("--namespace=%s", tt.namespace)) - } - - // Add no-color flag if true - if tt.noColor { - args = append(args, "--no-color") - } - - // Add JSON output format if specified - if tt.outputFormat == "json" { - args = append(args, "--output=json") - } - - // Add ignore-paths if specified - if len(tt.ignorePaths) > 0 { - for _, path := range tt.ignorePaths { - args = append(args, fmt.Sprintf("--ignore-paths=%s", path)) - } - } - - // Add function-credentials if specified - if tt.functionCredentials != "" { - args = append(args, fmt.Sprintf("--function-credentials=%s", tt.functionCredentials)) - } - - // Add eventual-state flag if specified (supported by both `xr` and `comp`). - if tt.eventualState { - args = append(args, "--eventual-state") - } - - // --dry-run-on lives on CommonCmdFields, so unlike --analyze-on it is threaded for both - // command types. An empty value relies on the flag's own default. - if tt.dryRunOn != "" { - args = append(args, "--dry-run-on="+tt.dryRunOn) - } - - // Add --resource flags (composition tests only) - if testType == CompositionDiffTest { - for _, r := range tt.resources { - args = append(args, fmt.Sprintf("--resource=%s", r)) - } - - if tt.resourcesCSV != "" { - args = append(args, fmt.Sprintf("--resource=%s", tt.resourcesCSV)) - } - - if tt.includeManual { - args = append(args, "--include-manual") - } - - if tt.analyzeUnchanged { - args = append(args, "--analyze-unchanged") - } - - if tt.analyzeOn != "" { - args = append(args, "--analyze-on="+tt.analyzeOn) - } - } - - // Add files as positional arguments - args = append(args, testFiles...) - - // The render engine runs functions as Docker containers. Without a Docker daemon no rendering - // case can run; cases that would not render anyway cannot be told apart upfront, so they skip. - if !dockerAvailable() { - t.Skip("env: no Docker daemon for the function render engine") - return - } - - // Start fails, rather than skips, a run without envtest's binaries or Crossplane's CRDs. - cluster := integration.Start(t, integration.Setup{ - CRDDirs: []string{"testdata/" + string(testType) + "/crds"}, - SetupFiles: tt.setupFiles, - UnestablishedXRDs: tt.unestablishedXRDs, - CrossplaneManagedResources: tt.crossplaneManagedResources, - XRDAPIVersion: tt.xrdAPIVersion, - FieldManagerApplies: tt.fieldManagerApplies, - DeleteAfterSetup: tt.deleteAfterSetup, - RBACRules: tt.rbacRules, - }) - - // Block until any ValidatingAdmissionPolicy in the setup is actually enforcing, so the diff - // cannot race the apiserver's policy informer. See the field's doc comment. - if err := integration.AwaitAdmissionDenial(cluster.Context, cluster.Client, tt.awaitAdmissionDenialFor); err != nil { - t.Fatalf("admission policy never took effect: %v", err) - } - - // runOnce executes the command against the running cluster, leaving its output in stdout / - // stderr. Everything the command layer touches is rebuilt per call, so a second run starts - // with cold client caches just as a second invocation of the CLI would. - runOnce := func(runCfg *rest.Config) (int, error) { - stdout.Reset() - stderr.Reset() - - return runSimulate(t, runCfg, testType, args, &stdout, &stderr) - } - - exitCode, err := runOnce(cluster.Config) - - if tt.assertDeterministicAcrossRuns { - first := stdout.String() - - exitCode, err = runOnce(cluster.Config) - - if second := stdout.String(); second != first { - t.Errorf("output differs between two runs against the same cluster; a value the apiserver generated has leaked into the diff.\nfirst run:\n%s\nsecond run:\n%s", first, second) - } - } - - if cluster.UserConfig != nil { - admin, adminErr := stdout.String(), err - - exitCode, err = runOnce(cluster.UserConfig) - - if restricted := stdout.String(); restricted != admin { - t.Errorf("output with only the case's RBAC rules differs from the admin's; the run needs access the rules don't grant.\nadmin (error: %v):\n%s\nrestricted (error: %v, stderr below):\n%s\n%s", - adminErr, admin, err, restricted, stderr.String()) - } - } - - // Check exit code matches expected - if exitCode != tt.expectedExitCode { - t.Errorf("expected exit code %d, got %d", tt.expectedExitCode, exitCode) - } - - // Assert stderr expectations before any of the early returns below, so they apply in every - // output mode (human-readable, JSON, and expected-error cases alike). - for _, want := range tt.expectedStderrContains { - if !strings.Contains(stderr.String(), want) { - t.Errorf("expected stderr to contain %q, got:\n%s", want, stderr.String()) - } - } - - if tt.expectedError && err == nil { - t.Fatal("expected error but got none") - } - - if !tt.expectedError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - // Check for specific error message if expected. - // - // Three modes: - // 1. expectedError + expectedErrorContains: substring-check the error. - // 2. expectedError + structured assertion: any error is fine here; - // the structured assertion below validates errors[]. - // 3. unexpected error: already caught by the t.Fatalf above. - if err != nil && tt.expectedError { - hasStructuredAssertion := tt.expectedStructured != nil - - switch { - case tt.expectedErrorContains != "": - if strings.Contains(err.Error(), tt.expectedErrorContains) { - t.Logf("Got expected error containing: %s", tt.expectedErrorContains) - } else { - t.Errorf("Expected error containing %q, got: %v", tt.expectedErrorContains, err) - } - case hasStructuredAssertion: - t.Logf("Got expected error; structured assertion will validate errors[]") - default: - t.Errorf("expectedError set without expectedErrorContains or structured assertion; got: %v", err) - } - } - - // Skip stdout check for expected-error cases that use the legacy - // substring assertion. Cases that pin a structured assertion fall - // through so the JSON path is exercised against errors[] / changes. - if tt.expectedError && tt.expectedErrorContains != "" && - tt.expectedStructured == nil { - return - } - - // Check the output - outputStr := stdout.String() - - // Handle JSON output format with structured assertions - if tt.outputFormat == "json" && tt.expectedStructured != nil { - simtest.AssertStructured(t, outputStr, tt.expectedStructured) - return - } - - // Using TrimSpace because the output might have trailing newlines - if !strings.Contains(strings.TrimSpace(outputStr), strings.TrimSpace(tt.expectedOutput)) { - // Strings aren't equal, *including* ansi. but we can compare ignoring ansi to determine what output to - // show for the failure. if the difference is only in color codes, we'll show escaped ansi codes. - out := outputStr - - expect := tt.expectedOutput - if tu.CompareIgnoringAnsi(strings.TrimSpace(outputStr), strings.TrimSpace(tt.expectedOutput)) { - out = strconv.QuoteToASCII(outputStr) - expect = strconv.QuoteToASCII(tt.expectedOutput) - } - - t.Fatalf("expected output to contain:\n%s\n\nbut got:\n%s", expect, out) - } -} - -// leastPrivilegeRules is the access a diff of an XNopResource composing XDownstreamResources needs, the -// types the testdata XRDs, compositions and CRDs use. It grants Crossplane's own types only get, except a -// list of CompositionRevisions, which are found by their composition's label. It grants list only where a -// lookup is scoped by a label. xrVerbs are the verbs on -// the XR type: get to read the existing XR, patch and create to dry-run it, and list only where the -// command finds composites by listing them. -func leastPrivilegeRules(xrVerbs ...string) []rbacv1.PolicyRule { - return []rbacv1.PolicyRule{ - // Fetched by name as the input needs them: the XR's XRD and CRD, its Composition, the - // pipeline's Functions, and any EnvironmentConfig a function requires by name. - {APIGroups: []string{"apiextensions.crossplane.io"}, Resources: []string{"compositeresourcedefinitions", "compositions", "environmentconfigs"}, Verbs: []string{"get"}}, - {APIGroups: []string{"apiextensions.k8s.io"}, Resources: []string{"customresourcedefinitions"}, Verbs: []string{"get"}}, - {APIGroups: []string{"pkg.crossplane.io"}, Resources: []string{"functions"}, Verbs: []string{"get"}}, - // A composition's revisions are listed by its crossplane.io/composition-name label. - {APIGroups: []string{"apiextensions.crossplane.io"}, Resources: []string{"compositionrevisions"}, Verbs: []string{"get", "list"}}, - // The XR type. - {APIGroups: []string{"ns.diff.example.org"}, Resources: []string{"xnopresources"}, Verbs: xrVerbs}, - // Composed resources: read from the XR's resource tree, listed by their crossplane.io/composite - // label to find a new XR's existing ones, and dry-run created or applied. - {APIGroups: []string{"ns.nop.example.org"}, Resources: []string{"xdownstreamresources"}, Verbs: []string{"get", "list", "create", "patch"}}, - } -} - -// TestDiffIntegration runs an integration test for the diff command. -func TestDiffIntegration(t *testing.T) { - t.Parallel() - - tests := map[string]IntegrationTestCase{ - "NewResourceDiff": { - reason: "Shows color diff for new resources", - inputFiles: []string{"testdata/diff/new-xr.yaml"}, - setupFiles: []string{ - // TODO: For v2, we need to upgrade the XRD apiversion to v2 + put the xr in a namespace. - // this might be better served as a separate test(s). - // since we aren't actually creating resources (there's no crossplane actually running in unit tests), - // we don't need to worry about upgrading providers or anything. - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - expectedOutput: strings.Join([]string{ - `+++ XDownstreamResource/test-resource -`, tu.Green(`+ apiVersion: ns.nop.example.org/v1alpha1 -+ kind: XDownstreamResource -+ metadata: -+ annotations: -+ crossplane.io/composition-resource-name: nop-resource -+ labels: -+ crossplane.io/composite: test-resource -+ name: test-resource -+ namespace: default -+ spec: -+ forProvider: -+ configData: new-value -`), ` ---- -+++ XNopResource/test-resource -`, tu.Green(`+ apiVersion: ns.diff.example.org/v1alpha1 -+ kind: XNopResource -+ metadata: -+ name: test-resource -+ namespace: default -+ spec: -+ coolField: new-value -`), ` ---- -`, - }, ""), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "AutomaticNamespacePropagation": { - reason: "Validates automatic namespace propagation for namespaced managed resources", - outputFormat: "json", - inputFiles: []string{"testdata/diff/new-xr.yaml"}, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition-no-namespace-patch.yaml", - "testdata/diff/resources/functions.yaml", - }, - expectedStructured: simtest.Expect(). - WithSummary(2, 0, 0). - WithAddedResource("XDownstreamResource", "test-resource", "default"). - WithField("spec.forProvider.configData", "new-value"). - And(). - WithAddedResource("XNopResource", "test-resource", "default"). - WithField("spec.coolField", "new-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // A built-in Kubernetes type has no CRD, so this exercises the paths - // that would otherwise read scope and schema from one. Before scope - // came from discovery this failed outright, with "cannot determine - // scope for resource ConfigMap/... CRD not found". - "RendersBuiltInResourceWithoutCRD": { - reason: "A composition rendering a built-in Kubernetes resource (no CRD) produces a diff for it", - outputFormat: "json", - inputFiles: []string{"testdata/diff/new-xr.yaml"}, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition-with-configmap.yaml", - "testdata/diff/resources/functions.yaml", - }, - expectedStructured: simtest.Expect(). - WithSummary(3, 0, 0). - WithAddedResource("ConfigMap", "test-resource-config", "default"). - WithField("data.configData", "new-value"). - And(). - WithAddedResource("XDownstreamResource", "test-resource", "default"). - WithField("spec.forProvider.configData", "new-value"). - And(). - WithAddedResource("XNopResource", "test-resource", "default"). - WithField("spec.coolField", "new-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "UnservedGroupIsReportedAsAnUnknownTypeNotAMissingNamespace": { - // From the PR #502 review. A resource whose group/version the cluster does not serve must be - // reported as an unknown type; blaming its namespace would send the user after the wrong root - // cause entirely, since the namespace here exists and is irrelevant. - // - // Worth being precise about what this covers, because it is NOT the sentinel branch in - // dryRunCreateAddition. For a *.k8s.io group, two earlier guards let the resource through — - // IsCRDRequired skips discovery for that suffix, and FetchCurrentObject swallows NotFound as - // "this is new" — but removeNamespacesFromClusterScopedResources then fails first, because - // scope determination cannot resolve the GVK either. So this pins the outer behaviour (an - // accurate, type-naming error rather than a namespace one) without depending on which layer - // produces it. - // - // The sentinel branch is what covers the case this CANNOT reach: scope determination falls - // back to a CRD lookup when discovery fails, whereas GVKToGVR is discovery-only, so a CRD - // whose version is not `served` passes the scope check and fails inside DryRunCreate. That - // route is unit-covered in diff_calculator_test.go, since arranging a served:false CRD whose - // scope still resolves is far more setup than the classification warrants. - reason: "An addition whose group/version the cluster does not serve is reported as an unknown type, not as a missing namespace", - outputFormat: "json", - inputFiles: []string{"testdata/diff/new-xr.yaml"}, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition-with-unserved-k8s-group.yaml", - "testdata/diff/resources/functions.yaml", - }, - expectedError: true, - expectedErrorContains: "totallynotserved.k8s.io/v1", - expectedExitCode: dp.ExitCodeToolError, - }, - "AdditionInMissingNamespaceDegradesRatherThanFailing": { - // Regression test for the e2e failure the first cut of this feature caused - // (TestDiffConcurrentDirectory diffs 21 XRs into a namespace that is never created). - // - // NamespaceLifecycle admission refuses a dry-run create into a namespace that does not exist. - // That must degrade, not fail the run: a Namespace and its contents are routinely applied - // together, so the namespace's absence at diff time says nothing about whether the apply will - // succeed — which is what separates it from a quota or webhook refusal, where the verdict - // describes the object and will still hold. - // - // The exit code is the real assertion. Before the fix this was exit 1 with no diff at all. - reason: "An addition whose namespace does not exist yet still produces a diff, marked as unverified", - outputFormat: "json", - inputFiles: []string{"testdata/diff/new-xr-missing-namespace.yaml"}, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - expectedStructured: simtest.Expect(). - WithSummary(2, 0, 0). - WithAddedResource("XNopResource", "test-resource", "nonexistent-namespace"). - WithField("spec.coolField", "new-value"). - WithDryRunSkipped("namespaceNotFound", "nonexistent-namespace"). - And(). - WithAddedResource("XDownstreamResource", "test-resource", "nonexistent-namespace"). - WithDryRunSkipped("namespaceNotFound", "nonexistent-namespace"). - And(). - // One warning per GVK, and each cause exactly the apiserver's message. Through the REAL - // ApplyClient, whose error is wrapped with "failed to dry-run create resource /": - // if that wrapper leaked into the cause, every resource would be a distinct warning. The unit - // test mirrors the wrapping by hand; this pins it against the client that actually produces it. - WithWarning("their namespace does not exist yet"). - WithWarningContext(map[string]string{ - "gvk": "ns.diff.example.org/v1alpha1, Kind=XNopResource", - "namespace": "nonexistent-namespace", - "cause": `namespaces "nonexistent-namespace" not found`, - "count": "1", - }). - And(). - WithWarning("their namespace does not exist yet"). - WithWarningContext(map[string]string{ - "gvk": "ns.nop.example.org/v1alpha1, Kind=XDownstreamResource", - "namespace": "nonexistent-namespace", - "cause": `namespaces "nonexistent-namespace" not found`, - "count": "1", - }), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // Human-readable output shows no dryRun at all, so stderr is the only place a person learns that - // additions went unverified, and how many. Two inputs into one missing namespace share each GVK's - // warning rather than raising one per resource. - "UnverifiedAdditionsAcrossInputsAreCountedInOneWarning": { - reason: "In text mode, additions that could not be verified are summarised on stderr once per GVK + namespace + cause, with a count of the resources behind it", - inputFiles: []string{ - "testdata/diff/new-xr-missing-namespace.yaml", - "testdata/diff/new-xr-missing-namespace-second.yaml", - }, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - noColor: true, - expectedStderrContains: []string{ - `WARNING: skipped apiserver verification of added resources: their namespace does not exist yet, so their diffs omit server-side defaulting and admission ` + - `(cause=namespaces "nonexistent-namespace" not found, count=2, gvk=ns.diff.example.org/v1alpha1, Kind=XNopResource, namespace=nonexistent-namespace)`, - `WARNING: skipped apiserver verification of added resources: their namespace does not exist yet, so their diffs omit server-side defaulting and admission ` + - `(cause=namespaces "nonexistent-namespace" not found, count=2, gvk=ns.nop.example.org/v1alpha1, Kind=XDownstreamResource, namespace=nonexistent-namespace)`, - }, - expectedOutput: "+++ XDownstreamResource/second-resource", - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "BuiltInResourceAdditionPicksUpApiserverDefaults": { - // The choice of a built-in type keeps this test non-vacuous whatever crossplane-diff defaults - // locally. The lenient Defaulter (defaulter.go) predicts CRD-derived `default:` values for an - // addition that gets no apiserver result, and passes anything without a CRD through unchanged. - // Deployment has no CRD, so IsCRDRequired is false, and the fields asserted below can ONLY have - // come from round-tripping the addition through the apiserver. - // - // Every asserted default is string-valued on purpose: assertChangeFields compares with - // reflect.DeepEqual against JSON-decoded values, so a numeric default (revisionHistoryLimit: 10) - // would arrive as float64 and an int literal here would fail for the wrong reason. - reason: "An added built-in resource is dry-run created against the apiserver, so apiserver-side defaults appear in its addition diff", - outputFormat: "json", - inputFiles: []string{"testdata/diff/new-xr.yaml"}, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition-with-builtin-defaults.yaml", - "testdata/diff/resources/functions.yaml", - }, - expectedStructured: simtest.Expect(). - WithSummary(2, 0, 0). - WithAddedResource("Deployment", "test-resource-deploy", "default"). - // Absence of dryRun is the machine-readable claim that this desired state was - // verified against the apiserver. Asserted alongside the defaults so that a - // regression which stopped dry-running additions fails on the contract as well as - // on whichever fields happen to depend on server-side defaulting. - WithDryRunPerformed(). - WithField("spec.strategy.type", "RollingUpdate"). - WithField("spec.template.spec.restartPolicy", "Always"). - WithField("spec.template.spec.dnsPolicy", "ClusterFirst"). - And(). - WithAddedResource("XNopResource", "test-resource", "default"). - WithDryRunPerformed(). - WithField("spec.coolField", "new-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // The inverse of the case above, and the pair is what makes either meaningful: on its own, - // "the defaults appear" could be satisfied by something other than the apiserver round-trip, - // and "the defaults are absent" could be satisfied by the feature being broken. Together they - // pin the flag as the thing that decides. - // - // This also pins that `existing` reproduces the behaviour from before additions were dry-run - // (#334), which is why the fields are asserted absent rather than merely unasserted. - "BuiltInResourceAdditionSkipsApiserverWhenDryRunOnExisting": { - reason: "With --dry-run-on=existing an added built-in resource is not sent to the apiserver, so its diff carries no server-side defaults and reports skipReason disabled", - outputFormat: "json", - dryRunOn: "existing", - inputFiles: []string{"testdata/diff/new-xr.yaml"}, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition-with-builtin-defaults.yaml", - "testdata/diff/resources/functions.yaml", - }, - expectedStructured: simtest.Expect(). - WithSummary(2, 0, 0). - WithAddedResource("Deployment", "test-resource-deploy", "default"). - // No detail substring: "disabled" is the one skip reason the user asked for, so - // there is no cluster explanation to propagate. - WithDryRunSkipped("disabled", ""). - WithFieldAbsent("spec.strategy.type"). - WithFieldAbsent("spec.template.spec.restartPolicy"). - WithFieldAbsent("spec.template.spec.dnsPolicy"). - // The rendered fields must still be there — `existing` degrades fidelity, it does - // not degrade the diff. - WithField("spec.template.spec.containers[0].image", "nginx:1.27"). - And(). - WithAddedResource("XNopResource", "test-resource", "default"). - WithDryRunSkipped("disabled", ""). - WithField("spec.coolField", "new-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // A status authored by the composition pipeline must survive the apiserver round-trip. - // XStatusResource declares the status subresource, so the apiserver strips status from the - // dry-run create and returns an object without one; the value below can therefore only come - // from the calculator re-attaching what was rendered. Every other composed kind in this - // testdata tree omits the subresource, which would have made this assertion vacuous. - // - // SKIPPED, and the reason is a finding rather than an environment problem: the re-attach - // works, but nothing renders it. See skipReason. - "AddedResourceRetainsCompositionAuthoredStatus": { - skip: true, - skipReason: "The re-attached status cannot be observed in any output, so this assertion " + - "cannot pass and could not be made to fail by breaking the production code it targets. " + - "cleanupForDiff (compare/compare.go) deletes metadata.status unconditionally from " + - "both sides of every diff, in the same block that deletes ownerReferences — the strip whose " + - "identical consequence for ownerReferences this feature's own spec documented (§1a) and then " + - "missed for status. ResourceViews.Clean is the only view any renderer reads" + - "; ResourceViews.Desired.Raw, which does carry the " + - "re-attached status, has no reader at all. Verified empirically: the apiserver's dry-run " + - "create response for XStatusResource carries no status, the object handed to cleanupForDiff " + - "carries status.phase=authored-by-composition, and the object cleanupForDiff returns does " + - "not. Deciding whether status belongs in diff output is a rendering-design question well " + - "outside a test task, so this is left failing-by-skip with its instrument intact rather " + - "than quietly reworded into an assertion that passes.", - reason: "A status written by the composition pipeline survives the dry-run create of an added resource", - outputFormat: "json", - inputFiles: []string{"testdata/diff/new-xr.yaml"}, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition-with-status.yaml", - "testdata/diff/resources/functions.yaml", - }, - expectedStructured: simtest.Expect(). - WithSummary(2, 0, 0). - WithAddedResource("XStatusResource", "test-resource", "default"). - WithDryRunPerformed(). - WithField("status.phase", "authored-by-composition"). - WithField("spec.forProvider.configData", "new-value"). - And(). - WithAddedResource("XNopResource", "test-resource", "default"). - WithField("spec.coolField", "new-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // The apiserver runs names.Generator in rest.BeforeCreate, ahead of the dry-run - // short-circuit at the storage layer, so it mints a real random name even for a request it - // never persists. Unless that identity is restored, a generateName addition's diff differs on - // every invocation — which only a repeated run can detect, hence assertDeterministicAcrossRuns. - "GenerateNameAdditionIsDeterministicAcrossRuns": { - reason: "An addition using generateName renders identically across two runs, i.e. no server-generated name leaks into the diff", - outputFormat: "json", - dryRunOn: "all", - assertDeterministicAcrossRuns: true, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/generated-name-composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - inputFiles: []string{"testdata/diff/generated-name-xr.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(2, 0, 0). - // Both the XR and its composed resource use generateName, so both exercise the - // identity restore. The pattern is the renderer's display form for a - // not-yet-named object; a leaked server name would be a random suffix instead. - WithAddedResource("XDownstreamResource", "", "default"). - WithDryRunPerformed(). - WithNamePattern(`^test-resource-\(generated\)$`). - WithField("spec.forProvider.configData", "new-value"). - And(). - WithAddedResource("XNopResource", "", "default"). - WithDryRunPerformed(). - WithNamePattern(`^generated-xr-\(generated\)$`). - WithField("spec.coolField", "new-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // Cluster rejection of an ADDITION. A ValidatingAdmissionPolicy is the instrument rather than a CEL - // x-kubernetes-validations rule on the CRD, because crossplane-diff evaluates CEL rules - // locally before the diff calculator runs: a CEL rule would reject the resource without the - // apiserver ever being consulted, so the test would pass on code that never dry-ran anything. - "AddedResourceRejectedByAdmissionPolicyIsAValidationFailure": { - reason: "An apiserver rejection of an added resource is exit 2 with a typed admission validation failure, not a bare tool error", - outputFormat: "json", - inputFiles: []string{"testdata/diff/new-xr.yaml"}, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition.yaml", - "testdata/diff/resources/functions.yaml", - "testdata/diff/resources/rejecting-admission-policy.yaml", - }, - awaitAdmissionDenialFor: "testdata/diff/resources/admission-policy-canary.yaml", - expectedError: true, - expectedExitCode: dp.ExitCodeSchemaValidation, - expectedStructured: simtest.Expect(). - // errors[].resourceID names the input the user supplied, so the rejected composed - // resource has to be identifiable from the message and the validationFailures entry. - WithError("XNopResource/test-resource"). - WithMessageContaining("the cluster rejected XDownstreamResource/test-resource"). - WithValidationFailure("ns.nop.example.org/v1alpha1", "XDownstreamResource", "test-resource", "default"). - WithStatus("invalid"). - // The "admission" type is what distinguishes a cluster rejection from a local schema - // rejection; field is empty because an admission refusal is not localized to one field. - WithFieldError("admission", ""). - WithMessageContaining("this cluster refuses that configData value"), - }, - // Cluster rejection of a MODIFICATION — the deliberate behaviour change. The identical cluster fact used to - // surface as a plain tool error (exit 1) for an existing resource while an addition's rejection - // would have been exit 2, i.e. the same rejection reported in two different tiers depending - // only on whether the resource happened to exist. This pins the reclassification. - "ModifiedResourceRejectedByAdmissionPolicyIsAValidationFailure": { - reason: "An apiserver rejection of an EXISTING resource's dry-run apply is reported in the same tier as a rejected addition", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition.yaml", - "testdata/diff/resources/functions.yaml", - "testdata/diff/resources/rejecting-admission-policy.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/diff/resources/existing-xr.yaml", - "testdata/diff/resources/existing-downstream-resource.yaml", - ), - }, - awaitAdmissionDenialFor: "testdata/diff/resources/admission-policy-canary.yaml", - inputFiles: []string{"testdata/diff/modified-xr.yaml"}, - expectedError: true, - expectedExitCode: dp.ExitCodeSchemaValidation, - expectedStructured: simtest.Expect(). - WithError("XNopResource/test-resource"). - WithMessageContaining("the cluster rejected XDownstreamResource/test-resource"). - WithValidationFailure("ns.nop.example.org/v1alpha1", "XDownstreamResource", "test-resource", "default"). - WithStatus("invalid"). - WithFieldError("admission", ""). - WithMessageContaining("this cluster refuses that configData value"), - }, - "MultipleXRsGroupedByInputXR": { - reason: "Two input XRs in one invocation are grouped per input XR in the xrs[] structured view", - outputFormat: "json", - inputFiles: []string{ - "testdata/diff/new-xr.yaml", - "testdata/diff/new-xr-second.yaml", - }, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - expectedStructured: simtest.Expect(). - // Aggregate flat view still reports the merged total (2 adds per XR). - WithSummary(4, 0, 0). - // Grouped view: one XR(...) sub-expression per input XR, each with - // its own changed tree. - WithXRs( - simtest.XR("XNopResource", "test-resource", "default"). - Status("changed"). - Summary(2, 0, 0). - Change("added", "XNopResource", "test-resource", "default"). - WithField("spec.coolField", "new-value"). - Change("added", "XDownstreamResource", "test-resource", "default"). - WithField("spec.forProvider.configData", "new-value"), - simtest.XR("XNopResource", "second-resource", "default"). - Status("changed"). - Summary(2, 0, 0). - Change("added", "XNopResource", "second-resource", "default"). - WithField("spec.coolField", "second-value"). - Change("added", "XDownstreamResource", "second-resource", "default"). - WithField("spec.forProvider.configData", "second-value"), - ). - Build(), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "MultipleXRsGroupedByInputXRV2": { - reason: "The composites[] entries group each input's changes", - outputFormat: "json", - inputFiles: []string{ - "testdata/diff/new-xr.yaml", - "testdata/diff/new-xr-second.yaml", - }, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - expectedStructured: simtest.Expect(). - // The top-level summary counts distinct resources. - WithSummary(4, 0, 0). - WithXRs( - simtest.XR("XNopResource", "test-resource", "default"). - Status("changed"). - Summary(2, 0, 0). - Change("added", "XNopResource", "test-resource", "default"). - WithField("spec.coolField", "new-value"). - Change("added", "XDownstreamResource", "test-resource", "default"). - WithField("spec.forProvider.configData", "new-value"), - simtest.XR("XNopResource", "second-resource", "default"). - Status("changed"). - Summary(2, 0, 0). - Change("added", "XNopResource", "second-resource", "default"). - WithField("spec.coolField", "second-value"). - Change("added", "XDownstreamResource", "second-resource", "default"). - WithField("spec.forProvider.configData", "second-value"), - ), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "ModifiedResourceDiff": { - reason: "Shows color diff for modified resources", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - // put an existing XR in the cluster to diff against - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/diff/resources/existing-xr.yaml", - "testdata/diff/resources/existing-downstream-resource.yaml", - ), - }, - inputFiles: []string{"testdata/diff/modified-xr.yaml"}, - expectedOutput: ` -~~~ XDownstreamResource/test-resource - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - annotations: - crossplane.io/composition-resource-name: nop-resource - labels: - crossplane.io/composite: test-resource - name: test-resource - namespace: default - spec: - forProvider: -` + tu.Red("- configData: existing-value") + ` -` + tu.Green("+ configData: modified-value") + ` - ---- -~~~ XNopResource/test-resource - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - metadata: - labels: - crossplane.io/composite: test-resource - name: test-resource - namespace: default - spec: -` + tu.Red("- coolField: existing-value") + ` -` + tu.Green("+ coolField: modified-value") + ` - crossplane: - compositionRef: - name: xnopresources.diff.example.org - compositionRevisionRef: - name: xnopresources.diff.example.org-b616215 - compositionUpdatePolicy: Automatic - ---- - -Summary: 2 modified`, - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "IgnorePathsArgoCD": { - reason: "Ignores ArgoCD annotations and labels when --ignore-paths is specified", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - // put an existing resource with different ArgoCD annotations - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/diff/resources/existing-xr-with-argocd.yaml", - "testdata/diff/resources/existing-downstream-resource-with-argocd.yaml", - ), - }, - inputFiles: []string{"testdata/diff/xr-with-argocd-annotations.yaml"}, - ignorePaths: []string{ - "metadata.annotations[argocd.argoproj.io/tracking-id]", - "metadata.labels[argocd.argoproj.io/instance]", - }, - outputFormat: "json", - expectedStructured: simtest.Expect().WithSummary(0, 0, 0), - expectedError: false, - }, - "IgnorePathsMixedChanges": { - // Regression test: when a resource has BOTH ignored path changes - // AND non-ignored spec changes, the summary must count the - // resource as modified once and the emitted diff bodies must not - // leak the ignored paths. The renderer-level unit test - // TestStructuredDiffRenderer_RespectsIgnorePaths asserts the - // absence of ignored fields in diff.old / diff.new; this test - // pins the summary counts and the visible non-ignored changes - // through the full CLI stack. - reason: "Ignores ArgoCD annotations/labels while still reporting a non-ignored spec change", - 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-with-argocd.yaml", - "testdata/diff/resources/existing-downstream-resource-with-argocd.yaml", - ), - }, - inputFiles: []string{"testdata/diff/xr-with-argocd-mixed-changes.yaml"}, - ignorePaths: []string{ - "metadata.annotations[argocd.argoproj.io/tracking-id]", - "metadata.labels[argocd.argoproj.io/instance]", - }, - outputFormat: "json", - expectedStructured: simtest.Expect(). - WithSummary(0, 2, 0). - WithModifiedResource("XDownstreamResource", "test-resource", "default"). - WithFieldChange("spec.forProvider.configData", "new-value", "modified-value"). - And(). - WithModifiedResource("XNopResource", "test-resource", "default"). - WithFieldChange("spec.coolField", "new-value", "modified-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "ModifiedXRCreatesDownstream": { - reason: "Shows diff when modified XR creates new downstream resource", - outputFormat: "json", - // The XR has its composition refs but no composed resource yet: Crossplane selects the - // composition and revision before it composes, so this is an XR whose first compose has not - // created its resource (a function error, say). That is what makes the resource an addition. - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition.yaml", - "testdata/diff/resources/functions.yaml", - "testdata/diff/resources/existing-xr.yaml", - }, - inputFiles: []string{"testdata/diff/modified-xr.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(1, 1, 0). - WithAddedResource("XDownstreamResource", "test-resource", "default"). - WithField("spec.forProvider.configData", "modified-value"). - And(). - WithModifiedResource("XNopResource", "test-resource", "default"). - WithFieldChange("spec.coolField", "existing-value", "modified-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // Issue #452, `xr` side, delivered through the warning channel added for #459. Unlike `comp`, - // which excludes deleting XRs from impact analysis, `xr` diffs exactly the resource the user - // named — so it still emits the diff and raises an advisory. This asserts BOTH halves of the - // dual emission: the stderr line for humans and the warnings[] entry for machines. - "DeletingXRWarnsButStillDiffs": { - reason: "xr against an XR whose cluster copy is being deleted still emits the diff, plus a warning on stderr and in warnings[]", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - // A Terminating XR still has the resource it composed until garbage collection removes it. - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes("testdata/diff/resources/existing-xr-deleting.yaml", "testdata/diff/resources/existing-downstream-resource.yaml"), - }, - deleteAfterSetup: []string{"testdata/diff/resources/existing-xr-deleting.yaml"}, - inputFiles: []string{"testdata/diff/modified-xr.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(0, 2, 0). - WithModifiedResource("XDownstreamResource", "test-resource", "default"). - WithFieldChange("spec.forProvider.configData", "existing-value", "modified-value"). - And(). - WithModifiedResource("XNopResource", "test-resource", "default"). - WithFieldChange("spec.coolField", "existing-value", "modified-value"). - And(). - WithWarning("being deleted in the cluster"), - expectedStderrContains: []string{ - "WARNING: The resource being diffed is being deleted in the cluster", - "resource=XNopResource/test-resource", - }, - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // Issue #495: the XR still references a composed resource that was deleted out of band. The - // resource tree records that child as NotFound, which is tolerated: the resource really is - // gone, so it is neither observed nor a removal — but the user is told. The other per-node - // errors (Forbidden, transport) are fatal; envtest grants the harness everything, so those are - // covered by the FetchObservedResources unit tests instead. - "ComposedResourceDeletedOutOfBandWarns": { - reason: "xr against an XR whose resourceRefs name a deleted composed resource omits it from observed state and warns", - outputFormat: "json", - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - { - OwnerFile: "testdata/diff/resources/existing-removal-test-xr.yaml", - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - "testdata/diff/resources/removal-test-ns-downstream-resource1.yaml": nil, - "testdata/diff/resources/removal-test-ns-downstream-resource2.yaml": nil, - }, - }, - }, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/removal-test-composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - deleteAfterSetup: []string{"testdata/diff/resources/removal-test-ns-downstream-resource2.yaml"}, - inputFiles: []string{"testdata/diff/unmodified-xr.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(0, 0, 0). - WithWarning("was not found in the cluster"). - WithWarningContext(map[string]string{ - "resource": "XDownstreamResource/resource-to-be-removed", - "namespace": "default", - }), - expectedStderrContains: []string{ - "WARNING: A composed resource referenced by its XR was not found in the cluster", - "resource=XDownstreamResource/resource-to-be-removed", - }, - expectedError: false, - expectedExitCode: dp.ExitCodeSuccess, - }, - "EnvironmentConfigIncorporation": { - reason: "Validates EnvironmentConfig (v1beta1) incorporation in diff", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xdownstreamenvresource-xrd.yaml", - "testdata/diff/resources/env-xrd.yaml", - "testdata/diff/resources/env-composition.yaml", - "testdata/diff/resources/functions.yaml", - "testdata/diff/resources/environment-config-v1beta1.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/diff/resources/existing-env-xr.yaml", - "testdata/diff/resources/existing-env-downstream-resource.yaml", - ), - }, - xrdAPIVersion: integration.XRDV1, // the XRD is v1, so Crossplane writes the XR's resourceRefs at spec.resourceRefs - inputFiles: []string{"testdata/diff/modified-env-xr.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(0, 2, 0). - WithModifiedResource("XDownstreamEnvResource", "test-env-resource", ""). - WithFieldChange("spec.forProvider.configData", "existing-config-value", "modified-config-value"). - And(). - WithModifiedResource("XEnvResource", "test-env-resource", ""). - WithFieldChange("spec.configKey", "existing-config-value", "modified-config-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "ExternalResourceDependencies": { - reason: "Validates diff with external resource dependencies via fn-external-resources", - outputFormat: "json", - // this test does a weird thing where it changes the XR but all the downstream changes come from external - // resources, including a field path from the XR itself. - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/functions.yaml", - "testdata/diff/resources/external-resource-configmap.yaml", - "testdata/diff/resources/external-res-fn-composition.yaml", - "testdata/diff/resources/external-named-clusterrole.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/diff/resources/existing-xr-with-external-dep.yaml", - "testdata/diff/resources/existing-downstream-with-external-dep.yaml", - ), - }, - inputFiles: []string{"testdata/diff/modified-xr-with-external-dep.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(0, 2, 0). - WithModifiedResource("XDownstreamResource", "test-resource", "default"). - WithFieldChange("spec.forProvider.configData", "existing-value", "testing-external-resource-data"). - WithFieldChange("spec.forProvider.roleName", "old-role-name", "external-named-clusterrole"). - And(). - WithModifiedResource("XNopResource", "test-resource", "default"). - WithFieldChange("spec.coolField", "existing-value", "modified-with-external-dep"). - WithFieldChange("spec.environment", "staging", "testing"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // Same-namespace case: the matchLabels ExtraResources selector omits the - // namespace, and the matched ConfigMap lives in "default" — the SAME - // namespace as the XR. This passed even before the #376 fix, because the - // old resolveNamespace defaulted an empty selector namespace to the XR - // namespace, which happened to be where the ConfigMap was. See - // "TemplatedExtraResourcesCrossNamespace" for the cross-namespace case - // that requires the all-namespaces lookup. - "TemplatedExtraResourcesSameNamespace": { - reason: "Validates diff with templated ExtraResources (matchLabels, no namespace) resolving a resource in the XR's own namespace", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/functions.yaml", - "testdata/diff/resources/external-resource-configmap.yaml", - "testdata/diff/resources/external-res-gotpl-composition.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/diff/resources/existing-xr-with-external-dep-templated.yaml", - "testdata/diff/resources/existing-downstream-with-external-dep.yaml", - ), - }, - inputFiles: []string{"testdata/diff/modified-xr-with-external-dep.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(0, 2, 0). - WithModifiedResource("XDownstreamResource", "test-resource", "default"). - WithFieldChange("spec.forProvider.configData", "existing-value", "modified-with-external-dep"). - WithFieldChange("spec.forProvider.roleName", "old-role-name", "templated-external-resource-testing"). - And(). - WithModifiedResource("XNopResource", "test-resource", "default"). - WithFieldChange("spec.coolField", "existing-value", "modified-with-external-dep"). - WithFieldChange("spec.environment", "staging", "testing"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // Reproduction for crossplane-contrib/crossplane-diff#376: a matchLabels - // ExtraResources selector that omits the namespace is a cluster-wide - // (all-namespaces) lookup in Crossplane (internal/xfn/required_resources.go - // lists with client.InNamespace(rs.GetNamespace()); empty = all - // namespaces). Here the matched ConfigMap lives in "other-namespace" while - // the XR is in "default". Pre-fix, resolveNamespace defaulted the empty - // selector namespace to the XR namespace ("default"), so the dynamic - // client listed only "default", found nothing, and the go-templating - // requirement came back empty — the downstream XDownstreamResource would - // not render at all (or a real template reading the value would fatal with - // ValueError). Post-fix the lookup spans all namespaces, finds the - // ConfigMap, and the downstream renders with roleName derived from it. - "TemplatedExtraResourcesCrossNamespace": { - reason: "matchLabels ExtraResources (no namespace) must resolve a resource in a DIFFERENT namespace than the XR via an all-namespaces lookup (issue #376)", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/functions.yaml", - "testdata/diff/resources/external-resource-configmap-other-namespace.yaml", - "testdata/diff/resources/external-res-gotpl-composition.yaml", - }, - inputFiles: []string{"testdata/diff/new-xr-with-cross-ns-external-dep.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(2, 0, 0). - WithAddedResource("XDownstreamResource", "test-resource", "default"). - WithField("spec.forProvider.configData", "new-value"). - // roleName is templated from the matched ConfigMap's name, proving - // the cross-namespace resource was resolved. - WithField("spec.forProvider.roleName", "templated-external-resource-crossns"). - And(). - WithAddedResource("XNopResource", "test-resource", "default"). - WithField("spec.coolField", "new-value"). - WithField("spec.environment", "crossns"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "TemplatedExtraResourcesMatchNameNotFound": { - // Regression test for crossplane-contrib/crossplane-diff#355: a - // go-templating ExtraResources block using matchName for a - // resource that does not exist in the cluster must NOT abort the - // diff. Pre-fix, the iterative render loop received an error - // from requirements_provider.processNameSelector and surfaced it - // as a per-XR failure. Post-fix, the unmet requirement is silently - // skipped (mirroring upstream xfn.required_resources.go) and the - // composition renders its downstream resource normally. - reason: "matchName requirement for a missing resource must not abort the diff (issue #355)", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/functions.yaml", - "testdata/diff/resources/external-res-matchname-gotpl-composition.yaml", - }, - inputFiles: []string{"testdata/diff/new-xr.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(2, 0, 0). - WithAddedResource("XDownstreamResource", "test-resource", "default"). - WithField("spec.forProvider.configData", "new-value"). - WithField("spec.forProvider.roleName", "static-role"). - And(). - WithAddedResource("XNopResource", "test-resource", "default"). - WithField("spec.coolField", "new-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "MultiStepFatalAfterExtraResources": { - // Reproduces the PR #295 bug #1 scenario: a multi-step pipeline where - // step 1 successfully emits ExtraResources requirements and composed - // resources, and a LATER step fatals. Per crossplane/cmd/crank/render/ - // render.go, a fatal in step N still returns the accumulated - // requirements from step 0 - // - // On iteration 2 (after requirements are cached) newReqCount == 0, - // and the OLD condition - // renderErr != nil && newReqCount == 0 && len(output.Requirements) == 0 - // is false — the error falls through to checkStability which declares - // the pipeline "stable" on an Outputs with nil CompositeResource, - // causing a SIGSEGV in CalculateNonRemovalDiffs. - // - // Expected behavior (with PR #295's render-loop fix): a clean fatal - // error bubbles up instead of a segfault. The diff is NOT produced - // because the pipeline legitimately failed. - reason: "Multi-step pipeline with step-1 requirements + later-step fatal should surface the error cleanly, not SIGSEGV", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/functions.yaml", - "testdata/diff/resources/external-resource-configmap.yaml", - "testdata/diff/resources/multistep-fatal-after-extra-res-composition.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/diff/resources/existing-xr-with-external-dep-multistep-fatal.yaml", - "testdata/diff/resources/existing-downstream-with-external-dep.yaml", - ), - }, - inputFiles: []string{"testdata/diff/modified-xr-with-external-dep.yaml"}, - expectedError: true, - expectedErrorContains: `"always-fatal" returned a fatal result`, - expectedExitCode: dp.ExitCodeToolError, - }, - "UnguardedTemplatedExtraResources": { - // Regression guard: a user template that accesses .extraResources - // directly (e.g. `index .extraResources "configmaps"`) without a - // `{{- with .extraResources }}` guard fails fatally on the first - // function invocation. function-go-templating's template data has no - // "extraResources" entry on that first call — crossplane's - // FetchingFunctionRunner only populates req.ExtraResources AFTER a - // function returns a non-fatal rsp.Requirements, and the Fatal from - // this template short-circuits before that ever happens. - // - // Verified empirically (2026-04-29) against a kind cluster with - // Crossplane + function-go-templating v0.11.0: a live controller - // produces the identical error, and the XR stays Synced=False with - // ReconcileError. So crossplane-diff must surface the same fatal - // error — not mask it with a diff — to match production behavior. - // - // This test also guards against a hypothetical "fixed" prefetch that - // pre-populated .extraResources for the template: that would make - // crossplane-diff report a successful diff for a composition that - // actually fails in-cluster, which is a false positive. If this test - // ever starts passing with an unexpected exit code or producing a - // diff, something has diverged from prod behavior. - reason: "Unguarded .extraResources access must surface a fatal error matching what a live Crossplane controller produces (verified empirically against kind cluster on 2026-04-29)", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/functions.yaml", - "testdata/diff/resources/external-resource-configmap.yaml", - "testdata/diff/resources/unguarded-external-res-gotpl-composition.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/diff/resources/existing-xr-with-external-dep-unguarded.yaml", - "testdata/diff/resources/existing-downstream-with-external-dep.yaml", - ), - }, - inputFiles: []string{"testdata/diff/modified-xr-with-external-dep.yaml"}, - expectedError: true, - expectedErrorContains: `: error calling index: index of untyped nil`, - expectedExitCode: dp.ExitCodeToolError, - }, - "CrossNamespaceResourceDependencies": { - reason: "Validates cross-namespace resource dependencies via fn-external-resources", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/functions.yaml", - "testdata/diff/resources/cross-namespace-configmap.yaml", - "testdata/diff/resources/cross-namespace-fn-composition.yaml", - "testdata/diff/resources/external-named-clusterrole.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/diff/resources/existing-cross-ns-xr.yaml", - "testdata/diff/resources/existing-cross-ns-downstream.yaml", - ), - }, - inputFiles: []string{"testdata/diff/modified-cross-ns-xr.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(0, 2, 0). - WithModifiedResource("XDownstreamResource", "test-cross-ns-resource", "default"). - WithFieldChange("spec.forProvider.configData", "existing-cross-ns-data-existing-named-data-old-cross-ns-role", "cross-namespace-data-another-cross-namespace-data-external-named-clusterrole"). - And(). - WithModifiedResource("XNopResource", "test-cross-ns-resource", "default"). - WithFieldChange("spec.coolField", "existing-cross-ns-value", "modified-cross-ns-value"). - WithFieldChange("spec.environment", "staging", "production"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "ResourceRemovalHierarchyV1ClusterScoped": { - reason: "Validates resource removal detection with hierarchy using v1 style resourceRefs and cluster scoped downstreams", - xrdAPIVersion: integration.XRDV1, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - { - OwnerFile: "testdata/diff/resources/existing-legacy-xr.yaml", - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - "testdata/diff/resources/removal-test-legacycluster-downstream-resource1.yaml": nil, // Will be kept - "testdata/diff/resources/removal-test-legacy-nested-xr.yaml": { - // A nested XR, no longer rendered: removed, with what it composes - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - "testdata/diff/resources/removal-test-legacy-nested-xr-managed.yaml": nil, // Child will also be removed - }, - }, - }, - }, - }, - setupFiles: []string{ - "testdata/diff/resources/legacy-xrd.yaml", - "testdata/diff/resources/removal-test-legacy-composition.yaml", - "testdata/diff/resources/removal-test-legacy-child-xrd.yaml", - "testdata/diff/resources/removal-test-legacy-child-composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - inputFiles: []string{"testdata/diff/modified-legacy-xr.yaml"}, - expectedOutput: ` -~~~ XDownstreamResource/resource-to-be-kept - apiVersion: legacycluster.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - annotations: - crossplane.io/composition-resource-name: resource1 - labels: - crossplane.io/composite: test-resource - name: resource-to-be-kept - spec: - forProvider: -- configData: existing-value -+ configData: modified-value - ---- ---- XDownstreamResource/resource-to-be-removed-managed -- apiVersion: legacycluster.nop.example.org/v1alpha1 -- kind: XDownstreamResource -- metadata: -- annotations: -- crossplane.io/composition-resource-name: managed-resource -- labels: -- crossplane.io/composite: test-resource -- name: resource-to-be-removed-managed -- spec: -- forProvider: -- configData: child-value - ---- ---- XLegacyChildResource/resource-to-be-removed -- apiVersion: legacycluster.diff.example.org/v1alpha1 -- kind: XLegacyChildResource -- metadata: -- annotations: -- crossplane.io/composition-resource-name: resource2 -- labels: -- crossplane.io/composite: test-resource -- name: resource-to-be-removed -- spec: -- childField: child-value -- compositionRef: -- name: xlegacychildresources.legacycluster.diff.example.org -- compositionRevisionRef: -- name: xlegacychildresources.legacycluster.diff.example.org-bf2c887 -- compositionUpdatePolicy: Automatic - ---- -~~~ XNopResource/test-resource - apiVersion: legacycluster.diff.example.org/v1alpha1 - kind: XNopResource - metadata: - labels: - crossplane.io/composite: test-resource - name: test-resource - spec: - compositionRef: - name: removal-test-composition - compositionRevisionRef: - name: removal-test-composition-4b6f9f1 - compositionUpdatePolicy: Automatic -- coolField: existing-value -+ coolField: modified-value - ---- - -Summary: 2 modified, 2 removed`, - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - noColor: true, - }, - // Issue #259: function-sequencer hides resources from later stages, but those - // resources should be shown as removed if they exist in the cluster. - // https://github.com/crossplane-contrib/crossplane-diff/issues/259 - "FunctionSequencerPreservesExistingResources": { - reason: "Function-sequencer should NOT hide resources that already exist in the cluster (fix for issue #259)", - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - { - OwnerFile: "testdata/diff/resources/existing-sequencer-xr.yaml", - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - // Stage 0 resource - exists in cluster - "testdata/diff/resources/sequencer-stage0-downstream.yaml": nil, - // Stage 1 resource - exists in cluster, should NOT be hidden by sequencer - // because observed resources are passed correctly to the function pipeline. - // Stage 0 has no Ready condition, so the sequencer would gate stage 1 if it - // were not observed: this case depends on the XR's own composed resources - // reaching render as observed state. - "testdata/diff/resources/sequencer-stage1-downstream.yaml": nil, - }, - }, - }, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/sequencer-composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - inputFiles: []string{"testdata/diff/resources/sequencer-xr.yaml"}, - outputFormat: "json", - // Expected: With the fix, observed resources ARE passed to function-sequencer. - // Function-sequencer sees both resources exist and does NOT remove them from desired state. - // Since existing resources match rendered resources, there should be no changes. - // XR diff outputs nothing when there are no changes (unlike comp diff which says "No changes detected"). - expectedStructured: simtest.Expect().WithSummary(0, 0, 0), - expectedError: false, - expectedExitCode: dp.ExitCodeSuccess, - }, - // Issue #259: Validates that removal detection works even when the XR has no changes. - // This is the core fix for the function-sequencer bug where existingXR was nil because - // the XR diff wasn't stored when there were no changes. - // https://github.com/crossplane-contrib/crossplane-diff/issues/259 - "ResourceRemovalWithUnmodifiedXR": { - reason: "Validates resource removal detection works when XR has no changes (uses existingXRFromCluster fallback)", - outputFormat: "json", - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - { - OwnerFile: "testdata/diff/resources/existing-removal-test-xr.yaml", - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - "testdata/diff/resources/removal-test-ns-downstream-resource1.yaml": nil, // Will be kept - "testdata/diff/resources/removal-test-nested-xr.yaml": { - // A nested XR, no longer rendered: removed, with what it composes - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - "testdata/diff/resources/removal-test-nested-xr-managed.yaml": nil, // Child will also be removed - }, - }, - }, - }, - }, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/removal-test-composition.yaml", - "testdata/diff/resources/nested/child-xrd.yaml", - "testdata/diff/resources/nested/child-composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - inputFiles: []string{"testdata/diff/unmodified-xr.yaml"}, // XR has NO changes - // Expected: the nested XR resource2 and what it composes are removed, but XR and resource1 have no changes - expectedStructured: simtest.Expect(). - WithSummary(0, 0, 2). - WithRemovedResource("XChildResource", "resource-to-be-removed", "default"). - // dryRun must be absent on a removal. A removal has no desired state to preview, so it can - // never carry a dryRun object, and one appearing would wrongly mark it unverified; WithDryRunPerformed asserts exactly that absence. The method - // name reads oddly here — nothing was dry run — but the claim it makes ("no dryRun - // object is present") is the right one, and having a second spelling for the same - // assertion would be worse. - WithDryRunPerformed(). - WithField("spec.childField", "child-value"). - And(). - WithRemovedResource("XDownstreamResource", "resource-to-be-removed-managed", "default"). - WithDryRunPerformed(). - WithField("spec.forProvider.configData", "child-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // #552: removal mirrors Crossplane's garbage collector, which deletes a composed resource only if - // the composite controls it. The XR's resourceRefs name four resources, and its render desires only - // the first: one it controls and renders, one it controls and no longer renders, one another XR - // controls, and one with no controller reference at all. Only the second is removed. - "ResourceRemovalSkipsResourcesTheXRDoesNotControl": { - reason: "A composed resource the XR's resourceRefs name but the XR does not control is not reported removed", - // What this case pins: test-resource's resourceRefs still name two resources it doesn't control. - outputFormat: "json", - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/diff/resources/existing-xr-listing-uncontrolled-resources.yaml", - "testdata/diff/resources/removal-test-ns-downstream-resource1.yaml", // still rendered: kept - "testdata/diff/resources/removal-test-ns-downstream-resource2.yaml", // no longer rendered: removed - ), - integration.Composes( - "testdata/diff/resources/existing-other-xr.yaml", - "testdata/diff/resources/existing-other-xr-downstream.yaml", - ), - }, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/removal-test-composition.yaml", - "testdata/diff/resources/composition.yaml", - "testdata/diff/resources/functions.yaml", - // Deliberately not in crossplaneManagedResources: its controller reference was removed, so - // no XR controls it, though test-resource's resourceRefs still name it. - "testdata/diff/resources/removal-test-ns-downstream-without-controller.yaml", - }, - inputFiles: []string{"testdata/diff/unmodified-xr.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(0, 0, 1). - WithRemovedResource("XDownstreamResource", "resource-to-be-removed", "default"). - WithField("spec.forProvider.configData", "existing-value"), - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "ResourceRemovalHierarchyV2Namespaced": { - reason: "Validates resource removal detection with hierarchy using v2 style resourceRefs and namespaced downstreams", - outputFormat: "json", - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - { - OwnerFile: "testdata/diff/resources/existing-removal-test-xr.yaml", - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - "testdata/diff/resources/removal-test-ns-downstream-resource1.yaml": nil, // Will be kept - "testdata/diff/resources/removal-test-nested-xr.yaml": { - // A nested XR, no longer rendered: removed, with what it composes - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - "testdata/diff/resources/removal-test-nested-xr-managed.yaml": nil, // Child will also be removed - }, - }, - }, - }, - }, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/removal-test-composition.yaml", - "testdata/diff/resources/nested/child-xrd.yaml", - "testdata/diff/resources/nested/child-composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - inputFiles: []string{"testdata/diff/modified-xr.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(0, 2, 2). - WithModifiedResource("XDownstreamResource", "resource-to-be-kept", "default"). - WithFieldChange("spec.forProvider.configData", "existing-value", "modified-value"). - And(). - WithRemovedResource("XChildResource", "resource-to-be-removed", "default"). - WithField("spec.childField", "child-value"). - And(). - WithRemovedResource("XDownstreamResource", "resource-to-be-removed-managed", "default"). - WithField("spec.forProvider.configData", "child-value"). - And(). - WithModifiedResource("XNopResource", "test-resource", "default"). - WithFieldChange("spec.coolField", "existing-value", "modified-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "ResourceRemovalHierarchyV2ClusterScoped": { - reason: "Validates resource removal detection with hierarchy using v2 style resourceRefs and cluster scoped downstreams", - outputFormat: "json", - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - { - OwnerFile: "testdata/diff/resources/existing-cluster-xr.yaml", - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - "testdata/diff/resources/removal-test-cluster-downstream-resource1.yaml": nil, // Will be kept - "testdata/diff/resources/removal-test-nested-xr.yaml": { - // A nested XR, no longer rendered: removed, with what it composes - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - "testdata/diff/resources/removal-test-nested-xr-managed.yaml": nil, // Child will also be removed - }, - }, - }, - }, - }, - setupFiles: []string{ - "testdata/diff/resources/cluster-xrd.yaml", - "testdata/diff/resources/removal-test-cluster-composition.yaml", - "testdata/diff/resources/nested/child-xrd.yaml", - "testdata/diff/resources/nested/child-composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - inputFiles: []string{"testdata/diff/modified-cluster-xr.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(0, 2, 2). - WithModifiedResource("XDownstreamResource", "resource-to-be-kept", ""). - WithFieldChange("spec.forProvider.configData", "existing-value", "modified-value"). - And(). - WithRemovedResource("XChildResource", "resource-to-be-removed", "default"). - WithField("spec.childField", "child-value"). - And(). - WithRemovedResource("XDownstreamResource", "resource-to-be-removed-managed", "default"). - WithField("spec.forProvider.configData", "child-value"). - And(). - WithModifiedResource("XNopResource", "test-resource", ""). - WithFieldChange("spec.coolField", "existing-value", "modified-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "ResourceWithGenerateName": { - reason: "Validates handling of resources with generateName", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/functions.yaml", - "testdata/diff/resources/generated-name-composition.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - { - // Set up the XR as the owner of the generated composed resource - OwnerFile: "testdata/diff/resources/existing-generate-name-xr.yaml", - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - // This file has a generated name and is owned by the XR - "testdata/diff/resources/existing-downstream-with-generated-name.yaml": nil, - }, - }, - }, - // Use a composition that uses generateName for composed resources - inputFiles: []string{"testdata/diff/new-xr.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(0, 2, 0). - WithModifiedResource("XDownstreamResource", "test-resource-abc123", "default"). - WithFieldChange("spec.forProvider.configData", "existing-value", "new-value"). - And(). - WithModifiedResource("XNopResource", "test-resource", "default"). - WithFieldChange("spec.coolField", "existing-value", "new-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // The generateName counterpart of NewXRWithExistingComposedResource. A new XR renders its composed - // resource with only a generateName, and nothing is observed for it, so no name identifies the - // existing resource: only its crossplane.io/composite label and composition-resource-name - // annotation can. ResourceWithGenerateName cannot reach that lookup, because there the XR exists and - // render names the resource after the observed one. - "NewXRAdoptsOrphanedGenerateNamedComposedResource": { - reason: "A new XR's generateName composed resource is matched to an existing orphan by its composite label", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/functions.yaml", - "testdata/diff/resources/generated-name-composition.yaml", - // Deliberately not in crossplaneManagedResources: an orphan has no controller - // ownerReference (the garbage collector strips it when the XR is deleted with - // --cascade=orphan) and no XR lists it, but it keeps its labels and annotations. - "testdata/diff/resources/existing-downstream-with-generated-name.yaml", - }, - inputFiles: []string{"testdata/diff/new-xr.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(1, 1, 0). - WithAddedResource("XNopResource", "test-resource", "default"). - And(). - WithModifiedResource("XDownstreamResource", "test-resource-abc123", "default"). - WithFieldChange("spec.forProvider.configData", "existing-value", "new-value"), - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // A generateName-only input next to an ordinary one is an ordinary invocation. The unnamed input - // has no identity, so input validation must not mistake it for the named XR's claim: an XR with - // no claimRef has an empty reference identity too, and the two must not match. - "GenerateNameInputAlongsideANamedInput": { - reason: "A generateName-only input beside a named one is valid; both are diffed", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - inputFiles: []string{"testdata/diff/new-xr.yaml", "testdata/diff/generated-name-xr.yaml"}, - expectedExitCode: dp.ExitCodeDiffDetected, - expectedStructured: simtest.Expect(). - WithSummary(4, 0, 0). - WithXRs( - simtest.XR("XNopResource", "test-resource", "default").Status("changed").Summary(2, 0, 0), - simtest.XR("XNopResource", "generated-xr-(generated)", "default").Status("changed").Summary(2, 0, 0), - ), - }, - "NewXRWithGenerateName": { - reason: "Shows diff for new XR with generateName", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition.yaml", - "testdata/diff/resources/functions.yaml", - // We don't add any existing XR, as we're testing creation of a new one - }, - inputFiles: []string{"testdata/diff/generated-name-xr.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(2, 0, 0). - WithAddedResource("XDownstreamResource", "", "default"). - WithNamePattern(`generated-xr-\(generated\)`). - WithField("spec.forProvider.configData", "new-value"). - And(). - WithAddedResource("XNopResource", "", "default"). - WithNamePattern(`generated-xr-\(generated\)`). - WithField("spec.coolField", "new-value"). - And(). - // Issue #477: the grouped view must attribute these changes to a - // named XR. metadata.name is empty on the input, so the identity - // carries the effective (synthesized) name — the same one the - // changes above are keyed by. - WithXRs( - simtest.XR("XNopResource", "generated-xr-(generated)", "default"). - Status("changed"). - Summary(2, 0, 0), - ), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // Issue #476. The same file passed twice — a fat-fingered command line, or CI enumerating one - // file twice — has one clear intent. It is diffed once, with a warning, and the run succeeds. - "IdenticalDuplicateInputIsDeduplicated": { - reason: "An input identical to an earlier one is diffed once, with a warning, not rejected", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - inputFiles: []string{ - "testdata/diff/new-xr.yaml", - "testdata/diff/new-xr.yaml", - }, - expectedExitCode: dp.ExitCodeDiffDetected, - expectedStderrContains: []string{"Ignoring a duplicate input: it is identical to an earlier one"}, - // The warning channel's contract is both halves: stderr above, and warnings[] here. - expectedStructured: simtest.Expect(). - WithSummary(2, 0, 0). - WithXRs(simtest.XR("XNopResource", "test-resource", "default").Status("changed").Summary(2, 0, 0)). - WithWarning("Ignoring a duplicate input: it is identical to an earlier one"). - WithWarningContext(map[string]string{"resource": "XNopResource/test-resource", "input": "2", "duplicateOf": "1"}), - }, - // The same object twice with different content fails loudly: applying both would leave whichever - // is applied last, and the order inputs are given in is not a statement of intent. - "SameObjectTwiceWithDifferentContentFails": { - reason: "One object given twice with different content is an input error on both inputs", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - inputFiles: []string{ - "testdata/diff/new-xr.yaml", - "testdata/diff/modified-xr.yaml", - }, - expectedError: true, - expectedErrorContains: "input 2 defines XNopResource/test-resource differently from input 1", - expectedExitCode: dp.ExitCodeToolError, - }, - // A child XR passed alongside the parent whose composition renders it: the parent's composition - // writes that child, so the child input would be a second writer. Rejected on the child; the - // parent's own diff is unaffected. - "InputComposedByAnotherInputIsRejected": { - reason: "An XR supplied alongside the input whose composition composes it is an input error", - setupFiles: []string{ - "testdata/diff/resources/nested/parent-xrd.yaml", - "testdata/diff/resources/nested/child-xrd.yaml", - "testdata/diff/resources/nested/parent-composition.yaml", - "testdata/diff/resources/nested/child-composition.yaml", - "testdata/diff/resources/xdownstreamenvresource-xrd.yaml", - "testdata/diff/resources/functions.yaml", - }, - inputFiles: []string{ - "testdata/diff/new-nested-xr.yaml", - "testdata/diff/new-nested-child-xr.yaml", - }, - expectedError: true, - expectedErrorContains: "XChildResource/test-parent-child is composed by input XParentResource/test-parent, " + - "whose composition writes it", - expectedExitCode: dp.ExitCodeToolError, - }, - // Issue #476. Two distinct XRs whose composition gives one child a fixed name render the same - // object — here with byte-identical content. Crossplane gives it to whichever XR creates it first - // and the other fails to reconcile it, so the run must fail however alike the renderings are. - // This depends on real renders carrying the controller reference; unit tests build that by hand. - "TwoXRsComposingOneObjectContend": { - reason: "Two XRs that would both control one object fail, even when their renderings are identical", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/shared-child-composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - inputFiles: []string{"testdata/diff/shared-child-xrs.yaml"}, - expectedError: true, - expectedExitCode: dp.ExitCodeToolError, - expectedErrorContains: `resource "ns.nop.example.org/v1alpha1/XDownstreamResource/default/shared-child" would be ` + - `controlled by more than one XR (XNopResource/shared-child-owner-a, XNopResource/shared-child-owner-b)`, - }, - // Reproduces the PR #294 scenario: a net-new XR is diffed while the - // composed resource it would manage already exists in the cluster - // (e.g. after the prior XR was deleted with --cascade=orphan, or while - // importing existing resources under Crossplane management). The render - // pipeline emits ownerReferences with an empty UID for new XRs; - // without the fallback in CalculateNonRemovalDiffs, UpdateOwnerRefs - // runs as a no-op and the real apiserver rejects the dry-run apply - // with "metadata.ownerReferences.uid: must not be empty". - "NewXRWithExistingComposedResource": { - reason: "Shows diff for new XR whose composed resource already exists in the cluster (PR #294)", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition.yaml", - "testdata/diff/resources/functions.yaml", - // Pre-existing composed resource; no backing XR in the cluster. Deliberately not in - // crossplaneManagedResources: an orphaned resource has no controller ownerReference (the - // garbage collector strips it when the XR is deleted with --cascade=orphan), and adopting - // exactly such a resource is what this case tests. - "testdata/diff/resources/existing-downstream-resource.yaml", - }, - inputFiles: []string{"testdata/diff/new-xr.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(1, 1, 0). - WithAddedResource("XNopResource", "test-resource", "default"). - WithField("spec.coolField", "new-value"). - And(). - WithModifiedResource("XDownstreamResource", "test-resource", "default"). - WithFieldChange("spec.forProvider.configData", "existing-value", "new-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "MultipleXRs": { - reason: "Validates diff for multiple XRs", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - // Add an existing XR and downstream resource to test modification - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/diff/resources/existing-xr.yaml", - "testdata/diff/resources/existing-downstream-resource.yaml", - ), - }, - inputFiles: []string{ - "testdata/diff/first-xr.yaml", - "testdata/diff/modified-xr.yaml", - }, - expectedStructured: simtest.Expect(). - WithSummary(2, 2, 0). - WithAddedResource("XDownstreamResource", "first-resource", "default"). - WithField("spec.forProvider.configData", "first-value"). - And(). - WithModifiedResource("XDownstreamResource", "test-resource", "default"). - WithFieldChange("spec.forProvider.configData", "existing-value", "modified-value"). - And(). - WithAddedResource("XNopResource", "first-resource", "default"). - WithField("spec.coolField", "first-value"). - And(). - WithModifiedResource("XNopResource", "test-resource", "default"). - WithFieldChange("spec.coolField", "existing-value", "modified-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "LeastPrivilegeCredentialsDiffSucceeds": { - reason: "A user who can only get Crossplane's types, not list them, gets the admin's diff of a new and a modified XR", - 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", - ), - }, - // The new XR names its composition: one that names none is matched against every Composition, - // which takes a list of them. - inputFiles: []string{"testdata/diff/first-xr-with-composition-ref.yaml", "testdata/diff/modified-xr.yaml"}, - rbacRules: leastPrivilegeRules("get", "create", "patch"), - outputFormat: "json", - expectedStructured: simtest.Expect(). - WithSummary(2, 2, 0). - WithAddedResource("XDownstreamResource", "first-resource", "default"). - WithField("spec.forProvider.configData", "first-value"). - And(). - WithModifiedResource("XDownstreamResource", "test-resource", "default"). - WithFieldChange("spec.forProvider.configData", "existing-value", "modified-value"). - And(). - WithAddedResource("XNopResource", "first-resource", "default"). - WithField("spec.coolField", "first-value"). - And(). - WithModifiedResource("XNopResource", "test-resource", "default"). - WithFieldChange("spec.coolField", "existing-value", "modified-value"), - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "UnrelatedXRDWithoutCRDDoesNotFailRun": { - reason: "An XRD unrelated to the input whose CRD does not exist yet does not affect the diff", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - unestablishedXRDs: []string{"testdata/diff/resources/unestablished-xrd.yaml"}, - inputFiles: []string{"testdata/diff/new-xr.yaml"}, - outputFormat: "json", - // The same diff NewResourceDiff shows, which has no such XRD. - expectedStructured: simtest.Expect(). - WithSummary(2, 0, 0). - WithAddedResource("XDownstreamResource", "test-resource", "default"). - WithField("spec.forProvider.configData", "new-value"). - And(). - WithAddedResource("XNopResource", "test-resource", "default"). - WithField("spec.coolField", "new-value"), - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "MissingFunctionFailsWithSameError": { - reason: "A pipeline naming a Function the cluster does not have fails, naming the function and its step", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition-with-missing-function.yaml", - "testdata/diff/resources/functions.yaml", - }, - inputFiles: []string{"testdata/diff/new-xr.yaml"}, - expectedError: true, - expectedErrorContains: `cannot get functions from pipeline: function "function-not-installed" referenced in pipeline step "generate-resources" not found`, - expectedExitCode: dp.ExitCodeToolError, - }, - "SelectCompositionByDirectReference": { - reason: "Validates composition selection by direct reference", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/functions.yaml", - // Add multiple compositions for the same XR type - "testdata/diff/resources/default-composition.yaml", - "testdata/diff/resources/production-composition.yaml", - "testdata/diff/resources/staging-composition.yaml", - }, - inputFiles: []string{ - "testdata/diff/xr-with-composition-ref.yaml", - }, - expectedStructured: simtest.Expect(). - WithSummary(2, 0, 0). - WithAddedResource("XDownstreamResource", "test-resource", "default"). - WithField("spec.forProvider.configData", "test-value"). - WithField("spec.forProvider.resourceTier", "production"). - And(). - WithAddedResource("XNopResource", "test-resource", "default"). - WithField("spec.coolField", "test-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "SelectCompositionByLabelSelector": { - reason: "Validates composition selection by label selector", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/functions.yaml", - // Add multiple compositions for the same XR type - "testdata/diff/resources/default-composition.yaml", - "testdata/diff/resources/production-composition.yaml", - "testdata/diff/resources/staging-composition.yaml", - }, - inputFiles: []string{ - "testdata/diff/xr-with-composition-selector.yaml", - }, - expectedStructured: simtest.Expect(). - WithSummary(2, 0, 0). - WithAddedResource("XDownstreamResource", "test-resource", "default"). - WithField("spec.forProvider.configData", "test-value"). - WithField("spec.forProvider.resourceTier", "staging"). - And(). - WithAddedResource("XNopResource", "test-resource", "default"). - WithField("spec.coolField", "test-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "AmbiguousCompositionSelection": { - reason: "Validates error on ambiguous composition selection", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/functions.yaml", - // Add multiple compositions for the same XR type - "testdata/diff/resources/default-composition.yaml", - "testdata/diff/resources/production-composition.yaml", - "testdata/diff/resources/staging-composition.yaml", - }, - inputFiles: []string{ - "testdata/diff/xr-with-ambiguous-selector.yaml", - }, - expectedError: true, - expectedExitCode: dp.ExitCodeToolError, - expectedErrorContains: "ambiguous composition selection: multiple compositions match", - noColor: true, - }, - "SchemaValidationError": { - reason: "Validates exit code 2 for schema validation errors and structured per-resource failure detail in JSON output", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - inputFiles: []string{ - "testdata/diff/invalid-schema-xr.yaml", - }, - outputFormat: "json", - expectedError: true, - expectedExitCode: dp.ExitCodeSchemaValidation, - // Structured assertion: lock down the typed - // validationFailures payload for both the failing XR and - // its composed resource. Field paths and error types are - // ours to assert; the embedded k8s message wording is - // left unasserted (apimachinery upgrades can shift its - // phrasing without changing our output contract). - expectedStructured: simtest.Expect(). - WithError("XNopResource/invalid-schema-xr"). - WithValidationFailure("ns.diff.example.org/v1alpha1", "XNopResource", "invalid-schema-xr", "default"). - WithStatus("invalid"). - WithFieldError("schema", "spec.coolField"). - AndFieldError(). - AndValidationFailure(). - WithValidationFailure("ns.nop.example.org/v1alpha1", "XDownstreamResource", "invalid-schema-xr", "default"). - WithStatus("invalid"). - WithFieldError("schema", "spec.forProvider.configData"). - AndFieldError(). - AndValidationFailure(). - AndError(), - noColor: true, - }, - "NewClaimShowsDiff": { - reason: "Shows diff for new claim", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/existing-namespace.yaml", - // Add the necessary CRDs and compositions for claim diffing - "testdata/diff/resources/claim-xrd.yaml", - "testdata/diff/resources/claim-composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - inputFiles: []string{"testdata/diff/new-claim.yaml"}, - // The claim is defaulted with its own CRD (generated from claim-xrd.yaml), which shares the XRD's - // user default spec.tier with the XR's CRD but, unlike it, does not default compositionUpdatePolicy. - expectedStructured: simtest.Expect(). - WithSummary(2, 0, 0). - WithAddedResource("NopClaim", "test-claim", "existing-namespace"). - WithField("spec.coolField", "new-value"). - WithField("spec.tier", "standard"). - WithFieldAbsent("spec.compositionUpdatePolicy"). - And(). - WithAddedResource("XDownstreamResource", "test-claim", ""). - WithField("spec.forProvider.configData", "new-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "NewClaimWithClaimRefComposition": { - reason: "Shows diff for new claim when composition uses spec.claimRef - claimRef is synthesized for new claims", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/existing-namespace.yaml", - "testdata/diff/resources/claim-xrd.yaml", - "testdata/diff/resources/claim-composition-with-claimref.yaml", - "testdata/diff/resources/functions.yaml", - }, - inputFiles: []string{"testdata/diff/new-claim-with-claimref-composition.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(2, 0, 0). - WithAddedResource("NopClaim", "test-claim", "existing-namespace"). - WithField("spec.coolField", "new-value"). - And(). - WithAddedResource("XDownstreamResource", "test-claim", ""). - WithField("spec.forProvider.configData", "existing-namespace/test-claim"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "ModifiedClaimShowsDiff": { - reason: "Shows diff for modified claim", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/existing-namespace.yaml", - // Add necessary CRDs and composition - "testdata/diff/resources/claim-xrd.yaml", - "testdata/diff/resources/claim-composition.yaml", - "testdata/diff/resources/functions.yaml", - // Add existing resources for comparison - "testdata/diff/resources/existing-claim.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/diff/resources/existing-claim-xr.yaml", - "testdata/diff/resources/existing-claim-downstream-resource.yaml", - ), - }, - xrdAPIVersion: integration.XRDV1, // the XRD is v1, so Crossplane writes the XR's resourceRefs at spec.resourceRefs - inputFiles: []string{"testdata/diff/modified-claim.yaml"}, - expectedStructured: simtest.Expect(). - // The composed resource is labelled with the backing XR the claim's resourceRef names, so it - // already belongs to this claim: no ownership advisory (#534). - WithNoWarnings(). - WithSummary(0, 2, 0). - WithModifiedResource("NopClaim", "test-claim", "existing-namespace"). - WithFieldChange("spec.coolField", "existing-value", "modified-value"). - And(). - WithModifiedResource("XDownstreamResource", "test-claim-82crv", ""). - WithFieldChange("spec.forProvider.configData", "existing-value", "modified-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // #503, for a claim. A claim is diffed as the claim itself, so its dry-run payload must carry - // none of the defaults its CRD declares for fields the manifest omits. - "XRDDefaultedFieldOnClaimOwnedByAnotherManagerIsNotReportedChanged": { - reason: "An XRD-defaulted field the claim manifest omits keeps another manager's non-default value in the diff", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/existing-namespace.yaml", - "testdata/diff/resources/claim-xrd.yaml", - "testdata/diff/resources/claim-composition.yaml", - "testdata/diff/resources/functions.yaml", - "testdata/diff/resources/existing-claim.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/diff/resources/existing-claim-xr.yaml", - "testdata/diff/resources/existing-claim-downstream-resource.yaml", - ), - }, - xrdAPIVersion: integration.XRDV1, // the XRD is v1, so Crossplane writes the XR's resourceRefs at spec.resourceRefs - fieldManagerApplies: []integration.FieldManagerApply{ - {File: "testdata/diff/resources/existing-claim-defaulted-fields-other-manager.yaml", FieldManager: "platform-operator"}, - }, - inputFiles: []string{"testdata/diff/modified-claim-omitting-defaulted-fields.yaml"}, - expectedStructured: simtest.Expect(). - // As in ModifiedClaimShowsDiff: the composed resource belongs to the claim's backing XR (#534). - WithNoWarnings(). - WithSummary(0, 2, 0). - WithModifiedResource("NopClaim", "test-claim", "existing-namespace"). - WithFieldChange("spec.coolField", "existing-value", "modified-value"). - WithFieldChange("spec.compositeDeletePolicy", "Foreground", "Foreground"). - WithFieldChange("spec.compositionUpdatePolicy", "Manual", "Manual"). - And(). - WithModifiedResource("XDownstreamResource", "test-claim-82crv", ""). - WithFieldChange("spec.forProvider.configData", "existing-value", "modified-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // A claim is defaulted with its own CRD, never its XR's; only the XR that render consumes gets - // the XR CRD's defaults. The claimnested CRDs mirror what Crossplane generates: both carry the - // XRD's user default spec.tier, only the claim's defaults spec.compositeDeletePolicy, and only - // the XR's defaults spec.compositionUpdatePolicy. The composition writes the render input's - // tier and policy into the composed resource, so each case pins all three views at once: the - // claim's payload, its predicted or apiserver-defaulted form, and the render input. - "NewClaimIsDefaultedWithItsOwnCRDWhenDryRunCreated": { - reason: "A new claim shows its own CRD's defaults, from the apiserver, and render sees its XR's", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/claim-nested/parent-definition.yaml", - "testdata/diff/resources/claim-nested/parent-defaults-composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - inputFiles: []string{"testdata/diff/new-parent-claim-omitting-defaulted-fields.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(2, 0, 0). - WithAddedResource("ParentNopClaim", "new-parent-claim", "default"). - WithDryRunPerformed(). - WithField("spec.tier", "standard"). - WithField("spec.compositeDeletePolicy", "Background"). - WithFieldAbsent("spec.compositionUpdatePolicy"). - And(). - WithAddedResource("XDownstreamResource", "new-parent-claim-defaults", ""). - WithField("spec.forProvider.configData", "standard/Automatic"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "NewClaimIsPredictedWithItsOwnCRDsDefaultsWithoutDryRun": { - reason: "With --dry-run-on=existing a new claim is predicted with its own CRD's defaults, not its XR's", - outputFormat: "json", - dryRunOn: "existing", - setupFiles: []string{ - "testdata/diff/resources/claim-nested/parent-definition.yaml", - "testdata/diff/resources/claim-nested/parent-defaults-composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - inputFiles: []string{"testdata/diff/new-parent-claim-omitting-defaulted-fields.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(2, 0, 0). - WithAddedResource("ParentNopClaim", "new-parent-claim", "default"). - WithDryRunSkipped("disabled", ""). - WithField("spec.tier", "standard"). - WithField("spec.compositeDeletePolicy", "Background"). - WithFieldAbsent("spec.compositionUpdatePolicy"). - And(). - WithAddedResource("XDownstreamResource", "new-parent-claim-defaults", ""). - WithDryRunSkipped("disabled", ""). - WithField("spec.forProvider.configData", "standard/Automatic"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "XRDDefaultsAppliedBeforeRendering": { - reason: "Validates that XRD defaults are applied to XR before rendering", - outputFormat: "json", - inputFiles: []string{"testdata/diff/xr-with-missing-defaults.yaml"}, - setupFiles: []string{ - "testdata/diff/resources/xrd-with-defaults.yaml", - "testdata/diff/resources/composition-with-defaults.yaml", - "testdata/diff/resources/functions.yaml", - }, - expectedStructured: simtest.Expect(). - WithSummary(1, 0, 0). - WithAddedResource("XTestDefaultResource", "test-resource-with-defaults", "default"). - WithField("spec.region", "us-east-1"). - WithField("spec.size", "large"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // The sibling of XRDDefaultsAppliedBeforeRendering, whose composition renders nothing, so it checks - // only the XR's dry run. Here a composition reads the composite's defaulted fields into a composed - // resource: a user default (spec.region) for a v2 and a legacy XR, and for the legacy XR also the - // machinery default spec.compositionUpdatePolicy. Rendering an undefaulted XR writes "". - "XRDDefaultsReachTheRender": { - reason: "Validates that a composition observes an XR's CRD defaults, for both a v2 and a legacy XR", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/rendered-defaults/definitions.yaml", - "testdata/diff/resources/functions.yaml", - }, - inputFiles: []string{"testdata/diff/new-rendered-defaults-xrs.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(4, 0, 0). - WithAddedResource("XRegionalDatabase", "modern-database", "default"). - WithField("spec.region", "us-east-1"). - And(). - WithAddedResource("XLegacyRegionalDatabase", "legacy-database", ""). - WithField("spec.region", "eu-west-1"). - And(). - WithAddedResource("XDownstreamResource", "modern-database", "default"). - WithField("spec.forProvider.configData", "us-east-1/large"). - And(). - WithAddedResource("XDownstreamResource", "legacy-database", ""). - WithField("spec.forProvider.configData", "eu-west-1/small/Automatic"), - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // #503: a composed resource's dry-run apply payload must be exactly what the composition - // rendered. If it carried a locally-defaulted spec.forProvider.size: small, the apply would - // claim the field away from the manager that set it to large, predicting a change real - // Crossplane (which never sends the field) would not make. The composition renders - // everything else identically, so any modification reported for the composed resource could - // only have come from the defaulted field. - "CRDDefaultedFieldOwnedByAnotherManagerIsNotReportedChanged": { - reason: "A CRD-defaulted field the composition does not render keeps another manager's non-default value in the diff", - outputFormat: "json", - inputFiles: []string{"testdata/diff/new-xr.yaml"}, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition-with-crd-defaulted-downstream.yaml", - "testdata/diff/resources/functions.yaml", - }, - // The composed resource deliberately has no controlling XR, as after its XR was deleted with - // --cascade=orphan: the XR is new, and what this case pins is the composed resource's field - // managers, which an orphan keeps. Giving it an XR would make the XR modified rather than new. - fieldManagerApplies: []integration.FieldManagerApply{ - { - File: "testdata/diff/resources/existing-defaulted-downstream-composed.yaml", - FieldManager: "apiextensions.crossplane.io/composed/2b9d3c4e-0000-4000-8000-000000000503", - }, - { - File: "testdata/diff/resources/existing-defaulted-downstream-other-manager.yaml", - FieldManager: "platform-operator", - }, - }, - expectedStructured: simtest.Expect(). - WithSummary(1, 0, 0). - WithAddedResource("XNopResource", "test-resource", "default"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // #503, for the XR itself. The XR is defaulted before rendering, and must be: the composition - // has to see the spec Crossplane would. But the XR's own dry-run apply payload must not carry - // those defaults, or it claims spec.region from the manager that set it to eu-west-1 and reports - // a change applying the manifest would not make. Nothing is composed, so the XR is the only - // thing that could be reported changed. - "XRDDefaultedFieldOwnedByAnotherManagerIsNotReportedChanged": { - reason: "An XRD-defaulted field the XR manifest omits keeps another manager's non-default value in the diff", - outputFormat: "json", - inputFiles: []string{"testdata/diff/xr-with-missing-defaults.yaml"}, - setupFiles: []string{ - "testdata/diff/resources/xrd-with-defaults.yaml", - "testdata/diff/resources/composition-with-defaults.yaml", - "testdata/diff/resources/functions.yaml", - }, - fieldManagerApplies: []integration.FieldManagerApply{ - {File: "testdata/diff/xr-with-missing-defaults.yaml", FieldManager: "kubectl"}, - {File: "testdata/diff/resources/existing-xr-with-defaults-reconciled.yaml", FieldManager: integration.CompositeReconcilerFieldManager}, - {File: "testdata/diff/resources/existing-xr-with-defaults-other-manager.yaml", FieldManager: "platform-operator"}, - }, - expectedStructured: simtest.Expect().WithSummary(0, 0, 0), - expectedError: false, - expectedExitCode: dp.ExitCodeSuccess, - }, - // With no apiserver result for an addition, its defaults have to be predicted locally. These - // pin that keeping local defaults out of dry-run payloads did not also take them out of the - // additions that are never sent: an XRD default on the XR, and a CRD default on a composed - // resource. - "XRDDefaultsShownForAddedXRWithoutDryRun": { - reason: "With --dry-run-on=existing an added XR still shows the defaults its XRD declares", - outputFormat: "json", - dryRunOn: "existing", - inputFiles: []string{"testdata/diff/xr-with-missing-defaults.yaml"}, - setupFiles: []string{ - "testdata/diff/resources/xrd-with-defaults.yaml", - "testdata/diff/resources/composition-with-defaults.yaml", - "testdata/diff/resources/functions.yaml", - }, - expectedStructured: simtest.Expect(). - WithSummary(1, 0, 0). - WithAddedResource("XTestDefaultResource", "test-resource-with-defaults", "default"). - WithDryRunSkipped("disabled", ""). - WithField("spec.region", "us-east-1"). - WithField("spec.size", "large"). - WithField("spec.tags.environment", "development"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "CRDDefaultsShownForAddedComposedResourceWithoutDryRun": { - reason: "With --dry-run-on=existing an added composed resource still shows the defaults its CRD declares", - outputFormat: "json", - dryRunOn: "existing", - inputFiles: []string{"testdata/diff/new-xr.yaml"}, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition-with-crd-defaulted-downstream.yaml", - "testdata/diff/resources/functions.yaml", - }, - expectedStructured: simtest.Expect(). - WithSummary(2, 0, 0). - WithAddedResource("XDefaultedDownstreamResource", "test-resource-defaulted", "default"). - WithDryRunSkipped("disabled", ""). - WithField("spec.forProvider.size", "small"). - WithField("spec.forProvider.configData", "new-value"). - And(). - WithAddedResource("XNopResource", "test-resource", "default"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // The local prediction defaults an addition against the version it was requested at. Its CRD - // stores another version, whose defaults (and conversion) the cluster may also apply, so the - // prediction cannot be trusted to match and the advisory says so — even under - // --dry-run-on=existing, whose unverified additions are otherwise not warned about. Defaulting - // itself is unchanged: size is the requested version's default, not the storage version's. - // The XR's own XRD has one version, so it raises no warning, which the count pins. - "AdditionAtANonStorageVersionWarnsThatItsDiffOmitsTheStorageVersion": { - reason: "With --dry-run-on=existing an addition requested at a version its CRD does not store warns that the cluster stores it at another version", - outputFormat: "json", - dryRunOn: "existing", - inputFiles: []string{"testdata/diff/new-xr.yaml"}, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition-with-non-storage-version-downstream.yaml", - "testdata/diff/resources/functions.yaml", - }, - expectedStructured: simtest.Expect(). - WithSummary(2, 0, 0). - WithAddedResource("XVersionedDownstreamResource", "test-resource-versioned", "default"). - WithDryRunSkipped("disabled", ""). - WithDryRunStorageVersion("v1"). - WithField("spec.forProvider.size", "small"). - And(). - WithAddedResource("XNopResource", "test-resource", "default"). - WithDryRunSkipped("disabled", ""). - And(). - WithWarning("they are stored at a version other than the one requested"). - WithWarningContext(map[string]string{ - "gvk": "ns.nop.example.org/v1alpha1, Kind=XVersionedDownstreamResource", - "namespace": "default", - "versions": "requested v1alpha1, stored v1", - "count": "1", - }), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "AdditionAtANonStorageVersionWarnsOnStderr": { - reason: "In text mode the storage-version note reaches stderr, the only place a person sees it", - dryRunOn: "existing", - inputFiles: []string{"testdata/diff/new-xr.yaml"}, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition-with-non-storage-version-downstream.yaml", - "testdata/diff/resources/functions.yaml", - }, - noColor: true, - expectedStderrContains: []string{ - `WARNING: did not verify added resources against the apiserver (--dry-run-on=existing); they are stored at a version other than the one requested, ` + - `whose defaults and conversion the cluster may apply but their diffs do not ` + - `(count=1, gvk=ns.nop.example.org/v1alpha1, Kind=XVersionedDownstreamResource, namespace=default, versions=requested v1alpha1, stored v1)`, - }, - expectedOutput: "+++ XVersionedDownstreamResource/test-resource-versioned", - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "XRDDefaultsNoOverride": { - reason: "Validates that XRD defaults do not override user-specified values", - outputFormat: "json", - inputFiles: []string{"testdata/diff/xr-with-overridden-defaults.yaml"}, - setupFiles: []string{ - "testdata/diff/resources/xrd-with-defaults.yaml", - "testdata/diff/resources/composition-with-defaults.yaml", - "testdata/diff/resources/functions.yaml", - }, - expectedStructured: simtest.Expect(). - WithSummary(1, 0, 0). - WithAddedResource("XTestDefaultResource", "test-resource-with-overrides", "default"). - WithField("spec.region", "us-west-2"). - WithField("spec.size", "xlarge"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "NewNestedXRCreatesChildren": { - reason: "Validates that new nested XR creates child XR and downstream resources", - outputFormat: "json", - setupFiles: []string{ - // XRDs for parent and child - "testdata/diff/resources/nested/parent-xrd.yaml", - "testdata/diff/resources/nested/child-xrd.yaml", - // Compositions for parent and child - "testdata/diff/resources/nested/parent-composition.yaml", - "testdata/diff/resources/nested/child-composition.yaml", - // XRD for downstream managed resource - "testdata/diff/resources/xdownstreamenvresource-xrd.yaml", - "testdata/diff/resources/functions.yaml", - }, - inputFiles: []string{"testdata/diff/new-nested-xr.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(3, 0, 0). - WithAddedResource("XChildResource", "test-parent-child", "default"). - WithField("spec.childField", "parent-value"). - And(). - WithAddedResource("XDownstreamResource", "test-parent-child-managed", "default"). - WithField("spec.forProvider.configData", "parent-value"). - And(). - WithAddedResource("XParentResource", "test-parent", "default"). - WithField("spec.parentField", "parent-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // Issue #485. The unit test for the depth guard uses a render stub that caps its own recursion, - // so it cannot show what a real cycle does across real renders: real templates, real names that - // grow at every level (kept under the generated CRDs' 63-character limit by a short suffix), real - // schema validation and dry-run at each level. This drives the whole path. It fails at exactly one level past the - // default --max-nested-depth of 10, attributed to the XR the user named. - "CyclicCompositionStopsAtMaxNestedDepth": { - reason: "A composition cycle (XCycleA -> XCycleB -> XCycleA ...) must stop at --max-nested-depth with a clear error, not recurse until the stack overflows (#485)", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/cycle/xrds.yaml", - "testdata/diff/resources/cycle/compositions.yaml", - "testdata/diff/resources/functions.yaml", - }, - inputFiles: []string{"testdata/diff/new-cycle-xr.yaml"}, - expectedError: true, - expectedErrorContains: "maximum nesting depth exceeded: XCycleB/test-cycle-c-c-c-c-c-c-c-c-c-c-c (nested depth 11) is nested 11 levels deep, but --max-nested-depth is 10", - expectedExitCode: dp.ExitCodeToolError, - expectedStructured: simtest.Expect(). - WithError("XCycleA/test-cycle"). - WithMessageContaining("maximum nesting depth exceeded"), - }, - "ModifiedNestedXRPropagatesChanges": { - reason: "Validates that modified nested XR propagates changes through child XR to downstream resources", - outputFormat: "json", - setupFiles: []string{ - // XRDs for parent and child - "testdata/diff/resources/nested/parent-xrd.yaml", - "testdata/diff/resources/nested/child-xrd.yaml", - // Compositions for parent and child - "testdata/diff/resources/nested/parent-composition.yaml", - "testdata/diff/resources/nested/child-composition.yaml", - // XRD for downstream managed resource - "testdata/diff/resources/xdownstreamenvresource-xrd.yaml", - "testdata/diff/resources/functions.yaml", - }, - // Existing resources: the parent XR composes the child XR, which composes the managed resource - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - { - OwnerFile: "testdata/diff/resources/nested/existing-parent-xr.yaml", - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - "testdata/diff/resources/nested/existing-child-xr.yaml": { - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - "testdata/diff/resources/nested/existing-managed-resource.yaml": nil, - }, - }, - }, - }, - }, - inputFiles: []string{"testdata/diff/modified-nested-xr.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(0, 3, 0). - WithModifiedResource("XChildResource", "test-parent-child", "default"). - WithFieldChange("spec.childField", "existing-value", "modified-value"). - And(). - WithModifiedResource("XDownstreamResource", "test-parent-child-managed", "default"). - WithFieldChange("spec.forProvider.configData", "existing-value", "modified-value"). - And(). - WithModifiedResource("XParentResource", "test-parent", "default"). - WithFieldChange("spec.parentField", "existing-value", "modified-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // #503, for a nested XR. It is defaulted before its own render, so that render input carries - // spec.tier: standard although the parent's composition never sets it. Sending it would claim - // spec.tier from the manager that set it to premium. - "XRDDefaultedFieldOnNestedXROwnedByAnotherManagerIsNotReportedChanged": { - reason: "An XRD-defaulted field a parent composition omits from a nested XR keeps another manager's non-default value in the diff", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/nested/parent-xrd.yaml", - "testdata/diff/resources/nested/child-xrd.yaml", - "testdata/diff/resources/nested/parent-composition.yaml", - "testdata/diff/resources/nested/child-composition.yaml", - "testdata/diff/resources/xdownstreamenvresource-xrd.yaml", - "testdata/diff/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - { - OwnerFile: "testdata/diff/resources/nested/existing-parent-xr.yaml", - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - "testdata/diff/resources/nested/existing-child-xr.yaml": { - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - "testdata/diff/resources/nested/existing-managed-resource.yaml": nil, - }, - }, - }, - }, - }, - fieldManagerApplies: []integration.FieldManagerApply{ - {File: "testdata/diff/resources/nested/existing-child-xr-tier-other-manager.yaml", FieldManager: "platform-operator"}, - }, - inputFiles: []string{"testdata/diff/modified-nested-xr.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(0, 3, 0). - WithModifiedResource("XChildResource", "test-parent-child", "default"). - WithFieldChange("spec.childField", "existing-value", "modified-value"). - WithFieldChange("spec.tier", "premium", "premium"). - And(). - WithModifiedResource("XDownstreamResource", "test-parent-child-managed", "default"). - WithFieldChange("spec.forProvider.configData", "existing-value", "modified-value"). - And(). - WithModifiedResource("XParentResource", "test-parent", "default"). - WithFieldChange("spec.parentField", "existing-value", "modified-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // Composition Revision tests for v2 XRDs - "V2ManualPolicyPinnedRevision": { - reason: "Validates v2 XR with Manual update policy stays on pinned revision", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition-revision-v1.yaml", - "testdata/diff/resources/composition-v2.yaml", // Current composition is v2 - "testdata/diff/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/diff/resources/existing-xr-manual-v1.yaml", - "testdata/diff/resources/existing-downstream-manual-v1.yaml", - ), - }, - inputFiles: []string{"testdata/diff/modified-xr-manual-v1.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(0, 2, 0). - WithModifiedResource("XDownstreamResource", "test-manual-v1", "default"). - WithFieldChange("spec.forProvider.configData", "v1-existing-value", "v1-modified-value"). - And(). - WithModifiedResource("XNopResource", "test-manual-v1", "default"). - WithFieldChange("spec.coolField", "existing-value", "modified-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "V2AutomaticPolicyLatestRevision": { - reason: "Validates v2 XR with Automatic update policy uses latest revision", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition-revision-v1.yaml", - "testdata/diff/resources/composition-v2.yaml", // Current composition is v2 - "testdata/diff/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - // The XR is still on v1, and its composed resource holds v1's data. - integration.Composes( - "testdata/diff/resources/existing-xr-automatic.yaml", - "testdata/diff/resources/existing-downstream-automatic.yaml", - ), - }, - inputFiles: []string{"testdata/diff/modified-xr-automatic.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(0, 2, 0). - WithModifiedResource("XDownstreamResource", "test-automatic", "default"). - WithFieldChange("spec.forProvider.configData", "v1-existing-value", "v2-modified-value"). - And(). - WithModifiedResource("XNopResource", "test-automatic", "default"). - WithFieldChange("spec.coolField", "existing-value", "modified-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "V2AutomaticPolicyRevisionSelectorPinsMatchingRevision": { - // Issue #388 (Bug B): under Automatic policy WITH a compositionRevisionSelector, the XR - // resolves to the latest revision MATCHING the selector, not the latest overall. Here the - // selector pins channel=stable (revision 1) even though channel=preview (revision 2) is - // newer — so the diff must render against the stable template (stable-*), not preview-*. - reason: "Validates v2 Automatic XR with compositionRevisionSelector resolves to the selector-matching revision, not the newest overall", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition-revision-stable.yaml", // revision 1, channel=stable - "testdata/diff/resources/composition-revision-preview.yaml", // revision 2, channel=preview (newest) - "testdata/diff/resources/composition-v2.yaml", // current composition - "testdata/diff/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/diff/resources/existing-xr-selector-stable.yaml", - "testdata/diff/resources/existing-downstream-selector-stable.yaml", - ), - }, - inputFiles: []string{"testdata/diff/modified-xr-selector-stable.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(0, 2, 0). - WithModifiedResource("XDownstreamResource", "test-selector", "default"). - // stable-* (not preview-*) proves the selector, not "latest overall", chose the revision. - WithFieldChange("spec.forProvider.configData", "stable-existing-value", "stable-modified-value"). - And(). - WithModifiedResource("XNopResource", "test-selector", "default"). - WithFieldChange("spec.coolField", "existing-value", "modified-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "V2AutomaticPolicyRevisionSelectorNoMatchErrors": { - // The mirror of the above: a selector matching NO revision must fail the diff rather than - // silently rendering against a non-matching revision (accuracy over guessing). - reason: "Validates v2 Automatic XR whose compositionRevisionSelector matches no revision fails the diff", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition-revision-stable.yaml", - "testdata/diff/resources/composition-revision-preview.yaml", - "testdata/diff/resources/composition-v2.yaml", - "testdata/diff/resources/functions.yaml", - }, - inputFiles: []string{"testdata/diff/modified-xr-selector-nomatch.yaml"}, - expectedError: true, - expectedErrorContains: "match selector", - expectedExitCode: dp.ExitCodeToolError, - }, - "V2ManualRevisionUpgradeDiff": { - reason: "Validates v2 XR changing revision in Manual mode shows upgrade diff", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition-revision-v1.yaml", - "testdata/diff/resources/composition-v2.yaml", - "testdata/diff/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/diff/resources/existing-xr-manual-v1.yaml", - "testdata/diff/resources/existing-downstream-manual-v1.yaml", - ), - }, - inputFiles: []string{"testdata/diff/modified-xr-manual-upgrade-to-v2.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(0, 2, 0). - WithModifiedResource("XDownstreamResource", "test-manual-v1", "default"). - WithFieldChange("spec.forProvider.configData", "v1-existing-value", "v2-modified-value"). - And(). - WithModifiedResource("XNopResource", "test-manual-v1", "default"). - WithFieldChange("spec.coolField", "existing-value", "modified-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "CompositionRevisionUpgradesResourceAPIVersion": { - // NOTE: This test validates that resources are correctly matched across API version changes, - // avoiding delete/recreate. The composition template changes from v1beta1 to v1beta2, but - // Kubernetes automatically converts resources between served API versions. When we query for - // the v1beta2 resource, Kubernetes finds the v1beta1 resource and returns it auto-converted - // to v1beta2. From Kubernetes' perspective, the resource exists as both versions simultaneously, - // so there's no apiVersion field change to show in the diff. The important thing is that the - // resource is matched (unchanged, not ---/+++), preventing delete/recreate operations. - // - // The XR composes the resource as Crossplane leaves it: controlled by the XR, and named in the - // XR's resourceRefs at v1beta1, the version it was composed at. Crossplane's garbage collector - // matches what an XR composed to what it renders by composition resource name, so it keeps the - // resource; removal detection must too, rather than report the v1beta1 reference unmatched by - // the v1beta2 render (#552). - reason: "Validates XR upgrading composition revision that changes resource API version shows as update not remove/add", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - // NOTE: xapimigrate CRD is auto-loaded from testdata/diff/crds/ - // We apply no XRD for XApiMigrateResource because it is a regular managed resource - // (not a composite), and an XRD would make it composite. - "testdata/diff/resources/api-version-composition-revision-v1.yaml", - "testdata/diff/resources/api-version-composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - { - OwnerFile: "testdata/diff/resources/existing-api-version-xr-rev1.yaml", - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - "testdata/diff/resources/existing-api-version-downstream-v1beta1.yaml": nil, - }, - }, - }, - inputFiles: []string{"testdata/diff/modified-api-version-xr-rev2.yaml"}, - // Key assertion: nothing is added or removed, proving API version migration works. The - // XApiMigrateResource is stored identically at either version, so it renders unchanged; an - // unmatched one would be a +++ addition plus a --- removal. Only the XR's revision ref changes. - expectedStructured: simtest.Expect(). - WithSummary(0, 1, 0). - WithModifiedResource("XNopResource", "test-api-version-xr", "default"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "CompositionRevisionUpgradesResourceAPIVersionAndChangesIt": { - // The same upgrade, with a change to the XR that the new revision renders into the resource too. - // The resource is one object, modified once at the version it is rendered at. Matched by API - // version, the v1beta1 reference was a removal contradicting that modification, and the xr - // command refused to combine the two diffs of one object (#552). - reason: "Validates an API version change plus a field change is one modification, not a removal and a modification", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/api-version-composition-revision-v1.yaml", - "testdata/diff/resources/api-version-composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - { - OwnerFile: "testdata/diff/resources/existing-api-version-xr-rev1.yaml", - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - "testdata/diff/resources/existing-api-version-downstream-v1beta1.yaml": nil, - }, - }, - }, - inputFiles: []string{"testdata/diff/modified-api-version-xr-rev2-changed-field.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(0, 2, 0). - WithModifiedResource("XApiMigrateResource", "test-api-version-xr-api-resource", "default"). - WithFieldChange("spec.forProvider.configData", "test-value", "changed-value"). - And(). - WithModifiedResource("XNopResource", "test-api-version-xr", "default"). - WithFieldChange("spec.coolField", "test-value", "changed-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "V2SwitchManualToAutomatic": { - reason: "Validates v2 XR switching from Manual to Automatic mode uses latest revision", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition-revision-v1.yaml", - "testdata/diff/resources/composition-v2.yaml", - "testdata/diff/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/diff/resources/existing-xr-manual-v1.yaml", - "testdata/diff/resources/existing-downstream-manual-v1.yaml", - ), - }, - inputFiles: []string{"testdata/diff/modified-xr-switch-to-automatic.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(0, 2, 0). - WithModifiedResource("XDownstreamResource", "test-manual-v1", "default"). - WithFieldChange("spec.forProvider.configData", "v1-existing-value", "v2-modified-value"). - And(). - WithModifiedResource("XNopResource", "test-manual-v1", "default"). - WithFieldChange("spec.coolField", "existing-value", "modified-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "V2NetNewManualNoRevRef": { - reason: "Validates v2 net new XR with Manual policy but no revision ref uses latest revision", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition-revision-v1.yaml", - "testdata/diff/resources/composition-v2.yaml", // Current composition is v2 - "testdata/diff/resources/functions.yaml", - }, - inputFiles: []string{"testdata/diff/new-xr-manual-no-ref.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(2, 0, 0). - WithAddedResource("XDownstreamResource", "test-manual-no-ref", "default"). - WithField("spec.forProvider.configData", "v2-new-value"). - And(). - WithAddedResource("XNopResource", "test-manual-no-ref", "default"). - WithField("spec.coolField", "new-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // Composition Revision tests for v1 XRDs (Crossplane 1.20 compatibility) - "V1ManualRevisionUpgradeDiff": { - reason: "Validates v1 XR with Manual update policy changing revision shows upgrade diff", - outputFormat: "json", - xrdAPIVersion: integration.XRDV1, - setupFiles: []string{ - "testdata/diff/resources/legacy-xrd.yaml", - "testdata/diff/resources/legacy-composition-revision-v1.yaml", - "testdata/diff/resources/legacy-composition-v2.yaml", - "testdata/diff/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/diff/resources/existing-legacy-xr-manual-v1.yaml", - "testdata/diff/resources/existing-legacy-downstream-manual-v1.yaml", - ), - }, - inputFiles: []string{"testdata/diff/modified-legacy-xr-manual-upgrade-to-v2.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(0, 2, 0). - WithModifiedResource("XDownstreamResource", "test-legacy-manual-v1", ""). - WithFieldChange("spec.forProvider.configData", "v1-existing-value", "v2-modified-value"). - And(). - WithModifiedResource("XNopResource", "test-legacy-manual-v1", ""). - WithFieldChange("spec.coolField", "existing-value", "modified-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // Issue #499: Crossplane's composite reconciler writes compositionRevisionRef, so a hand-written - // manifest omits it — and an apply that omits a field leaves it where it is. The render must - // therefore see the cluster copy's ref. Here the composition propagates the revision name into the - // composed resource, so a render that dropped the ref reported configData changing from the real - // revision name to empty for an input identical to what is deployed. - "ExistingXROmittingRevisionRefRendersWithClusterRef": { - reason: "An existing XR whose input omits compositionRevisionRef renders with the cluster's ref, so a template reading the revision name shows no spurious change", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/comp/resources/revision-templating-composition.yaml", - "testdata/comp/resources/revision-templating-composition-revision.yaml", - "testdata/diff/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/comp/resources/existing-xr-revision-ref.yaml", - "testdata/comp/resources/existing-downstream-revision-ref.yaml", - ), - }, - inputFiles: []string{"testdata/diff/existing-xr-revision-ref-omitted.yaml"}, - expectedStructured: simtest.Expect().WithSummary(0, 0, 0), - expectedExitCode: dp.ExitCodeSuccess, - }, - // Issue #499, the consequence for which revision renders. The cluster's ref is inherited before the - // composition is resolved, so a Manual composite whose input omits the ref stays on the revision it - // is pinned to (-911318d, the v1 template), as it would in the cluster, instead of being rendered - // against the latest one (-430f3f3, v2). Compare V2ManualPolicyPinnedRevision, the same change with - // the ref spelled out. - "V2ManualPolicyOmittedRevisionRefStaysPinned": { - reason: "Validates v2 XR with Manual update policy whose input omits compositionRevisionRef stays on the cluster's pinned revision", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/composition-revision-v1.yaml", - "testdata/diff/resources/composition-v2.yaml", // Current composition is v2 - "testdata/diff/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/diff/resources/existing-xr-manual-v1.yaml", - "testdata/diff/resources/existing-downstream-manual-v1.yaml", - ), - }, - inputFiles: []string{"testdata/diff/modified-xr-manual-v1-omits-revision-ref.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(0, 2, 0). - WithModifiedResource("XDownstreamResource", "test-manual-v1", "default"). - // v1-* (not v2-*) proves the pinned revision, not the latest, rendered. - WithFieldChange("spec.forProvider.configData", "v1-existing-value", "v1-modified-value"). - And(). - WithModifiedResource("XNopResource", "test-manual-v1", "default"). - WithFieldChange("spec.coolField", "existing-value", "modified-value"), - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // The v1-path (spec.compositionRevisionRef) twin of the above. - "V1ManualPolicyOmittedRevisionRefStaysPinned": { - reason: "Validates v1 XR with Manual update policy whose input omits compositionRevisionRef stays on the cluster's pinned revision", - outputFormat: "json", - xrdAPIVersion: integration.XRDV1, - setupFiles: []string{ - "testdata/diff/resources/legacy-xrd.yaml", - "testdata/diff/resources/legacy-composition-revision-v1.yaml", - "testdata/diff/resources/legacy-composition-v2.yaml", - "testdata/diff/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/diff/resources/existing-legacy-xr-manual-v1.yaml", - "testdata/diff/resources/existing-legacy-downstream-manual-v1.yaml", - ), - }, - inputFiles: []string{"testdata/diff/modified-legacy-xr-manual-v1-omits-revision-ref.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(0, 2, 0). - WithModifiedResource("XDownstreamResource", "test-legacy-manual-v1", ""). - WithFieldChange("spec.forProvider.configData", "v1-existing-value", "v1-modified-value"). - And(). - WithModifiedResource("XNopResource", "test-legacy-manual-v1", ""). - WithFieldChange("spec.coolField", "existing-value", "modified-value"), - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // Issue #499 for a nested XR. It is rendered from its parent's output, which never carries - // compositionRevisionRef, so it must inherit the cluster copy's, which ProcessNestedXRs hands it - // rather than fetching it again. The child's composition propagates the revision name into the - // managed resource; the parent's change reaches the child's childField but not that name, so the - // managed resource must not be reported. - "NestedXRInheritsClusterRevisionRef": { - reason: "A nested XR renders with its cluster copy's compositionRevisionRef, so a template reading the revision name shows no spurious change", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/nested/parent-xrd.yaml", - "testdata/diff/resources/nested/child-xrd.yaml", - "testdata/diff/resources/nested/parent-composition.yaml", - "testdata/diff/resources/nested/child-revision-templating-composition.yaml", - "testdata/diff/resources/xdownstreamenvresource-xrd.yaml", - "testdata/diff/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - { - OwnerFile: "testdata/diff/resources/nested/existing-parent-xr.yaml", - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - "testdata/diff/resources/nested/existing-child-xr-revision-ref.yaml": { - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - "testdata/diff/resources/nested/existing-managed-resource-revision-ref.yaml": nil, - }, - }, - }, - }, - }, - inputFiles: []string{"testdata/diff/modified-nested-xr.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(0, 2, 0). - WithModifiedResource("XChildResource", "test-parent-child", "default"). - WithFieldChange("spec.childField", "existing-value", "modified-value"). - // The nested XR's own reconciler wrote the ref, not the parent's composed apply, so the - // parent's render, which omits it, leaves it in place. - WithFieldChange("spec.crossplane.compositionRevisionRef.name", - "xchildresources.nested.example.org-96afc06", "xchildresources.nested.example.org-96afc06"). - And(). - WithModifiedResource("XParentResource", "test-parent", "default"). - WithFieldChange("spec.parentField", "existing-value", "modified-value"), - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // Issue #498, as a user meets it: an Automatic claim's manifest omits the compositionRevisionRef the - // claim syncer copied onto the cluster copy. Under Automatic the claim's ref is never propagated to - // the backing XR anyway; the XR controller owns the XR's own, which is what render must see. Before, - // the backing XR's ref was dropped, so the template read no revision name and configData was - // reported changing to "v2::existing-value" for an unchanged claim. - "AutomaticClaimOmittingRevisionRefRendersWithBackingXRRef": { - reason: "An Automatic claim renders with its backing XR's compositionRevisionRef, so a template reading the revision name shows no spurious change", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/existing-namespace.yaml", - "testdata/diff/resources/pinned-claim/definitions.yaml", - "testdata/diff/resources/functions.yaml", - "testdata/diff/resources/pinned-claim/existing-automatic.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/diff/resources/pinned-claim/existing-automatic-xr.yaml", - "testdata/diff/resources/pinned-claim/existing-automatic-downstream.yaml", - ), - }, - xrdAPIVersion: integration.XRDV1, // the XRD is v1, so Crossplane writes the XR's resourceRefs at spec.resourceRefs - inputFiles: []string{"testdata/diff/unchanged-automatic-pinned-claim.yaml"}, - // No warnings either: the composed resource belongs to the claim's backing XR (#534). - expectedStructured: simtest.Expect().WithNoWarnings().WithSummary(0, 0, 0), - expectedExitCode: dp.ExitCodeSuccess, - }, - // A Manual claim's pin lives on its backing XR: the claim syncer copies the ref back to a claim only - // under Automatic. Composition resolution and render must both see that pin. Before, the composition - // was resolved from the claim, which names no revision, so the latest one (v2) was chosen, while the - // render read the backing XR's ref (revision 1): configData became "v2:...-3d33597:...", a revision's template - // paired with another revision's name. - "ManualClaimRendersAgainstItsBackingXRsPinnedRevision": { - reason: "A Manual claim whose backing XR is pinned to an older revision renders that revision, with that revision's name", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/existing-namespace.yaml", - "testdata/diff/resources/pinned-claim/definitions.yaml", - "testdata/diff/resources/functions.yaml", - "testdata/diff/resources/pinned-claim/existing-manual.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/diff/resources/pinned-claim/existing-manual-xr.yaml", - "testdata/diff/resources/pinned-claim/existing-manual-downstream.yaml", - ), - }, - xrdAPIVersion: integration.XRDV1, // the XRD is v1, so Crossplane writes the XR's resourceRefs at spec.resourceRefs - inputFiles: []string{"testdata/diff/modified-manual-pinned-claim.yaml"}, - expectedStructured: simtest.Expect(). - // The composed resource is labelled with the backing XR's name, which the claim's resourceRef - // names, so it already belongs to this claim: no ownership advisory (#534). - WithNoWarnings(). - WithSummary(0, 2, 0). - WithModifiedResource("Pinned", "manual-claim", "existing-namespace"). - WithFieldChange("spec.coolField", "existing-value", "modified-value"). - And(). - WithModifiedResource("XDownstreamResource", "manual-claim-x1y2z", ""). - WithFieldChange("spec.forProvider.configData", - "v1:xpinneds.pinned.diff.example.org-3d33597:existing-value", - "v1:xpinneds.pinned.diff.example.org-3d33597:modified-value"), - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // Issue #536. Crossplane's composite reconciler resolves the revision, writes it to - // compositionRevisionRef, and then composes, so an Automatic composite whose ref lags behind the - // latest revision is composed from the latest one AND sees its name. Before, the latest revision - // rendered while the template read the stale ref: configData became "v2:...-3d33597:...", a revision's - // template paired with another revision's name. - "AutomaticXRWithLaggingRevisionRefRendersLatestRevisionAndName": { - reason: "An Automatic XR whose cluster ref names an older revision renders the latest revision, with that revision's name", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/pinned-claim/definitions.yaml", - "testdata/diff/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/diff/resources/pinned-claim/existing-lagging-automatic-xr.yaml", - "testdata/diff/resources/pinned-claim/existing-lagging-automatic-downstream.yaml", - ), - }, - xrdAPIVersion: integration.XRDV1, // the XRD is v1, so Crossplane writes the XR's resourceRefs at spec.resourceRefs - inputFiles: []string{"testdata/diff/unchanged-lagging-automatic-xr.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(0, 1, 0). - WithModifiedResource("XDownstreamResource", "lagging-xr", ""). - WithFieldChange("spec.forProvider.configData", - "v1:xpinneds.pinned.diff.example.org-3d33597:existing-value", - "v2:xpinneds.pinned.diff.example.org-806289b:existing-value"), - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // An XRD's defaultCompositionUpdatePolicy applies to a new composite before Crossplane resolves its - // revision, so a new composite that names a revision and leaves the policy to an XRD defaulting to - // Manual renders the revision it names. Before, the composition was resolved before defaulting, as - // Automatic, so the latest revision (v2) rendered. - "NewXRUnderManualDefaultingXRDRendersTheRevisionItNames": { - reason: "A new XR whose XRD defaults its update policy to Manual is resolved as Manual", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/manual-default/definitions.yaml", - "testdata/diff/resources/functions.yaml", - }, - inputFiles: []string{"testdata/diff/new-manual-default-xr.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(2, 0, 0). - WithAddedResource("XManualDefault", "new-manual-default", "default"). - And(). - WithAddedResource("XDownstreamResource", "new-manual-default", "default"). - WithField("spec.forProvider.configData", "v1-new-value"), - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // Crossplane selects a composition only for a composite with no compositionRef, and never - // re-selects. A composite whose input relies on a selector therefore keeps the compositionRef - // Crossplane wrote, here composition A, even though the selector now matches two compositions. - // Before, the selector was re-evaluated against the input, and the diff failed as ambiguous. - "ExistingXRSelectingItsCompositionKeepsTheClusterCompositionRef": { - reason: "An existing XR whose input selects its composition by label renders with the composition Crossplane already bound it to", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/functions.yaml", - "testdata/diff/resources/selector-bound/setup.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/diff/resources/selector-bound/existing-xr.yaml", - "testdata/diff/resources/selector-bound/existing-downstream.yaml", - ), - }, - inputFiles: []string{"testdata/diff/modified-xr-selector-bound.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(0, 2, 0). - WithModifiedResource("XDownstreamResource", "selector-bound", "default"). - WithFieldChange("spec.forProvider.configData", "a-existing-value", "a-modified-value"). - And(). - WithModifiedResource("XNopResource", "selector-bound", "default"). - WithFieldChange("spec.coolField", "existing-value", "modified-value"), - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // v2 XRD with v1-style composition paths (issue #206) - "V2XRDWithV1StyleCompositionPaths": { - reason: "Validates v2 XRD using v1-style spec.compositionRef paths are correctly recognized (issue #206)", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/v2-xrd-with-v1-paths.yaml", - "testdata/diff/resources/v2-xrd-with-v1-paths-composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/diff/resources/existing-v2xrd-v1paths-xr.yaml", - "testdata/diff/resources/existing-v2xrd-v1paths-downstream.yaml", - ), - }, - inputFiles: []string{"testdata/diff/resources/modified-v2xrd-v1paths-xr.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(0, 2, 0). - WithModifiedResource("XDownstreamResource", "test-v2xrd-v1paths", "default"). - WithFieldChange("spec.forProvider.configData", "existing-value", "modified-value"). - And(). - WithModifiedResource("XNopResource", "test-v2xrd-v1paths", "default"). - WithFieldChange("spec.coolField", "existing-value", "modified-value"), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - // A claim passed alongside its backing XR: Crossplane's claim controller writes that XR (a - // server-side apply with forced ownership, stamping claimRef and the claim labels), so the XR - // input would be a second writer. Rejected on the XR; the claim's diff still runs. The binding - // is read from the claim's cluster copy (spec.resourceRef) and from the XR's claim labels. - "ClaimPassedWithItsBackingXRIsRejected": { - reason: "A claim and the XR bound to it, passed together, is an input error on the XR", - setupFiles: []string{ - "testdata/diff/resources/existing-namespace.yaml", - "testdata/diff/resources/claim-nested/parent-definition.yaml", - "testdata/diff/resources/claim-nested/child-definition.yaml", - "testdata/diff/resources/claim-nested/parent-composition.yaml", - "testdata/diff/resources/claim-nested/child-composition.yaml", - "testdata/diff/resources/functions.yaml", - "testdata/diff/resources/claim-nested/existing-claim.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - { - OwnerFile: "testdata/diff/resources/claim-nested/existing-parent-xr.yaml", - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - "testdata/diff/resources/claim-nested/existing-child-xr.yaml": { - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - "testdata/diff/resources/claim-nested/existing-managed-resource.yaml": nil, - }, - }, - }, - }, - }, - xrdAPIVersion: integration.XRDV1, // the XRDs are v1, so Crossplane writes the XRs' resourceRefs at spec.resourceRefs - inputFiles: []string{ - "testdata/diff/modified-claim-nested.yaml", - "testdata/diff/resources/claim-nested/existing-parent-xr.yaml", - }, - expectedError: true, - expectedErrorContains: "XNopParent/existing-parent-claim-82crv is the XR bound to claim " + - "ParentNopClaim/existing-parent-claim, which Crossplane's claim controller writes", - expectedExitCode: dp.ExitCodeToolError, - }, - "ModifiedClaimWithNestedXRsShowsDiff": { - reason: "Validates that modified Claims with nested XRs show proper diff (3 modified resources)", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/existing-namespace.yaml", - // XRDs for parent and child Claims (the harness generates their XR and Claim CRDs) - "testdata/diff/resources/claim-nested/parent-definition.yaml", - "testdata/diff/resources/claim-nested/child-definition.yaml", - // Compositions for parent and child - "testdata/diff/resources/claim-nested/parent-composition.yaml", - "testdata/diff/resources/claim-nested/child-composition.yaml", - "testdata/diff/resources/functions.yaml", - // Claim is set up separately (not via owner refs - it uses spec.resourceRef to link to backing XR) - "testdata/diff/resources/claim-nested/existing-claim.yaml", - }, - // Owner ref hierarchy matches Crossplane's actual ownership model: - // - Backing XR is the root of the owner ref tree (no owner refs) - // - Nested XR has owner ref to backing XR - // - Managed resource has owner ref to nested XR - // Note: Claim is NOT in this hierarchy - it links to backing XR via spec.resourceRef, not owner refs - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - { - // Backing XR is the root of owner ref tree - OwnerFile: "testdata/diff/resources/claim-nested/existing-parent-xr.yaml", - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - // Nested XR owned by backing XR - "testdata/diff/resources/claim-nested/existing-child-xr.yaml": { - // Managed resource owned by nested XR - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - "testdata/diff/resources/claim-nested/existing-managed-resource.yaml": nil, - }, - }, - }, - }, - }, - inputFiles: []string{"testdata/diff/modified-claim-nested.yaml"}, - expectedStructured: simtest.Expect(). - WithSummary(0, 3, 0). - WithModifiedResource("ClusterNopResource", "existing-parent-claim-82crv-nop", ""). - And(). - WithModifiedResource("ParentNopClaim", "existing-parent-claim", "default"). - WithFieldChange("spec.parentField", "existing-parent-value", "modified-parent-value"). - And(). - WithModifiedResource("XNopChild", "existing-parent-claim-82crv-child", ""). - WithFieldChange("spec.childField", "existing-parent-value", "modified-parent-value"). - // The nested XR's own reconciler wrote its composition refs, not the parent's composed - // apply, so the parent's render, which omits them, leaves them in place. - WithFieldChange("spec.compositionRef.name", "child-nop-claim-composition", "child-nop-claim-composition"). - WithFieldChange("spec.compositionRevisionRef.name", "child-nop-claim-composition-db22ad5", "child-nop-claim-composition-db22ad5"), - xrdAPIVersion: integration.XRDV1, // Use V1 style resourceRefs since XRDs have claims - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - }, - "FunctionCredentialsAutoFetch": { - reason: "Successfully renders XR when composition references credentials that exist in cluster", - outputFormat: "json", - inputFiles: []string{"testdata/diff/new-xr-with-creds.yaml"}, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/credentials/composition-with-creds.yaml", - "testdata/diff/resources/credentials/function-credentials-secret.yaml", - "testdata/diff/resources/functions.yaml", - }, - expectedExitCode: dp.ExitCodeDiffDetected, - expectedStructured: simtest.Expect(). - WithSummary(2, 0, 0). - WithAddedResource("XDownstreamResource", "test-resource-with-creds", "default"). - WithField("spec.forProvider.configData", "test-value-creds"). - And(). - WithAddedResource("XNopResource", "test-resource-with-creds", "default"). - WithField("spec.coolField", "test-value-creds"), - expectedError: false, - }, - "FunctionCredentialsFromCLI": { - reason: "Successfully renders XR when credentials provided via --function-credentials flag", - outputFormat: "json", - inputFiles: []string{"testdata/diff/new-xr-with-creds.yaml"}, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/credentials/composition-with-creds.yaml", - // Note: NOT setting up the secret in cluster - testing CLI override - "testdata/diff/resources/functions.yaml", - }, - // Credentials loaded from CLI flag file - functionCredentials: "testdata/diff/resources/credentials/cli-credentials.yaml", - expectedExitCode: dp.ExitCodeDiffDetected, - // WithNoWarnings is the point of this case for issue #481: the secret the composition - // references is deliberately absent from the cluster and supplied by --function-credentials - // instead, so the shortfall advisory must NOT fire. It used to, because the advisory was - // raised inside the credential client, which runs before the CLI-supplied secrets are merged - // in — telling a user who had just solved the problem to go and solve it. - expectedStructured: simtest.Expect(). - WithSummary(2, 0, 0). - WithNoWarnings(). - WithAddedResource("XDownstreamResource", "test-resource-with-creds", "default"). - WithField("spec.forProvider.configData", "test-value-creds"). - And(). - WithAddedResource("XNopResource", "test-resource-with-creds", "default"). - WithField("spec.coolField", "test-value-creds"), - expectedError: false, - }, - // Paired sequencer gating tests — the fixture sequencer-gating-composition.yaml - // is deliberately correct (rules match composition-resource-names stage0-/stage1- - // AND auto-ready runs BEFORE sequencer so DesiredComposed.Ready is populated - // before sequencer's gating check). Together these two cases discriminate - // --eventual-state from default behavior: only eventual-state surfaces stage1. - "SequencerGatesLaterStageWithoutEventualState": { - reason: "Without --eventual-state, function-sequencer hides stage1 until stage0 is ready", - timeout: longTimeout, - outputFormat: "json", - // eventualState deliberately false - inputFiles: []string{"testdata/diff/resources/sequencer-xr.yaml"}, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/sequencer-gating-composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - expectedExitCode: dp.ExitCodeDiffDetected, - // Expect: XR + stage0 added; stage1 NOT present (gated). - expectedStructured: simtest.Expect(). - WithSummary(2, 0, 0). - WithAddedResource("XNopResource", "sequencer-test", "default"). - WithField("spec.coolField", "test-value"). - And(). - WithAddedResource("XDownstreamResource", "0-stage0-resource", "default"). - WithField("spec.forProvider.configData", "test-value"), - expectedError: false, - }, - "SequencerGatedStageAppearsWithEventualState": { - reason: "With --eventual-state, sequencer-gated stage1 surfaces via synthesize-Ready iteration", - timeout: longTimeout, // Multiple simulation iterations need more time - outputFormat: "json", - eventualState: true, - inputFiles: []string{"testdata/diff/resources/sequencer-xr.yaml"}, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/sequencer-gating-composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - expectedExitCode: dp.ExitCodeDiffDetected, - // Expect: XR + stage0 + stage1 all added (stage1 unlocked via eventual-state). - expectedStructured: simtest.Expect(). - WithSummary(3, 0, 0). - WithAddedResource("XNopResource", "sequencer-test", "default"). - WithField("spec.coolField", "test-value"). - And(). - WithAddedResource("XDownstreamResource", "0-stage0-resource", "default"). - WithField("spec.forProvider.configData", "test-value"). - And(). - WithAddedResource("XDownstreamResource", "1-stage1-resource", "default"). - WithField("spec.forProvider.configData", "test-value"), - expectedError: false, - }, - // Issue #564: an existing XR whose stage0 exists but is not Ready yet, the normal state of an - // XR whose first stage is still provisioning. Nothing renders that is not already observed, so - // eventual state used to stop at once and never synthesize stage0's Ready; but Crossplane - // composes stage1 as soon as stage0 becomes Ready. - "SequencerGatedStageAppearsForExistingXRWithNotReadyStage0WithEventualState": { - reason: "With --eventual-state, stage1 surfaces when stage0 already exists but is not Ready", - timeout: longTimeout, // Multiple simulation iterations need more time - outputFormat: "json", - eventualState: true, - inputFiles: []string{"testdata/diff/resources/sequencer-xr.yaml"}, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/sequencer-gating-composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes("testdata/diff/resources/existing-sequencer-gating-xr.yaml", "testdata/diff/resources/existing-sequencer-gating-stage0.yaml"), - }, - expectedExitCode: dp.ExitCodeDiffDetected, - // Expect: XR and stage0 modified, stage1 added (unlocked by stage0's synthesized Ready). - expectedStructured: simtest.Expect(). - WithSummary(1, 2, 0). - WithModifiedResource("XNopResource", "sequencer-test", "default"). - WithFieldChange("spec.coolField", "existing-value", "test-value"). - And(). - WithModifiedResource("XDownstreamResource", "0-stage0-resource", "default"). - WithFieldChange("spec.forProvider.configData", "existing-value", "test-value"). - And(). - WithAddedResource("XDownstreamResource", "1-stage1-resource", "default"). - WithField("spec.forProvider.configData", "test-value"), - expectedError: false, - }, - // The converged counterpart: both stages exist and are Ready, so synthesizing Ready changes - // nothing and eventual state is the first render. Only the spec changes the input makes show. - "EventualStateAddsNothingWhenEveryStageIsReady": { - reason: "With --eventual-state, an XR whose composed resources are all Ready shows only its real changes", - timeout: longTimeout, - outputFormat: "json", - eventualState: true, - inputFiles: []string{"testdata/diff/resources/sequencer-xr.yaml"}, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/sequencer-gating-composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes("testdata/diff/resources/existing-sequencer-gating-xr.yaml", - "testdata/diff/resources/existing-sequencer-gating-stage0-ready.yaml", - "testdata/diff/resources/existing-sequencer-gating-stage1-ready.yaml"), - }, - expectedExitCode: dp.ExitCodeDiffDetected, - expectedStructured: simtest.Expect(). - WithSummary(0, 3, 0). - WithModifiedResource("XNopResource", "sequencer-test", "default"). - WithFieldChange("spec.coolField", "existing-value", "test-value"). - And(). - WithModifiedResource("XDownstreamResource", "0-stage0-resource", "default"). - WithFieldChange("spec.forProvider.configData", "existing-value", "test-value"). - And(). - WithModifiedResource("XDownstreamResource", "1-stage1-resource", "default"). - WithFieldChange("spec.forProvider.configData", "existing-value", "test-value"), - expectedError: false, - }, - // Paired composition-driven gating tests — the fixture conditional-gating-composition.yaml - // encodes gating in go-templating itself (no function-sequencer): stage1 is only rendered - // when observed stage0 has Ready=True. This isolates eventual-state simulation from - // function-sequencer and exercises the synthesize-Ready path directly. - "ConditionalGatingHidesLaterStageWithoutEventualState": { - reason: "Without --eventual-state, template conditionally gates stage1 because observed stage0 is not Ready", - timeout: longTimeout, - outputFormat: "json", - // eventualState deliberately false - inputFiles: []string{"testdata/diff/resources/conditional-gating-xr.yaml"}, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/conditional-gating-composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - expectedExitCode: dp.ExitCodeDiffDetected, - // Expect: XR + stage0 added; stage1 NOT present (gated by the template's Ready check). - expectedStructured: simtest.Expect(). - WithSummary(2, 0, 0). - WithAddedResource("XNopResource", "conditional-gating-test", "default"). - WithField("spec.coolField", "test-value"). - And(). - WithAddedResource("XDownstreamResource", "0-stage0-resource", "default"). - WithField("spec.forProvider.configData", "test-value"), - expectedError: false, - }, - "ConditionalGatedStageAppearsWithEventualState": { - reason: "With --eventual-state, synthesize-Ready flips stage0 Ready=True in observed so the template renders stage1", - timeout: longTimeout, // Multiple simulation iterations need more time - outputFormat: "json", - eventualState: true, - inputFiles: []string{"testdata/diff/resources/conditional-gating-xr.yaml"}, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/conditional-gating-composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - expectedExitCode: dp.ExitCodeDiffDetected, - // Expect: XR + stage0 + stage1 all added. - expectedStructured: simtest.Expect(). - WithSummary(3, 0, 0). - WithAddedResource("XNopResource", "conditional-gating-test", "default"). - WithField("spec.coolField", "test-value"). - And(). - WithAddedResource("XDownstreamResource", "0-stage0-resource", "default"). - WithField("spec.forProvider.configData", "test-value"). - And(). - WithAddedResource("XDownstreamResource", "1-stage1-resource", "default"). - WithField("spec.forProvider.configData", "test-value"), - expectedError: false, - }, - "EventualStateWithSequencerAndEnvironmentConfigs": { - reason: "Shows eventual state with function-sequencer AND function-environment-configs to verify requirements resolution during simulation", - timeout: longTimeout, // Multiple simulation iterations with requirements resolution need more time - // This test verifies that the eventual state simulation correctly resolves requirements - // (like environment configs) during its iterative rendering, not just in the final render. - // The composition uses: - // 1. function-environment-configs to fetch env config (requires requirements iteration) - // 2. function-go-templating to generate resources using env config data - // 3. function-sequencer to stage resources - // 4. function-auto-ready - outputFormat: "json", - eventualState: true, - inputFiles: []string{"testdata/diff/resources/sequencer-with-envconfig-xr.yaml"}, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/sequencer-with-envconfig-composition.yaml", - "testdata/diff/resources/functions.yaml", - "testdata/diff/resources/environment-config-v1beta1.yaml", - }, - expectedExitCode: dp.ExitCodeDiffDetected, - expectedStructured: simtest.Expect(). - WithSummary(3, 0, 0). // 2 downstream resources + 1 XR - WithAddedResource("XDownstreamResource", "0-stage0-resource", "default"). - WithField("spec.forProvider.configData", "test-value"). - WithField("spec.forProvider.resourceTier", "staging"). // From environment config - And(). - WithAddedResource("XDownstreamResource", "1-stage1-resource", "default"). - WithField("spec.forProvider.configData", "test-value"). - WithField("spec.forProvider.resourceTier", "staging"). // From environment config - And(). - WithAddedResource("XNopResource", "sequencer-envconfig-test", "default"). - WithField("spec.coolField", "test-value"), - expectedError: false, - }, - } - - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - runIntegrationTest(t, XRDiffTest, tt) - }) - } -} - -// TestCompDiffIntegration runs an integration test for the composition diff command. -func TestCompDiffIntegration(t *testing.T) { - t.Parallel() - - tests := map[string]IntegrationTestCase{ - "CompositionChangeImpactsXRs": { - reason: "Validates composition change impacts existing XRs", - // Set up existing XRs that use the original composition - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/original-composition.yaml", - "testdata/comp/resources/functions.yaml", - }, - // Add existing XRs that use the composition - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/comp/resources/existing-xr-1.yaml", - "testdata/comp/resources/existing-downstream-1.yaml", - ), - integration.Composes( - "testdata/comp/resources/existing-xr-2.yaml", - "testdata/comp/resources/existing-downstream-2.yaml", - ), - }, - // New composition files that will be diffed - inputFiles: []string{"testdata/comp/updated-composition.yaml"}, - namespace: "default", - expectedOutput: ` -=== Composition Changes === - -~~~ Composition/xnopresources.diff.example.org - apiVersion: apiextensions.crossplane.io/v1 - kind: Composition - metadata: - name: xnopresources.diff.example.org - spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: -- configData: {{ .observed.composite.resource.spec.coolField }} -- resourceTier: basic -+ configData: updated-{{ .observed.composite.resource.spec.coolField }} -+ resourceTier: premium - kind: GoTemplate - source: Inline - step: generate-resources - - functionRef: - name: function-auto-ready - step: automatically-detect-ready-composed-resources - ---- - -Summary: 1 modified - -Applying this composition creates a new CompositionRevision, which 2 composites would adopt. - -=== Affected Composite Resources === - - ⚠ XNopResource/another-resource (namespace: default) - ⚠ XNopResource/test-resource (namespace: default) - -Summary: 2 resources with changes - -=== Impact Analysis === - -~~~ XDownstreamResource/another-resource - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - annotations: - crossplane.io/composition-resource-name: nop-resource - labels: - crossplane.io/composite: another-resource - name: another-resource - namespace: default - spec: - forProvider: -- configData: another-existing-value -- resourceTier: basic -+ configData: updated-another-existing-value -+ resourceTier: premium - ---- -~~~ XDownstreamResource/test-resource - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - annotations: - crossplane.io/composition-resource-name: nop-resource - labels: - crossplane.io/composite: test-resource - name: test-resource - namespace: default - spec: - forProvider: -- configData: existing-value -- resourceTier: basic -+ configData: updated-existing-value -+ resourceTier: premium - ---- - -Summary: 2 modified`, - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - noColor: true, - }, - // Issue #452: an XR with a deletionTimestamp is on Crossplane's teardown path and will never - // adopt the diffed composition's revision. Rendering it yields no composed resources (the - // composite reconciler takes its deletion path), so including it produces a meaningless - // impact analysis. It must be excluded and counted, leaving only the live XR evaluated. - "CompositionDiffExcludesDeletingXRs": { - reason: "A deleting XR is excluded from impact analysis and counted as filteredByDeletion; the live XR is still evaluated", - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/original-composition.yaml", - "testdata/comp/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - { - OwnerFile: "testdata/comp/resources/existing-xr-1.yaml", - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - "testdata/comp/resources/existing-downstream-1.yaml": nil, - }, - }, - { - OwnerFile: "testdata/comp/resources/existing-xr-deleting.yaml", - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - "testdata/comp/resources/existing-downstream-deleting.yaml": nil, - }, - }, - }, - deleteAfterSetup: []string{"testdata/comp/resources/existing-xr-deleting.yaml"}, - inputFiles: []string{"testdata/comp/updated-composition.yaml"}, - namespace: "default", - outputFormat: "json", - expectedExitCode: dp.ExitCodeDiffDetected, // the composition itself changed - // Total counts both discovered XRs; only the live one is evaluated (1 with changes), and - // the deleting one is reported via filteredByDeletion. In default-discovery mode filtered - // XRs are counted but not surfaced as impact entries, so only test-resource appears. - expectedStructured: simtest.Expect(). - WithComposition("xnopresources.diff.example.org"). - WithCompositionModified(). - WithAffectedResources(2, 1, 0, 0). - WithFilteredByDeletion(1). - WithXRImpact("XNopResource", "test-resource", "default", "changed"), - }, - // Issue #452, --resource mode: filtered XRs are surfaced explicitly there, so a user who names - // a deleting composite is told why it was skipped rather than getting silence. - "ResourceFilterSurfacesDeletingXR": { - reason: "--resource matching a deleting composite surfaces it as filtered with reason deleting", - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/original-composition.yaml", - "testdata/comp/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - { - OwnerFile: "testdata/comp/resources/existing-xr-deleting.yaml", - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - "testdata/comp/resources/existing-downstream-deleting.yaml": nil, - }, - }, - }, - deleteAfterSetup: []string{"testdata/comp/resources/existing-xr-deleting.yaml"}, - inputFiles: []string{"testdata/comp/updated-composition.yaml"}, - resources: []string{"default/deleting-resource"}, - outputFormat: "json", - expectedExitCode: dp.ExitCodeDiffDetected, // the composition itself changed - expectedStructured: simtest.Expect(). - WithComposition("xnopresources.diff.example.org"). - WithCompositionModified(). - WithFilteredByDeletion(1). - WithXRImpact("XNopResource", "deleting-resource", "default", "filtered"). - WithFilterReason("deleting"), - }, - // Issue #495, `comp` side: impact analysis renders each affected XR through the same observed- - // resource fetch as `xr`, so a composed resource deleted out of band is tolerated with the same - // warning. Being genuinely absent, it is then reported as something the XR will create. - "CompositionDiffComposedResourceDeletedOutOfBandWarns": { - reason: "comp against an XR whose resourceRefs name a deleted composed resource warns and reports it as an addition", - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/original-composition.yaml", - "testdata/comp/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - { - OwnerFile: "testdata/comp/resources/existing-xr-1.yaml", - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - "testdata/comp/resources/existing-downstream-1.yaml": nil, - }, - }, - }, - deleteAfterSetup: []string{"testdata/comp/resources/existing-downstream-1.yaml"}, - inputFiles: []string{"testdata/comp/updated-composition.yaml"}, - namespace: "default", - outputFormat: "json", - expectedExitCode: dp.ExitCodeDiffDetected, - expectedStructured: simtest.Expect(). - WithWarning("was not found in the cluster"). - WithWarningContext(map[string]string{ - "resource": "XDownstreamResource/test-resource", - "namespace": "default", - }). - And(). - WithComposition("xnopresources.diff.example.org"). - WithCompositionModified(). - WithAffectedResources(1, 1, 0, 0). - WithXRImpact("XNopResource", "test-resource", "default", "changed"). - WithDownstreamSummary(1, 0, 0). - WithDownstreamResource("added", "XDownstreamResource", "test-resource", "default"), - }, - // The comp side of TestDiffIntegration/ResourceRemovalSkipsResourcesTheXRDoesNotControl (#552): - // the updated composition stops rendering resource2, which the XR controls, so it is removed; - // the XR's resourceRefs also name a resource another XR controls and one with no controller, - // which the composition doesn't render either, and neither of those is removed. - "CompositionDropsResourceRemovingOnlyWhatTheXRControls": { - reason: "A composition that stops rendering resources removes only those the XR controls", - // What this case pins: test-resource's resourceRefs still name two resources it doesn't control. - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/removal-test-original-composition.yaml", - "testdata/diff/resources/composition.yaml", - "testdata/comp/resources/functions.yaml", - // Deliberately not in crossplaneManagedResources: its controller reference was removed, so - // no XR controls it, though test-resource's resourceRefs still name it. - "testdata/diff/resources/removal-test-ns-downstream-without-controller.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/comp/resources/existing-xr-listing-uncontrolled-resources.yaml", - "testdata/diff/resources/removal-test-ns-downstream-resource1.yaml", // still rendered: kept - "testdata/diff/resources/removal-test-ns-downstream-resource2.yaml", // no longer rendered: removed - ), - integration.Composes( - "testdata/diff/resources/existing-other-xr.yaml", - "testdata/diff/resources/existing-other-xr-downstream.yaml", - ), - }, - inputFiles: []string{"testdata/diff/resources/removal-test-composition.yaml"}, - namespace: "default", - outputFormat: "json", - expectedExitCode: dp.ExitCodeDiffDetected, - expectedStructured: simtest.Expect(). - WithComposition("removal-test-composition"). - WithCompositionModified(). - WithAffectedResources(1, 1, 0, 0). - WithXRImpact("XNopResource", "test-resource", "default", "changed"). - WithDownstreamSummary(0, 0, 1). - WithDownstreamResource("removed", "XDownstreamResource", "resource-to-be-removed", "default"), - }, - "CompositionDiffIgnorePaths": { - reason: "Validates that ArgoCD annotations are ignored in composition diffs", - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/original-composition.yaml", - "testdata/comp/resources/functions.yaml", - }, - // Add existing XR with ArgoCD annotations - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/comp/resources/existing-xr-with-argocd.yaml", - "testdata/comp/resources/existing-downstream-with-argocd.yaml", - ), - }, - inputFiles: []string{"testdata/comp/composition-no-changes.yaml"}, - namespace: "default", - // The composition is unchanged, so impact analysis would be skipped by default (issue - // #453). This case is about XR/downstream-level ignore paths, so it opts back in. - analyzeUnchanged: true, - ignorePaths: []string{ - "metadata.annotations[argocd.argoproj.io/tracking-id]", - "metadata.labels[argocd.argoproj.io/instance]", - }, - expectedOutput: ` -=== Composition Changes === - -No changes detected in composition xnopresources.diff.example.org - -=== Affected Composite Resources === - - ✓ XNopResource/test-resource (namespace: default) - -Summary: 1 resource unchanged - -=== Impact Analysis === - -All composite resources are up-to-date. No downstream resource changes detected. - -`, - expectedError: false, - noColor: true, - }, - // Issue #453: any revision an unchanged composition produced would carry the same spec, so no XR - // renders differently and the affected-XR / impact-analysis sections are replaced by a skip note. - // - // These fixtures are deliberately ones that DO produce a downstream delta when evaluated (see - // UnchangedCompositionAnalyzeUnchangedEvaluatesXRs, which asserts exactly that against the same - // setup). So this case proves two things at once: the skip suppresses a report that would - // otherwise be emitted, and the exit code is 0 rather than the 3 that report would have caused - // — a delta not attributable to any composition change must not gate a CI pipeline. - "UnchangedCompositionSkipsImpactAnalysis": { - reason: "An unchanged composition skips impact analysis entirely and says so", - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/original-composition.yaml", - "testdata/comp/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/comp/resources/existing-xr-1.yaml", - "testdata/comp/resources/existing-downstream-1.yaml", - ), - }, - inputFiles: []string{"testdata/comp/composition-no-changes.yaml"}, - namespace: "default", - expectedOutput: ` -=== Composition Changes === - -No changes detected in composition xnopresources.diff.example.org - -Impact analysis skipped: this composition is identical to the cluster's, so applying it creates no new CompositionRevision and no composite resource could change as a result. Pass --analyze-on=always to evaluate them anyway. - -`, - expectedError: false, - // No composition change and no evaluated XRs, so nothing was detected. - expectedExitCode: dp.ExitCodeSuccess, - noColor: true, - }, - // Issue #467: the cluster composition was applied with a client-side `kubectl apply`, so it - // carries kubectl.kubernetes.io/last-applied-configuration; the file being diffed never does. - // That annotation is suppressed from the rendered diff — it is a multi-KB serialization of the - // object, useless to show — but Crossplane's Composition.Hash() covers annotations, so applying - // this DOES create a new CompositionRevision that composites re-point to, and whose name a - // template can read off the XR's compositionRevisionRef. A suppression made for readability - // must not decide that away, so the composites are evaluated. Contrast the sibling above, which - // is byte-identical and needs --analyze-unchanged to evaluate anything. - // - // This runs the real CLI wiring, which the unit coverage in - // TestDefaultCompDiffProcessor_calculateCompositionDiff cannot reach: it pins that - // defaultProcessorOptions does not fold the annotation into --ignore-paths, and that the - // display-only suppression does not leak into the change verdict. - // - // The evaluated composite renders unchanged, because the new revision's spec is identical, so - // nothing renders differently and the exit code is 0 (as with AnalyzeOnSpecChangeSkipsMetadataOnlyChange). - // What proves evaluation is that the composite is counted, rather than impact analysis being skipped. - "CompositionAppliedWithKubectlEvaluatesXRs": { - reason: "A composition differing only in kubectl's last-applied-configuration still counts as changed, because applying it creates a new CompositionRevision", - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/original-composition-kubectl-applied.yaml", - "testdata/comp/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/comp/resources/existing-xr-1-kubectl-applied.yaml", - "testdata/comp/resources/existing-downstream-1.yaml", - ), - }, - inputFiles: []string{"testdata/comp/composition-no-changes.yaml"}, - namespace: "default", - outputFormat: "json", - expectedExitCode: dp.ExitCodeSuccess, - expectedStderrContains: []string{ - // Issue #474: the composites are still evaluated, but they are rendered with their existing - // compositionRevisionRef, because the name of the revision this would create cannot be - // predicted for a client-side-applied composition. Saying so is the honest position: if this - // composition's template read the revision name, the resulting change would go undetected. - "Could not predict the name of the CompositionRevision", - }, - expectedStructured: simtest.Expect(). - // The same advisory reaches warnings[] for machine consumers, keyed by composition so that two - // compositions declining to predict stay two warnings rather than being merged as duplicates. - WithWarning("Could not predict the name of the CompositionRevision"). - WithWarningContext(map[string]string{"composition": "xnopresources.diff.example.org"}). - And(). - WithComposition("xnopresources.diff.example.org"). - // createsRevision stays true while the name is withheld: the annotation delta is real, and a - // stale annotation genuinely would be rewritten and mint a revision. Only the identity is - // unknowable, which is why the guard makes the weak claim rather than "no revision". - WithRevisionImpact("metadata", true, 1). - WithoutPredictedRevisionName(). - WithAffectedResources(1, 0, 1, 0). - WithXRImpact("XNopResource", "test-resource", "default", "unchanged"), - }, - // Issue #500: the same asymmetry, but the cluster's last-applied-configuration is the one a - // client-side `kubectl apply` of this very file would write. Re-applying it computes the same - // annotation, the patch is empty and kubectl writes nothing, so no CompositionRevision is created. - // The sibling above differs only in that its annotation is stale, which a re-apply does rewrite. - "CompositionReappliedWithKubectlCreatesNoRevision": { - reason: "A composition whose live last-applied-configuration already describes the file is unchanged, because re-applying it writes nothing", - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/original-composition-kubectl-applied-current.yaml", - "testdata/comp/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/comp/resources/existing-xr-1-kubectl-applied-current.yaml", - "testdata/comp/resources/existing-downstream-1.yaml", - ), - }, - inputFiles: []string{"testdata/comp/composition-no-changes.yaml"}, - namespace: "default", - outputFormat: "json", - expectedExitCode: dp.ExitCodeSuccess, - expectedStructured: simtest.Expect(). - WithNoWarnings(). - WithComposition("xnopresources.diff.example.org"). - WithImpactAnalysisSkipped(). - WithRevisionImpact("none", false, 0), - }, - // Issue #474: the case that makes "creates a revision but renders identically" a claim to be - // verified rather than assumed. The composition edit is metadata-only — one added label, identical - // spec — so the CompositionRevision it mints carries a byte-identical spec. But the template reads - // the revision's own name off the composite and propagates it into the composed resource, so the - // rendered output genuinely changes. - // - // Before the fix, the composite was rendered with its EXISTING compositionRevisionRef: configData - // came out as the old revision's name, matched the cluster, and the tool reported unchanged with - // exit 0. Seeding the predicted ref is what turns that into the exit 3 below. This is the - // regression test for the whole feature — stop comp's composition provider resolving the predicted - // revision, or stop the processor writing the resolved revision to the ref, and this is what fails. - // - // Note also what is NOT in the output: the composite's own - // spec.crossplane.compositionRevisionRef diff. The predicted ref is written only to the render input, - // never to the composite's dry-run payload, which is why the exit code reflects rendering rather than - // bookkeeping; predictedRevisionName is what keeps the downstream change interpretable without it. - "RevisionNamePropagatesToComposedResource": { - reason: "A metadata-only composition edit whose template reads the CompositionRevision name changes rendered output, and is detected rather than assumed away", - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/revision-templating-composition.yaml", - "testdata/comp/resources/revision-templating-composition-revision.yaml", - "testdata/comp/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/comp/resources/existing-xr-revision-ref.yaml", - "testdata/comp/resources/existing-downstream-revision-ref.yaml", - ), - }, - inputFiles: []string{"testdata/comp/revision-templating-updated-composition.yaml"}, - namespace: "default", - outputFormat: "json", - expectedExitCode: dp.ExitCodeDiffDetected, - expectedStructured: simtest.Expect(). - // The name was predictable, so the could-not-predict advisory must stay silent. - WithNoWarnings(). - WithComposition("xrevisionrefs.diff.example.org"). - WithRevisionImpact("metadata", true, 1). - // Matched by pattern, not literally: the suffix is the first 7 hex digits of the - // composition's hash, so pinning it would make any edit to the fixture a test failure. The - // derivation is pinned exactly by TestRevisionIdentity, which is where that belongs. - WithPredictedRevisionNamePattern(`^xrevisionrefs\.diff\.example\.org-[0-9a-f]{7}$`). - WithAffectedResources(1, 1, 0, 0). - WithXRImpact("XNopResource", "revision-ref-resource", "default", "changed"). - WithDownstreamSummary(0, 1, 0). - WithDownstreamResource("modified", "XDownstreamResource", "revision-ref-resource", "default"). - // The old value is the revision the composite currently tracks; the new one is the predicted - // revision, i.e. the thing this whole feature exists to surface. Both are real revision names, - // so both match the pattern above: only the exact values tell them apart. - WithFieldChange("spec.forProvider.configData", - "xrevisionrefs.diff.example.org-e36719b", "xrevisionrefs.diff.example.org-ec5496c"), - }, - // Issue #536 / #479: the same edit with --include-manual, which keeps a Manual composite for analysis - // without making it adopt the new revision. Only the Automatic composite is rendered with the - // predicted revision name; the Manual one stays pinned, so its template-read name, and with it its - // composed resource, is unchanged. Giving every composite the name would report the pinned one as - // changing to a revision it never uses. - "RevisionNameReachesOnlyRepointingCompositesWithIncludeManual": { - reason: "Under --include-manual, the predicted revision name reaches an Automatic composite but not a Manual one pinned to an older revision", - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/revision-templating-composition.yaml", - "testdata/comp/resources/revision-templating-composition-revision.yaml", - "testdata/comp/resources/revision-templating-composition-older-revision.yaml", - "testdata/comp/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/comp/resources/existing-xr-revision-ref.yaml", - "testdata/comp/resources/existing-downstream-revision-ref.yaml", - ), - integration.Composes( - "testdata/comp/resources/existing-xr-pinned-revision-ref.yaml", - "testdata/comp/resources/existing-downstream-pinned-revision-ref.yaml", - ), - }, - inputFiles: []string{"testdata/comp/revision-templating-updated-composition.yaml"}, - namespace: "default", - includeManual: true, - outputFormat: "json", - expectedExitCode: dp.ExitCodeDiffDetected, - expectedStructured: simtest.Expect(). - WithNoWarnings(). - WithComposition("xrevisionrefs.diff.example.org"). - // One composite re-points; the Manual one is kept but adopts nothing. - WithRevisionImpact("metadata", true, 1). - WithPredictedRevisionNamePattern(`^xrevisionrefs\.diff\.example\.org-[0-9a-f]{7}$`). - WithAffectedResources(2, 1, 1, 0). - WithXRImpact("XNopResource", "pinned-revision-ref-resource", "default", "unchanged"). - WithDownstreamSummary(0, 0, 0). - AndComp(). - WithXRImpact("XNopResource", "revision-ref-resource", "default", "changed"). - WithDownstreamSummary(0, 1, 0). - WithDownstreamResource("modified", "XDownstreamResource", "revision-ref-resource", "default"). - WithFieldChange("spec.forProvider.configData", - "xrevisionrefs.diff.example.org-e36719b", "xrevisionrefs.diff.example.org-ec5496c"), - }, - // Issue #498, the claim variant of RevisionNamePropagatesToComposedResource. A claim is rendered from its backing XR with the - // claim's spec synced in. Under Automatic the claim's own ref is never propagated (the XR controller - // owns the backing XR's), so seeding the claim would reach nothing: comp's composition provider is - // handed the backing XR the render consumes, and resolves that to the predicted revision. Without it, - // the backing XR's stale ref renders and the change goes unreported. - "ClaimRevisionNamePropagatesToComposedResource": { - reason: "A metadata-only composition edit is rendered for an Automatic claim with the revision its backing XR would re-point to", - setupFiles: []string{ - "testdata/comp/resources/claim-xrd.yaml", - "testdata/comp/resources/claim-revision-templating-composition.yaml", - "testdata/comp/resources/functions.yaml", - "testdata/comp/resources/test-namespace.yaml", - "testdata/comp/resources/existing-claim-revision-ref.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/comp/resources/existing-claim-revision-ref-xr.yaml", - "testdata/comp/resources/existing-claim-revision-ref-downstream.yaml", - ), - }, - xrdAPIVersion: integration.XRDV1, // the XRD is v1, so Crossplane writes the XR's resourceRefs at spec.resourceRefs - inputFiles: []string{"testdata/comp/claim-revision-templating-updated-composition.yaml"}, - namespace: "test-namespace", - outputFormat: "json", - expectedExitCode: dp.ExitCodeDiffDetected, - expectedStructured: simtest.Expect(). - // The claim's composed resource is labelled with its backing XR's name, which is the claim's - // owner, so no "already belongs to another composite" advisory is raised (#534). - WithNoWarnings(). - WithComposition("xrevisionrefclaims.diff.example.org"). - WithRevisionImpact("metadata", true, 1). - WithPredictedRevisionNamePattern(`^xrevisionrefclaims\.diff\.example\.org-[0-9a-f]{7}$`). - WithAffectedResources(1, 1, 0, 0). - WithXRImpact("NopClaim", "revision-ref-claim", "test-namespace", "changed"). - WithDownstreamSummary(0, 1, 0). - WithDownstreamResource("modified", "XDownstreamResource", "revision-ref-claim-xr", ""). - // The old value is the revision the backing XR tracks now; the new one is the predicted name. - WithFieldChange("spec.forProvider.configData", - "xrevisionrefclaims.diff.example.org-98b484d", "xrevisionrefclaims.diff.example.org-55c3a3b"), - }, - // Issue #498, the other half: when nothing is seeded, an Automatic claim renders against the - // revision its backing XR tracks now, not against no revision at all. Nothing is seeded here - // because the composition differs from the cluster's only by a stale kubectl annotation, so the - // name of the revision that would mint is unknowable (see CompositionAppliedWithKubectlEvaluatesXRs); - // the claim is still rendered, so the backing XR's ref is what the template reads. - "ClaimRendersWithBackingXRRevisionWhenNothingIsSeeded": { - reason: "An Automatic claim with nothing seeded renders with its backing XR's compositionRevisionRef, so a template reading the revision name shows no spurious change", - setupFiles: []string{ - "testdata/comp/resources/claim-xrd.yaml", - "testdata/comp/resources/claim-revision-templating-composition-kubectl-applied.yaml", - "testdata/comp/resources/functions.yaml", - "testdata/comp/resources/test-namespace.yaml", - "testdata/comp/resources/existing-claim-revision-ref-kubectl-applied.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/comp/resources/existing-claim-revision-ref-kubectl-applied-xr.yaml", - "testdata/comp/resources/existing-claim-revision-ref-kubectl-applied-downstream.yaml", - ), - }, - xrdAPIVersion: integration.XRDV1, // the XRD is v1, so Crossplane writes the XR's resourceRefs at spec.resourceRefs - inputFiles: []string{"testdata/comp/resources/claim-revision-templating-composition.yaml"}, - namespace: "test-namespace", - outputFormat: "json", - expectedExitCode: dp.ExitCodeSuccess, - expectedStderrContains: []string{"Could not predict the name of the CompositionRevision"}, - expectedStructured: simtest.Expect(). - // The could-not-predict advisory is the only warning: the claim's composed resource is labelled - // with its backing XR's name, so it raises no ownership advisory (#534). - WithWarning("Could not predict the name of the CompositionRevision"). - WithWarningContext(map[string]string{"composition": "xrevisionrefclaims.diff.example.org"}). - And(). - WithComposition("xrevisionrefclaims.diff.example.org"). - WithRevisionImpact("metadata", true, 1). - WithoutPredictedRevisionName(). - WithAffectedResources(1, 0, 1, 0). - WithXRImpact("NopClaim", "revision-ref-claim", "test-namespace", "unchanged"). - WithDownstreamSummary(0, 0, 0), - }, - // Issue #472: the same metadata-only change, with the user opting out of paying a render per - // composite for it. The composites go unevaluated — but the mutative consequence is still - // reported, which is what keeps --analyze-on a cost knob rather than a correctness mode. So the - // skip is recorded, and revisionImpact says a CompositionRevision is created and one composite - // would adopt it. Exit code 0: nothing renders differently, and a GitOps loop re-applying the - // same manifests must not fail its gate forever. - "AnalyzeOnSpecChangeSkipsMetadataOnlyChange": { - reason: "--analyze-on=spec-change leaves a metadata-only change unevaluated, while still reporting that it creates a CompositionRevision", - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/original-composition-kubectl-applied.yaml", - "testdata/comp/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/comp/resources/existing-xr-1-kubectl-applied.yaml", - "testdata/comp/resources/existing-downstream-1.yaml", - ), - }, - inputFiles: []string{"testdata/comp/composition-no-changes.yaml"}, - namespace: "default", - analyzeOn: "spec-change", - outputFormat: "json", - expectedExitCode: dp.ExitCodeSuccess, - expectedStructured: simtest.Expect(). - WithComposition("xnopresources.diff.example.org"). - WithImpactAnalysisSkipped(). - WithRevisionImpact("metadata", true, 1). - // Issue #474: no predicted name here, and that absence is the point. The compositions - // differ by nothing but kubectl's last-applied-configuration, whose post-apply value is a - // function of how the user applies rather than of the file — so the hash, and therefore the - // revision's name, is unknowable. createsRevision stays true; only the identity is withheld. - WithoutPredictedRevisionName(), - }, - // The same setup with --analyze-unchanged evaluates the XRs after all (the pre-edit - // convergence-baseline workflow). Here the composed resource has drifted out of band, so the - // evaluation reports a downstream modification even though the composition is byte-identical - // to the cluster's. That delta is not caused by this composition — it is exactly the class of - // finding the default skip keeps out of the "impact of your composition change" report, and why - // opting in is explicit. - "UnchangedCompositionAnalyzeUnchangedEvaluatesXRs": { - reason: "--analyze-unchanged evaluates affected XRs even though the composition is unchanged, surfacing deltas not caused by it", - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/original-composition.yaml", - "testdata/comp/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/comp/resources/existing-xr-1.yaml", - "testdata/comp/resources/existing-downstream-drifted.yaml", - ), - }, - inputFiles: []string{"testdata/comp/composition-no-changes.yaml"}, - namespace: "default", - analyzeUnchanged: true, - outputFormat: "json", - expectedExitCode: dp.ExitCodeDiffDetected, - expectedStructured: simtest.Expect(). - WithComposition("xnopresources.diff.example.org"). - WithAffectedResources(1, 1, 0, 0). - WithXRImpact("XNopResource", "test-resource", "default", "changed"). - WithDownstreamSummary(0, 1, 0). - WithDownstreamResource("modified", "XDownstreamResource", "test-resource", "default"). - WithFieldChange("spec.forProvider.configData", "drifted-value", "existing-value"), - }, - "CompositionDiffCustomNamespace": { - reason: "Validates composition diff with custom namespace", - // Set up existing XRs in both custom and default namespaces to validate filtering - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/original-composition.yaml", - "testdata/comp/resources/functions.yaml", - // Create the custom namespace first - "testdata/comp/resources/custom-namespace.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - // Add existing XRs in custom namespace (should be included in output) - integration.Composes( - "testdata/comp/resources/existing-custom-ns-xr.yaml", - "testdata/comp/resources/existing-custom-ns-downstream.yaml", - ), - // Add existing XRs in default namespace (should be filtered out) - integration.Composes( - "testdata/comp/resources/existing-xr-1.yaml", - "testdata/comp/resources/existing-downstream-1.yaml", - ), - }, - // New composition files that will be diffed - inputFiles: []string{"testdata/comp/updated-composition.yaml"}, - namespace: "custom-namespace", - // Expected output should only show custom-namespace resources, NOT default namespace resources - // This validates that namespace filtering works correctly - expectedOutput: ` -=== Composition Changes === - -~~~ Composition/xnopresources.diff.example.org - apiVersion: apiextensions.crossplane.io/v1 - kind: Composition - metadata: - name: xnopresources.diff.example.org - spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: -- configData: {{ .observed.composite.resource.spec.coolField }} -- resourceTier: basic -+ configData: updated-{{ .observed.composite.resource.spec.coolField }} -+ resourceTier: premium - kind: GoTemplate - source: Inline - step: generate-resources - - functionRef: - name: function-auto-ready - step: automatically-detect-ready-composed-resources - ---- - -Summary: 1 modified - -Applying this composition creates a new CompositionRevision, which 1 composite would adopt. - -=== Affected Composite Resources === - - ⚠ XNopResource/custom-namespace-resource (namespace: custom-namespace) - -Summary: 1 resource with changes - -=== Impact Analysis === - -~~~ XDownstreamResource/custom-namespace-resource - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - annotations: - crossplane.io/composition-resource-name: nop-resource - labels: - crossplane.io/composite: custom-namespace-resource - name: custom-namespace-resource - namespace: custom-namespace - spec: - forProvider: -- configData: custom-ns-existing-value -- resourceTier: basic -+ configData: updated-custom-ns-existing-value -+ resourceTier: premium - ---- - -Summary: 1 modified`, - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - noColor: true, - }, - // Two compositions that BOTH genuinely change, each with its own affected XR. This is the - // aggregation case: one CompositionDiff entry per input, each carrying its own impact analysis, - // with hasDiffs ORed across them. MultipleCompositionDiffImpact below covers the mixed - // changed/unchanged variant; before this case existed, nothing covered two compositions that - // both report impacts — the second composition there has no affected XR at all. - "MultipleChangedCompositionsBothReportImpact": { - reason: "Two changed compositions each report their own affected XR's downstream changes", - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/original-composition.yaml", - "testdata/comp/resources/original-composition-2.yaml", - "testdata/comp/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - // XR using composition 1 - integration.Composes( - "testdata/comp/resources/existing-xr-1.yaml", - "testdata/comp/resources/existing-downstream-1.yaml", - ), - // XR using composition 2 - integration.Composes( - "testdata/comp/resources/existing-xr-v2.yaml", - "testdata/comp/resources/existing-downstream-v2.yaml", - ), - }, - inputFiles: []string{ - "testdata/comp/updated-composition.yaml", - "testdata/comp/updated-composition-2-changed.yaml", - }, - namespace: "default", - outputFormat: "json", - expectedExitCode: dp.ExitCodeDiffDetected, - expectedStructured: simtest.Expect(). - WithComposition("xnopresources.diff.example.org"). - WithCompositionModified(). - WithAffectedResources(1, 1, 0, 0). - WithXRImpact("XNopResource", "test-resource", "default", "changed"). - WithDownstreamSummary(0, 1, 0). - WithDownstreamResource("modified", "XDownstreamResource", "test-resource", "default"). - WithFieldChange("spec.forProvider.configData", "existing-value", "updated-existing-value"). - WithFieldChange("spec.forProvider.resourceTier", "basic", "premium"). - AndComposition("xnopresources-v2.diff.example.org"). - WithCompositionModified(). - WithAffectedResources(1, 1, 0, 0). - WithXRImpact("XNopResource", "v2-resource", "default", "changed"). - WithDownstreamSummary(0, 1, 0). - WithDownstreamResource("modified", "XDownstreamResource", "v2-resource", "default"). - WithFieldChange("spec.forProvider.configData", "v2-updated-v2-existing-value", "v2-changed-v2-existing-value"). - WithFieldChange("spec.forProvider.resourceTier", "enterprise", "premium-plus"), - }, - // Note the second input composition (xnopresources-v2.diff.example.org) is byte-identical to its - // in-cluster version, so this case doubles as the mixed changed/unchanged scenario: the changed - // composition reports its impact while the unchanged one is skipped per-composition (issue - // #453). It is not a broken fixture. - "MultipleCompositionDiffImpact": { - reason: "Validates multiple compositions in one invocation: the changed one reports impact on existing XRs, the unchanged one is skipped", - // Set up existing XRs that use both compositions - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/original-composition.yaml", - "testdata/comp/resources/original-composition-2.yaml", - "testdata/comp/resources/functions.yaml", - }, - // Add existing XRs that use the compositions - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/comp/resources/existing-xr-1.yaml", - "testdata/comp/resources/existing-downstream-1.yaml", - ), - integration.Composes( - "testdata/comp/resources/existing-xr-2.yaml", - "testdata/comp/resources/existing-downstream-2.yaml", - ), - }, - // Multiple composition files that will be diffed - inputFiles: []string{ - "testdata/comp/updated-composition.yaml", - "testdata/comp/updated-composition-2.yaml", - }, - namespace: "default", - expectedOutput: ` -=== Composition Changes === - -~~~ Composition/xnopresources.diff.example.org - apiVersion: apiextensions.crossplane.io/v1 - kind: Composition - metadata: - name: xnopresources.diff.example.org - spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: -- configData: {{ .observed.composite.resource.spec.coolField }} -- resourceTier: basic -+ configData: updated-{{ .observed.composite.resource.spec.coolField }} -+ resourceTier: premium - kind: GoTemplate - source: Inline - step: generate-resources - - functionRef: - name: function-auto-ready - step: automatically-detect-ready-composed-resources - ---- - -Summary: 1 modified - -Applying this composition creates a new CompositionRevision, which 2 composites would adopt. - -=== Affected Composite Resources === - - ⚠ XNopResource/another-resource (namespace: default) - ⚠ XNopResource/test-resource (namespace: default) - -Summary: 2 resources with changes - -=== Impact Analysis === - -~~~ XDownstreamResource/another-resource - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - annotations: - crossplane.io/composition-resource-name: nop-resource - labels: - crossplane.io/composite: another-resource - name: another-resource - namespace: default - spec: - forProvider: -- configData: another-existing-value -- resourceTier: basic -+ configData: updated-another-existing-value -+ resourceTier: premium - ---- -~~~ XDownstreamResource/test-resource - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - annotations: - crossplane.io/composition-resource-name: nop-resource - labels: - crossplane.io/composite: test-resource - name: test-resource - namespace: default - spec: - forProvider: -- configData: existing-value -- resourceTier: basic -+ configData: updated-existing-value -+ resourceTier: premium - ---- - -Summary: 2 modified - -================================================================================ - -=== Composition Changes === - -No changes detected in composition xnopresources-v2.diff.example.org - -Impact analysis skipped: this composition is identical to the cluster's, so applying it creates no new CompositionRevision and no composite resource could change as a result. Pass --analyze-on=always to evaluate them anyway. -`, - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - noColor: true, - }, - "CompositionDiffFiltersManualXRs": { - reason: "Validates composition diff filters Manual policy XRs by default", - // Set up existing XRs - one with Automatic policy and one with Manual policy - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/original-composition.yaml", - "testdata/comp/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - // Add existing XR with Automatic policy (should be included) - integration.Composes( - "testdata/comp/resources/existing-xr-1.yaml", - "testdata/comp/resources/existing-downstream-1.yaml", - ), - // Add existing XR with Manual policy (should be filtered out by default) - integration.Composes( - "testdata/comp/resources/existing-xr-manual.yaml", - "testdata/comp/resources/existing-downstream-manual.yaml", - ), - }, - // Updated composition - inputFiles: []string{"testdata/comp/updated-composition.yaml"}, - namespace: "default", - // Expected output should only show test-resource (Automatic), NOT manual-resource (Manual) - expectedOutput: ` -=== Composition Changes === - -~~~ Composition/xnopresources.diff.example.org - apiVersion: apiextensions.crossplane.io/v1 - kind: Composition - metadata: - name: xnopresources.diff.example.org - spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: -- configData: {{ .observed.composite.resource.spec.coolField }} -- resourceTier: basic -+ configData: updated-{{ .observed.composite.resource.spec.coolField }} -+ resourceTier: premium - kind: GoTemplate - source: Inline - step: generate-resources - - functionRef: - name: function-auto-ready - step: automatically-detect-ready-composed-resources - ---- - -Summary: 1 modified - -Applying this composition creates a new CompositionRevision, which 1 composite would adopt. - -=== Affected Composite Resources === - - ⚠ XNopResource/test-resource (namespace: default) - -Summary: 1 resource with changes - -=== Impact Analysis === - -~~~ XDownstreamResource/test-resource - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - annotations: - crossplane.io/composition-resource-name: nop-resource - labels: - crossplane.io/composite: test-resource - name: test-resource - namespace: default - spec: - forProvider: -- configData: existing-value -- resourceTier: basic -+ configData: updated-existing-value -+ resourceTier: premium - ---- - -Summary: 1 modified`, - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - noColor: true, - }, - "CompositionUpgradesResourceAPIVersion": { - // NOTE: This test validates that resources are correctly matched across API version changes, - // avoiding delete/recreate. The composition template changes from v1beta1 to v1beta2, but - // Kubernetes automatically converts resources between served API versions. When we query for - // the v1beta2 resource, Kubernetes finds the v1beta1 resource and returns it auto-converted - // to v1beta2. From Kubernetes' perspective, the resource exists as both versions simultaneously, - // so there's no apiVersion field change to show in the diff. The important thing is that the - // resource is matched, preventing delete/recreate operations: since the stored object is - // identical at either version it renders unchanged, where an unmatched one would show as a - // +++ addition and a --- removal. The composition diff itself WILL show the template change - // from v1beta1 to v1beta2. - // - // As in TestDiffIntegration/CompositionRevisionUpgradesResourceAPIVersion, the XR composes the - // resource as Crossplane leaves it, named in its resourceRefs at v1beta1, so this also checks - // that it is not reported as removed (#552). - reason: "Validates composition upgrade that changes resource API version shows as update not remove/add", - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - // NOTE: xapimigrate CRD is auto-loaded from testdata/comp/crds/ - // We apply no XRD for XApiMigrateResource because it is a regular managed resource - // (not a composite), and an XRD would make it composite, causing infinite recursion. - "testdata/comp/resources/api-version-original-composition.yaml", - "testdata/comp/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - { - OwnerFile: "testdata/comp/resources/existing-api-version-xr.yaml", - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - "testdata/comp/resources/existing-api-version-downstream-v1beta1.yaml": nil, - }, - }, - }, - inputFiles: []string{"testdata/comp/api-version-updated-composition.yaml"}, - namespace: "default", - expectedOutput: ` -=== Composition Changes === - -~~~ Composition/xapimigrateresources.example.org - apiVersion: apiextensions.crossplane.io/v1 - kind: Composition - metadata: - name: xapimigrateresources.example.org - spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - inline: - template: | -- apiVersion: comp.example.org/v1beta1 -+ apiVersion: comp.example.org/v1beta2 - kind: XApiMigrateResource - metadata: - name: {{ .observed.composite.resource.metadata.name }}-api-resource - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: api-migrate-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - kind: GoTemplate - source: Inline - step: generate-api-versioned-resources - - functionRef: - name: function-auto-ready - step: automatically-detect-ready-composed-resources - ---- - -Summary: 1 modified - -Applying this composition creates a new CompositionRevision, which 1 composite would adopt. - -=== Affected Composite Resources === - - ✓ XNopResource/test-api-version (namespace: default) - -Summary: 1 resource unchanged - -=== Impact Analysis === - -All composite resources are up-to-date. No downstream resource changes detected. -`, - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - noColor: true, - }, - "NetNewCompositionNoImpact": { - reason: "Validates net-new composition with no downstream impact", - // Set up cluster with existing resources but no composition that matches the new one - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/original-composition.yaml", - "testdata/comp/resources/functions.yaml", - }, - // Add existing XRs that use different compositions (won't be affected) - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/comp/resources/existing-xr-1.yaml", - "testdata/comp/resources/existing-downstream-1.yaml", - ), - }, - // Net-new composition file that doesn't exist in cluster and targets different XR type - inputFiles: []string{"testdata/comp/net-new-composition.yaml"}, - namespace: "default", - expectedOutput: ` -=== Composition Changes === - -+++ Composition/xnewresources.diff.example.org -+ apiVersion: apiextensions.crossplane.io/v1 -+ kind: Composition -+ metadata: -+ labels: -+ environment: staging -+ provider: aws -+ name: xnewresources.diff.example.org -+ spec: -+ compositeTypeRef: -+ apiVersion: staging.diff.example.org/v1alpha1 -+ kind: XNewResource -+ mode: Pipeline -+ pipeline: -+ - functionRef: -+ name: function-go-templating -+ input: -+ apiVersion: template.fn.crossplane.io/v1beta1 -+ inline: -+ template: | -+ apiVersion: staging.nop.example.org/v1alpha1 -+ kind: XStagingResource -+ metadata: -+ name: {{ .observed.composite.resource.metadata.name }} -+ namespace: {{ .observed.composite.resource.metadata.namespace }} -+ annotations: -+ gotemplating.fn.crossplane.io/composition-resource-name: staging-resource -+ spec: -+ forProvider: -+ environment: staging -+ configData: new-{{ .observed.composite.resource.spec.newField }} -+ resourceTier: premium -+ kind: GoTemplate -+ source: Inline -+ step: generate-new-resources -+ - functionRef: -+ name: function-auto-ready -+ step: automatically-detect-ready-composed-resources - ---- - -Summary: 1 added - -No XRs found using composition xnewresources.diff.example.org`, - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - noColor: true, - }, - "CompositionChangeWithUnchangedDownstreamResources": { - reason: "Validates status indicators for unchanged downstream resources", - // Set up existing XRs that use the original composition - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/status-indicator-composition.yaml", - "testdata/comp/resources/functions.yaml", - }, - // Add existing XRs that use the composition - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/comp/resources/status-xr-1.yaml", - "testdata/comp/resources/status-downstream-1.yaml", - ), - integration.Composes( - "testdata/comp/resources/status-xr-2.yaml", - "testdata/comp/resources/status-downstream-2.yaml", - ), - }, - // Updated composition with only metadata change (no downstream impact) - inputFiles: []string{"testdata/comp/status-indicator-updated-composition.yaml"}, - namespace: "default", - expectedOutput: ` -=== Composition Changes === - -~~~ Composition/xstatus.diff.example.org - apiVersion: apiextensions.crossplane.io/v1 - kind: Composition - metadata: -+ labels: -+ environment: production - name: xstatus.diff.example.org - spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: stable-value - resourceTier: standard - kind: GoTemplate - source: Inline - step: generate-resources - - functionRef: - name: function-auto-ready - step: automatically-detect-ready-composed-resources - ---- - -Summary: 1 modified - -Applying this composition creates a new CompositionRevision, which 2 composites would adopt. - -=== Affected Composite Resources === - - ✓ XNopResource/status-test-xr-1 (namespace: default) - ✓ XNopResource/status-test-xr-2 (namespace: default) - -Summary: 2 resources unchanged - -=== Impact Analysis === - -All composite resources are up-to-date. No downstream resource changes detected.`, - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - noColor: true, - }, - "CompositionChangeWithMixedStatusAndColors": { - reason: "Validates status indicators with colorization for mixed changed/unchanged XRs", - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/mixed-status-composition.yaml", - "testdata/comp/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - // XR 1 with downstream resources (standard tier - will change) - integration.Composes( - "testdata/comp/resources/mixed-xr-1.yaml", - "testdata/comp/resources/mixed-downstream-db-1.yaml", - "testdata/comp/resources/mixed-downstream-storage-1.yaml", - "testdata/comp/resources/mixed-downstream-network-1.yaml", - ), - // XR 2 with downstream resources (standard tier - will change) - integration.Composes( - "testdata/comp/resources/mixed-xr-2.yaml", - "testdata/comp/resources/mixed-downstream-db-2.yaml", - "testdata/comp/resources/mixed-downstream-storage-2.yaml", - "testdata/comp/resources/mixed-downstream-network-2.yaml", - ), - // XR 3 with downstream resources (already premium tier - no change) - integration.Composes( - "testdata/comp/resources/mixed-xr-3.yaml", - "testdata/comp/resources/mixed-downstream-db-3.yaml", - "testdata/comp/resources/mixed-downstream-storage-3.yaml", - "testdata/comp/resources/mixed-downstream-network-3.yaml", - ), - }, - inputFiles: []string{"testdata/comp/mixed-status-updated-composition.yaml"}, - namespace: "default", - expectedOutput: strings.Join([]string{ - ` -=== Composition Changes === - -~~~ Composition/xmixed.diff.example.org - apiVersion: apiextensions.crossplane.io/v1 - kind: Composition - metadata: - name: xmixed.diff.example.org - spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - inline: - template: | - --- - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }}-database - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: database - spec: - forProvider: - resourceType: database -`, tu.Red(`- tier: standard`), ` -`, tu.Green(`+ tier: premium`), ` -`, tu.Green(`+ backupEnabled: true`), ` - --- - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }}-storage - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: storage - spec: - forProvider: - resourceType: storage - tier: standard - --- - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }}-network - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: network - spec: - forProvider: - resourceType: network - tier: standard - kind: GoTemplate - source: Inline - step: generate-resources - - functionRef: - name: function-auto-ready - step: automatically-detect-ready-composed-resources - ---- - -Summary: 1 modified - -Applying this composition creates a new CompositionRevision, which 3 composites would adopt. - -=== Affected Composite Resources === - -`, tu.Yellow(` ⚠ XNopResource/mixed-test-xr-1 (namespace: default)`), ` -`, tu.Yellow(` ⚠ XNopResource/mixed-test-xr-2 (namespace: default)`), ` -`, tu.Green(` ✓ XNopResource/mixed-test-xr-3 (namespace: default)`), ` - -Summary: 2 resources with changes, 1 resource unchanged - -=== Impact Analysis === - -~~~ XDownstreamResource/mixed-test-xr-1-database - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - annotations: - crossplane.io/composition-resource-name: database - labels: - crossplane.io/composite: mixed-test-xr-1 - name: mixed-test-xr-1-database - namespace: default - spec: - forProvider: -`, tu.Red(`- resourceType: database`), ` -`, tu.Red(`- tier: standard`), ` -`, tu.Green(`+ backupEnabled: true`), ` -`, tu.Green(`+ resourceType: database`), ` -`, tu.Green(`+ tier: premium`), ` - - - ---- -~~~ XDownstreamResource/mixed-test-xr-2-database - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - annotations: - crossplane.io/composition-resource-name: database - labels: - crossplane.io/composite: mixed-test-xr-2 - name: mixed-test-xr-2-database - namespace: default - spec: - forProvider: -`, tu.Red(`- resourceType: database`), ` -`, tu.Red(`- tier: standard`), ` -`, tu.Green(`+ backupEnabled: true`), ` -`, tu.Green(`+ resourceType: database`), ` -`, tu.Green(`+ tier: premium`), ` - - - ---- - -Summary: 2 modified -`, - }, ""), - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - noColor: false, - }, - "CompositionChangeImpactsClaims": { - reason: "Validates composition change impacts existing Claims (issue #120)", - // Set up existing Claims and their XRs that use the original composition - setupFiles: []string{ - // XRD, composition, and functions - "testdata/comp/resources/claim-xrd.yaml", - "testdata/comp/resources/claim-composition.yaml", - "testdata/comp/resources/functions.yaml", - // Test namespace - "testdata/comp/resources/test-namespace.yaml", - // Existing Claims and their corresponding XRs - "testdata/comp/resources/existing-claim-1.yaml", - "testdata/comp/resources/existing-claim-2.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/comp/resources/existing-claim-1-xr.yaml", - "testdata/comp/resources/existing-claim-downstream-1.yaml", - ), - integration.Composes( - "testdata/comp/resources/existing-claim-2-xr.yaml", - "testdata/comp/resources/existing-claim-downstream-2.yaml", - ), - }, - xrdAPIVersion: integration.XRDV1, // the XRD is v1, so Crossplane writes the XR's resourceRefs at spec.resourceRefs - // Updated composition that will be diffed - inputFiles: []string{"testdata/comp/updated-claim-composition.yaml"}, - namespace: "test-namespace", - expectedOutput: ` -=== Composition Changes === - -~~~ Composition/nopclaims.diff.example.org - apiVersion: apiextensions.crossplane.io/v1 - kind: Composition - metadata: - name: nopclaims.diff.example.org - spec: - compositeTypeRef: - apiVersion: diff.example.org/v1alpha1 - kind: XNop - mode: Pipeline - pipeline: - - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - inline: - template: | - apiVersion: nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - name: {{ .observed.composite.resource.metadata.name }} - spec: - forProvider: -- configData: {{ .observed.composite.resource.spec.coolField }} -- resourceTier: basic -+ configData: updated-{{ .observed.composite.resource.spec.coolField }} -+ resourceTier: premium - kind: GoTemplate - source: Inline - step: generate-resources - - functionRef: - name: function-auto-ready - step: automatically-detect-ready-composed-resources - ---- - -Summary: 1 modified - -Applying this composition creates a new CompositionRevision, which 2 composites would adopt. - -=== Affected Composite Resources === - - ⚠ NopClaim/test-claim-1 (namespace: test-namespace) - ⚠ NopClaim/test-claim-2 (namespace: test-namespace) - -Summary: 2 resources with changes - -=== Impact Analysis === - -~~~ XDownstreamResource/test-claim-1-xr - apiVersion: nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - annotations: - crossplane.io/composition-resource-name: nop-resource - labels: - crossplane.io/claim-name: test-claim-1 - crossplane.io/claim-namespace: test-namespace - crossplane.io/composite: test-claim-1-xr - name: test-claim-1-xr - spec: - forProvider: -- configData: claim-value-1 -- resourceTier: basic -+ configData: updated-claim-value-1 -+ resourceTier: premium - ---- -~~~ XDownstreamResource/test-claim-2-xr - apiVersion: nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - annotations: - crossplane.io/composition-resource-name: nop-resource - labels: - crossplane.io/claim-name: test-claim-2 - crossplane.io/claim-namespace: test-namespace - crossplane.io/composite: test-claim-2-xr - name: test-claim-2-xr - spec: - forProvider: -- configData: claim-value-2 -- resourceTier: basic -+ configData: updated-claim-value-2 -+ resourceTier: premium - ---- - -Summary: 2 modified`, - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - noColor: true, - }, - // Without -n every namespace is listed, so discovery finds each claim and its cluster-scoped - // backing XR. The claim renders through that XR, so the XR is dropped: each change is reported - // once, under the claim. - "CompositionChangeImpactsClaimsAllNamespaces": { - reason: "A claim's backing XR discovered alongside it is not reported a second time", - setupFiles: []string{ - "testdata/comp/resources/claim-xrd.yaml", - "testdata/comp/resources/claim-composition.yaml", - "testdata/comp/resources/functions.yaml", - "testdata/comp/resources/test-namespace.yaml", - "testdata/comp/resources/existing-claim-1.yaml", - "testdata/comp/resources/existing-claim-2.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/comp/resources/existing-claim-1-xr.yaml", - "testdata/comp/resources/existing-claim-downstream-1.yaml", - ), - integration.Composes( - "testdata/comp/resources/existing-claim-2-xr.yaml", - "testdata/comp/resources/existing-claim-downstream-2.yaml", - ), - }, - xrdAPIVersion: integration.XRDV1, - inputFiles: []string{"testdata/comp/updated-claim-composition.yaml"}, - outputFormat: "json", - expectedExitCode: dp.ExitCodeDiffDetected, - expectedStructured: simtest.Expect(). - WithComposition("nopclaims.diff.example.org"). - WithCompositionModified(). - WithAffectedResources(2, 2, 0, 0). - WithXRImpact("NopClaim", "test-claim-1", "test-namespace", "changed").AndComp(). - WithXRImpact("NopClaim", "test-claim-2", "test-namespace", "changed"), - }, - // Composition mode checks the composites it renders for overlaps as xr mode checks its inputs: - // two XRs on one composition that render one fixed-name child differently would both control it. - // Contention is checked before differing renderings, so it is the error the run fails with. - "SharedChildDifferingRendersFail": { - reason: "Two XRs a composition change renders one child for, differently, fail the run", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/shared-child-composition.yaml", - "testdata/diff/resources/functions.yaml", - "testdata/comp/resources/shared-child-xrs-differing.yaml", - }, - inputFiles: []string{"testdata/comp/updated-shared-child-composition.yaml"}, - namespace: "default", - expectedError: true, - expectedExitCode: dp.ExitCodeToolError, - expectedErrorContains: `resource "ns.nop.example.org/v1alpha1/XDownstreamResource/default/shared-child" would be ` + - `controlled by more than one XR (XNopResource/shared-child-owner-a, XNopResource/shared-child-owner-b)`, - }, - "SharedChildContendedEvenWhenIdentical": { - reason: "Two XRs a composition change renders one child for, identically, still contend for it", - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/shared-child-composition.yaml", - "testdata/diff/resources/functions.yaml", - "testdata/diff/shared-child-xrs.yaml", - }, - inputFiles: []string{"testdata/comp/updated-shared-child-composition.yaml"}, - namespace: "default", - expectedError: true, - expectedExitCode: dp.ExitCodeToolError, - expectedErrorContains: `resource "ns.nop.example.org/v1alpha1/XDownstreamResource/default/shared-child" would be ` + - `controlled by more than one XR (XNopResource/shared-child-owner-a, XNopResource/shared-child-owner-b)`, - }, - "SSAFieldRemovalDetection": { - reason: "Validates that field removals are correctly detected when resources are created via Server-Side Apply with Crossplane's field manager (issue #121)", - // Set up XRD, functions, and original composition with the removable field - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/functions.yaml", - "testdata/comp/resources/field-removal/composition-with-field.yaml", - }, - // XR and downstream resource applied via SSA with Crossplane field manager - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - { - OwnerFile: "testdata/comp/resources/field-removal/existing-xr.yaml", - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - "testdata/comp/resources/field-removal/existing-downstream.yaml": nil, - }, - }, - }, - // Updated composition that removes the removableField - inputFiles: []string{"testdata/comp/field-removal-updated-composition.yaml"}, - namespace: "default", - expectedOutput: ` -=== Composition Changes === - -~~~ Composition/xfieldremoval.diff.example.org - apiVersion: apiextensions.crossplane.io/v1 - kind: Composition - metadata: - name: xfieldremoval.diff.example.org - spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} -- removableField: will-be-removed - kind: GoTemplate - source: Inline - step: generate-resources - - functionRef: - name: function-auto-ready - step: automatically-detect-ready-composed-resources - ---- - -Summary: 1 modified - -Applying this composition creates a new CompositionRevision, which 1 composite would adopt. - -=== Affected Composite Resources === - - ⚠ XNopResource/field-removal-test (namespace: default) - -Summary: 1 resource with changes - -=== Impact Analysis === - -~~~ XDownstreamResource/field-removal-test - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - annotations: - crossplane.io/composition-resource-name: nop-resource - labels: - crossplane.io/composite: field-removal-test - name: field-removal-test - namespace: default - spec: - forProvider: - configData: test-value -- removableField: will-be-removed - ---- - -Summary: 1 modified`, - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - noColor: true, - }, - "SHA256DigestFunctionReference": { - reason: "Validates composition diff works with functions referenced by SHA256 digest instead of tag", - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/sha256-composition.yaml", - "testdata/comp/resources/functions-sha256.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/comp/resources/existing-sha256-xr.yaml", - "testdata/comp/resources/existing-sha256-downstream.yaml", - ), - }, - inputFiles: []string{"testdata/comp/updated-sha256-composition.yaml"}, - namespace: "default", - expectedOutput: ` -=== Composition Changes === - -~~~ Composition/xnopresources-sha256.diff.example.org - apiVersion: apiextensions.crossplane.io/v1 - kind: Composition - metadata: - name: xnopresources-sha256.diff.example.org - spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - functionRef: - name: function-go-templating-sha256 - input: - apiVersion: template.fn.crossplane.io/v1beta1 - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: -- configData: {{ .observed.composite.resource.spec.coolField }} -- resourceTier: basic -+ configData: updated-{{ .observed.composite.resource.spec.coolField }} -+ resourceTier: premium - kind: GoTemplate - source: Inline - step: generate-resources - - functionRef: - name: function-auto-ready - step: automatically-detect-ready-composed-resources - ---- - -Summary: 1 modified - -Applying this composition creates a new CompositionRevision, which 1 composite would adopt. - -=== Affected Composite Resources === - - ⚠ XNopResource/sha256-test-resource (namespace: default) - -Summary: 1 resource with changes - -=== Impact Analysis === - -~~~ XDownstreamResource/sha256-test-resource - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - annotations: - crossplane.io/composition-resource-name: nop-resource - labels: - crossplane.io/composite: sha256-test-resource - name: sha256-test-resource - namespace: default - spec: - forProvider: -- configData: sha256-value -- resourceTier: basic -+ configData: updated-sha256-value -+ resourceTier: premium - ---- - -Summary: 1 modified`, - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - noColor: true, - }, - // Issue #485, comp's half. comp re-renders each discovered composite through DiffSingleResource, - // the same recursion xr uses, so one fix covers both — this pins that for comp rather than - // assuming it. comp's own error only counts the failures, so the assertion that matters is that - // the depth message reaches the failing composite's impact entry: without it, a user would be - // told something failed but not that the composition recurses into itself. - "CyclicCompositionStopsAtMaxNestedDepth": { - reason: "comp shares xr's recursion path (it enters via DiffSingleResource), so a composition cycle must stop at --max-nested-depth here too, and say so on the failing composite (#485)", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/cycle/xrds.yaml", - "testdata/diff/resources/cycle/compositions.yaml", - "testdata/comp/resources/functions.yaml", - // Composed nothing: with a cyclic composition, Crossplane would nest XRs without end, so - // the case models the XR before its first compose rather than an endless tree. - "testdata/comp/resources/cycle/existing-cycle-xr.yaml", - }, - inputFiles: []string{"testdata/comp/updated-cycle-composition.yaml"}, - namespace: "default", - expectedError: true, - expectedErrorContains: "impact analysis failed for 1 XR(s)", - expectedExitCode: dp.ExitCodeToolError, - expectedStructured: simtest.Expect(). - WithComposition("xcycleas.cycle.example.org"). - WithAffectedResources(1, 0, 0, 1). - WithXRImpact("XCycleA", "test-cycle", "default", "error"). - WithErrorContaining("maximum nesting depth exceeded"), - }, - // The same failure, asserting the composite's error is recorded against it in the top-level errors[]. - "CyclicCompositionStopsAtMaxNestedDepthV2": { - reason: "A failing composite's error is a structured errors[] entry recorded against it", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/cycle/xrds.yaml", - "testdata/diff/resources/cycle/compositions.yaml", - "testdata/comp/resources/functions.yaml", - // Composed nothing, as in CyclicCompositionStopsAtMaxNestedDepth. - "testdata/comp/resources/cycle/existing-cycle-xr.yaml", - }, - inputFiles: []string{"testdata/comp/updated-cycle-composition.yaml"}, - namespace: "default", - expectedError: true, - expectedErrorContains: "impact analysis failed for 1 XR(s)", - expectedExitCode: dp.ExitCodeToolError, - expectedStructured: simtest.Expect(). - WithErrorID("XCycleA/test-cycle"). - WithComposition("xcycleas.cycle.example.org"). - WithAffectedResources(1, 0, 0, 1). - WithXRImpact("XCycleA", "test-cycle", "default", "error"). - WithErrorContaining("maximum nesting depth exceeded"). - WithErrorResourceID("XCycleA/test-cycle"), - }, - "NestedXRUsesOwnComposition": { - reason: "Validates that nested XRs use their own composition from the cluster, not the parent's CLI composition", - // Set up XRDs, compositions, and functions - setupFiles: []string{ - // Child XRD and composition (XNopResource uses xnopresources.diff.example.org) - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/original-composition.yaml", - "testdata/comp/resources/functions.yaml", - // Parent XRD and composition - "testdata/comp/resources/nested-xr/parent-xrd.yaml", - "testdata/comp/resources/nested-xr/parent-composition.yaml", - }, - // Use crossplaneManagedResources to set up proper ownership hierarchy - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - { - // Parent XR owns both direct downstream and nested child XR - OwnerFile: "testdata/comp/resources/nested-xr/existing-parent-xr.yaml", - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - // Direct downstream owned by parent XR - "testdata/comp/resources/nested-xr/existing-parent-direct-downstream.yaml": nil, - // Nested child XR owned by parent XR, which owns its own downstream - "testdata/comp/resources/nested-xr/existing-nested-child-xr.yaml": { - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - // Child's downstream owned by nested child XR - "testdata/comp/resources/nested-xr/existing-child-downstream.yaml": nil, - }, - }, - }, - }, - }, - // Diff the updated parent composition - inputFiles: []string{"testdata/comp/updated-parent-composition.yaml"}, - namespace: "default", - // Expected: Only the parent's direct downstream shows changes (tier change). - // The nested child's downstream should NOT change because: - // 1. The nested XR (XNopResource) uses its own composition (xnopresources.diff.example.org) - // 2. That child composition was NOT modified - // Bug scenario: If nested XR incorrectly uses parent composition, we'd see wrong changes to child's downstream. - // Expected output: Only the parent's direct downstream should show in the Impact Analysis. - // The child's downstream (XDownstreamResource/test-parent-nested) should NOT appear here. - // If it did, it would mean the nested XR incorrectly used the parent's CLI composition. - expectedOutput: `=== Impact Analysis === - -~~~ XDownstreamResource/test-parent-direct - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - annotations: - crossplane.io/composition-resource-name: direct-resource - labels: - crossplane.io/composite: test-parent - name: test-parent-direct - namespace: default - spec: - forProvider: - configData: parent-parent-value -- resourceTier: parent-tier -+ resourceTier: premium-tier - ---- - -Summary: 1 modified`, - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - noColor: true, - }, - "CrossNamespaceResourceCollision": { - reason: "Validates that resources with the same name in different namespaces are correctly distinguished", - skip: true, - skipReason: "Blocked on https://github.com/crossplane-contrib/function-extra-resources/issues/106. " + - "function-extra-resources (v0.2.0/v0.3.0) emits ResourceSelector{Namespace=\"\"} for " + - "by-name references that omit `ref.namespace` — it only forwards user-yaml verbatim, " + - "never inferring from the XR's namespace. Crossplane v2.3+'s render binary then does " + - "InMemoryClient.Get(name, namespace=\"\"), a strict (GVK, ns, name) match that doesn't " + - "find our resolved ConfigMap (stored at namespace=ns-{a,b}). By-label selectors aren't " + - "affected — List(InNamespace(\"\")) lists across all namespaces — which is why our " + - "other extra-resources tests still pass. Re-enable once a function release defaults " + - "selector.Namespace to the XR's namespace.", - timeout: longTimeout, // Test involves multiple namespaces and can be slow with Docker contention - // This test catches a bug where the cache key didn't include namespace, causing - // ConfigMaps with the same name in different namespaces to collide. - // The XR in ns-a should get ConfigMap from ns-a (value: from-ns-a) - // The XR in ns-b should get ConfigMap from ns-b (value: from-ns-b) - // Bug: Both XRs would get the same ConfigMap (whichever was cached first) - setupFiles: []string{ - // Create namespaces first - "testdata/comp/resources/namespace-collision/namespace-ns-a.yaml", - "testdata/comp/resources/namespace-collision/namespace-ns-b.yaml", - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/functions.yaml", - "testdata/comp/resources/namespace-collision/collision-composition.yaml", - "testdata/comp/resources/namespace-collision/collision-composition-revision.yaml", - // ConfigMaps with same name in different namespaces - "testdata/comp/resources/namespace-collision/configmap-ns-a.yaml", - "testdata/comp/resources/namespace-collision/configmap-ns-b.yaml", - }, - // XRs and their downstream resources in different namespaces - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - { - OwnerFile: "testdata/comp/resources/namespace-collision/existing-xr-ns-a.yaml", - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - "testdata/comp/resources/namespace-collision/existing-downstream-ns-a.yaml": nil, - }, - }, - { - OwnerFile: "testdata/comp/resources/namespace-collision/existing-xr-ns-b.yaml", - OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ - "testdata/comp/resources/namespace-collision/existing-downstream-ns-b.yaml": nil, - }, - }, - }, - inputFiles: []string{"testdata/comp/updated-collision-composition.yaml"}, - // Expected: Each XR's downstream should show the correct prefix applied to its namespace's ConfigMap value. - // XR in ns-a: from-ns-a → prefix-from-ns-a - // XR in ns-b: from-ns-b → prefix-from-ns-b - // Bug scenario: If namespace collision occurs, ns-b would incorrectly show: - // from-ns-b → prefix-from-ns-a (wrong ConfigMap cached from ns-a) - expectedOutput: `=== Impact Analysis === - -~~~ XDownstreamResource/xr-in-ns-a - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - annotations: - crossplane.io/composition-resource-name: collision-resource - labels: - crossplane.io/composite: xr-in-ns-a - name: xr-in-ns-a - namespace: ns-a - spec: - forProvider: -- configData: from-ns-a -+ configData: prefix-from-ns-a - ---- -~~~ XDownstreamResource/xr-in-ns-b - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - annotations: - crossplane.io/composition-resource-name: collision-resource - labels: - crossplane.io/composite: xr-in-ns-b - name: xr-in-ns-b - namespace: ns-b - spec: - forProvider: -- configData: from-ns-b -+ configData: prefix-from-ns-b - ---- - -Summary: 2 modified`, - expectedError: false, - expectedExitCode: dp.ExitCodeDiffDetected, - noColor: true, - }, - // Paired comp sequencer-gating tests — mirror of the XR pair. Verifies that - // --eventual-state on `comp` surfaces downstream resources hidden by - // function-sequencer in the impact analysis. - "CompSequencerGatesLaterStageWithoutEventualState": { - reason: "`comp` without --eventual-state: sequencer-gated stage1 is absent from the impact analysis", - timeout: longTimeout, - outputFormat: "json", - // eventualState deliberately false - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/sequencer-gating-composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - // The XR references sequencer-gating-composition by name, so comp discovers it. It has - // composed stage0, which isn't Ready, so stage1 stays gated. - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes("testdata/comp/resources/existing-sequencer-gating-xr.yaml", "testdata/comp/resources/existing-sequencer-gating-stage0.yaml"), - }, - inputFiles: []string{"testdata/comp/updated-sequencer-gating-composition.yaml"}, - namespace: "default", - expectedExitCode: dp.ExitCodeDiffDetected, - // Expect: impact shows stage0 modified (its configData changed); stage1 is absent from - // downstream impact because sequencer gates it. - expectedStructured: simtest.Expect(). - WithComposition("sequencer-gating-composition"). - WithCompositionModified(). - WithXRImpact("XNopResource", "sequencer-gating-test", "default", "changed"). - WithDownstreamSummary(0, 1, 0). - WithDownstreamResource("modified", "XDownstreamResource", "0-stage0-resource", "default"). - WithFieldChange("spec.forProvider.configData", "existing-value", "updated-existing-value"), - expectedError: false, - }, - "CompSequencerGatedStageAppearsWithEventualState": { - reason: "`comp` with --eventual-state: sequencer-gated stage1 surfaces in the impact analysis", - timeout: longTimeout, // Multiple simulation iterations need more time - outputFormat: "json", - eventualState: true, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/sequencer-gating-composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - // The XR has composed stage0, which isn't Ready, so stage1 stays gated until eventual - // state synthesizes stage0's Ready (#564). - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes("testdata/comp/resources/existing-sequencer-gating-xr.yaml", "testdata/comp/resources/existing-sequencer-gating-stage0.yaml"), - }, - inputFiles: []string{"testdata/comp/updated-sequencer-gating-composition.yaml"}, - namespace: "default", - expectedExitCode: dp.ExitCodeDiffDetected, - // Expect: impact shows stage0 modified and stage1 added (eventual-state unlocks stage1). - expectedStructured: simtest.Expect(). - WithComposition("sequencer-gating-composition"). - WithCompositionModified(). - WithXRImpact("XNopResource", "sequencer-gating-test", "default", "changed"). - WithDownstreamSummary(1, 1, 0). - WithDownstreamResource("modified", "XDownstreamResource", "0-stage0-resource", "default"). - WithFieldChange("spec.forProvider.configData", "existing-value", "updated-existing-value"). - AndXR(). - WithDownstreamResource("added", "XDownstreamResource", "1-stage1-resource", "default"). - // dryRun absence through `comp`: a downstream addition that reached the apiserver - // carries no dryRun object. Piggy-backed on an existing case rather than given its - // own envtest instance, since it is a pure additional assertion. - WithDryRunPerformed(). - WithField("spec.forProvider.configData", "updated-existing-value"), - expectedError: false, - }, - // Paired comp composition-driven gating tests — gating encoded in go-templating, - // no function-sequencer. Mirrors the XR composition-driven pair. - "CompConditionalGatingHidesLaterStageWithoutEventualState": { - reason: "`comp` without --eventual-state: template-gated stage1 is absent from the impact analysis", - timeout: longTimeout, - outputFormat: "json", - // eventualState deliberately false - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/conditional-gating-composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - // The XR has composed stage0, which isn't Ready, so stage1 stays gated. - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes("testdata/comp/resources/existing-conditional-gating-xr.yaml", "testdata/comp/resources/existing-conditional-gating-stage0.yaml"), - }, - inputFiles: []string{"testdata/comp/updated-conditional-gating-composition.yaml"}, - namespace: "default", - expectedExitCode: dp.ExitCodeDiffDetected, - expectedStructured: simtest.Expect(). - WithComposition("conditional-gating-composition"). - WithCompositionModified(). - WithXRImpact("XNopResource", "conditional-gating-test", "default", "changed"). - WithDownstreamSummary(0, 1, 0). - WithDownstreamResource("modified", "XDownstreamResource", "0-stage0-resource", "default"). - WithFieldChange("spec.forProvider.configData", "existing-value", "updated-existing-value"), - expectedError: false, - }, - "CompConditionalGatedStageAppearsWithEventualState": { - reason: "`comp` with --eventual-state: template-gated stage1 surfaces in the impact analysis", - timeout: longTimeout, // Multiple simulation iterations need more time - outputFormat: "json", - eventualState: true, - setupFiles: []string{ - "testdata/diff/resources/xrd.yaml", - "testdata/diff/resources/conditional-gating-composition.yaml", - "testdata/diff/resources/functions.yaml", - }, - // The XR has composed stage0, which isn't Ready, so stage1 stays gated until eventual - // state synthesizes stage0's Ready (#564). - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes("testdata/comp/resources/existing-conditional-gating-xr.yaml", "testdata/comp/resources/existing-conditional-gating-stage0.yaml"), - }, - inputFiles: []string{"testdata/comp/updated-conditional-gating-composition.yaml"}, - namespace: "default", - expectedExitCode: dp.ExitCodeDiffDetected, - expectedStructured: simtest.Expect(). - WithComposition("conditional-gating-composition"). - WithCompositionModified(). - WithXRImpact("XNopResource", "conditional-gating-test", "default", "changed"). - WithDownstreamSummary(1, 1, 0). - WithDownstreamResource("modified", "XDownstreamResource", "0-stage0-resource", "default"). - WithFieldChange("spec.forProvider.configData", "existing-value", "updated-existing-value"). - AndXR(). - WithDownstreamResource("added", "XDownstreamResource", "1-stage1-resource", "default"). - WithField("spec.forProvider.configData", "updated-existing-value"), - expectedError: false, - }, - // dryRun presence through `comp`. dryRun lives on ChangeDetail, which `xr` reaches via - // changes[] and `comp` via impactAnalysis[].downstreamChanges.changes[] — one shared wire - // shape, but only an assertion through the comp renderer proves comp actually populates it. - // The composed resource is deliberately absent from the setup so the impact is an ADDITION, - // which is the only kind of change the dryRun field applies to: the XR has its composition - // refs, but its first compose has not created its resource. - "CompDownstreamAdditionReportsDryRunSkippedWhenDryRunOnExisting": { - reason: "`comp` structured output carries dryRun.skipReason on a downstream addition when --dry-run-on=existing", - outputFormat: "json", - dryRunOn: "existing", - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/original-composition.yaml", - "testdata/comp/resources/functions.yaml", - // existing-downstream-1.yaml is intentionally NOT applied: with no composed resource - // in the cluster, the impact analysis reports an addition rather than a modification. - "testdata/comp/resources/existing-xr-1.yaml", - }, - inputFiles: []string{"testdata/comp/updated-composition.yaml"}, - namespace: "default", - expectedExitCode: dp.ExitCodeDiffDetected, - expectedStructured: simtest.Expect(). - WithComposition("xnopresources.diff.example.org"). - WithCompositionModified(). - WithXRImpact("XNopResource", "test-resource", "default", "changed"). - WithDownstreamSummary(1, 0, 0). - WithDownstreamResource("added", "XDownstreamResource", "test-resource", "default"). - WithDryRunSkipped("disabled", ""). - WithField("spec.forProvider.configData", "updated-existing-value"), - expectedError: false, - }, - // The unverified-addition warning through `comp`: one run-wide summary in the top-level - // warnings[], counting the downstream additions behind it, rather than one per composition. - // A cluster-scoped XR is what lets the composition reach a namespace that does not exist. - "CompDownstreamAdditionsInMissingNamespaceAreCountedInOneWarning": { - reason: "`comp` summarises downstream additions it could not verify as one warning per GVK + namespace + cause, with a count", - outputFormat: "json", - setupFiles: []string{ - "testdata/diff/resources/cluster-xrd.yaml", - "testdata/comp/resources/unverified-cluster-composition.yaml", - "testdata/comp/resources/functions.yaml", - "testdata/comp/resources/existing-unverified-cluster-xr.yaml", - }, - inputFiles: []string{"testdata/comp/updated-unverified-cluster-composition.yaml"}, - expectedExitCode: dp.ExitCodeDiffDetected, - expectedStderrContains: []string{ - `WARNING: skipped apiserver verification of added resources: their namespace does not exist yet, so their diffs omit server-side defaulting and admission ` + - `(cause=namespaces "nonexistent-namespace" not found, count=2, gvk=/v1, Kind=ConfigMap, namespace=nonexistent-namespace)`, - }, - expectedStructured: simtest.Expect(). - WithWarning("their namespace does not exist yet"). - WithWarningContext(map[string]string{ - "gvk": "/v1, Kind=ConfigMap", - "namespace": "nonexistent-namespace", - "cause": `namespaces "nonexistent-namespace" not found`, - "count": "2", - }). - And(). - WithComposition("xnopresources-unverified.diff.example.org"). - WithCompositionModified(). - WithXRImpact("XNopResource", "unverified-xr", "", "changed"). - WithDownstreamSummary(2, 0, 0). - WithDownstreamResource("added", "ConfigMap", "unverified-xr-first", "nonexistent-namespace"). - WithDryRunSkipped("namespaceNotFound", "nonexistent-namespace"). - AndXR(). - WithDownstreamResource("added", "ConfigMap", "unverified-xr-second", "nonexistent-namespace"). - WithDryRunSkipped("namespaceNotFound", "nonexistent-namespace"), - expectedError: false, - }, - // --resource flag tests (issue #321) - "ResourceFilterScopesAffectedXRs": { - reason: "--resource limits impact analysis to the named composites and ignores the rest", - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/original-composition.yaml", - "testdata/comp/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( // test-resource (default ns) - "testdata/comp/resources/existing-xr-1.yaml", - "testdata/comp/resources/existing-downstream-1.yaml", - ), - integration.Composes( // another-resource (default ns) - "testdata/comp/resources/existing-xr-2.yaml", - "testdata/comp/resources/existing-downstream-2.yaml", - ), - }, - inputFiles: []string{"testdata/comp/updated-composition.yaml"}, - resources: []string{"default/test-resource"}, - outputFormat: "json", - expectedExitCode: dp.ExitCodeDiffDetected, - expectedStructured: simtest.Expect(). - WithComposition("xnopresources.diff.example.org"). - WithCompositionModified(). - WithAffectedResources(1, 1, 0, 0). - WithXRImpact("XNopResource", "test-resource", "default", "changed"), - }, - "LeastPrivilegeCredentialsDiffSucceeds": { - reason: "A user who can list only the composition's XR type, not Crossplane's own types, gets the admin's impact analysis", - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/original-composition.yaml", - "testdata/comp/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/comp/resources/existing-xr-1.yaml", - "testdata/comp/resources/existing-downstream-1.yaml", - ), - integration.Composes( - "testdata/comp/resources/existing-xr-2.yaml", - "testdata/comp/resources/existing-downstream-2.yaml", - ), - }, - inputFiles: []string{"testdata/comp/updated-composition.yaml"}, - namespace: "default", - // list on the XR type: without --resource, comp finds the composition's XRs by listing them. - rbacRules: leastPrivilegeRules("get", "list", "create", "patch"), - outputFormat: "json", - expectedExitCode: dp.ExitCodeDiffDetected, - expectedStructured: simtest.Expect(). - WithComposition("xnopresources.diff.example.org"). - WithCompositionModified(). - WithAffectedResources(2, 2, 0, 0). - WithXRImpact("XNopResource", "test-resource", "default", "changed"). - AndComp(). - WithXRImpact("XNopResource", "another-resource", "default", "changed"), - }, - "NameScopedRunSucceedsWithoutCompositeList": { - reason: "--resource looks its composites up by name, so a user who cannot list the XR type gets the admin's impact analysis", - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/original-composition.yaml", - "testdata/comp/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/comp/resources/existing-xr-1.yaml", - "testdata/comp/resources/existing-downstream-1.yaml", - ), - integration.Composes( - "testdata/comp/resources/existing-xr-2.yaml", - "testdata/comp/resources/existing-downstream-2.yaml", - ), - }, - inputFiles: []string{"testdata/comp/updated-composition.yaml"}, - resources: []string{"default/test-resource"}, - rbacRules: leastPrivilegeRules("get", "create", "patch"), - outputFormat: "json", - expectedExitCode: dp.ExitCodeDiffDetected, - expectedStructured: simtest.Expect(). - WithComposition("xnopresources.diff.example.org"). - WithCompositionModified(). - WithAffectedResources(1, 1, 0, 0). - WithXRImpact("XNopResource", "test-resource", "default", "changed"), - }, - "ResourceFilterCommaSeparated": { - reason: "--resource accepts comma-separated values via kong's auto-parsing", - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/original-composition.yaml", - "testdata/comp/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/comp/resources/existing-xr-1.yaml", - "testdata/comp/resources/existing-downstream-1.yaml", - ), - integration.Composes( - "testdata/comp/resources/existing-xr-2.yaml", - "testdata/comp/resources/existing-downstream-2.yaml", - ), - }, - inputFiles: []string{"testdata/comp/updated-composition.yaml"}, - resourcesCSV: "default/test-resource,default/another-resource", - outputFormat: "json", - expectedExitCode: dp.ExitCodeDiffDetected, - expectedStructured: simtest.Expect(). - WithComposition("xnopresources.diff.example.org"). - WithCompositionModified(). - WithAffectedResources(2, 2, 0, 0). - WithXRImpact("XNopResource", "test-resource", "default", "changed"). - AndComp(). - WithXRImpact("XNopResource", "another-resource", "default", "changed"), - }, - "ResourceFilterUnmatched_FailsBeforeRendering": { - reason: "--resource naming a non-existent composite fails fast with an error before any rendering", - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/original-composition.yaml", - "testdata/comp/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/comp/resources/existing-xr-1.yaml", - "testdata/comp/resources/existing-downstream-1.yaml", - ), - }, - inputFiles: []string{"testdata/comp/updated-composition.yaml"}, - resources: []string{"default/does-not-exist"}, - expectedError: true, - expectedErrorContains: "does-not-exist", - expectedExitCode: dp.ExitCodeToolError, - }, - "ResourceFilterRespectsManualPolicy_WithoutIncludeManual": { - reason: "--resource matching a Manual-policy composite surfaces it as filtered (reason manual_policy) when --include-manual is unset", - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/original-composition.yaml", - "testdata/comp/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/comp/resources/existing-xr-manual.yaml", - "testdata/comp/resources/existing-downstream-manual.yaml", - ), - }, - inputFiles: []string{"testdata/comp/updated-composition.yaml"}, - resources: []string{"default/manual-resource"}, - outputFormat: "json", - expectedExitCode: dp.ExitCodeDiffDetected, // composition itself changed - expectedStructured: simtest.Expect(). - WithComposition("xnopresources.diff.example.org"). - WithCompositionModified(). - WithXRImpact("XNopResource", "manual-resource", "default", "filtered"). - WithFilterReason("manualPolicy"), - }, - "ResourceFilterRespectsManualPolicy_WithIncludeManual": { - reason: "--include-manual evaluates the Manual-policy composite normally instead of marking it filtered", - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/original-composition.yaml", - "testdata/comp/resources/functions.yaml", - }, - crossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ - integration.Composes( - "testdata/comp/resources/existing-xr-manual.yaml", - "testdata/comp/resources/existing-downstream-manual.yaml", - ), - }, - inputFiles: []string{"testdata/comp/updated-composition.yaml"}, - resources: []string{"default/manual-resource"}, - includeManual: true, - outputFormat: "json", - expectedExitCode: dp.ExitCodeDiffDetected, - expectedStructured: simtest.Expect(). - WithComposition("xnopresources.diff.example.org"). - WithCompositionModified(). - WithXRImpact("XNopResource", "manual-resource", "default", "changed"), - }, - // Issue #388 (Bug A): an Automatic-policy XR whose compositionRevisionSelector does not match - // the edited composition's labels would not adopt the resulting revision, so it must be - // surfaced as filtered (reason revision_selector_mismatch), NOT evaluated for changes. - "ResourceFilterRespectsRevisionSelectorMismatch": { - reason: "--resource matching an Automatic XR whose compositionRevisionSelector does not match the composition's labels surfaces it as filtered (reason revision_selector_mismatch)", - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/original-composition.yaml", - "testdata/comp/resources/functions.yaml", - "testdata/comp/resources/existing-xr-selector-mismatch.yaml", - }, - inputFiles: []string{"testdata/comp/updated-composition-labeled.yaml"}, - resources: []string{"default/selector-mismatch-resource"}, - outputFormat: "json", - expectedExitCode: dp.ExitCodeDiffDetected, // composition itself changed - expectedStructured: simtest.Expect(). - WithComposition("xnopresources.diff.example.org"). - WithCompositionModified(). - WithXRImpact("XNopResource", "selector-mismatch-resource", "default", "filtered"). - WithFilterReason("revisionSelectorMismatch"), - // Note: that --include-manual does NOT rescue a selector-mismatched Automatic XR is - // proven at the unit level (TestDefaultCompDiffProcessor_partitionXRsByUpdatePolicy / - // IncludeManualTrue_StillDropsSelectorMismatchedAutomaticXRs). A second slow - // docker-render integration case for that flag interaction would be redundant. - }, - // Issue #388 (Bug A, positive): an Automatic XR whose compositionRevisionSelector DOES match - // the edited composition's labels is kept and evaluated for downstream changes (not filtered). - "ResourceFilterRespectsRevisionSelectorMatch": { - reason: "--resource matching an Automatic XR whose compositionRevisionSelector matches the composition's labels keeps it and reports downstream changes", - setupFiles: []string{ - "testdata/comp/resources/xrd.yaml", - "testdata/comp/resources/original-composition.yaml", - "testdata/comp/resources/functions.yaml", - "testdata/comp/resources/existing-xr-selector-match.yaml", - }, - inputFiles: []string{"testdata/comp/updated-composition-labeled.yaml"}, - resources: []string{"default/selector-match-resource"}, - outputFormat: "json", - expectedExitCode: dp.ExitCodeDiffDetected, - expectedStructured: simtest.Expect(). - WithComposition("xnopresources.diff.example.org"). - WithCompositionModified(). - WithXRImpact("XNopResource", "selector-match-resource", "default", "changed"), - }, - } - - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - runIntegrationTest(t, CompositionDiffTest, tt) - }) - } -} diff --git a/cmd/crossplane/project/testdata/README.md b/cmd/crossplane/project/testdata/README.md index 3c0e7e78..c86a2ab1 100644 --- a/cmd/crossplane/project/testdata/README.md +++ b/cmd/crossplane/project/testdata/README.md @@ -1,24 +1,26 @@ -# Simulate integration tests +# Project simulate integration tests The `integration*_test.go` files in `cmd/crossplane/project` run `crossplane project simulate -f ` against fixture projects under `testdata/project`, each against its own envtest cluster seeded from -`testdata/comp/resources`. `TestCompDiffIntegration` runs -`crossplane composition simulate -f ` with the -crossplane-diff cases and fixtures under `testdata/comp`, plus a few shared -ones under `testdata/diff`. -`TestDiffIntegration` runs `crossplane xr simulate -f ` with -crossplane-diff's XR cases and the same fixtures. +the shared fixtures in `internal/testutils/integration/testdata/comp`. +`TestSimulatePlanOutput` and `TestSimulateProviderPlan` also run +`composition simulate` and `xr simulate` for their output formats. The +`composition` and `xr` suites, with their own `integration_test.go`, are +in `cmd/crossplane/composition` and `cmd/crossplane/xr`; the harness all +three share is `internal/testutils/integration/simharness`, and the +fixtures more than one suite uses are in +`internal/testutils/integration/testdata`. ## How a case runs A case copies its project fixture to a temp dir and runs the real command through kong with `runSimulateArgvWith` in -`integration_simulate_harness_test.go`. `simulate.Hooks` points the +`internal/testutils/integration/simharness`. `simulate.Hooks` points the command at the case's envtest cluster in place of a kubeconfig, so the cases run in parallel. -A `TestCompDiffIntegration` or `TestDiffIntegration` case keeps its crossplane-diff args; +A `TestCompDiffIntegration` (`cmd/crossplane/composition`) or `TestDiffIntegration` (`cmd/crossplane/xr`) case keeps its crossplane-diff args; `runSimulate` translates them into `composition simulate`'s or `xr simulate`'s flags (its files as `-f`, `--resource` as `--name`, `--include-manual` as @@ -39,7 +41,7 @@ unset. # Full suite (Docker + envtest assets required): CROSSPLANE_CRDS_DIR= \ KUBEBUILDER_ASSETS=$(setup-envtest use -p path) \ -go test -tags integration -v ./cmd/crossplane/project/ +go test -tags integration -v ./cmd/crossplane/project/ ./cmd/crossplane/composition/ ./cmd/crossplane/xr/ # One case: go test -tags integration -run 'TestProjectSimulateIntegration/PlainProject' ./cmd/crossplane/project/ diff --git a/cmd/crossplane/project/testdata/comp/api-version-updated-composition.yaml b/cmd/crossplane/project/testdata/comp/api-version-updated-composition.yaml deleted file mode 100644 index 02748913..00000000 --- a/cmd/crossplane/project/testdata/comp/api-version-updated-composition.yaml +++ /dev/null @@ -1,32 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xapimigrateresources.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-api-versioned-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: comp.example.org/v1beta2 - kind: XApiMigrateResource - metadata: - name: {{ .observed.composite.resource.metadata.name }}-api-resource - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: api-migrate-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/claim-revision-templating-updated-composition.yaml b/cmd/crossplane/project/testdata/comp/claim-revision-templating-updated-composition.yaml deleted file mode 100644 index 60bfc213..00000000 --- a/cmd/crossplane/project/testdata/comp/claim-revision-templating-updated-composition.yaml +++ /dev/null @@ -1,37 +0,0 @@ -# A metadata-only edit of resources/claim-revision-templating-composition.yaml: one added label, -# identical spec. Applying it mints a new CompositionRevision, which an Automatic claim's backing XR -# re-points to — and the template reads that revision's name, so the composed resource changes. -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xrevisionrefclaims.diff.example.org - labels: - channel: preview -spec: - compositeTypeRef: - apiVersion: diff.example.org/v1alpha1 - kind: XNop - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - name: {{ .observed.composite.resource.metadata.name }} - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.compositionRevisionRef.name }} - resourceTier: basic - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/composition-no-changes.yaml b/cmd/crossplane/project/testdata/comp/composition-no-changes.yaml deleted file mode 100644 index 556cf7bf..00000000 --- a/cmd/crossplane/project/testdata/comp/composition-no-changes.yaml +++ /dev/null @@ -1,33 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnopresources.diff.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - resourceTier: basic - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/field-removal-updated-composition.yaml b/cmd/crossplane/project/testdata/comp/field-removal-updated-composition.yaml deleted file mode 100644 index 7cff974f..00000000 --- a/cmd/crossplane/project/testdata/comp/field-removal-updated-composition.yaml +++ /dev/null @@ -1,32 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xfieldremoval.diff.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/mixed-status-updated-composition.yaml b/cmd/crossplane/project/testdata/comp/mixed-status-updated-composition.yaml deleted file mode 100644 index 3e01b1e8..00000000 --- a/cmd/crossplane/project/testdata/comp/mixed-status-updated-composition.yaml +++ /dev/null @@ -1,59 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xmixed.diff.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - --- - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }}-database - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: database - spec: - forProvider: - resourceType: database - tier: premium - backupEnabled: true - --- - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }}-storage - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: storage - spec: - forProvider: - resourceType: storage - tier: standard - --- - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }}-network - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: network - spec: - forProvider: - resourceType: network - tier: standard - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/net-new-composition.yaml b/cmd/crossplane/project/testdata/comp/net-new-composition.yaml deleted file mode 100644 index 4bfa487f..00000000 --- a/cmd/crossplane/project/testdata/comp/net-new-composition.yaml +++ /dev/null @@ -1,37 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnewresources.diff.example.org - labels: - environment: staging - provider: aws -spec: - compositeTypeRef: - apiVersion: staging.diff.example.org/v1alpha1 - kind: XNewResource - mode: Pipeline - pipeline: - - step: generate-new-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: staging.nop.example.org/v1alpha1 - kind: XStagingResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: staging-resource - spec: - forProvider: - environment: staging - configData: new-{{ .observed.composite.resource.spec.newField }} - resourceTier: premium - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready \ No newline at end of file diff --git a/cmd/crossplane/project/testdata/comp/resources/api-version-original-composition.yaml b/cmd/crossplane/project/testdata/comp/resources/api-version-original-composition.yaml deleted file mode 100644 index 00221d82..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/api-version-original-composition.yaml +++ /dev/null @@ -1,32 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xapimigrateresources.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-api-versioned-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: comp.example.org/v1beta1 - kind: XApiMigrateResource - metadata: - name: {{ .observed.composite.resource.metadata.name }}-api-resource - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: api-migrate-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/resources/claim-composition.yaml b/cmd/crossplane/project/testdata/comp/resources/claim-composition.yaml deleted file mode 100644 index 58219d58..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/claim-composition.yaml +++ /dev/null @@ -1,32 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: nopclaims.diff.example.org -spec: - compositeTypeRef: - apiVersion: diff.example.org/v1alpha1 - kind: XNop - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - name: {{ .observed.composite.resource.metadata.name }} - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - resourceTier: basic - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/resources/claim-revision-templating-composition-kubectl-applied.yaml b/cmd/crossplane/project/testdata/comp/resources/claim-revision-templating-composition-kubectl-applied.yaml deleted file mode 100644 index f7a6b7c3..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/claim-revision-templating-composition-kubectl-applied.yaml +++ /dev/null @@ -1,41 +0,0 @@ -# claim-revision-templating-composition.yaml as the cluster holds it after a client-side -# `kubectl apply` of an older file: identical content, plus a last-applied-configuration annotation -# that no longer describes the file. Diffing claim-revision-templating-composition.yaml against this is -# a change whose only delta is that annotation, so the name of the revision it would mint cannot be -# predicted, nothing is seeded, and every composite renders against the revision it tracks now. -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xrevisionrefclaims.diff.example.org - annotations: - # Deliberately stale: it describes some earlier file, not the one diffed against this. - kubectl.kubernetes.io/last-applied-configuration: | - {"apiVersion":"apiextensions.crossplane.io/v1","kind":"Composition","metadata":{"name":"xrevisionrefclaims.diff.example.org"},"spec":{"mode":"Pipeline"}} -spec: - compositeTypeRef: - apiVersion: diff.example.org/v1alpha1 - kind: XNop - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - name: {{ .observed.composite.resource.metadata.name }} - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.compositionRevisionRef.name }} - resourceTier: basic - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/resources/claim-revision-templating-composition.yaml b/cmd/crossplane/project/testdata/comp/resources/claim-revision-templating-composition.yaml deleted file mode 100644 index 0f5cd92c..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/claim-revision-templating-composition.yaml +++ /dev/null @@ -1,39 +0,0 @@ -# The claim-side counterpart of revision-templating-composition.yaml: the in-cluster copy of a -# composition whose template reads the CompositionRevision's own name off the composite and propagates -# it into a composed resource. XNop is a legacy (claim-capable) XR, so the ref is at the v1 path, -# spec.compositionRevisionRef. -# -# Paired with ../claim-revision-templating-updated-composition.yaml (a label-only edit) and with -# claim-revision-templating-composition-kubectl-applied.yaml (this, plus a stale kubectl annotation). -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xrevisionrefclaims.diff.example.org -spec: - compositeTypeRef: - apiVersion: diff.example.org/v1alpha1 - kind: XNop - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - name: {{ .observed.composite.resource.metadata.name }} - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.compositionRevisionRef.name }} - resourceTier: basic - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/resources/claim-xrd.yaml b/cmd/crossplane/project/testdata/comp/resources/claim-xrd.yaml deleted file mode 100644 index b2b8dfa7..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/claim-xrd.yaml +++ /dev/null @@ -1,27 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: CompositeResourceDefinition -metadata: - name: xnops.diff.example.org -spec: - group: diff.example.org - names: - kind: XNop - plural: xnops - claimNames: - kind: NopClaim - plural: nopclaims - versions: - - name: v1alpha1 - served: true - referenceable: true - schema: - openAPIV3Schema: - type: object - properties: - spec: - type: object - properties: - coolField: - type: string - writeConnectionSecretsToNamespace: - type: string diff --git a/cmd/crossplane/project/testdata/comp/resources/custom-namespace.yaml b/cmd/crossplane/project/testdata/comp/resources/custom-namespace.yaml deleted file mode 100644 index bb71c9a0..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/custom-namespace.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: custom-namespace \ No newline at end of file diff --git a/cmd/crossplane/project/testdata/comp/resources/cycle/existing-cycle-xr.yaml b/cmd/crossplane/project/testdata/comp/resources/cycle/existing-cycle-xr.yaml deleted file mode 100644 index 38d622c3..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/cycle/existing-cycle-xr.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# An existing XR on the XCycleA composition, which comp discovers and re-renders against the -# updated composition. Its composition starts the XCycleA -> XCycleB -> XCycleA cycle. -apiVersion: ns.cycle.example.org/v1alpha1 -kind: XCycleA -metadata: - name: test-cycle - namespace: default -spec: - cycleField: "cycle-value" - crossplane: - compositionRef: - name: xcycleas.cycle.example.org diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-api-version-downstream-v1beta1.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-api-version-downstream-v1beta1.yaml deleted file mode 100644 index 8b63f58b..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-api-version-downstream-v1beta1.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: comp.example.org/v1beta1 -kind: XApiMigrateResource -metadata: - name: test-api-version-api-resource - namespace: default - annotations: - crossplane.io/composition-resource-name: api-migrate-resource -spec: - forProvider: - configData: test-value diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-api-version-xr.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-api-version-xr.yaml deleted file mode 100644 index 05f4d7b2..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-api-version-xr.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-api-version - namespace: default -spec: - crossplane: - compositionRef: - name: xapimigrateresources.example.org - compositionUpdatePolicy: Automatic - coolField: test-value diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-claim-1-xr.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-claim-1-xr.yaml deleted file mode 100644 index 24de708a..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-claim-1-xr.yaml +++ /dev/null @@ -1,21 +0,0 @@ -# The backing XR Crossplane's claim syncer created for existing-claim-1.yaml. Its composed resource is -# existing-claim-downstream-1.yaml. -apiVersion: diff.example.org/v1alpha1 -kind: XNop -metadata: - name: test-claim-1-xr -spec: - claimRef: - apiVersion: diff.example.org/v1alpha1 - kind: NopClaim - name: test-claim-1 - namespace: test-namespace - coolField: claim-value-1 - compositeDeletePolicy: Background - compositionUpdatePolicy: Automatic - compositionRef: - name: nopclaims.diff.example.org - resourceRefs: - - apiVersion: nop.example.org/v1alpha1 - kind: XDownstreamResource - name: test-claim-1-xr diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-claim-1.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-claim-1.yaml deleted file mode 100644 index dac0a2b9..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-claim-1.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: diff.example.org/v1alpha1 -kind: NopClaim -metadata: - name: test-claim-1 - namespace: test-namespace -spec: - coolField: claim-value-1 - compositeDeletePolicy: Background - compositionRef: - name: nopclaims.diff.example.org - # Copied back from the backing XR, as the claim syncer does under the XR's Automatic update policy. - compositionRevisionRef: - name: nopclaims.diff.example.org-e164b8b - # Bound to its backing XR, existing-claim-1-xr.yaml, as Crossplane's claim syncer binds it. - resourceRef: - apiVersion: diff.example.org/v1alpha1 - kind: XNop - name: test-claim-1-xr diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-claim-2-xr.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-claim-2-xr.yaml deleted file mode 100644 index de2703cb..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-claim-2-xr.yaml +++ /dev/null @@ -1,21 +0,0 @@ -# The backing XR Crossplane's claim syncer created for existing-claim-2.yaml. Its composed resource is -# existing-claim-downstream-2.yaml. -apiVersion: diff.example.org/v1alpha1 -kind: XNop -metadata: - name: test-claim-2-xr -spec: - claimRef: - apiVersion: diff.example.org/v1alpha1 - kind: NopClaim - name: test-claim-2 - namespace: test-namespace - coolField: claim-value-2 - compositeDeletePolicy: Background - compositionUpdatePolicy: Automatic - compositionRef: - name: nopclaims.diff.example.org - resourceRefs: - - apiVersion: nop.example.org/v1alpha1 - kind: XDownstreamResource - name: test-claim-2-xr diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-claim-2.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-claim-2.yaml deleted file mode 100644 index 7f7b89e1..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-claim-2.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: diff.example.org/v1alpha1 -kind: NopClaim -metadata: - name: test-claim-2 - namespace: test-namespace -spec: - coolField: claim-value-2 - compositeDeletePolicy: Background - compositionRef: - name: nopclaims.diff.example.org - # Copied back from the backing XR, as the claim syncer does under the XR's Automatic update policy. - compositionRevisionRef: - name: nopclaims.diff.example.org-e164b8b - # Bound to its backing XR, existing-claim-2-xr.yaml, as Crossplane's claim syncer binds it. - resourceRef: - apiVersion: diff.example.org/v1alpha1 - kind: XNop - name: test-claim-2-xr diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-claim-downstream-1.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-claim-downstream-1.yaml deleted file mode 100644 index 23fd1f94..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-claim-downstream-1.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - annotations: - crossplane.io/composition-resource-name: nop-resource - name: test-claim-1-xr -spec: - forProvider: - configData: claim-value-1 - resourceTier: basic diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-claim-downstream-2.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-claim-downstream-2.yaml deleted file mode 100644 index f0848c7b..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-claim-downstream-2.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - annotations: - crossplane.io/composition-resource-name: nop-resource - name: test-claim-2-xr -spec: - forProvider: - configData: claim-value-2 - resourceTier: basic diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-claim-revision-ref-downstream.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-claim-revision-ref-downstream.yaml deleted file mode 100644 index 4803e305..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-claim-revision-ref-downstream.yaml +++ /dev/null @@ -1,14 +0,0 @@ -# The composed resource of existing-claim-revision-ref-xr.yaml, rendered from the revision that XR -# tracks, so configData holds that revision's name. Carries crossplane.io/composition-resource-name (and, -# from the harness, the composite and claim labels) because the render adds all of them: a fixture missing -# any is reported as modified on every run, which would hide whether the revision name propagated. -apiVersion: nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: revision-ref-claim-xr - annotations: - crossplane.io/composition-resource-name: nop-resource -spec: - forProvider: - configData: xrevisionrefclaims.diff.example.org-98b484d - resourceTier: basic diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-claim-revision-ref-kubectl-applied-downstream.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-claim-revision-ref-kubectl-applied-downstream.yaml deleted file mode 100644 index 865fbace..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-claim-revision-ref-kubectl-applied-downstream.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# existing-claim-revision-ref-downstream.yaml as rendered from the revision -# existing-claim-revision-ref-kubectl-applied-xr.yaml tracks, so configData holds that revision's name. -apiVersion: nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: revision-ref-claim-xr - annotations: - crossplane.io/composition-resource-name: nop-resource -spec: - forProvider: - configData: xrevisionrefclaims.diff.example.org-329a7cd - resourceTier: basic diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-claim-revision-ref-kubectl-applied-xr.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-claim-revision-ref-kubectl-applied-xr.yaml deleted file mode 100644 index 9aa0011f..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-claim-revision-ref-kubectl-applied-xr.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# existing-claim-revision-ref-xr.yaml where the composition was applied with a client-side `kubectl -# apply`. Composition.Hash() covers annotations, so the revision it tracks has a different name. -apiVersion: diff.example.org/v1alpha1 -kind: XNop -metadata: - name: revision-ref-claim-xr -spec: - claimRef: - apiVersion: diff.example.org/v1alpha1 - kind: NopClaim - name: revision-ref-claim - namespace: test-namespace - coolField: revision-ref-claim-value - compositionRef: - name: xrevisionrefclaims.diff.example.org diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-claim-revision-ref-kubectl-applied.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-claim-revision-ref-kubectl-applied.yaml deleted file mode 100644 index 76a758b2..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-claim-revision-ref-kubectl-applied.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# existing-claim-revision-ref.yaml where the composition was applied with a client-side `kubectl apply` -# (claim-revision-templating-composition-kubectl-applied.yaml). Composition.Hash() covers annotations, -# so that composition's revision has another name, which the claim carries like its backing XR, -# existing-claim-revision-ref-kubectl-applied-xr.yaml. -apiVersion: diff.example.org/v1alpha1 -kind: NopClaim -metadata: - name: revision-ref-claim - namespace: test-namespace -spec: - coolField: revision-ref-claim-value - compositeDeletePolicy: Background - compositionRef: - name: xrevisionrefclaims.diff.example.org - compositionRevisionRef: - name: xrevisionrefclaims.diff.example.org-329a7cd - resourceRef: - apiVersion: diff.example.org/v1alpha1 - kind: XNop - name: revision-ref-claim-xr diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-claim-revision-ref-xr.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-claim-revision-ref-xr.yaml deleted file mode 100644 index 09cb9326..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-claim-revision-ref-xr.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# The backing XR of existing-claim-revision-ref.yaml, tracking the current revision of -# claim-revision-templating-composition.yaml as Crossplane's composite reconciler leaves it. -apiVersion: diff.example.org/v1alpha1 -kind: XNop -metadata: - name: revision-ref-claim-xr -spec: - claimRef: - apiVersion: diff.example.org/v1alpha1 - kind: NopClaim - name: revision-ref-claim - namespace: test-namespace - coolField: revision-ref-claim-value - compositionRef: - name: xrevisionrefclaims.diff.example.org diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-claim-revision-ref.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-claim-revision-ref.yaml deleted file mode 100644 index 6bb6c655..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-claim-revision-ref.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# A Claim at the default (Automatic) update policy, bound to existing-claim-revision-ref-xr.yaml. It -# carries that XR's compositionRevisionRef, because under Automatic the claim syncer copies the XR's ref -# back onto the claim. Going the other way the claim's ref is not propagated: under Automatic the XR -# controller owns the backing XR's ref, so that is the one render must see (issue #498). -apiVersion: diff.example.org/v1alpha1 -kind: NopClaim -metadata: - name: revision-ref-claim - namespace: test-namespace -spec: - coolField: revision-ref-claim-value - compositeDeletePolicy: Background - compositionRef: - name: xrevisionrefclaims.diff.example.org - compositionRevisionRef: - name: xrevisionrefclaims.diff.example.org-98b484d - resourceRef: - apiVersion: diff.example.org/v1alpha1 - kind: XNop - name: revision-ref-claim-xr diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-conditional-gating-stage0.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-conditional-gating-stage0.yaml deleted file mode 100644 index 2e0c9b2c..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-conditional-gating-stage0.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# The stage0 resource conditional-gating-composition.yaml composed for existing-conditional-gating-xr.yaml. -# It has no Ready condition yet, so stage1 stays gated until --eventual-state synthesizes one. -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - annotations: - crossplane.io/composition-resource-name: stage0-resource - name: 0-stage0-resource - namespace: default -spec: - forProvider: - configData: existing-value diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-conditional-gating-xr.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-conditional-gating-xr.yaml deleted file mode 100644 index 0a9b0167..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-conditional-gating-xr.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: conditional-gating-test - namespace: default -spec: - coolField: existing-value - crossplane: - compositionRef: - name: conditional-gating-composition diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-custom-ns-downstream.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-custom-ns-downstream.yaml deleted file mode 100644 index f22ee1bf..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-custom-ns-downstream.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: custom-namespace-resource - namespace: custom-namespace - annotations: - crossplane.io/composition-resource-name: nop-resource -spec: - forProvider: - configData: custom-ns-existing-value - resourceTier: basic \ No newline at end of file diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-custom-ns-xr.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-custom-ns-xr.yaml deleted file mode 100644 index c8c5d766..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-custom-ns-xr.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: custom-namespace-resource - namespace: custom-namespace -spec: - coolField: custom-ns-existing-value - crossplane: - compositionRef: - name: xnopresources.diff.example.org diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-downstream-deleting.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-downstream-deleting.yaml deleted file mode 100644 index 3dd7c99d..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-downstream-deleting.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: deleting-resource - namespace: default - annotations: - crossplane.io/composition-resource-name: nop-resource -spec: - forProvider: - configData: existing-value - resourceTier: basic diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-downstream-drifted.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-downstream-drifted.yaml deleted file mode 100644 index 347c4221..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-downstream-drifted.yaml +++ /dev/null @@ -1,14 +0,0 @@ -# existing-downstream-1.yaml after an out-of-band edit: someone changed configData in the cluster, so -# it no longer matches what the composition renders for existing-xr-1.yaml. Re-rendering an unchanged -# composition therefore surfaces a delta the composition did not cause. -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: test-resource - namespace: default - annotations: - crossplane.io/composition-resource-name: nop-resource -spec: - forProvider: - configData: drifted-value - resourceTier: basic diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-downstream-pinned-revision-ref.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-downstream-pinned-revision-ref.yaml deleted file mode 100644 index eea3c1cc..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-downstream-pinned-revision-ref.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# The composed resource of existing-xr-pinned-revision-ref.yaml as its pinned revision rendered it, so -# configData holds that revision's name. It carries crossplane.io/composition-resource-name as well as the -# gotemplating annotation, as a rendered resource does; see existing-downstream-revision-ref.yaml for why a -# fixture without it would show a diff on every run. -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: pinned-revision-ref-resource - namespace: default - annotations: - crossplane.io/composition-resource-name: nop-resource -spec: - forProvider: - configData: xrevisionrefs.diff.example.org-0d65896 - resourceTier: basic diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-downstream-v2.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-downstream-v2.yaml deleted file mode 100644 index 709ba671..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-downstream-v2.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# What original-composition-2.yaml currently renders for existing-xr-v2, so the diff against the -# changed composition shows only the fields that composition actually changed. -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: v2-resource - namespace: default - annotations: - crossplane.io/composition-resource-name: nop-resource-v2 -spec: - forProvider: - configData: v2-updated-v2-existing-value - resourceTier: enterprise diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-downstream-with-argocd.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-downstream-with-argocd.yaml deleted file mode 100644 index 73dcfbfc..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-downstream-with-argocd.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - annotations: - crossplane.io/composition-resource-name: nop-resource - argocd.argoproj.io/tracking-id: "test-app:/XDownstreamResource:default/test-resource" - labels: - argocd.argoproj.io/instance: test-app - name: test-resource - namespace: default -spec: - forProvider: - configData: existing-value - resourceTier: basic diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-sequencer-gating-stage0.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-sequencer-gating-stage0.yaml deleted file mode 100644 index 9f520639..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-sequencer-gating-stage0.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# The stage0 resource sequencer-gating-composition.yaml composed for existing-sequencer-gating-xr.yaml. -# It has no Ready condition yet, so stage1 stays gated until --eventual-state synthesizes one. -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - annotations: - crossplane.io/composition-resource-name: stage0-resource - name: 0-stage0-resource - namespace: default -spec: - forProvider: - configData: existing-value diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-sequencer-gating-xr.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-sequencer-gating-xr.yaml deleted file mode 100644 index bf00981f..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-sequencer-gating-xr.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: sequencer-gating-test - namespace: default -spec: - coolField: existing-value - crossplane: - compositionRef: - name: sequencer-gating-composition diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-sha256-downstream.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-sha256-downstream.yaml deleted file mode 100644 index c5c48f16..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-sha256-downstream.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: sha256-test-resource - namespace: default - annotations: - crossplane.io/composition-resource-name: nop-resource -spec: - forProvider: - configData: sha256-value - resourceTier: basic diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-sha256-xr.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-sha256-xr.yaml deleted file mode 100644 index 3409f380..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-sha256-xr.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: sha256-test-resource - namespace: default -spec: - coolField: sha256-value - crossplane: - compositionRef: - name: xnopresources-sha256.diff.example.org diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-unverified-cluster-xr.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-unverified-cluster-xr.yaml deleted file mode 100644 index 4b75574e..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-unverified-cluster-xr.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# A cluster-scoped XR using unverified-cluster-composition.yaml. It has composed nothing: the -# composition renders ConfigMaps into a namespace that does not exist, so Crossplane cannot create them. -apiVersion: diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: unverified-xr -spec: - coolField: existing-value - crossplane: - compositionRef: - name: xnopresources-unverified.diff.example.org diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-xr-1-kubectl-applied-current.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-xr-1-kubectl-applied-current.yaml deleted file mode 100644 index b7ff7ef3..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-xr-1-kubectl-applied-current.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# existing-xr-1.yaml where the composition was applied with a client-side `kubectl apply` of the -# file being diffed (original-composition-kubectl-applied-current.yaml). Composition.Hash() covers -# annotations, so that composition's revision has a different name, and this is the one the XR tracks. -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-resource - namespace: default -spec: - coolField: existing-value - crossplane: - compositionRef: - name: xnopresources.diff.example.org diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-xr-1-kubectl-applied.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-xr-1-kubectl-applied.yaml deleted file mode 100644 index ea3e88fe..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-xr-1-kubectl-applied.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# existing-xr-1.yaml where the composition was applied with a client-side `kubectl apply` -# (original-composition-kubectl-applied.yaml). Composition.Hash() covers annotations, so that -# composition's revision has a different name, and this is the one the XR tracks. -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-resource - namespace: default -spec: - coolField: existing-value - crossplane: - compositionRef: - name: xnopresources.diff.example.org diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-xr-deleting.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-xr-deleting.yaml deleted file mode 100644 index c4d717a5..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-xr-deleting.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: deleting-resource - namespace: default - # The finalizer keeps the object around after a delete is issued, so envtest (which runs no - # controllers) leaves it with metadata.deletionTimestamp set — a resource mid-deletion. - finalizers: - - composite.apiextensions.crossplane.io -spec: - coolField: existing-value - crossplane: - compositionRef: - name: xnopresources.diff.example.org diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-xr-listing-uncontrolled-resources.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-xr-listing-uncontrolled-resources.yaml deleted file mode 100644 index c03ffe8e..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-xr-listing-uncontrolled-resources.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# ../../diff/resources/existing-xr-listing-uncontrolled-resources.yaml, tracking the in-cluster -# removal-test-original-composition.yaml instead. Its resourceRefs still name two composed resources it -# no longer controls: one ../../diff/resources/existing-other-xr.yaml now controls, and one whose -# controller reference was removed. The harness appends the resources this XR does control. -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-resource - namespace: default -spec: - coolField: "existing-value" - crossplane: - compositionRef: - name: removal-test-composition - compositionUpdatePolicy: Automatic - resourceRefs: - - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - name: other-resource - - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - name: resource-without-controller diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-xr-pinned-revision-ref.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-xr-pinned-revision-ref.yaml deleted file mode 100644 index ff2586ee..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-xr-pinned-revision-ref.yaml +++ /dev/null @@ -1,18 +0,0 @@ -# A Manual composite of the revision-templating composition, pinned to an older CompositionRevision than -# the one existing-xr-revision-ref.yaml tracks: revision 1, -# revision-templating-composition-older-revision.yaml. A Manual composite never re-points, so applying -# an edit of its composition leaves it on this revision, and a template reading the revision name must -# keep reading this one. Kept for analysis only under --include-pinned. -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: pinned-revision-ref-resource - namespace: default -spec: - coolField: pinned-revision-ref-existing-value - crossplane: - compositionRef: - name: xrevisionrefs.diff.example.org - compositionUpdatePolicy: Manual - compositionRevisionRef: - name: xrevisionrefs.diff.example.org-0d65896 diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-xr-selector-match.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-xr-selector-match.yaml deleted file mode 100644 index ef9c030d..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-xr-selector-match.yaml +++ /dev/null @@ -1,18 +0,0 @@ -# Deliberately no compositionRevisionRef. The selector matches no revision in the cluster: -# original-composition.yaml has no version label, so neither has its one revision. Crossplane cannot -# select a revision for this XR, so it never writes the ref, and the XR waits for a composition -# labelled version=0.0.2, like updated-composition-labeled.yaml. -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: selector-match-resource - namespace: default -spec: - coolField: selector-match-existing-value - crossplane: - compositionRef: - name: xnopresources.diff.example.org - compositionUpdatePolicy: Automatic - compositionRevisionSelector: - matchLabels: - version: 0.0.2 diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-xr-selector-mismatch.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-xr-selector-mismatch.yaml deleted file mode 100644 index a7f18cdc..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-xr-selector-mismatch.yaml +++ /dev/null @@ -1,18 +0,0 @@ -# Deliberately no compositionRevisionRef. The selector matches no revision in the cluster: -# original-composition.yaml has no version label, so neither has its one revision. Crossplane cannot -# select a revision for this XR, so it never writes the ref, and the XR waits for a composition -# labelled version=0.0.1 (updated-composition-labeled.yaml is 0.0.2, so it is not that one). -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: selector-mismatch-resource - namespace: default -spec: - coolField: selector-existing-value - crossplane: - compositionRef: - name: xnopresources.diff.example.org - compositionUpdatePolicy: Automatic - compositionRevisionSelector: - matchLabels: - version: 0.0.1 diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-xr-v2.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-xr-v2.yaml deleted file mode 100644 index 10b2dad7..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-xr-v2.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# An XR that uses the SECOND composition (xnopresources-v2), so a multi-composition run can have -# affected XRs under both compositions rather than only the first. -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: v2-resource - namespace: default -spec: - coolField: v2-existing-value - crossplane: - compositionRef: - name: xnopresources-v2.diff.example.org diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-xr-with-argocd.yaml b/cmd/crossplane/project/testdata/comp/resources/existing-xr-with-argocd.yaml deleted file mode 100644 index 2b4fce63..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/existing-xr-with-argocd.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-resource - namespace: default - annotations: - argocd.argoproj.io/tracking-id: "test-app:/XNopResource:default/test-resource" - labels: - argocd.argoproj.io/instance: test-app -spec: - coolField: "existing-value" - crossplane: - compositionRef: - name: xnopresources.diff.example.org diff --git a/cmd/crossplane/project/testdata/comp/resources/field-removal/composition-with-field.yaml b/cmd/crossplane/project/testdata/comp/resources/field-removal/composition-with-field.yaml deleted file mode 100644 index 78a14146..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/field-removal/composition-with-field.yaml +++ /dev/null @@ -1,33 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xfieldremoval.diff.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - removableField: will-be-removed - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/resources/field-removal/existing-downstream.yaml b/cmd/crossplane/project/testdata/comp/resources/field-removal/existing-downstream.yaml deleted file mode 100644 index 0c1491f2..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/field-removal/existing-downstream.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: field-removal-test - namespace: default - annotations: - crossplane.io/composition-resource-name: nop-resource -spec: - forProvider: - configData: test-value - removableField: will-be-removed diff --git a/cmd/crossplane/project/testdata/comp/resources/field-removal/existing-xr.yaml b/cmd/crossplane/project/testdata/comp/resources/field-removal/existing-xr.yaml deleted file mode 100644 index 831e9b86..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/field-removal/existing-xr.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: field-removal-test - namespace: default -spec: - coolField: test-value - crossplane: - compositionRef: - name: xfieldremoval.diff.example.org diff --git a/cmd/crossplane/project/testdata/comp/resources/functions-sha256.yaml b/cmd/crossplane/project/testdata/comp/resources/functions-sha256.yaml deleted file mode 100644 index 0fda179d..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/functions-sha256.yaml +++ /dev/null @@ -1,17 +0,0 @@ ---- -# Function referenced by SHA256 digest instead of tag. -# This tests that a function whose package is a digest reference renders. -# The digest is for function-go-templating:v0.11.0 - same image, different reference format. -apiVersion: pkg.crossplane.io/v1beta1 -kind: Function -metadata: - name: function-go-templating-sha256 -spec: - package: xpkg.crossplane.io/crossplane-contrib/function-go-templating@sha256:34f1ca0ff2e1702ae0a5088a96f31e992eb9b15356af3b0f1a2068603e767753 ---- -apiVersion: pkg.crossplane.io/v1beta1 -kind: Function -metadata: - name: function-auto-ready -spec: - package: xpkg.crossplane.io/crossplane-contrib/function-auto-ready:v0.4.2 diff --git a/cmd/crossplane/project/testdata/comp/resources/mixed-downstream-db-1.yaml b/cmd/crossplane/project/testdata/comp/resources/mixed-downstream-db-1.yaml deleted file mode 100644 index 02c75e6e..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/mixed-downstream-db-1.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: mixed-test-xr-1-database - namespace: default - annotations: - crossplane.io/composition-resource-name: database -spec: - forProvider: - resourceType: database - tier: standard diff --git a/cmd/crossplane/project/testdata/comp/resources/mixed-downstream-db-2.yaml b/cmd/crossplane/project/testdata/comp/resources/mixed-downstream-db-2.yaml deleted file mode 100644 index 55cbbba5..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/mixed-downstream-db-2.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: mixed-test-xr-2-database - namespace: default - annotations: - crossplane.io/composition-resource-name: database -spec: - forProvider: - resourceType: database - tier: standard diff --git a/cmd/crossplane/project/testdata/comp/resources/mixed-downstream-db-3.yaml b/cmd/crossplane/project/testdata/comp/resources/mixed-downstream-db-3.yaml deleted file mode 100644 index 25ddf6f4..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/mixed-downstream-db-3.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: mixed-test-xr-3-database - namespace: default - annotations: - crossplane.io/composition-resource-name: database -spec: - forProvider: - resourceType: database - tier: premium - backupEnabled: true diff --git a/cmd/crossplane/project/testdata/comp/resources/mixed-downstream-network-1.yaml b/cmd/crossplane/project/testdata/comp/resources/mixed-downstream-network-1.yaml deleted file mode 100644 index f9672d09..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/mixed-downstream-network-1.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: mixed-test-xr-1-network - namespace: default - annotations: - crossplane.io/composition-resource-name: network -spec: - forProvider: - resourceType: network - tier: standard diff --git a/cmd/crossplane/project/testdata/comp/resources/mixed-downstream-network-2.yaml b/cmd/crossplane/project/testdata/comp/resources/mixed-downstream-network-2.yaml deleted file mode 100644 index e368615b..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/mixed-downstream-network-2.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: mixed-test-xr-2-network - namespace: default - annotations: - crossplane.io/composition-resource-name: network -spec: - forProvider: - resourceType: network - tier: standard diff --git a/cmd/crossplane/project/testdata/comp/resources/mixed-downstream-network-3.yaml b/cmd/crossplane/project/testdata/comp/resources/mixed-downstream-network-3.yaml deleted file mode 100644 index 7186308e..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/mixed-downstream-network-3.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: mixed-test-xr-3-network - namespace: default - annotations: - crossplane.io/composition-resource-name: network -spec: - forProvider: - resourceType: network - tier: standard diff --git a/cmd/crossplane/project/testdata/comp/resources/mixed-downstream-storage-1.yaml b/cmd/crossplane/project/testdata/comp/resources/mixed-downstream-storage-1.yaml deleted file mode 100644 index 1ef2aeff..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/mixed-downstream-storage-1.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: mixed-test-xr-1-storage - namespace: default - annotations: - crossplane.io/composition-resource-name: storage -spec: - forProvider: - resourceType: storage - tier: standard diff --git a/cmd/crossplane/project/testdata/comp/resources/mixed-downstream-storage-2.yaml b/cmd/crossplane/project/testdata/comp/resources/mixed-downstream-storage-2.yaml deleted file mode 100644 index d6bcfd9d..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/mixed-downstream-storage-2.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: mixed-test-xr-2-storage - namespace: default - annotations: - crossplane.io/composition-resource-name: storage -spec: - forProvider: - resourceType: storage - tier: standard diff --git a/cmd/crossplane/project/testdata/comp/resources/mixed-downstream-storage-3.yaml b/cmd/crossplane/project/testdata/comp/resources/mixed-downstream-storage-3.yaml deleted file mode 100644 index af025860..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/mixed-downstream-storage-3.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: mixed-test-xr-3-storage - namespace: default - annotations: - crossplane.io/composition-resource-name: storage -spec: - forProvider: - resourceType: storage - tier: standard diff --git a/cmd/crossplane/project/testdata/comp/resources/mixed-status-composition.yaml b/cmd/crossplane/project/testdata/comp/resources/mixed-status-composition.yaml deleted file mode 100644 index f3c367ee..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/mixed-status-composition.yaml +++ /dev/null @@ -1,58 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xmixed.diff.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - --- - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }}-database - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: database - spec: - forProvider: - resourceType: database - tier: standard - --- - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }}-storage - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: storage - spec: - forProvider: - resourceType: storage - tier: standard - --- - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }}-network - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: network - spec: - forProvider: - resourceType: network - tier: standard - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/resources/mixed-xr-1.yaml b/cmd/crossplane/project/testdata/comp/resources/mixed-xr-1.yaml deleted file mode 100644 index 8a44ded7..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/mixed-xr-1.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: mixed-test-xr-1 - namespace: default -spec: - coolField: mixed-value-1 - crossplane: - compositionRef: - name: xmixed.diff.example.org diff --git a/cmd/crossplane/project/testdata/comp/resources/mixed-xr-2.yaml b/cmd/crossplane/project/testdata/comp/resources/mixed-xr-2.yaml deleted file mode 100644 index 413db4d2..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/mixed-xr-2.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: mixed-test-xr-2 - namespace: default -spec: - coolField: mixed-value-2 - crossplane: - compositionRef: - name: xmixed.diff.example.org diff --git a/cmd/crossplane/project/testdata/comp/resources/mixed-xr-3.yaml b/cmd/crossplane/project/testdata/comp/resources/mixed-xr-3.yaml deleted file mode 100644 index a6fb9cd6..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/mixed-xr-3.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: mixed-test-xr-3 - namespace: default -spec: - coolField: mixed-value-3 - crossplane: - compositionRef: - name: xmixed.diff.example.org diff --git a/cmd/crossplane/project/testdata/comp/resources/namespace-collision/collision-composition-revision.yaml b/cmd/crossplane/project/testdata/comp/resources/namespace-collision/collision-composition-revision.yaml deleted file mode 100644 index 12a756b2..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/namespace-collision/collision-composition-revision.yaml +++ /dev/null @@ -1,56 +0,0 @@ -# The CompositionRevision Crossplane creates for collision-composition.yaml, -# named and labelled from its hash as Crossplane names it. envtest runs no Crossplane controllers, -# so it must be declared. The XRs in existing-xr-ns-{a,b}.yaml track it. -apiVersion: apiextensions.crossplane.io/v1 -kind: CompositionRevision -metadata: - name: xnopresources.collision.example.org-163b210 - labels: - crossplane.io/composition-name: xnopresources.collision.example.org - crossplane.io/composition-hash: 163b2101da346b4554697172116dfe489afe2605a36a3af8122f643c3f13e81 -spec: - revision: 1 - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: fetch-external-resources - functionRef: - name: function-extra-resources - input: - apiVersion: extra-resources.fn.crossplane.io/v1beta1 - kind: Input - spec: - extraResources: - # Fetch ConfigMap by name - uses XR's namespace since no namespace specified - - apiVersion: v1 - kind: ConfigMap - into: nsConfig - type: Reference - ref: - name: collision-config - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - {{- $config := index .context "apiextensions.crossplane.io/extra-resources" "nsConfig" 0 -}} - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: collision-resource - spec: - forProvider: - # This should contain the ConfigMap data from the XR's namespace - configData: {{ index $config "data" "value" }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/resources/namespace-collision/collision-composition.yaml b/cmd/crossplane/project/testdata/comp/resources/namespace-collision/collision-composition.yaml deleted file mode 100644 index eff808b4..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/namespace-collision/collision-composition.yaml +++ /dev/null @@ -1,52 +0,0 @@ -# Composition that fetches a ConfigMap by name from the XR's namespace. -# This is used to test the namespace collision bug where resources with -# the same name in different namespaces collide in the cache. -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnopresources.collision.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: fetch-external-resources - functionRef: - name: function-extra-resources - input: - apiVersion: extra-resources.fn.crossplane.io/v1beta1 - kind: Input - spec: - extraResources: - # Fetch ConfigMap by name - uses XR's namespace since no namespace specified - - apiVersion: v1 - kind: ConfigMap - into: nsConfig - type: Reference - ref: - name: collision-config - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - {{- $config := index .context "apiextensions.crossplane.io/extra-resources" "nsConfig" 0 -}} - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: collision-resource - spec: - forProvider: - # This should contain the ConfigMap data from the XR's namespace - configData: {{ index $config "data" "value" }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/resources/namespace-collision/configmap-ns-a.yaml b/cmd/crossplane/project/testdata/comp/resources/namespace-collision/configmap-ns-a.yaml deleted file mode 100644 index bc1a9387..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/namespace-collision/configmap-ns-a.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# ConfigMap in namespace ns-a - will be fetched by XR in ns-a -apiVersion: v1 -kind: ConfigMap -metadata: - name: collision-config - namespace: ns-a -data: - value: from-ns-a diff --git a/cmd/crossplane/project/testdata/comp/resources/namespace-collision/configmap-ns-b.yaml b/cmd/crossplane/project/testdata/comp/resources/namespace-collision/configmap-ns-b.yaml deleted file mode 100644 index 3f5ba205..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/namespace-collision/configmap-ns-b.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# ConfigMap in namespace ns-b - will be fetched by XR in ns-b -apiVersion: v1 -kind: ConfigMap -metadata: - name: collision-config - namespace: ns-b -data: - value: from-ns-b diff --git a/cmd/crossplane/project/testdata/comp/resources/namespace-collision/existing-downstream-ns-a.yaml b/cmd/crossplane/project/testdata/comp/resources/namespace-collision/existing-downstream-ns-a.yaml deleted file mode 100644 index eccd4905..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/namespace-collision/existing-downstream-ns-a.yaml +++ /dev/null @@ -1,14 +0,0 @@ -# Downstream resource for XR in ns-a -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: xr-in-ns-a - namespace: ns-a - annotations: - crossplane.io/composition-resource-name: collision-resource - labels: - crossplane.io/composite: xr-in-ns-a -spec: - forProvider: - # Current value from the ConfigMap in ns-a - configData: from-ns-a diff --git a/cmd/crossplane/project/testdata/comp/resources/namespace-collision/existing-downstream-ns-b.yaml b/cmd/crossplane/project/testdata/comp/resources/namespace-collision/existing-downstream-ns-b.yaml deleted file mode 100644 index 75a16cdf..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/namespace-collision/existing-downstream-ns-b.yaml +++ /dev/null @@ -1,14 +0,0 @@ -# Downstream resource for XR in ns-b -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: xr-in-ns-b - namespace: ns-b - annotations: - crossplane.io/composition-resource-name: collision-resource - labels: - crossplane.io/composite: xr-in-ns-b -spec: - forProvider: - # Current value from the ConfigMap in ns-b - configData: from-ns-b diff --git a/cmd/crossplane/project/testdata/comp/resources/namespace-collision/existing-xr-ns-a.yaml b/cmd/crossplane/project/testdata/comp/resources/namespace-collision/existing-xr-ns-a.yaml deleted file mode 100644 index 6e1aad43..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/namespace-collision/existing-xr-ns-a.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# XR in namespace ns-a -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: xr-in-ns-a - labels: - crossplane.io/composite: xr-in-ns-a - namespace: ns-a -spec: - coolField: test-value-a - crossplane: - compositionRef: - name: xnopresources.collision.example.org - compositionRevisionRef: - name: xnopresources.collision.example.org-163b210 diff --git a/cmd/crossplane/project/testdata/comp/resources/namespace-collision/existing-xr-ns-b.yaml b/cmd/crossplane/project/testdata/comp/resources/namespace-collision/existing-xr-ns-b.yaml deleted file mode 100644 index a7e9c5a0..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/namespace-collision/existing-xr-ns-b.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# XR in namespace ns-b -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: xr-in-ns-b - labels: - crossplane.io/composite: xr-in-ns-b - namespace: ns-b -spec: - coolField: test-value-b - crossplane: - compositionRef: - name: xnopresources.collision.example.org - compositionRevisionRef: - name: xnopresources.collision.example.org-163b210 diff --git a/cmd/crossplane/project/testdata/comp/resources/namespace-collision/namespace-ns-a.yaml b/cmd/crossplane/project/testdata/comp/resources/namespace-collision/namespace-ns-a.yaml deleted file mode 100644 index cbf261f5..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/namespace-collision/namespace-ns-a.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: ns-a diff --git a/cmd/crossplane/project/testdata/comp/resources/namespace-collision/namespace-ns-b.yaml b/cmd/crossplane/project/testdata/comp/resources/namespace-collision/namespace-ns-b.yaml deleted file mode 100644 index 0dc47b53..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/namespace-collision/namespace-ns-b.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: ns-b diff --git a/cmd/crossplane/project/testdata/comp/resources/nested-xr/existing-child-downstream.yaml b/cmd/crossplane/project/testdata/comp/resources/nested-xr/existing-child-downstream.yaml deleted file mode 100644 index 3094b7df..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/nested-xr/existing-child-downstream.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# Downstream resource created by the CHILD'S composition (xnopresources.diff.example.org). -# This resource should NOT be affected when we diff the parent composition, -# because the nested XR should use its own composition. -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: test-parent-nested - namespace: default - annotations: - crossplane.io/composition-resource-name: nop-resource -spec: - forProvider: - # This value comes from the CHILD's composition (xnopresources.diff.example.org) - # which uses: configData: {{ .observed.composite.resource.spec.coolField }} - configData: child-config-value - resourceTier: basic diff --git a/cmd/crossplane/project/testdata/comp/resources/nested-xr/existing-nested-child-xr.yaml b/cmd/crossplane/project/testdata/comp/resources/nested-xr/existing-nested-child-xr.yaml deleted file mode 100644 index e9ff65cb..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/nested-xr/existing-nested-child-xr.yaml +++ /dev/null @@ -1,14 +0,0 @@ -# Nested XNopResource created by the parent composition. -# This XR uses its OWN composition (xnopresources.diff.example.org) to create downstream resources. -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-parent-nested - namespace: default - annotations: - crossplane.io/composition-resource-name: nested-xr -spec: - coolField: child-config-value - crossplane: - compositionRef: - name: xnopresources.diff.example.org diff --git a/cmd/crossplane/project/testdata/comp/resources/nested-xr/existing-parent-direct-downstream.yaml b/cmd/crossplane/project/testdata/comp/resources/nested-xr/existing-parent-direct-downstream.yaml deleted file mode 100644 index 7d6b8b19..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/nested-xr/existing-parent-direct-downstream.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# Direct downstream resource created by the parent composition -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: test-parent-direct - namespace: default - annotations: - crossplane.io/composition-resource-name: direct-resource -spec: - forProvider: - configData: parent-parent-value - resourceTier: parent-tier diff --git a/cmd/crossplane/project/testdata/comp/resources/nested-xr/existing-parent-xr.yaml b/cmd/crossplane/project/testdata/comp/resources/nested-xr/existing-parent-xr.yaml deleted file mode 100644 index 6b88e567..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/nested-xr/existing-parent-xr.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# Existing parent XR that uses the parent composition -apiVersion: parent.diff.example.org/v1alpha1 -kind: XParentResource -metadata: - name: test-parent - namespace: default -spec: - parentField: parent-value - childConfig: child-config-value - crossplane: - compositionRef: - name: xparentresources.parent.diff.example.org diff --git a/cmd/crossplane/project/testdata/comp/resources/nested-xr/parent-composition.yaml b/cmd/crossplane/project/testdata/comp/resources/nested-xr/parent-composition.yaml deleted file mode 100644 index 066be354..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/nested-xr/parent-composition.yaml +++ /dev/null @@ -1,49 +0,0 @@ -# Parent composition that creates a nested XNopResource. -# The nested XNopResource uses its OWN composition (xnopresources.diff.example.org) -# to create its downstream resources. -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xparentresources.parent.diff.example.org -spec: - compositeTypeRef: - apiVersion: parent.diff.example.org/v1alpha1 - kind: XParentResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - --- - # Direct downstream resource owned by the parent XR - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }}-direct - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: direct-resource - spec: - forProvider: - configData: parent-{{ .observed.composite.resource.spec.parentField }} - resourceTier: parent-tier - --- - # Nested XR of type XNopResource - this should use its OWN composition - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - metadata: - name: {{ .observed.composite.resource.metadata.name }}-nested - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nested-xr - spec: - coolField: {{ .observed.composite.resource.spec.childConfig }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/resources/nested-xr/parent-xrd.yaml b/cmd/crossplane/project/testdata/comp/resources/nested-xr/parent-xrd.yaml deleted file mode 100644 index ffe6e36d..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/nested-xr/parent-xrd.yaml +++ /dev/null @@ -1,50 +0,0 @@ -# Parent XRD that creates nested XRs of type XNopResource -apiVersion: apiextensions.crossplane.io/v2 -kind: CompositeResourceDefinition -metadata: - name: xparentresources.parent.diff.example.org -spec: - group: parent.diff.example.org - names: - kind: XParentResource - plural: xparentresources - scope: Namespaced - versions: - - name: v1alpha1 - served: true - referenceable: true - schema: - openAPIV3Schema: - type: object - properties: - spec: - type: object - properties: - parentField: - type: string - childConfig: - type: string - status: - type: object - properties: - conditions: - type: array - items: - type: object - required: - - lastTransitionTime - - reason - - status - - type - properties: - lastTransitionTime: - type: string - format: date-time - message: - type: string - reason: - type: string - status: - type: string - type: - type: string diff --git a/cmd/crossplane/project/testdata/comp/resources/original-composition-2.yaml b/cmd/crossplane/project/testdata/comp/resources/original-composition-2.yaml deleted file mode 100644 index a6605cec..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/original-composition-2.yaml +++ /dev/null @@ -1,33 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnopresources-v2.diff.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource-v2 - spec: - forProvider: - configData: v2-updated-{{ .observed.composite.resource.spec.coolField }} - resourceTier: enterprise - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready \ No newline at end of file diff --git a/cmd/crossplane/project/testdata/comp/resources/original-composition-kubectl-applied-current.yaml b/cmd/crossplane/project/testdata/comp/resources/original-composition-kubectl-applied-current.yaml deleted file mode 100644 index 0b2fe1dd..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/original-composition-kubectl-applied-current.yaml +++ /dev/null @@ -1,41 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnopresources.diff.example.org - annotations: - # Content-identical to original-composition.yaml apart from this annotation, whose value is what a - # client-side `kubectl apply` of composition-no-changes.yaml writes: that file minus this annotation, - # JSON-encoded (kubectl keeps the emptied annotations map, hence "annotations":{}). Unlike - # original-composition-kubectl-applied.yaml, whose value is stale, re-applying that file over this - # one is a no-op: kubectl recomputes the same annotation, the patch is empty, and nothing is written. - kubectl.kubernetes.io/last-applied-configuration: | - {"apiVersion":"apiextensions.crossplane.io/v1","kind":"Composition","metadata":{"name":"xnopresources.diff.example.org","annotations":{}},"spec":{"compositeTypeRef":{"apiVersion":"ns.diff.example.org/v1alpha1","kind":"XNopResource"},"mode":"Pipeline","pipeline":[{"step":"generate-resources","functionRef":{"name":"function-go-templating"},"input":{"apiVersion":"template.fn.crossplane.io/v1beta1","kind":"GoTemplate","source":"Inline","inline":{"template":"apiVersion: ns.nop.example.org/v1alpha1\nkind: XDownstreamResource\nmetadata:\n name: {{ .observed.composite.resource.metadata.name }}\n namespace: {{ .observed.composite.resource.metadata.namespace }}\n annotations:\n gotemplating.fn.crossplane.io/composition-resource-name: nop-resource\nspec:\n forProvider:\n configData: {{ .observed.composite.resource.spec.coolField }}\n resourceTier: basic\n"}}},{"step":"automatically-detect-ready-composed-resources","functionRef":{"name":"function-auto-ready"}}]}} -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - resourceTier: basic - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/resources/original-composition-kubectl-applied.yaml b/cmd/crossplane/project/testdata/comp/resources/original-composition-kubectl-applied.yaml deleted file mode 100644 index f13c7064..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/original-composition-kubectl-applied.yaml +++ /dev/null @@ -1,40 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnopresources.diff.example.org - annotations: - # Content-identical to original-composition.yaml apart from this annotation, which is what a - # client-side `kubectl apply` stamps on the cluster copy. The file a user diffs never carries it, - # so this is the asymmetry the tool must not mistake for an edit. The value below is abridged — - # only its presence matters, never its content. - kubectl.kubernetes.io/last-applied-configuration: | - {"apiVersion":"apiextensions.crossplane.io/v1","kind":"Composition","metadata":{"name":"xnopresources.diff.example.org"},"spec":{"compositeTypeRef":{"apiVersion":"ns.diff.example.org/v1alpha1","kind":"XNopResource"},"mode":"Pipeline"}} -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - resourceTier: basic - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/resources/removal-test-original-composition.yaml b/cmd/crossplane/project/testdata/comp/resources/removal-test-original-composition.yaml deleted file mode 100644 index 1c07815c..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/removal-test-original-composition.yaml +++ /dev/null @@ -1,45 +0,0 @@ -# The in-cluster version of removal-test-composition, which still renders resource2. Diffing -# ../../diff/resources/removal-test-composition.yaml, which no longer does, against it removes resource2. -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: removal-test-composition -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: resource-to-be-kept - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: resource1 - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - --- - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: resource-to-be-removed - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: resource2 - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/resources/revision-templating-composition-older-revision.yaml b/cmd/crossplane/project/testdata/comp/resources/revision-templating-composition-older-revision.yaml deleted file mode 100644 index a6a9dff3..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/revision-templating-composition-older-revision.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# An earlier revision of revision-templating-composition.yaml, from when the composition carried a -# channel=stable label it has since dropped: same spec, so only its name differs from revision 2's. -# The harness names and labels it from the hash of the composition it was cut from, as Crossplane does. -# The Manual XR in existing-xr-pinned-revision-ref.yaml is pinned to it. -apiVersion: apiextensions.crossplane.io/v1 -kind: CompositionRevision -metadata: - labels: - channel: "stable" - crossplane.io/composition-name: xrevisionrefs.diff.example.org -spec: - revision: 1 - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.crossplane.compositionRevisionRef.name }} - resourceTier: basic - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/resources/sha256-composition.yaml b/cmd/crossplane/project/testdata/comp/resources/sha256-composition.yaml deleted file mode 100644 index e08a7433..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/sha256-composition.yaml +++ /dev/null @@ -1,33 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnopresources-sha256.diff.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating-sha256 - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - resourceTier: basic - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/resources/shared-child-xrs-differing.yaml b/cmd/crossplane/project/testdata/comp/resources/shared-child-xrs-differing.yaml deleted file mode 100644 index c9421c59..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/shared-child-xrs-differing.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# Two XRs on the shared-child composition, in one namespace, with DIFFERENT specs, so a change to that -# composition renders its fixed-name child differently for each. No one diff is correct for both. -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: shared-child-owner-a - namespace: default -spec: - coolField: value-a - crossplane: - compositionRef: - name: xnopresources-shared-child.diff.example.org ---- -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: shared-child-owner-b - namespace: default -spec: - coolField: value-b - crossplane: - compositionRef: - name: xnopresources-shared-child.diff.example.org diff --git a/cmd/crossplane/project/testdata/comp/resources/status-downstream-1.yaml b/cmd/crossplane/project/testdata/comp/resources/status-downstream-1.yaml deleted file mode 100644 index a84d5a67..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/status-downstream-1.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: status-test-xr-1 - namespace: default - annotations: - crossplane.io/composition-resource-name: nop-resource -spec: - forProvider: - configData: stable-value - resourceTier: standard diff --git a/cmd/crossplane/project/testdata/comp/resources/status-downstream-2.yaml b/cmd/crossplane/project/testdata/comp/resources/status-downstream-2.yaml deleted file mode 100644 index 1fee2a23..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/status-downstream-2.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: status-test-xr-2 - namespace: default - annotations: - crossplane.io/composition-resource-name: nop-resource -spec: - forProvider: - configData: stable-value - resourceTier: standard diff --git a/cmd/crossplane/project/testdata/comp/resources/status-indicator-composition.yaml b/cmd/crossplane/project/testdata/comp/resources/status-indicator-composition.yaml deleted file mode 100644 index 08fea7b5..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/status-indicator-composition.yaml +++ /dev/null @@ -1,33 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xstatus.diff.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: stable-value - resourceTier: standard - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/resources/status-xr-1.yaml b/cmd/crossplane/project/testdata/comp/resources/status-xr-1.yaml deleted file mode 100644 index d22c27e2..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/status-xr-1.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: status-test-xr-1 - namespace: default -spec: - coolField: test-value-1 - crossplane: - compositionRef: - name: xstatus.diff.example.org diff --git a/cmd/crossplane/project/testdata/comp/resources/status-xr-2.yaml b/cmd/crossplane/project/testdata/comp/resources/status-xr-2.yaml deleted file mode 100644 index 2ef34578..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/status-xr-2.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: status-test-xr-2 - namespace: default -spec: - coolField: test-value-2 - crossplane: - compositionRef: - name: xstatus.diff.example.org diff --git a/cmd/crossplane/project/testdata/comp/resources/test-namespace.yaml b/cmd/crossplane/project/testdata/comp/resources/test-namespace.yaml deleted file mode 100644 index f8db044d..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/test-namespace.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: test-namespace diff --git a/cmd/crossplane/project/testdata/comp/resources/unverified-cluster-composition.yaml b/cmd/crossplane/project/testdata/comp/resources/unverified-cluster-composition.yaml deleted file mode 100644 index 5c757c43..00000000 --- a/cmd/crossplane/project/testdata/comp/resources/unverified-cluster-composition.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# The in-cluster version of updated-unverified-cluster-composition.yaml: a composition for the -# cluster-scoped XNopResource that composes nothing yet. -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnopresources-unverified.diff.example.org -spec: - compositeTypeRef: - apiVersion: diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/revision-templating-updated-composition.yaml b/cmd/crossplane/project/testdata/comp/revision-templating-updated-composition.yaml deleted file mode 100644 index 263ad44c..00000000 --- a/cmd/crossplane/project/testdata/comp/revision-templating-updated-composition.yaml +++ /dev/null @@ -1,43 +0,0 @@ -# A metadata-only edit of resources/revision-templating-composition.yaml: one added label, identical -# spec. Crossplane's Composition.Hash() covers labels, so applying this mints a new CompositionRevision -# with a byte-identical spec — and because the template below reads the revision's own name, the -# composed resource genuinely changes. -# -# Before issue #474 the tool rendered this composite with its *existing* compositionRevisionRef, saw no -# downstream delta, and exited 0. The label is the only difference; everything else must stay identical -# to the cluster copy or the test stops exercising the metadata-only path. -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xrevisionrefs.diff.example.org - labels: - channel: preview -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.crossplane.compositionRevisionRef.name }} - resourceTier: basic - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/status-indicator-updated-composition.yaml b/cmd/crossplane/project/testdata/comp/status-indicator-updated-composition.yaml deleted file mode 100644 index a6a6a6a7..00000000 --- a/cmd/crossplane/project/testdata/comp/status-indicator-updated-composition.yaml +++ /dev/null @@ -1,35 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xstatus.diff.example.org - labels: - environment: production -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: stable-value - resourceTier: standard - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/updated-claim-composition.yaml b/cmd/crossplane/project/testdata/comp/updated-claim-composition.yaml deleted file mode 100644 index f619a369..00000000 --- a/cmd/crossplane/project/testdata/comp/updated-claim-composition.yaml +++ /dev/null @@ -1,32 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: nopclaims.diff.example.org -spec: - compositeTypeRef: - apiVersion: diff.example.org/v1alpha1 - kind: XNop - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - name: {{ .observed.composite.resource.metadata.name }} - spec: - forProvider: - configData: updated-{{ .observed.composite.resource.spec.coolField }} - resourceTier: premium - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/updated-collision-composition.yaml b/cmd/crossplane/project/testdata/comp/updated-collision-composition.yaml deleted file mode 100644 index 88a356ef..00000000 --- a/cmd/crossplane/project/testdata/comp/updated-collision-composition.yaml +++ /dev/null @@ -1,50 +0,0 @@ -# Updated composition - adds a prefix to trigger a diff -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnopresources.collision.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: fetch-external-resources - functionRef: - name: function-extra-resources - input: - apiVersion: extra-resources.fn.crossplane.io/v1beta1 - kind: Input - spec: - extraResources: - # Fetch ConfigMap by name - uses XR's namespace since no namespace specified - - apiVersion: v1 - kind: ConfigMap - into: nsConfig - type: Reference - ref: - name: collision-config - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - {{- $config := index .context "apiextensions.crossplane.io/extra-resources" "nsConfig" 0 -}} - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: collision-resource - spec: - forProvider: - # CHANGED: Added prefix to configData - configData: prefix-{{ index $config "data" "value" }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/updated-composition-2-changed.yaml b/cmd/crossplane/project/testdata/comp/updated-composition-2-changed.yaml deleted file mode 100644 index 83dfeec3..00000000 --- a/cmd/crossplane/project/testdata/comp/updated-composition-2-changed.yaml +++ /dev/null @@ -1,36 +0,0 @@ -# A genuinely-changed version of original-composition-2.yaml (configData prefix and resourceTier -# differ). Distinct from updated-composition-2.yaml, which is byte-identical to the original and is -# used for the mixed changed/unchanged case. -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnopresources-v2.diff.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource-v2 - spec: - forProvider: - configData: v2-changed-{{ .observed.composite.resource.spec.coolField }} - resourceTier: premium-plus - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/updated-composition-2.yaml b/cmd/crossplane/project/testdata/comp/updated-composition-2.yaml deleted file mode 100644 index a6605cec..00000000 --- a/cmd/crossplane/project/testdata/comp/updated-composition-2.yaml +++ /dev/null @@ -1,33 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnopresources-v2.diff.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource-v2 - spec: - forProvider: - configData: v2-updated-{{ .observed.composite.resource.spec.coolField }} - resourceTier: enterprise - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready \ No newline at end of file diff --git a/cmd/crossplane/project/testdata/comp/updated-composition-labeled.yaml b/cmd/crossplane/project/testdata/comp/updated-composition-labeled.yaml deleted file mode 100644 index 6768f115..00000000 --- a/cmd/crossplane/project/testdata/comp/updated-composition-labeled.yaml +++ /dev/null @@ -1,35 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnopresources.diff.example.org - labels: - version: 0.0.2 -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: updated-{{ .observed.composite.resource.spec.coolField }} - resourceTier: premium - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/updated-conditional-gating-composition.yaml b/cmd/crossplane/project/testdata/comp/updated-conditional-gating-composition.yaml deleted file mode 100644 index 6d5a3ed9..00000000 --- a/cmd/crossplane/project/testdata/comp/updated-conditional-gating-composition.yaml +++ /dev/null @@ -1,57 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: conditional-gating-composition -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: 0-stage0-resource - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: stage0-resource - spec: - forProvider: - configData: updated-{{ .observed.composite.resource.spec.coolField }} - {{- $ready := false -}} - {{- with .observed.resources -}} - {{- with (index . "stage0-resource") -}} - {{- with .resource.status.conditions -}} - {{- range . -}} - {{- if and (eq .type "Ready") (eq .status "True") -}} - {{- $ready = true -}} - {{- end -}} - {{- end -}} - {{- end -}} - {{- end -}} - {{- end -}} - {{- if $ready }} - --- - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: 1-stage1-resource - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: stage1-resource - spec: - forProvider: - configData: updated-{{ .observed.composite.resource.spec.coolField }} - {{- end }} - - step: mark-ready-from-observed - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/updated-cycle-composition.yaml b/cmd/crossplane/project/testdata/comp/updated-cycle-composition.yaml deleted file mode 100644 index a1807a2d..00000000 --- a/cmd/crossplane/project/testdata/comp/updated-cycle-composition.yaml +++ /dev/null @@ -1,35 +0,0 @@ -# testdata/diff/resources/cycle/compositions.yaml's XCycleA composition with a spec change (a label -# on the composed XCycleB), so comp evaluates its composites at every --analyze-on setting. -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xcycleas.cycle.example.org -spec: - compositeTypeRef: - apiVersion: ns.cycle.example.org/v1alpha1 - kind: XCycleA - mode: Pipeline - pipeline: - - step: generate-cycle-b - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.cycle.example.org/v1alpha1 - kind: XCycleB - metadata: - name: {{ .observed.composite.resource.metadata.name }}-c - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: cycle-b - labels: - cycle.example.org/edited: "true" - spec: - cycleField: {{ .observed.composite.resource.spec.cycleField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/updated-parent-composition.yaml b/cmd/crossplane/project/testdata/comp/updated-parent-composition.yaml deleted file mode 100644 index b4e7093b..00000000 --- a/cmd/crossplane/project/testdata/comp/updated-parent-composition.yaml +++ /dev/null @@ -1,50 +0,0 @@ -# Updated parent composition - changes the direct downstream resource tier from parent-tier to premium-tier. -# The nested XNopResource and its downstream should NOT be affected because: -# 1. The nested XR uses its own composition (xnopresources.diff.example.org) -# 2. That composition is NOT being diffed here -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xparentresources.parent.diff.example.org -spec: - compositeTypeRef: - apiVersion: parent.diff.example.org/v1alpha1 - kind: XParentResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - --- - # Direct downstream resource owned by the parent XR - CHANGED tier - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }}-direct - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: direct-resource - spec: - forProvider: - configData: parent-{{ .observed.composite.resource.spec.parentField }} - resourceTier: premium-tier - --- - # Nested XR of type XNopResource - unchanged - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - metadata: - name: {{ .observed.composite.resource.metadata.name }}-nested - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nested-xr - spec: - coolField: {{ .observed.composite.resource.spec.childConfig }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/updated-sequencer-gating-composition.yaml b/cmd/crossplane/project/testdata/comp/updated-sequencer-gating-composition.yaml deleted file mode 100644 index 603c3ec0..00000000 --- a/cmd/crossplane/project/testdata/comp/updated-sequencer-gating-composition.yaml +++ /dev/null @@ -1,53 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: sequencer-gating-composition -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-all-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: 0-stage0-resource - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: stage0-resource - spec: - forProvider: - configData: updated-{{ .observed.composite.resource.spec.coolField }} - --- - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: 1-stage1-resource - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: stage1-resource - spec: - forProvider: - configData: updated-{{ .observed.composite.resource.spec.coolField }} - - step: mark-ready-from-observed - functionRef: - name: function-auto-ready - - step: sequence-resources - functionRef: - name: function-sequencer - input: - apiVersion: sequencer.fn.crossplane.io/v1beta1 - kind: Input - rules: - - sequence: - - ^stage0- - - ^stage1- diff --git a/cmd/crossplane/project/testdata/comp/updated-sha256-composition.yaml b/cmd/crossplane/project/testdata/comp/updated-sha256-composition.yaml deleted file mode 100644 index de89c21e..00000000 --- a/cmd/crossplane/project/testdata/comp/updated-sha256-composition.yaml +++ /dev/null @@ -1,33 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnopresources-sha256.diff.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating-sha256 - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: updated-{{ .observed.composite.resource.spec.coolField }} - resourceTier: premium - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/updated-shared-child-composition.yaml b/cmd/crossplane/project/testdata/comp/updated-shared-child-composition.yaml deleted file mode 100644 index e0751a68..00000000 --- a/cmd/crossplane/project/testdata/comp/updated-shared-child-composition.yaml +++ /dev/null @@ -1,36 +0,0 @@ -# The shared-child composition (testdata/diff/resources/shared-child-composition.yaml) with a label and -# a changed template, so diffing it analyses the XRs that use it. -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnopresources-shared-child.diff.example.org - labels: - version: 0.0.2 -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-shared-child - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: shared-child - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: shared-child - spec: - forProvider: - configData: updated-{{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/comp/updated-unverified-cluster-composition.yaml b/cmd/crossplane/project/testdata/comp/updated-unverified-cluster-composition.yaml deleted file mode 100644 index 0f80e57d..00000000 --- a/cmd/crossplane/project/testdata/comp/updated-unverified-cluster-composition.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# Adds two ConfigMaps in a namespace that does not exist in the cluster. -# -# A cluster-scoped XR may compose resources in any namespace (a namespaced one is confined to its -# own), which is what lets `comp` reach a missing namespace from an XR that already exists. Both -# additions' dry-run creates are refused for want of the namespace, so both are diffed unverified, -# and the warning saying so counts them. -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnopresources-unverified.diff.example.org -spec: - compositeTypeRef: - apiVersion: diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: v1 - kind: ConfigMap - metadata: - name: {{ .observed.composite.resource.metadata.name }}-first - namespace: nonexistent-namespace - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: first-config - data: - configData: {{ .observed.composite.resource.spec.coolField }} - --- - apiVersion: v1 - kind: ConfigMap - metadata: - name: {{ .observed.composite.resource.metadata.name }}-second - namespace: nonexistent-namespace - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: second-config - data: - configData: {{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/existing-xr-revision-ref-omitted.yaml b/cmd/crossplane/project/testdata/diff/existing-xr-revision-ref-omitted.yaml deleted file mode 100644 index fa56bf15..00000000 --- a/cmd/crossplane/project/testdata/diff/existing-xr-revision-ref-omitted.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# testdata/comp/resources/existing-xr-revision-ref.yaml exactly as a user would write it: unchanged, -# but without the compositionRef and compositionRevisionRef that Crossplane's composite reconciler -# wrote into the cluster copy. Applying this leaves those refs where they are, so nothing would change — -# and the composition this composite uses (testdata/comp/resources/revision-templating-composition.yaml) -# reads the revision name into a composed resource. Only the inherited compositionRef resolves the -# revision whose name render must see, so a render that drops the cluster's refs shows a spurious -# change. See issue #499. -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: revision-ref-resource - namespace: default -spec: - coolField: revision-ref-existing-value - crossplane: - compositionUpdatePolicy: Automatic diff --git a/cmd/crossplane/project/testdata/diff/first-xr-with-composition-ref.yaml b/cmd/crossplane/project/testdata/diff/first-xr-with-composition-ref.yaml deleted file mode 100644 index 9c567aa2..00000000 --- a/cmd/crossplane/project/testdata/diff/first-xr-with-composition-ref.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# first-xr.yaml naming its composition, so finding the composition needs no list of every Composition. -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: first-resource - namespace: default -spec: - coolField: "first-value" - crossplane: - compositionRef: - name: xnopresources.diff.example.org diff --git a/cmd/crossplane/project/testdata/diff/first-xr.yaml b/cmd/crossplane/project/testdata/diff/first-xr.yaml deleted file mode 100644 index cc6c2fe4..00000000 --- a/cmd/crossplane/project/testdata/diff/first-xr.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: first-resource - namespace: default -spec: - coolField: "first-value" diff --git a/cmd/crossplane/project/testdata/diff/generated-name-xr.yaml b/cmd/crossplane/project/testdata/diff/generated-name-xr.yaml deleted file mode 100644 index 0be857b8..00000000 --- a/cmd/crossplane/project/testdata/diff/generated-name-xr.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - generateName: generated-xr- - namespace: default -spec: - coolField: "new-value" diff --git a/cmd/crossplane/project/testdata/diff/invalid-schema-xr.yaml b/cmd/crossplane/project/testdata/diff/invalid-schema-xr.yaml deleted file mode 100644 index 785a6207..00000000 --- a/cmd/crossplane/project/testdata/diff/invalid-schema-xr.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# XR with invalid schema - coolField should be a string, not an integer -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: invalid-schema-xr - namespace: default -spec: - coolField: 12345 diff --git a/cmd/crossplane/project/testdata/diff/modified-api-version-xr-rev2-changed-field.yaml b/cmd/crossplane/project/testdata/diff/modified-api-version-xr-rev2-changed-field.yaml deleted file mode 100644 index 49be07d7..00000000 --- a/cmd/crossplane/project/testdata/diff/modified-api-version-xr-rev2-changed-field.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-api-version-xr - namespace: default -spec: - crossplane: - compositionRef: - name: xapimigrateresources.example.org - compositionRevisionRef: - name: xapimigrateresources.example.org-311ee4f - compositionUpdatePolicy: Manual - coolField: changed-value diff --git a/cmd/crossplane/project/testdata/diff/modified-api-version-xr-rev2.yaml b/cmd/crossplane/project/testdata/diff/modified-api-version-xr-rev2.yaml deleted file mode 100644 index 0baf92f0..00000000 --- a/cmd/crossplane/project/testdata/diff/modified-api-version-xr-rev2.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-api-version-xr - namespace: default -spec: - crossplane: - compositionRef: - name: xapimigrateresources.example.org - compositionRevisionRef: - name: xapimigrateresources.example.org-311ee4f - compositionUpdatePolicy: Manual - coolField: test-value diff --git a/cmd/crossplane/project/testdata/diff/modified-claim-nested.yaml b/cmd/crossplane/project/testdata/diff/modified-claim-nested.yaml deleted file mode 100644 index 67844ef1..00000000 --- a/cmd/crossplane/project/testdata/diff/modified-claim-nested.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: claimnested.diff.example.org/v1alpha1 -kind: ParentNopClaim -metadata: - name: existing-parent-claim - namespace: default - labels: - new-label: added-value -spec: - parentField: modified-parent-value - compositeDeletePolicy: Background - compositionRef: - name: parent-nop-claim-composition diff --git a/cmd/crossplane/project/testdata/diff/modified-claim-omitting-defaulted-fields.yaml b/cmd/crossplane/project/testdata/diff/modified-claim-omitting-defaulted-fields.yaml deleted file mode 100644 index 3c6645d8..00000000 --- a/cmd/crossplane/project/testdata/diff/modified-claim-omitting-defaulted-fields.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# modified-claim.yaml without spec.compositeDeletePolicy, which the claim's CRD defaults (as it does -# spec.compositionUpdatePolicy, also omitted). -apiVersion: diff.example.org/v1alpha1 -kind: NopClaim -metadata: - name: test-claim - namespace: existing-namespace -spec: - coolField: modified-value - compositionRef: - name: claim-composition diff --git a/cmd/crossplane/project/testdata/diff/modified-claim.yaml b/cmd/crossplane/project/testdata/diff/modified-claim.yaml deleted file mode 100644 index 3116b173..00000000 --- a/cmd/crossplane/project/testdata/diff/modified-claim.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: diff.example.org/v1alpha1 -kind: NopClaim -metadata: - name: test-claim - namespace: existing-namespace -spec: - compositeDeletePolicy: Background - coolField: modified-value - compositionRef: - name: claim-composition diff --git a/cmd/crossplane/project/testdata/diff/modified-cluster-xr.yaml b/cmd/crossplane/project/testdata/diff/modified-cluster-xr.yaml deleted file mode 100644 index dcbaf051..00000000 --- a/cmd/crossplane/project/testdata/diff/modified-cluster-xr.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-resource -spec: - coolField: "modified-value" diff --git a/cmd/crossplane/project/testdata/diff/modified-cross-ns-xr.yaml b/cmd/crossplane/project/testdata/diff/modified-cross-ns-xr.yaml deleted file mode 100644 index 78068e4d..00000000 --- a/cmd/crossplane/project/testdata/diff/modified-cross-ns-xr.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-cross-ns-resource - namespace: default -spec: - coolField: modified-cross-ns-value - environment: production \ No newline at end of file diff --git a/cmd/crossplane/project/testdata/diff/modified-env-xr.yaml b/cmd/crossplane/project/testdata/diff/modified-env-xr.yaml deleted file mode 100644 index 3664be56..00000000 --- a/cmd/crossplane/project/testdata/diff/modified-env-xr.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XEnvResource -metadata: - name: test-env-resource -spec: - compositionUpdatePolicy: Automatic - configKey: "modified-config-value" diff --git a/cmd/crossplane/project/testdata/diff/modified-legacy-xr-manual-upgrade-to-v2.yaml b/cmd/crossplane/project/testdata/diff/modified-legacy-xr-manual-upgrade-to-v2.yaml deleted file mode 100644 index 92f64376..00000000 --- a/cmd/crossplane/project/testdata/diff/modified-legacy-xr-manual-upgrade-to-v2.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: legacycluster.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-legacy-manual-v1 -spec: - compositionRef: - name: xlegacynopresources.diff.example.org - compositionRevisionRef: - name: xlegacynopresources.diff.example.org-97a39c0 - compositionUpdatePolicy: Manual - coolField: modified-value diff --git a/cmd/crossplane/project/testdata/diff/modified-legacy-xr-manual-v1-omits-revision-ref.yaml b/cmd/crossplane/project/testdata/diff/modified-legacy-xr-manual-v1-omits-revision-ref.yaml deleted file mode 100644 index 12f18008..00000000 --- a/cmd/crossplane/project/testdata/diff/modified-legacy-xr-manual-v1-omits-revision-ref.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# The v1-path (spec.compositionRevisionRef) twin of modified-xr-manual-v1-omits-revision-ref.yaml: -# resources/existing-legacy-xr-manual-v1.yaml with coolField modified and compositionRevisionRef left -# out, so it must stay pinned to the -2f488b6 revision (the v1 template). See issue #499. -apiVersion: legacycluster.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-legacy-manual-v1 -spec: - compositionRef: - name: xlegacynopresources.diff.example.org - compositionUpdatePolicy: Manual - coolField: modified-value diff --git a/cmd/crossplane/project/testdata/diff/modified-legacy-xr.yaml b/cmd/crossplane/project/testdata/diff/modified-legacy-xr.yaml deleted file mode 100644 index 47df1975..00000000 --- a/cmd/crossplane/project/testdata/diff/modified-legacy-xr.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: legacycluster.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-resource -spec: - compositionUpdatePolicy: Automatic - coolField: "modified-value" diff --git a/cmd/crossplane/project/testdata/diff/modified-manual-pinned-claim.yaml b/cmd/crossplane/project/testdata/diff/modified-manual-pinned-claim.yaml deleted file mode 100644 index be846bd7..00000000 --- a/cmd/crossplane/project/testdata/diff/modified-manual-pinned-claim.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# resources/pinned-claim/existing-manual.yaml's claim as its author keeps it, with coolField changed. -# Like the cluster copy it names no revision: the pin is the backing XR's. -apiVersion: pinned.diff.example.org/v1alpha1 -kind: Pinned -metadata: - name: manual-claim - namespace: existing-namespace -spec: - coolField: modified-value - compositionUpdatePolicy: Manual - compositionRef: - name: xpinneds.pinned.diff.example.org diff --git a/cmd/crossplane/project/testdata/diff/modified-nested-xr.yaml b/cmd/crossplane/project/testdata/diff/modified-nested-xr.yaml deleted file mode 100644 index 842e568b..00000000 --- a/cmd/crossplane/project/testdata/diff/modified-nested-xr.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# A modified parent XR - value changed from "existing-value" to "modified-value" -apiVersion: ns.nested.example.org/v1alpha1 -kind: XParentResource -metadata: - name: test-parent - namespace: default -spec: - parentField: "modified-value" diff --git a/cmd/crossplane/project/testdata/diff/modified-xr-automatic.yaml b/cmd/crossplane/project/testdata/diff/modified-xr-automatic.yaml deleted file mode 100644 index b0e59196..00000000 --- a/cmd/crossplane/project/testdata/diff/modified-xr-automatic.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-automatic - namespace: default -spec: - crossplane: - compositionRef: - name: xnopresources.diff.example.org - compositionUpdatePolicy: Automatic - coolField: modified-value diff --git a/cmd/crossplane/project/testdata/diff/modified-xr-manual-upgrade-to-v2.yaml b/cmd/crossplane/project/testdata/diff/modified-xr-manual-upgrade-to-v2.yaml deleted file mode 100644 index bb285ab2..00000000 --- a/cmd/crossplane/project/testdata/diff/modified-xr-manual-upgrade-to-v2.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-manual-v1 - namespace: default -spec: - crossplane: - compositionRef: - name: xnopresources.diff.example.org - compositionRevisionRef: - name: xnopresources.diff.example.org-430f3f3 - compositionUpdatePolicy: Manual - coolField: modified-value diff --git a/cmd/crossplane/project/testdata/diff/modified-xr-manual-v1-omits-revision-ref.yaml b/cmd/crossplane/project/testdata/diff/modified-xr-manual-v1-omits-revision-ref.yaml deleted file mode 100644 index 7216d4f3..00000000 --- a/cmd/crossplane/project/testdata/diff/modified-xr-manual-v1-omits-revision-ref.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# resources/existing-xr-manual-v1.yaml with coolField modified, and with compositionRevisionRef left -# out — as a hand-written manifest would, since Crossplane is what writes that field. An apply that -# omits a field does not remove it, so the composite stays pinned to the revision the cluster copy -# names (-911318d, the v1 template). Rendering against the latest revision instead would show the v2 -# template's output for a change that cannot cause it. See issue #499. -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-manual-v1 - namespace: default -spec: - crossplane: - compositionRef: - name: xnopresources.diff.example.org - compositionUpdatePolicy: Manual - coolField: modified-value diff --git a/cmd/crossplane/project/testdata/diff/modified-xr-manual-v1.yaml b/cmd/crossplane/project/testdata/diff/modified-xr-manual-v1.yaml deleted file mode 100644 index 9ed3f2cd..00000000 --- a/cmd/crossplane/project/testdata/diff/modified-xr-manual-v1.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-manual-v1 - namespace: default -spec: - crossplane: - compositionRef: - name: xnopresources.diff.example.org - compositionRevisionRef: - name: xnopresources.diff.example.org-911318d - compositionUpdatePolicy: Manual - coolField: modified-value diff --git a/cmd/crossplane/project/testdata/diff/modified-xr-selector-bound.yaml b/cmd/crossplane/project/testdata/diff/modified-xr-selector-bound.yaml deleted file mode 100644 index e2e6c3a2..00000000 --- a/cmd/crossplane/project/testdata/diff/modified-xr-selector-bound.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# resources/selector-bound/setup.yaml's composite as its author keeps it, with coolField changed. It -# selects its composition by label and leaves compositionRef to Crossplane, which already set it. -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: selector-bound - namespace: default -spec: - coolField: modified-value - crossplane: - compositionSelector: - matchLabels: - binding: shared diff --git a/cmd/crossplane/project/testdata/diff/modified-xr-selector-nomatch.yaml b/cmd/crossplane/project/testdata/diff/modified-xr-selector-nomatch.yaml deleted file mode 100644 index 0f45934e..00000000 --- a/cmd/crossplane/project/testdata/diff/modified-xr-selector-nomatch.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-selector-nomatch - namespace: default -spec: - crossplane: - compositionRef: - name: xnopresources.diff.example.org - compositionUpdatePolicy: Automatic - compositionRevisionSelector: - matchLabels: - channel: nonexistent - coolField: modified-value diff --git a/cmd/crossplane/project/testdata/diff/modified-xr-selector-stable.yaml b/cmd/crossplane/project/testdata/diff/modified-xr-selector-stable.yaml deleted file mode 100644 index b4dfec12..00000000 --- a/cmd/crossplane/project/testdata/diff/modified-xr-selector-stable.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-selector - namespace: default -spec: - crossplane: - compositionRef: - name: xnopresources.diff.example.org - compositionUpdatePolicy: Automatic - compositionRevisionSelector: - matchLabels: - channel: stable - coolField: modified-value diff --git a/cmd/crossplane/project/testdata/diff/modified-xr-switch-to-automatic.yaml b/cmd/crossplane/project/testdata/diff/modified-xr-switch-to-automatic.yaml deleted file mode 100644 index 8d721546..00000000 --- a/cmd/crossplane/project/testdata/diff/modified-xr-switch-to-automatic.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-manual-v1 - namespace: default -spec: - crossplane: - compositionRef: - name: xnopresources.diff.example.org - compositionUpdatePolicy: Automatic - coolField: modified-value diff --git a/cmd/crossplane/project/testdata/diff/modified-xr-with-external-dep.yaml b/cmd/crossplane/project/testdata/diff/modified-xr-with-external-dep.yaml deleted file mode 100644 index eba2a6e3..00000000 --- a/cmd/crossplane/project/testdata/diff/modified-xr-with-external-dep.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-resource - namespace: default -spec: - coolField: modified-with-external-dep - environment: testing diff --git a/cmd/crossplane/project/testdata/diff/new-claim-with-claimref-composition.yaml b/cmd/crossplane/project/testdata/diff/new-claim-with-claimref-composition.yaml deleted file mode 100644 index bd63e462..00000000 --- a/cmd/crossplane/project/testdata/diff/new-claim-with-claimref-composition.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: diff.example.org/v1alpha1 -kind: NopClaim -metadata: - name: test-claim - namespace: existing-namespace -spec: - compositeDeletePolicy: Background - compositionRef: - name: claim-composition-with-claimref - coolField: new-value diff --git a/cmd/crossplane/project/testdata/diff/new-claim.yaml b/cmd/crossplane/project/testdata/diff/new-claim.yaml deleted file mode 100644 index 6d153fee..00000000 --- a/cmd/crossplane/project/testdata/diff/new-claim.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: diff.example.org/v1alpha1 -kind: NopClaim -metadata: - name: test-claim - namespace: existing-namespace -spec: - compositeDeletePolicy: Background - compositionRef: - name: claim-composition - coolField: new-value - diff --git a/cmd/crossplane/project/testdata/diff/new-cycle-xr.yaml b/cmd/crossplane/project/testdata/diff/new-cycle-xr.yaml deleted file mode 100644 index 45f9b375..00000000 --- a/cmd/crossplane/project/testdata/diff/new-cycle-xr.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# A new XR whose composition starts the XCycleA -> XCycleB -> XCycleA cycle. -apiVersion: ns.cycle.example.org/v1alpha1 -kind: XCycleA -metadata: - name: test-cycle - namespace: default -spec: - cycleField: "cycle-value" diff --git a/cmd/crossplane/project/testdata/diff/new-manual-default-xr.yaml b/cmd/crossplane/project/testdata/diff/new-manual-default-xr.yaml deleted file mode 100644 index 89fbe026..00000000 --- a/cmd/crossplane/project/testdata/diff/new-manual-default-xr.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# A new composite pinned to revision 1, the older of its composition's two revisions, that leaves its -# update policy to its XRD's default (Manual). Under Manual, Crossplane renders a composite against the -# revision it names, so this one renders revision 1; under Automatic it would render the latest. -apiVersion: manualdefault.diff.example.org/v1alpha1 -kind: XManualDefault -metadata: - name: new-manual-default - namespace: default -spec: - coolField: new-value - crossplane: - compositionRef: - name: xmanualdefaults.manualdefault.diff.example.org - compositionRevisionRef: - name: xmanualdefaults.manualdefault.diff.example.org-373e832 diff --git a/cmd/crossplane/project/testdata/diff/new-nested-child-xr.yaml b/cmd/crossplane/project/testdata/diff/new-nested-child-xr.yaml deleted file mode 100644 index 37a82217..00000000 --- a/cmd/crossplane/project/testdata/diff/new-nested-child-xr.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# The child XR that new-nested-xr.yaml's composition renders (named -child). Passed alongside -# that parent it is an input error: the parent's composition writes it, so this would be a second writer. -apiVersion: ns.nested.example.org/v1alpha1 -kind: XChildResource -metadata: - name: test-parent-child - namespace: default -spec: - childField: "parent-value" diff --git a/cmd/crossplane/project/testdata/diff/new-nested-xr.yaml b/cmd/crossplane/project/testdata/diff/new-nested-xr.yaml deleted file mode 100644 index 07ea96d7..00000000 --- a/cmd/crossplane/project/testdata/diff/new-nested-xr.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# A new parent XR that will create a child XR, which will then create managed resources -apiVersion: ns.nested.example.org/v1alpha1 -kind: XParentResource -metadata: - name: test-parent - namespace: default -spec: - parentField: "parent-value" diff --git a/cmd/crossplane/project/testdata/diff/new-parent-claim-omitting-defaulted-fields.yaml b/cmd/crossplane/project/testdata/diff/new-parent-claim-omitting-defaulted-fields.yaml deleted file mode 100644 index 143783ac..00000000 --- a/cmd/crossplane/project/testdata/diff/new-parent-claim-omitting-defaulted-fields.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# A new claim that omits every field a CRD defaults: spec.tier (defaulted by both the claim's and the -# XR's CRD), spec.compositeDeletePolicy (claim CRD only) and spec.compositionUpdatePolicy (XR CRD only). -apiVersion: claimnested.diff.example.org/v1alpha1 -kind: ParentNopClaim -metadata: - name: new-parent-claim - namespace: default -spec: - parentField: new-value - compositionRef: - name: parent-nop-claim-defaults-composition diff --git a/cmd/crossplane/project/testdata/diff/new-rendered-defaults-xrs.yaml b/cmd/crossplane/project/testdata/diff/new-rendered-defaults-xrs.yaml deleted file mode 100644 index b11d2e92..00000000 --- a/cmd/crossplane/project/testdata/diff/new-rendered-defaults-xrs.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# New composites that set only size, leaving region and compositionUpdatePolicy to their CRDs' -# defaults; see resources/rendered-defaults/definitions.yaml. -apiVersion: rendered-defaults.diff.example.org/v1alpha1 -kind: XRegionalDatabase -metadata: - name: modern-database - namespace: default -spec: - size: large ---- -apiVersion: rendered-defaults.diff.example.org/v1alpha1 -kind: XLegacyRegionalDatabase -metadata: - name: legacy-database -spec: - size: small diff --git a/cmd/crossplane/project/testdata/diff/new-xr-manual-no-ref.yaml b/cmd/crossplane/project/testdata/diff/new-xr-manual-no-ref.yaml deleted file mode 100644 index 21557827..00000000 --- a/cmd/crossplane/project/testdata/diff/new-xr-manual-no-ref.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-manual-no-ref - namespace: default -spec: - crossplane: - compositionRef: - name: xnopresources.diff.example.org - compositionUpdatePolicy: Manual - coolField: new-value diff --git a/cmd/crossplane/project/testdata/diff/new-xr-missing-namespace-second.yaml b/cmd/crossplane/project/testdata/diff/new-xr-missing-namespace-second.yaml deleted file mode 100644 index e5abd110..00000000 --- a/cmd/crossplane/project/testdata/diff/new-xr-missing-namespace-second.yaml +++ /dev/null @@ -1,10 +0,0 @@ -# A second XR targeting the same namespace as new-xr-missing-namespace.yaml, which does not exist in -# the cluster. Diffed together, the two give the unverified-addition warning more than one resource -# to count. -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: second-resource - namespace: nonexistent-namespace -spec: - coolField: "second-value" diff --git a/cmd/crossplane/project/testdata/diff/new-xr-missing-namespace.yaml b/cmd/crossplane/project/testdata/diff/new-xr-missing-namespace.yaml deleted file mode 100644 index 03399959..00000000 --- a/cmd/crossplane/project/testdata/diff/new-xr-missing-namespace.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# An XR targeting a namespace that does not exist in the cluster. -# -# This is a supported workflow, not a mistake: a Namespace and the resources inside it are routinely -# applied together (one `kubectl apply -f ./manifests/`), so a user previewing that apply legitimately -# has resources whose namespace is not there yet. The dry-run create cannot be performed, so the -# resources degrade to rendered output and say so — they must NOT fail the run. -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-resource - namespace: nonexistent-namespace -spec: - coolField: "new-value" diff --git a/cmd/crossplane/project/testdata/diff/new-xr-second.yaml b/cmd/crossplane/project/testdata/diff/new-xr-second.yaml deleted file mode 100644 index b31482df..00000000 --- a/cmd/crossplane/project/testdata/diff/new-xr-second.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: second-resource - namespace: default -spec: - coolField: "second-value" diff --git a/cmd/crossplane/project/testdata/diff/new-xr-with-creds.yaml b/cmd/crossplane/project/testdata/diff/new-xr-with-creds.yaml deleted file mode 100644 index 07c0fa53..00000000 --- a/cmd/crossplane/project/testdata/diff/new-xr-with-creds.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-resource-with-creds - namespace: default -spec: - coolField: test-value-creds - crossplane: - compositionRef: - name: xnopresources-with-creds.diff.example.org diff --git a/cmd/crossplane/project/testdata/diff/new-xr-with-cross-ns-external-dep.yaml b/cmd/crossplane/project/testdata/diff/new-xr-with-cross-ns-external-dep.yaml deleted file mode 100644 index d1997d5c..00000000 --- a/cmd/crossplane/project/testdata/diff/new-xr-with-cross-ns-external-dep.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-resource - namespace: default -spec: - coolField: "new-value" - # Matches the ConfigMap in "other-namespace" (labelled environment: crossns). - # The go-templating matchLabels ExtraResources selector omits the namespace, - # so resolving it requires an all-namespaces lookup (issue #376). - environment: crossns diff --git a/cmd/crossplane/project/testdata/diff/resources/admission-policy-canary.yaml b/cmd/crossplane/project/testdata/diff/resources/admission-policy-canary.yaml deleted file mode 100644 index 2130c04e..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/admission-policy-canary.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# A resource that rejecting-admission-policy.yaml must refuse. -# -# The harness dry-run creates this (never persisting it) until the apiserver rejects it, which is how -# a test learns that the policy is actually enforcing. Admission policies reach the apiserver's -# admission plugin through an informer, so for a short window after creation the cluster still -# accepts what the policy will later refuse; a diff issued inside that window would observe no -# rejection at all. -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: admission-policy-canary - namespace: default -spec: - forProvider: - configData: new-value diff --git a/cmd/crossplane/project/testdata/diff/resources/api-version-composition-revision-v1.yaml b/cmd/crossplane/project/testdata/diff/resources/api-version-composition-revision-v1.yaml deleted file mode 100644 index 887eb013..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/api-version-composition-revision-v1.yaml +++ /dev/null @@ -1,34 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: CompositionRevision -metadata: - labels: - crossplane.io/composition-name: xapimigrateresources.example.org -spec: - revision: 1 - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-api-versioned-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: diff.example.org/v1beta1 - kind: XApiMigrateResource - metadata: - name: {{ .observed.composite.resource.metadata.name }}-api-resource - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: api-migrate-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/api-version-composition.yaml b/cmd/crossplane/project/testdata/diff/resources/api-version-composition.yaml deleted file mode 100644 index defbc52a..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/api-version-composition.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# The in-cluster xapimigrateresources.example.org, which renders the resource at v1beta2. The harness -# creates its latest revision from it; api-version-composition-revision-v1.yaml is the earlier one. -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xapimigrateresources.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-api-versioned-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: diff.example.org/v1beta2 - kind: XApiMigrateResource - metadata: - name: {{ .observed.composite.resource.metadata.name }}-api-resource - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: api-migrate-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/claim-composition-with-claimref.yaml b/cmd/crossplane/project/testdata/diff/resources/claim-composition-with-claimref.yaml deleted file mode 100644 index c468c446..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/claim-composition-with-claimref.yaml +++ /dev/null @@ -1,35 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: claim-composition-with-claimref -spec: - compositeTypeRef: - apiVersion: diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - name: {{ .observed.composite.resource.metadata.name }} - spec: - forProvider: - {{- if .observed.composite.resource.spec.claimRef }} - configData: {{ .observed.composite.resource.spec.claimRef.namespace }}/{{ .observed.composite.resource.spec.claimRef.name }} - {{- else }} - configData: {{ .observed.composite.resource.spec.coolField }} - {{- end }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/claim-composition.yaml b/cmd/crossplane/project/testdata/diff/resources/claim-composition.yaml deleted file mode 100644 index f07fb97e..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/claim-composition.yaml +++ /dev/null @@ -1,31 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: claim-composition -spec: - compositeTypeRef: - apiVersion: diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - name: {{ .observed.composite.resource.metadata.name }} - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/claim-nested/child-composition.yaml b/cmd/crossplane/project/testdata/diff/resources/claim-nested/child-composition.yaml deleted file mode 100644 index 824162c4..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/claim-nested/child-composition.yaml +++ /dev/null @@ -1,34 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: child-nop-claim-composition -spec: - compositeTypeRef: - apiVersion: claimnested.diff.example.org/v1alpha1 - kind: XNopChild - mode: Pipeline - pipeline: - - step: create-nop-resource - functionRef: - name: function-go-templating - input: - apiVersion: gotemplating.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: nop.crossplane.io/v1alpha1 - kind: ClusterNopResource - metadata: - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - child-field: {{ .observed.composite.resource.spec.childField }} - spec: - forProvider: - conditionAfter: - - conditionType: Ready - conditionStatus: "True" - time: 0s - - step: auto-ready - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/claim-nested/child-definition.yaml b/cmd/crossplane/project/testdata/diff/resources/claim-nested/child-definition.yaml deleted file mode 100644 index cc63922d..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/claim-nested/child-definition.yaml +++ /dev/null @@ -1,29 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: CompositeResourceDefinition -metadata: - name: xnopchildren.claimnested.diff.example.org -spec: - group: claimnested.diff.example.org - names: - kind: XNopChild - plural: xnopchildren - versions: - - name: v1alpha1 - served: true - referenceable: true - schema: - openAPIV3Schema: - type: object - properties: - spec: - type: object - properties: - childField: - type: string - required: - - childField - status: - type: object - properties: - message: - type: string diff --git a/cmd/crossplane/project/testdata/diff/resources/claim-nested/existing-child-xr.yaml b/cmd/crossplane/project/testdata/diff/resources/claim-nested/existing-child-xr.yaml deleted file mode 100644 index 4b39acb4..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/claim-nested/existing-child-xr.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: claimnested.diff.example.org/v1alpha1 -kind: XNopChild -metadata: - name: existing-parent-claim-82crv-child - generateName: existing-parent-claim-82crv- - annotations: - crossplane.io/composition-resource-name: child-xr -spec: - childField: existing-parent-value - compositionRef: - name: child-nop-claim-composition diff --git a/cmd/crossplane/project/testdata/diff/resources/claim-nested/existing-claim.yaml b/cmd/crossplane/project/testdata/diff/resources/claim-nested/existing-claim.yaml deleted file mode 100644 index ba4e28ca..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/claim-nested/existing-claim.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: claimnested.diff.example.org/v1alpha1 -kind: ParentNopClaim -metadata: - name: existing-parent-claim - namespace: default -spec: - parentField: existing-parent-value - compositeDeletePolicy: Background - compositionRef: - name: parent-nop-claim-composition - # Copied back from the backing XR, as the claim syncer does under the XR's Automatic update policy. - compositionRevisionRef: - name: parent-nop-claim-composition-cfac93e - # Bound to its backing XR, existing-parent-xr.yaml, as Crossplane's claim syncer binds it. - resourceRef: - apiVersion: claimnested.diff.example.org/v1alpha1 - kind: XNopParent - name: existing-parent-claim-82crv diff --git a/cmd/crossplane/project/testdata/diff/resources/claim-nested/existing-managed-resource.yaml b/cmd/crossplane/project/testdata/diff/resources/claim-nested/existing-managed-resource.yaml deleted file mode 100644 index 21a97b6d..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/claim-nested/existing-managed-resource.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: nop.crossplane.io/v1alpha1 -kind: ClusterNopResource -metadata: - name: existing-parent-claim-82crv-nop - generateName: existing-parent-claim-82crv- - annotations: - child-field: existing-parent-value - crossplane.io/composition-resource-name: nop-resource -spec: - forProvider: - conditionAfter: - - conditionType: Ready - conditionStatus: "True" - time: 0s diff --git a/cmd/crossplane/project/testdata/diff/resources/claim-nested/existing-parent-xr.yaml b/cmd/crossplane/project/testdata/diff/resources/claim-nested/existing-parent-xr.yaml deleted file mode 100644 index 20399443..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/claim-nested/existing-parent-xr.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# The backing XR Crossplane's claim syncer created for existing-claim.yaml, tracking its composition's -# current revision. A case also diffs this file as an input, so it spells out what the harness would -# otherwise derive. -apiVersion: claimnested.diff.example.org/v1alpha1 -kind: XNopParent -metadata: - name: existing-parent-claim-82crv - generateName: existing-parent-claim- - labels: - crossplane.io/claim-name: existing-parent-claim - crossplane.io/claim-namespace: default - crossplane.io/composite: existing-parent-claim-82crv -spec: - claimRef: - apiVersion: claimnested.diff.example.org/v1alpha1 - kind: ParentNopClaim - name: existing-parent-claim - namespace: default - parentField: existing-parent-value - compositionUpdatePolicy: Automatic - compositionRef: - name: parent-nop-claim-composition - compositionRevisionRef: - name: parent-nop-claim-composition-cfac93e diff --git a/cmd/crossplane/project/testdata/diff/resources/claim-nested/parent-composition.yaml b/cmd/crossplane/project/testdata/diff/resources/claim-nested/parent-composition.yaml deleted file mode 100644 index f2475f2d..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/claim-nested/parent-composition.yaml +++ /dev/null @@ -1,31 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: parent-nop-claim-composition -spec: - compositeTypeRef: - apiVersion: claimnested.diff.example.org/v1alpha1 - kind: XNopParent - mode: Pipeline - pipeline: - - step: create-child-xr - functionRef: - name: function-go-templating - input: - apiVersion: gotemplating.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: claimnested.diff.example.org/v1alpha1 - kind: XNopChild - metadata: - generateName: {{ .observed.composite.resource.metadata.name }}- - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: child-xr - spec: - childField: {{ .observed.composite.resource.spec.parentField }} - - step: auto-ready - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/claim-nested/parent-defaults-composition.yaml b/cmd/crossplane/project/testdata/diff/resources/claim-nested/parent-defaults-composition.yaml deleted file mode 100644 index d3e331ca..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/claim-nested/parent-defaults-composition.yaml +++ /dev/null @@ -1,35 +0,0 @@ -# Renders, into a composed resource, the two fields of its XR that only the XR's CRD defaults: -# spec.tier (a user default, which the claim's CRD shares) and spec.compositionUpdatePolicy (machinery, -# which the claim's CRD does not default). A claim whose manifest omits both therefore shows, in this -# resource, whether the XR that render consumed was defaulted with the XR's CRD. -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: parent-nop-claim-defaults-composition -spec: - compositeTypeRef: - apiVersion: claimnested.diff.example.org/v1alpha1 - kind: XNopParent - mode: Pipeline - pipeline: - - step: render-defaults - functionRef: - name: function-go-templating - input: - apiVersion: gotemplating.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }}-defaults - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: defaults - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.tier }}/{{ .observed.composite.resource.spec.compositionUpdatePolicy }} - - step: auto-ready - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/claim-nested/parent-definition.yaml b/cmd/crossplane/project/testdata/diff/resources/claim-nested/parent-definition.yaml deleted file mode 100644 index d3ef4162..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/claim-nested/parent-definition.yaml +++ /dev/null @@ -1,35 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: CompositeResourceDefinition -metadata: - name: xnopparents.claimnested.diff.example.org -spec: - group: claimnested.diff.example.org - names: - kind: XNopParent - plural: xnopparents - claimNames: - kind: ParentNopClaim - plural: parentnopclaims - versions: - - name: v1alpha1 - served: true - referenceable: true - schema: - openAPIV3Schema: - type: object - properties: - spec: - type: object - properties: - parentField: - type: string - tier: - type: string - default: standard - required: - - parentField - status: - type: object - properties: - message: - type: string diff --git a/cmd/crossplane/project/testdata/diff/resources/claim-xrd.yaml b/cmd/crossplane/project/testdata/diff/resources/claim-xrd.yaml deleted file mode 100644 index af86a8af..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/claim-xrd.yaml +++ /dev/null @@ -1,30 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: CompositeResourceDefinition -metadata: - name: xnopresources.diff.example.org -spec: - group: diff.example.org - names: - kind: XNopResource - plural: xnopresources - claimNames: - kind: NopClaim - plural: nopclaims - versions: - - name: v1alpha1 - served: true - referenceable: true - schema: - openAPIV3Schema: - type: object - properties: - spec: - type: object - properties: - coolField: - type: string - tier: - type: string - default: standard - writeConnectionSecretsToNamespace: - type: string diff --git a/cmd/crossplane/project/testdata/diff/resources/composition-no-namespace-patch.yaml b/cmd/crossplane/project/testdata/diff/resources/composition-no-namespace-patch.yaml deleted file mode 100644 index a4cb8739..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/composition-no-namespace-patch.yaml +++ /dev/null @@ -1,32 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnopresources.diff.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - # NOTE: No namespace template - testing automatic namespace propagation - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready \ No newline at end of file diff --git a/cmd/crossplane/project/testdata/diff/resources/composition-revision-preview.yaml b/cmd/crossplane/project/testdata/diff/resources/composition-revision-preview.yaml deleted file mode 100644 index 18d67a21..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/composition-revision-preview.yaml +++ /dev/null @@ -1,38 +0,0 @@ -# A later revision of xnopresources.diff.example.org, from when the composition carried the -# channel=preview label. The harness names and labels it from the hash of the composition it was made -# from, as Crossplane does. -apiVersion: apiextensions.crossplane.io/v1 -kind: CompositionRevision -metadata: - labels: - crossplane.io/composition-name: xnopresources.diff.example.org - channel: preview -spec: - revision: 2 - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: preview-{{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/composition-revision-stable.yaml b/cmd/crossplane/project/testdata/diff/resources/composition-revision-stable.yaml deleted file mode 100644 index 5dff855b..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/composition-revision-stable.yaml +++ /dev/null @@ -1,38 +0,0 @@ -# An earlier revision of xnopresources.diff.example.org, from when the composition carried the -# channel=stable label. The harness names and labels it from the hash of the composition it was made -# from, as Crossplane does. The XR in existing-xr-selector-stable.yaml tracks it. -apiVersion: apiextensions.crossplane.io/v1 -kind: CompositionRevision -metadata: - labels: - crossplane.io/composition-name: xnopresources.diff.example.org - channel: stable -spec: - revision: 1 - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: stable-{{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/composition-revision-v1.yaml b/cmd/crossplane/project/testdata/diff/resources/composition-revision-v1.yaml deleted file mode 100644 index 109940aa..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/composition-revision-v1.yaml +++ /dev/null @@ -1,34 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: CompositionRevision -metadata: - labels: - crossplane.io/composition-name: xnopresources.diff.example.org -spec: - revision: 1 - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: v1-{{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/composition-v2.yaml b/cmd/crossplane/project/testdata/diff/resources/composition-v2.yaml deleted file mode 100644 index 17c012c6..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/composition-v2.yaml +++ /dev/null @@ -1,32 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnopresources.diff.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: v2-{{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/composition-with-builtin-defaults.yaml b/cmd/crossplane/project/testdata/diff/resources/composition-with-builtin-defaults.yaml deleted file mode 100644 index c032a4cd..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/composition-with-builtin-defaults.yaml +++ /dev/null @@ -1,61 +0,0 @@ -# Renders a built-in Kubernetes resource whose apiserver defaults are NOT applied locally. -# -# This fixture exists to exercise the apiserver round-trip for additions (issue #334), and the -# choice of a built-in type is load-bearing rather than incidental. -# -# The lenient Defaulter predicts CRD-derived `default:` values locally for an addition that gets no -# apiserver result, so a CRD-backed composed resource can show its defaults in a `+++` diff with no -# apiserver involvement. Deployment has no CRD, so IsCRDRequired is false and the Defaulter passes it -# through unchanged — which makes it the smallest resource whose defaulted fields can only come from -# the apiserver. -# -# The Deployment deliberately sets only the fields a caller must set (selector, template, one -# container). Everything asserted in the corresponding test — spec.strategy.type, -# spec.template.spec.restartPolicy, spec.template.spec.dnsPolicy — is left for the apiserver to -# populate. Deployment is preferred over Service here because Service creation allocates a -# clusterIP even under dry-run, which would put a non-deterministic value in the diff; Deployment's -# defaults are pure schema defaulting with no allocator involved. -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnopresources.diff.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: apps/v1 - kind: Deployment - metadata: - name: {{ .observed.composite.resource.metadata.name }}-deploy - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: deployment - spec: - selector: - matchLabels: - app: {{ .observed.composite.resource.metadata.name }} - template: - metadata: - labels: - app: {{ .observed.composite.resource.metadata.name }} - spec: - containers: - - name: main - image: nginx:1.27 - env: - - name: CONFIG_DATA - value: {{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/composition-with-configmap.yaml b/cmd/crossplane/project/testdata/diff/resources/composition-with-configmap.yaml deleted file mode 100644 index d0ca422f..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/composition-with-configmap.yaml +++ /dev/null @@ -1,48 +0,0 @@ -# Renders a built-in Kubernetes resource (ConfigMap) alongside a composed XR. -# -# ConfigMap has no CRD, so every code path that determines a resource's scope -# or validates it against a schema has to cope with its absence. Scope comes -# from the discovery API; schema validation comes from the embedded Kubernetes -# scheme in crossplane/cli's validate package. -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnopresources.diff.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - --- - apiVersion: v1 - kind: ConfigMap - metadata: - name: {{ .observed.composite.resource.metadata.name }}-config - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: config-map - data: - configData: {{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/composition-with-crd-defaulted-downstream.yaml b/cmd/crossplane/project/testdata/diff/resources/composition-with-crd-defaulted-downstream.yaml deleted file mode 100644 index 48f1a0af..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/composition-with-crd-defaulted-downstream.yaml +++ /dev/null @@ -1,33 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnopresources-defaulted-downstream.diff.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - # spec.forProvider.size is deliberately not rendered: its CRD defaults it. - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDefaultedDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }}-defaulted - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: defaulted-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/composition-with-defaults.yaml b/cmd/crossplane/project/testdata/diff/resources/composition-with-defaults.yaml deleted file mode 100644 index 0eedc809..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/composition-with-defaults.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xdefaultresources.ns.diff.example.org - labels: - provider: nop - service: nop -spec: - mode: Pipeline - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XTestDefaultResource - pipeline: - - step: auto-ready - functionRef: - name: function-auto-ready \ No newline at end of file diff --git a/cmd/crossplane/project/testdata/diff/resources/composition-with-missing-function.yaml b/cmd/crossplane/project/testdata/diff/resources/composition-with-missing-function.yaml deleted file mode 100644 index cb17b40f..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/composition-with-missing-function.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# Names a function, function-not-installed, that no Function on the cluster provides. -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnopresources.diff.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-not-installed - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/composition-with-non-storage-version-downstream.yaml b/cmd/crossplane/project/testdata/diff/resources/composition-with-non-storage-version-downstream.yaml deleted file mode 100644 index c0edae36..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/composition-with-non-storage-version-downstream.yaml +++ /dev/null @@ -1,33 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnopresources-non-storage-version-downstream.diff.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - # Rendered at v1alpha1, which its CRD serves but does not store. - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XVersionedDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }}-versioned - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: versioned-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/composition-with-status.yaml b/cmd/crossplane/project/testdata/diff/resources/composition-with-status.yaml deleted file mode 100644 index be9332b7..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/composition-with-status.yaml +++ /dev/null @@ -1,41 +0,0 @@ -# A composition whose pipeline authors a `status` on the composed resource it renders. -# -# Composition pipelines are explicitly allowed to write their own status, so a rendered status is -# user-authored output rather than a stale read of the cluster. The composed kind here -# (XStatusResource) declares the status subresource, so the apiserver strips status from the dry-run -# create and returns an object with none — which means the status below can only reach the `+++` -# diff if the calculator re-attaches it (crossplane-diff#334). -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnopresources.diff.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XStatusResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: status-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - status: - phase: authored-by-composition - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/composition-with-unserved-k8s-group.yaml b/cmd/crossplane/project/testdata/diff/resources/composition-with-unserved-k8s-group.yaml deleted file mode 100644 index 17ecbf25..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/composition-with-unserved-k8s-group.yaml +++ /dev/null @@ -1,42 +0,0 @@ -# Renders a composed resource in a *.k8s.io group that the cluster does not serve. -# -# This shape slips past every earlier guard. IsCRDRequired (schema_client.go) assumes any group -# ending in .k8s.io other than apiextensions.k8s.io is built-in and returns false WITHOUT consulting -# discovery, so no CRD is demanded. FetchCurrentObject then propagates only non-NotFound errors, and -# a discovery 404 for an unserved group/version is NotFound-shaped, so it is swallowed as "this -# resource is new". The resource therefore reaches the addition path with a GVK that cannot be -# resolved at all. -# -# It stands in for a typo in a group name, an apiVersion skew, a stripped-down cluster, or an -# aggregated API that was never deployed. -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnopresources.diff.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: totallynotserved.k8s.io/v1 - kind: Widget - metadata: - name: {{ .observed.composite.resource.metadata.name }}-widget - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: widget - spec: - size: large - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/conditional-gating-xr.yaml b/cmd/crossplane/project/testdata/diff/resources/conditional-gating-xr.yaml deleted file mode 100644 index b871f6bd..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/conditional-gating-xr.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: conditional-gating-test - namespace: default -spec: - coolField: "test-value" diff --git a/cmd/crossplane/project/testdata/diff/resources/credentials/cli-credentials.yaml b/cmd/crossplane/project/testdata/diff/resources/credentials/cli-credentials.yaml deleted file mode 100644 index 73d57dbd..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/credentials/cli-credentials.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: function-credentials - namespace: crossplane-system -type: Opaque -data: - # base64 encoded "cli-override-key" - credentials: Y2xpLW92ZXJyaWRlLWtleQ== diff --git a/cmd/crossplane/project/testdata/diff/resources/credentials/composition-with-creds.yaml b/cmd/crossplane/project/testdata/diff/resources/credentials/composition-with-creds.yaml deleted file mode 100644 index 2820c988..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/credentials/composition-with-creds.yaml +++ /dev/null @@ -1,39 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnopresources-with-creds.diff.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - # Credentials reference - this tests auto-fetch from cluster - credentials: - - name: test-creds - source: Secret - secretRef: - namespace: crossplane-system - name: function-credentials - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/credentials/function-credentials-secret.yaml b/cmd/crossplane/project/testdata/diff/resources/credentials/function-credentials-secret.yaml deleted file mode 100644 index 7c070b9f..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/credentials/function-credentials-secret.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: crossplane-system ---- -apiVersion: v1 -kind: Secret -metadata: - name: function-credentials - namespace: crossplane-system -type: Opaque -data: - # base64 encoded "test-api-key-12345" - credentials: dGVzdC1hcGkta2V5LTEyMzQ1 diff --git a/cmd/crossplane/project/testdata/diff/resources/cross-namespace-configmap.yaml b/cmd/crossplane/project/testdata/diff/resources/cross-namespace-configmap.yaml deleted file mode 100644 index 4fa3771b..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/cross-namespace-configmap.yaml +++ /dev/null @@ -1,21 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: cross-namespace-config - namespace: kube-system - labels: - app: cross-namespace-app - environment: production -data: - config-data: "cross-namespace-data" ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: another-cross-namespace-config - namespace: kube-public - labels: - app: another-cross-namespace-app - environment: staging -data: - config-data: "another-cross-namespace-data" \ No newline at end of file diff --git a/cmd/crossplane/project/testdata/diff/resources/cross-namespace-fn-composition.yaml b/cmd/crossplane/project/testdata/diff/resources/cross-namespace-fn-composition.yaml deleted file mode 100644 index a1bcb0e0..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/cross-namespace-fn-composition.yaml +++ /dev/null @@ -1,72 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnopresources.cross-namespace.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: fetch-cross-namespace-resources - functionRef: - name: function-extra-resources - input: - apiVersion: extra-resources.fn.crossplane.io/v1beta1 - kind: Input - spec: - extraResources: - # Cross-namespace ConfigMap by label selector - - apiVersion: v1 - kind: ConfigMap - into: crossNamespaceConfig - type: Selector - namespace: kube-system - selector: - matchLabels: - - key: app - type: Value - value: cross-namespace-app - - key: environment - type: Value - value: production - # Another cross-namespace ConfigMap by name reference - - apiVersion: v1 - kind: ConfigMap - into: namedCrossNamespaceConfig - type: Reference - namespace: kube-public - ref: - name: another-cross-namespace-config - # Cluster-scoped resource for comparison - - apiVersion: rbac.authorization.k8s.io/v1 - kind: ClusterRole - into: namedClusterRole - type: Reference - ref: - name: external-named-clusterrole - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - {{- $crossNsConfig := index .context "apiextensions.crossplane.io/extra-resources" "crossNamespaceConfig" 0 -}} - {{- $namedCrossNsConfig := index .context "apiextensions.crossplane.io/extra-resources" "namedCrossNamespaceConfig" 0 -}} - {{- $clusterRole := index .context "apiextensions.crossplane.io/extra-resources" "namedClusterRole" 0 -}} - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: cross-ns-resource - spec: - forProvider: - configData: {{ index $crossNsConfig "data" "config-data" }}-{{ index $namedCrossNsConfig "data" "config-data" }}-{{ $clusterRole.metadata.name }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready \ No newline at end of file diff --git a/cmd/crossplane/project/testdata/diff/resources/default-composition.yaml b/cmd/crossplane/project/testdata/diff/resources/default-composition.yaml deleted file mode 100644 index b7ccde46..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/default-composition.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: default-composition - labels: - provider: default - environment: any -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-default-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: default-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - resourceTier: "default" - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/env-composition.yaml b/cmd/crossplane/project/testdata/diff/resources/env-composition.yaml deleted file mode 100644 index 6583c4de..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/env-composition.yaml +++ /dev/null @@ -1,46 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xenvresources.diff.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XEnvResource - mode: Pipeline - pipeline: - - step: environmentConfigs - functionRef: - name: function-environment-configs - input: - apiVersion: environmentconfigs.fn.crossplane.io/v1beta1 - kind: Input - spec: - environmentConfigs: - - type: Reference - ref: - name: test-env-config - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - {{ $envConfig := index .context "apiextensions.crossplane.io/environment" }} - apiVersion: nop.example.org/v1alpha1 - kind: XDownstreamEnvResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: env-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.configKey }} - region: {{ $envConfig.region }} - environment: {{ $envConfig.environment }} - serviceLevel: {{ $envConfig.serviceLevel }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready \ No newline at end of file diff --git a/cmd/crossplane/project/testdata/diff/resources/env-xrd.yaml b/cmd/crossplane/project/testdata/diff/resources/env-xrd.yaml deleted file mode 100644 index 9d137cac..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/env-xrd.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: CompositeResourceDefinition -metadata: - name: xenvresources.ns.diff.example.org -spec: - group: ns.diff.example.org - names: - kind: XEnvResource - plural: xenvresources - versions: - - name: v1alpha1 - served: true - referenceable: true - schema: - openAPIV3Schema: - type: object - properties: - spec: - type: object - properties: - configKey: - type: string diff --git a/cmd/crossplane/project/testdata/diff/resources/environment-config-v1beta1.yaml b/cmd/crossplane/project/testdata/diff/resources/environment-config-v1beta1.yaml deleted file mode 100644 index ddb284c0..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/environment-config-v1beta1.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1beta1 -kind: EnvironmentConfig -metadata: - name: test-env-config -data: - region: us-west-2 - environment: staging - serviceLevel: premium diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-api-version-downstream-v1beta1.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-api-version-downstream-v1beta1.yaml deleted file mode 100644 index 7caf33a2..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-api-version-downstream-v1beta1.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: diff.example.org/v1beta1 -kind: XApiMigrateResource -metadata: - name: test-api-version-xr-api-resource - namespace: default - annotations: - crossplane.io/composition-resource-name: api-migrate-resource -spec: - forProvider: - configData: test-value diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-api-version-xr-rev1.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-api-version-xr-rev1.yaml deleted file mode 100644 index 558b7168..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-api-version-xr-rev1.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-api-version-xr - namespace: default -spec: - crossplane: - compositionRef: - name: xapimigrateresources.example.org - compositionRevisionRef: - name: xapimigrateresources.example.org-2067855 - compositionUpdatePolicy: Manual - coolField: test-value diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-claim-defaulted-fields-other-manager.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-claim-defaulted-fields-other-manager.yaml deleted file mode 100644 index ab5d01ab..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-claim-defaulted-fields-other-manager.yaml +++ /dev/null @@ -1,10 +0,0 @@ -# A different field manager sets two fields the claim's CRD defaults to non-default values on the -# claim, whose manifest (modified-claim-omitting-defaulted-fields.yaml) omits both. -apiVersion: diff.example.org/v1alpha1 -kind: NopClaim -metadata: - name: test-claim - namespace: existing-namespace -spec: - compositeDeletePolicy: Foreground - compositionUpdatePolicy: Manual diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-claim-downstream-resource.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-claim-downstream-resource.yaml deleted file mode 100644 index bdea322d..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-claim-downstream-resource.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - annotations: - crossplane.io/composition-resource-name: nop-resource - name: test-claim-82crv -spec: - forProvider: - configData: existing-value diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-claim-xr.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-claim-xr.yaml deleted file mode 100644 index e5ed847a..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-claim-xr.yaml +++ /dev/null @@ -1,21 +0,0 @@ -# The backing XR Crossplane's claim syncer created for existing-claim.yaml, and whose name it wrote to -# that claim's spec.resourceRef. Its composed resource is existing-claim-downstream-resource.yaml. -apiVersion: diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-claim-82crv - generateName: test-claim- -spec: - coolField: existing-value - compositionUpdatePolicy: Automatic - compositionRef: - name: claim-composition - claimRef: - apiVersion: diff.example.org/v1alpha1 - kind: NopClaim - name: test-claim - namespace: existing-namespace - resourceRefs: - - apiVersion: nop.example.org/v1alpha1 - kind: XDownstreamResource - name: test-claim-82crv diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-claim.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-claim.yaml deleted file mode 100644 index 0de70f11..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-claim.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: diff.example.org/v1alpha1 -kind: NopClaim -metadata: - name: test-claim - namespace: existing-namespace -spec: - coolField: existing-value - compositeDeletePolicy: Background - compositionRef: - name: claim-composition - # Copied back from the backing XR, as the claim syncer does under the XR's Automatic update policy. - compositionRevisionRef: - name: claim-composition-caff3a7 - # Bound to its backing XR, existing-claim-xr.yaml, as Crossplane's claim syncer binds it. - resourceRef: - apiVersion: diff.example.org/v1alpha1 - kind: XNopResource - name: test-claim-82crv diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-cluster-xr.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-cluster-xr.yaml deleted file mode 100644 index 03e34f05..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-cluster-xr.yaml +++ /dev/null @@ -1,10 +0,0 @@ -# TODO: use opposite pattern of prefixing namespaced api group instead of prefixing cluster api group -apiVersion: diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-resource -spec: - coolField: "existing-value" - crossplane: - compositionRef: - name: removal-test-composition diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-cross-ns-downstream.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-cross-ns-downstream.yaml deleted file mode 100644 index 45fdbc88..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-cross-ns-downstream.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: test-cross-ns-resource - namespace: default - annotations: - crossplane.io/composition-resource-name: cross-ns-resource -spec: - forProvider: - configData: existing-cross-ns-data-existing-named-data-old-cross-ns-role \ No newline at end of file diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-cross-ns-xr.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-cross-ns-xr.yaml deleted file mode 100644 index 5830cd0e..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-cross-ns-xr.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-cross-ns-resource - namespace: default -spec: - coolField: existing-cross-ns-value - environment: staging - crossplane: - compositionRef: - name: xnopresources.cross-namespace.example.org diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-defaulted-downstream-composed.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-defaulted-downstream-composed.yaml deleted file mode 100644 index cb92c60d..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-defaulted-downstream-composed.yaml +++ /dev/null @@ -1,14 +0,0 @@ -# The fields Crossplane's composed-resource field manager applies. spec.forProvider.size is absent, -# exactly as the composition renders it. -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDefaultedDownstreamResource -metadata: - name: test-resource-defaulted - namespace: default - annotations: - crossplane.io/composition-resource-name: defaulted-resource - labels: - crossplane.io/composite: test-resource -spec: - forProvider: - configData: new-value diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-defaulted-downstream-other-manager.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-defaulted-downstream-other-manager.yaml deleted file mode 100644 index 04a6509a..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-defaulted-downstream-other-manager.yaml +++ /dev/null @@ -1,10 +0,0 @@ -# A different field manager (a user or another controller) sets the CRD-defaulted field to a -# non-default value on the composed resource Crossplane already manages. -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDefaultedDownstreamResource -metadata: - name: test-resource-defaulted - namespace: default -spec: - forProvider: - size: large diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-downstream-automatic.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-downstream-automatic.yaml deleted file mode 100644 index f8abdd1d..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-downstream-automatic.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: test-automatic - namespace: default - annotations: - crossplane.io/composition-resource-name: nop-resource -spec: - forProvider: - configData: v1-existing-value diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-downstream-manual-v1.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-downstream-manual-v1.yaml deleted file mode 100644 index 8909fb23..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-downstream-manual-v1.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: test-manual-v1 - namespace: default - annotations: - crossplane.io/composition-resource-name: nop-resource -spec: - forProvider: - configData: v1-existing-value diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-downstream-resource-with-argocd.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-downstream-resource-with-argocd.yaml deleted file mode 100644 index 650c6e68..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-downstream-resource-with-argocd.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - annotations: - crossplane.io/composition-resource-name: nop-resource - argocd.argoproj.io/tracking-id: "test-app:/XDownstreamResource:default/test-resource" - kubectl.kubernetes.io/last-applied-configuration: | - {"apiVersion":"ns.nop.example.org/v1alpha1","kind":"XDownstreamResource"} - labels: - argocd.argoproj.io/instance: test-app - name: test-resource - namespace: default -spec: - forProvider: - configData: new-value diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-downstream-selector-stable.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-downstream-selector-stable.yaml deleted file mode 100644 index 5f53076d..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-downstream-selector-stable.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: test-selector - namespace: default - annotations: - crossplane.io/composition-resource-name: nop-resource -spec: - forProvider: - configData: stable-existing-value diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-downstream-with-external-dep.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-downstream-with-external-dep.yaml deleted file mode 100644 index f25c13dc..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-downstream-with-external-dep.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - annotations: - crossplane.io/composition-resource-name: nop-resource - name: test-resource - namespace: default -spec: - forProvider: - configData: existing-value - roleName: old-role-name diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-downstream-with-generated-name.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-downstream-with-generated-name.yaml deleted file mode 100644 index e8cc8c56..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-downstream-with-generated-name.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - annotations: - crossplane.io/composition-resource-name: nop-resource - generateName: test-resource- - labels: - crossplane.io/composite: test-resource - name: test-resource-abc123 - namespace: default -spec: - forProvider: - configData: existing-value diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-env-downstream-resource.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-env-downstream-resource.yaml deleted file mode 100644 index 19da0e39..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-env-downstream-resource.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# Deliberately no compositionRef or compositionRevisionRef. XDownstreamEnvResource is an XR kind (its -# XRD is xdownstreamenvresource-xrd.yaml), but no Composition targets it, so Crossplane can select -# none and never writes either ref. The diff likewise skips rendering it as a nested XR. -apiVersion: nop.example.org/v1alpha1 -kind: XDownstreamEnvResource -metadata: - annotations: - crossplane.io/composition-resource-name: env-resource - name: test-env-resource -spec: - compositionUpdatePolicy: Automatic - forProvider: - configData: existing-config-value - environment: staging - region: us-west-2 - serviceLevel: premium \ No newline at end of file diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-env-xr.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-env-xr.yaml deleted file mode 100644 index 1768c99a..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-env-xr.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XEnvResource -metadata: - name: test-env-resource -spec: - compositionRef: - name: xenvresources.diff.example.org - compositionUpdatePolicy: Automatic - configKey: "existing-config-value" diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-generate-name-xr.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-generate-name-xr.yaml deleted file mode 100644 index 72d7f866..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-generate-name-xr.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# existing-xr.yaml as Crossplane leaves it once generated-name-composition.yaml has composed it, so it -# tracks that composition's revision rather than composition.yaml's. -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-resource - namespace: default -spec: - coolField: "existing-value" - crossplane: - compositionRef: - name: xnopresources.diff.example.org - compositionUpdatePolicy: Automatic diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-legacy-downstream-manual-v1.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-legacy-downstream-manual-v1.yaml deleted file mode 100644 index 0134d82c..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-legacy-downstream-manual-v1.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: legacycluster.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: test-legacy-manual-v1 - annotations: - crossplane.io/composition-resource-name: nop-resource -spec: - forProvider: - configData: v1-existing-value diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-legacy-xr-manual-v1.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-legacy-xr-manual-v1.yaml deleted file mode 100644 index 4999cd65..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-legacy-xr-manual-v1.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: legacycluster.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-legacy-manual-v1 -spec: - compositionRef: - name: xlegacynopresources.diff.example.org - compositionRevisionRef: - name: xlegacynopresources.diff.example.org-2f488b6 - compositionUpdatePolicy: Manual - coolField: existing-value diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-legacy-xr.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-legacy-xr.yaml deleted file mode 100644 index 5c375919..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-legacy-xr.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: legacycluster.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-resource -spec: - compositionRef: - name: removal-test-composition - compositionUpdatePolicy: Automatic - coolField: "existing-value" diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-namespace.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-namespace.yaml deleted file mode 100644 index 3fc6cd48..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-namespace.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: existing-namespace \ No newline at end of file diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-removal-test-xr.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-removal-test-xr.yaml deleted file mode 100644 index bc841ee6..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-removal-test-xr.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# existing-xr.yaml as Crossplane leaves it once removal-test-composition.yaml has composed it, so it -# tracks that composition and its revision. -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-resource - namespace: default -spec: - coolField: "existing-value" - crossplane: - compositionRef: - name: removal-test-composition - compositionUpdatePolicy: Automatic diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-sequencer-gating-stage0-ready.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-sequencer-gating-stage0-ready.yaml deleted file mode 100644 index 8ce45267..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-sequencer-gating-stage0-ready.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# The stage0 resource sequencer-gating-composition.yaml composed for existing-sequencer-gating-xr.yaml, -# once it has become Ready. Crossplane then composed stage1 too -# (existing-sequencer-gating-stage1-ready.yaml). -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - annotations: - crossplane.io/composition-resource-name: stage0-resource - name: 0-stage0-resource - namespace: default -spec: - forProvider: - configData: existing-value -status: - conditions: - - type: Ready - status: "True" - reason: Available - lastTransitionTime: "2024-01-01T00:00:00Z" diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-sequencer-gating-stage0.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-sequencer-gating-stage0.yaml deleted file mode 100644 index e84cc250..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-sequencer-gating-stage0.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# The stage0 resource sequencer-gating-composition.yaml composed for existing-sequencer-gating-xr.yaml, -# still provisioning: it is not Ready, so function-sequencer keeps stage1 gated until -# --eventual-state synthesizes its Ready. -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - annotations: - crossplane.io/composition-resource-name: stage0-resource - name: 0-stage0-resource - namespace: default -spec: - forProvider: - configData: existing-value -status: - conditions: - - type: Ready - status: "False" - reason: Creating - lastTransitionTime: "2024-01-01T00:00:00Z" diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-sequencer-gating-stage1-ready.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-sequencer-gating-stage1-ready.yaml deleted file mode 100644 index a209807c..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-sequencer-gating-stage1-ready.yaml +++ /dev/null @@ -1,18 +0,0 @@ -# The stage1 resource sequencer-gating-composition.yaml composed for existing-sequencer-gating-xr.yaml -# once its stage0 (existing-sequencer-gating-stage0-ready.yaml) became Ready. It is Ready too. -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - annotations: - crossplane.io/composition-resource-name: stage1-resource - name: 1-stage1-resource - namespace: default -spec: - forProvider: - configData: existing-value -status: - conditions: - - type: Ready - status: "True" - reason: Available - lastTransitionTime: "2024-01-01T00:00:00Z" diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-sequencer-gating-xr.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-sequencer-gating-xr.yaml deleted file mode 100644 index b259f22a..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-sequencer-gating-xr.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# sequencer-xr.yaml as it stood in the cluster before its coolField was changed, once Crossplane had -# reconciled it against sequencer-gating-composition.yaml, recording the composition it selected at -# spec.crossplane.* as the v2 XRD has them. -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: sequencer-test - namespace: default -spec: - coolField: existing-value - crossplane: - compositionRef: - name: sequencer-gating-composition diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-sequencer-xr.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-sequencer-xr.yaml deleted file mode 100644 index 6c50f601..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-sequencer-xr.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# sequencer-xr.yaml as it stands in the cluster once Crossplane has reconciled it against -# sequencer-composition.yaml. -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: sequencer-test - namespace: default -spec: - coolField: "test-value" - crossplane: - compositionRef: - name: sequencer-composition diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-v2xrd-v1paths-downstream.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-v2xrd-v1paths-downstream.yaml deleted file mode 100644 index e1c4dcb2..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-v2xrd-v1paths-downstream.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: test-v2xrd-v1paths - namespace: default - annotations: - crossplane.io/composition-resource-name: nop-resource -spec: - forProvider: - configData: existing-value diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-v2xrd-v1paths-xr.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-v2xrd-v1paths-xr.yaml deleted file mode 100644 index 2f1b1b25..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-v2xrd-v1paths-xr.yaml +++ /dev/null @@ -1,14 +0,0 @@ -# v2 XRD with v1-style composition paths test case (issue #206) -# This XR uses v1-style spec.compositionRef instead of spec.crossplane.compositionRef -apiVersion: v2withv1paths.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-v2xrd-v1paths - namespace: default -spec: - # v1-style composition reference (at spec.compositionRef instead of spec.crossplane.compositionRef), - # which is what this case is about. The harness derives the revision ref at the same v1-style path. - compositionRef: - name: xnopresources.v2withv1paths.diff.example.org - compositionUpdatePolicy: Automatic - coolField: existing-value diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-xr-automatic.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-xr-automatic.yaml deleted file mode 100644 index 52060d5d..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-xr-automatic.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-automatic - namespace: default -spec: - crossplane: - compositionRef: - name: xnopresources.diff.example.org - compositionRevisionRef: - name: xnopresources.diff.example.org-911318d # Currently on v1, but should auto-upgrade - compositionUpdatePolicy: Automatic - coolField: existing-value diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-xr-deleting.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-xr-deleting.yaml deleted file mode 100644 index b84ed1af..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-xr-deleting.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-resource - namespace: default - # The finalizer keeps the object around after a delete is issued, so envtest (which runs no - # controllers) leaves it with metadata.deletionTimestamp set — a resource mid-deletion. - finalizers: - - composite.apiextensions.crossplane.io -spec: - coolField: "existing-value" - crossplane: - compositionRef: - name: xnopresources.diff.example.org diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-xr-listing-uncontrolled-resources.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-xr-listing-uncontrolled-resources.yaml deleted file mode 100644 index a68ebbd7..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-xr-listing-uncontrolled-resources.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# existing-removal-test-xr.yaml whose resourceRefs still name two composed resources it no longer -# controls: one another XR (existing-other-xr.yaml) now controls, and one whose controller reference -# was removed. Crossplane's garbage collector deletes only a resource the composite controls, so -# neither is removed although the composition no longer renders them. The harness appends the -# resources this XR does control. -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-resource - namespace: default -spec: - coolField: "existing-value" - crossplane: - compositionRef: - name: removal-test-composition - compositionUpdatePolicy: Automatic - resourceRefs: - - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - name: other-resource - - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - name: resource-without-controller diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-xr-manual-v1.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-xr-manual-v1.yaml deleted file mode 100644 index 61c2c65c..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-xr-manual-v1.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-manual-v1 - namespace: default -spec: - crossplane: - compositionRef: - name: xnopresources.diff.example.org - compositionRevisionRef: - name: xnopresources.diff.example.org-911318d - compositionUpdatePolicy: Manual - coolField: existing-value diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-xr-selector-stable.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-xr-selector-stable.yaml deleted file mode 100644 index bb1edeb2..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-xr-selector-stable.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-selector - namespace: default -spec: - crossplane: - compositionRef: - name: xnopresources.diff.example.org - compositionRevisionRef: - name: xnopresources.diff.example.org-1623e01 - compositionUpdatePolicy: Automatic - compositionRevisionSelector: - matchLabels: - channel: stable - coolField: existing-value diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-xr-with-argocd.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-xr-with-argocd.yaml deleted file mode 100644 index af4bc154..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-xr-with-argocd.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-resource - namespace: default - annotations: - argocd.argoproj.io/tracking-id: "test-app-OLD:/XNopResource:default/test-resource" - kubectl.kubernetes.io/last-applied-configuration: | - {"apiVersion":"ns.diff.example.org/v1alpha1","kind":"XNopResource","metadata":{"name":"test-resource","namespace":"default"},"spec":{"coolField":"old-value"}} - labels: - argocd.argoproj.io/instance: test-app-OLD -spec: - coolField: "new-value" - # What any XR Crossplane has reconciled carries: the reconciler writes spec.crossplane, and the - # XR's generated CRD then defaults the policy. - crossplane: - compositionRef: - name: xnopresources.diff.example.org - compositionUpdatePolicy: Automatic diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-xr-with-defaults-other-manager.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-xr-with-defaults-other-manager.yaml deleted file mode 100644 index 678d6a0e..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-xr-with-defaults-other-manager.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# A different field manager (a user or another controller) sets the XRD-defaulted spec.region to a -# non-default value on an XR whose own manifest (xr-with-missing-defaults.yaml) omits it. -apiVersion: ns.diff.example.org/v1alpha1 -kind: XTestDefaultResource -metadata: - name: test-resource-with-defaults - namespace: default -spec: - region: eu-west-1 diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-xr-with-defaults-reconciled.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-xr-with-defaults-reconciled.yaml deleted file mode 100644 index 6a855f8d..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-xr-with-defaults-reconciled.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# What Crossplane's composite reconciler writes to xr-with-missing-defaults.yaml once it has reconciled -# it: its own composite label, and the composition and revision it selected (the revision the harness -# creates for composition-with-defaults.yaml, as the revision controller does). -apiVersion: ns.diff.example.org/v1alpha1 -kind: XTestDefaultResource -metadata: - name: test-resource-with-defaults - namespace: default - labels: - crossplane.io/composite: test-resource-with-defaults -spec: - crossplane: - compositionRef: - name: xdefaultresources.ns.diff.example.org - compositionRevisionRef: - name: xdefaultresources.ns.diff.example.org-d0e5887 diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-xr-with-external-dep-multistep-fatal.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-xr-with-external-dep-multistep-fatal.yaml deleted file mode 100644 index 8a000a9b..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-xr-with-external-dep-multistep-fatal.yaml +++ /dev/null @@ -1,14 +0,0 @@ -# existing-xr-with-external-dep.yaml as Crossplane leaves it once -# multistep-fatal-after-extra-res-composition.yaml has composed it, so it tracks that composition and -# its revision. -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-resource - namespace: default -spec: - coolField: existing-value - environment: staging - crossplane: - compositionRef: - name: multistep-fatal-after-extra-res.diff.example.org diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-xr-with-external-dep-templated.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-xr-with-external-dep-templated.yaml deleted file mode 100644 index 7c16d606..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-xr-with-external-dep-templated.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# existing-xr-with-external-dep.yaml as Crossplane leaves it once external-res-gotpl-composition.yaml -# has composed it, so it tracks that composition and its revision. -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-resource - namespace: default -spec: - coolField: existing-value - environment: staging - crossplane: - compositionRef: - name: templated-extra-resources.diff.example.org diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-xr-with-external-dep-unguarded.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-xr-with-external-dep-unguarded.yaml deleted file mode 100644 index 2f09685b..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-xr-with-external-dep-unguarded.yaml +++ /dev/null @@ -1,14 +0,0 @@ -# existing-xr-with-external-dep.yaml as Crossplane leaves it once -# unguarded-external-res-gotpl-composition.yaml has composed it, so it tracks that composition and its -# revision. -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-resource - namespace: default -spec: - coolField: existing-value - environment: staging - crossplane: - compositionRef: - name: unguarded-templated-extra-resources.diff.example.org diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-xr-with-external-dep.yaml b/cmd/crossplane/project/testdata/diff/resources/existing-xr-with-external-dep.yaml deleted file mode 100644 index 852cd656..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/existing-xr-with-external-dep.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-resource - namespace: default -spec: - coolField: existing-value - environment: staging - crossplane: - compositionRef: - name: xnopresources.diff.example.org diff --git a/cmd/crossplane/project/testdata/diff/resources/external-named-clusterrole.yaml b/cmd/crossplane/project/testdata/diff/resources/external-named-clusterrole.yaml deleted file mode 100644 index 3f6b02ef..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/external-named-clusterrole.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: external-named-clusterrole -rules: - - apiGroups: [""] - resources: ["pods"] - verbs: ["get", "list", "watch"] diff --git a/cmd/crossplane/project/testdata/diff/resources/external-res-fn-composition.yaml b/cmd/crossplane/project/testdata/diff/resources/external-res-fn-composition.yaml deleted file mode 100644 index dacd3b43..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/external-res-fn-composition.yaml +++ /dev/null @@ -1,64 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnopresources.diff.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: fetch-external-resources - functionRef: - name: function-extra-resources - input: - apiVersion: extra-resources.fn.crossplane.io/v1beta1 - kind: Input - spec: - extraResources: - - apiVersion: v1 - kind: ConfigMap - into: cfgMap - type: Selector - selector: - matchLabels: - - key: app - type: Value - value: test-app - - key: environment - type: FromCompositeFieldPath - valueFromFieldPath: spec.environment - - apiVersion: rbac.authorization.k8s.io/v1 - kind: ClusterRole - # function-extra-resources only supports Cluster-scoped resources as name refs - into: namedClusterRole - type: Reference - ref: - name: external-named-clusterrole - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - {{- /* we can't use the same call to 'index' on these because it goes map -> array -> map */ -}} - {{- $extras := index .context "apiextensions.crossplane.io/extra-resources" -}} - {{- $cfgMap := index $extras "cfgMap" 0 -}} - {{- $clusterRole := index $extras "namedClusterRole" 0 -}} - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: {{ index $cfgMap "data" "resource-data" }} - roleName: {{ $clusterRole.metadata.name }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/external-res-gotpl-composition.yaml b/cmd/crossplane/project/testdata/diff/resources/external-res-gotpl-composition.yaml deleted file mode 100644 index eb3c88aa..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/external-res-gotpl-composition.yaml +++ /dev/null @@ -1,46 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: templated-extra-resources.diff.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: render-templates - functionRef: - name: function-go-templating - input: - apiVersion: gotemplating.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - --- - apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1 - kind: ExtraResources - requirements: - configmaps: - apiVersion: v1 - kind: ConfigMap - matchLabels: - app: test-app - environment: {{ .observed.composite.resource.spec.environment }} - {{- with .extraResources }} - {{ $configMaps := index . "configmaps" }} - {{- range $i, $configMap := $configMaps.items }} - --- - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ $.observed.composite.resource.metadata.name }} - namespace: {{ $.observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: {{ $.observed.composite.resource.spec.coolField }} - roleName: templated-{{ $configMap.resource.metadata.name }} - {{- end }} - {{- end }} diff --git a/cmd/crossplane/project/testdata/diff/resources/external-res-matchname-gotpl-composition.yaml b/cmd/crossplane/project/testdata/diff/resources/external-res-matchname-gotpl-composition.yaml deleted file mode 100644 index a886976c..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/external-res-matchname-gotpl-composition.yaml +++ /dev/null @@ -1,46 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: templated-extra-resources-matchname.diff.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: render-templates - functionRef: - name: function-go-templating - input: - apiVersion: gotemplating.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - # Regression scenario for crossplane-contrib/crossplane-diff#355: - # ExtraResources block uses matchName to reference a Secret that - # does not exist in the cluster. Pre-fix this aborted the diff with - # "cannot get referenced resource"; post-fix the unmet requirement - # is silently skipped (mirroring upstream xfn behavior) and the - # composition continues to render its downstream resource. - template: | - --- - apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1 - kind: ExtraResources - requirements: - optional-secret: - apiVersion: v1 - kind: Secret - matchName: nonexistent-secret - namespace: default - --- - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - roleName: static-role diff --git a/cmd/crossplane/project/testdata/diff/resources/external-resource-configmap-other-namespace.yaml b/cmd/crossplane/project/testdata/diff/resources/external-resource-configmap-other-namespace.yaml deleted file mode 100644 index 4a02772e..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/external-resource-configmap-other-namespace.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# Reproduction fixture for crossplane-contrib/crossplane-diff#376. -# -# A ConfigMap matched by a matchLabels ExtraResources selector (no namespace) -# that lives in a DIFFERENT namespace than the XR being diffed. The XR is in -# "default"; this resource is in "other-namespace". A correct all-namespaces -# lookup (matching upstream Crossplane) finds it; a lookup wrongly scoped to the -# XR namespace does not. -apiVersion: v1 -kind: Namespace -metadata: - name: other-namespace ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: external-resource-crossns - namespace: other-namespace - labels: - app: test-app - environment: crossns -data: - resource-data: "cross-namespace-external-resource-data" diff --git a/cmd/crossplane/project/testdata/diff/resources/external-resource-configmap.yaml b/cmd/crossplane/project/testdata/diff/resources/external-resource-configmap.yaml deleted file mode 100644 index 53200b9e..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/external-resource-configmap.yaml +++ /dev/null @@ -1,21 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: external-resource-staging - namespace: default - labels: - app: test-app - environment: staging -data: - resource-data: "staging-external-resource-data" ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: external-resource-testing - namespace: default - labels: - app: test-app - environment: testing -data: - resource-data: "testing-external-resource-data" \ No newline at end of file diff --git a/cmd/crossplane/project/testdata/diff/resources/generated-name-composition.yaml b/cmd/crossplane/project/testdata/diff/resources/generated-name-composition.yaml deleted file mode 100644 index f16e10c1..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/generated-name-composition.yaml +++ /dev/null @@ -1,32 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnopresources.diff.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - generateName: test-resource- - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - namespace: {{ .observed.composite.resource.metadata.namespace }} - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/legacy-composition-revision-v1.yaml b/cmd/crossplane/project/testdata/diff/resources/legacy-composition-revision-v1.yaml deleted file mode 100644 index 8f7eec5d..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/legacy-composition-revision-v1.yaml +++ /dev/null @@ -1,33 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: CompositionRevision -metadata: - labels: - crossplane.io/composition-name: xlegacynopresources.diff.example.org -spec: - revision: 1 - compositeTypeRef: - apiVersion: legacycluster.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: legacycluster.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: v1-{{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/legacy-composition-v2.yaml b/cmd/crossplane/project/testdata/diff/resources/legacy-composition-v2.yaml deleted file mode 100644 index fc492bb9..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/legacy-composition-v2.yaml +++ /dev/null @@ -1,31 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xlegacynopresources.diff.example.org -spec: - compositeTypeRef: - apiVersion: legacycluster.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: legacycluster.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: v2-{{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/legacy-xrd.yaml b/cmd/crossplane/project/testdata/diff/resources/legacy-xrd.yaml deleted file mode 100644 index c8648a5a..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/legacy-xrd.yaml +++ /dev/null @@ -1,49 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: CompositeResourceDefinition -metadata: - name: xnopresources.legacycluster.diff.example.org -spec: - group: legacycluster.diff.example.org - names: - kind: XNopResource - plural: xnopresources - versions: - - name: v1alpha1 - served: true - referenceable: true - schema: - openAPIV3Schema: - type: object - properties: - spec: - type: object - properties: - coolField: - type: string - environment: - type: string - status: - type: object - properties: - conditions: - description: Conditions of the resource. - type: array - items: - type: object - required: - - lastTransitionTime - - reason - - status - - type - properties: - lastTransitionTime: - type: string - format: date-time - message: - type: string - reason: - type: string - status: - type: string - type: - type: string diff --git a/cmd/crossplane/project/testdata/diff/resources/manual-default/definitions.yaml b/cmd/crossplane/project/testdata/diff/resources/manual-default/definitions.yaml deleted file mode 100644 index 11c29f88..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/manual-default/definitions.yaml +++ /dev/null @@ -1,96 +0,0 @@ -# An XRD whose composites default to the Manual update policy, and a composition with two revisions. -# Each revision's template writes its own version into the composed resource, so the value shows which -# revision rendered. The composition itself is the newest revision (v2), as it is in a cluster, and the -# harness creates that revision from it; revision 1 is declared here. -apiVersion: apiextensions.crossplane.io/v2 -kind: CompositeResourceDefinition -metadata: - name: xmanualdefaults.manualdefault.diff.example.org -spec: - group: manualdefault.diff.example.org - names: - kind: XManualDefault - plural: xmanualdefaults - scope: Namespaced - defaultCompositionUpdatePolicy: Manual - versions: - - name: v1alpha1 - served: true - referenceable: true - schema: - openAPIV3Schema: - type: object - properties: - spec: - type: object - properties: - coolField: - type: string ---- -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xmanualdefaults.manualdefault.diff.example.org -spec: - compositeTypeRef: - apiVersion: manualdefault.diff.example.org/v1alpha1 - kind: XManualDefault - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: v2-{{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready ---- -apiVersion: apiextensions.crossplane.io/v1 -kind: CompositionRevision -metadata: - labels: - crossplane.io/composition-name: xmanualdefaults.manualdefault.diff.example.org -spec: - revision: 1 - compositeTypeRef: - apiVersion: manualdefault.diff.example.org/v1alpha1 - kind: XManualDefault - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: v1-{{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/modified-v2xrd-v1paths-xr.yaml b/cmd/crossplane/project/testdata/diff/resources/modified-v2xrd-v1paths-xr.yaml deleted file mode 100644 index a974f9f5..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/modified-v2xrd-v1paths-xr.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# v2 XRD with v1-style composition paths test case (issue #206) -# This XR uses v1-style spec.compositionRef instead of spec.crossplane.compositionRef -apiVersion: v2withv1paths.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-v2xrd-v1paths - namespace: default -spec: - # v1-style composition reference (at spec.compositionRef instead of spec.crossplane.compositionRef) - compositionRef: - name: xnopresources.v2withv1paths.diff.example.org - compositionUpdatePolicy: Automatic - coolField: modified-value diff --git a/cmd/crossplane/project/testdata/diff/resources/multistep-fatal-after-extra-res-composition.yaml b/cmd/crossplane/project/testdata/diff/resources/multistep-fatal-after-extra-res-composition.yaml deleted file mode 100644 index 55419e07..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/multistep-fatal-after-extra-res-composition.yaml +++ /dev/null @@ -1,65 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: multistep-fatal-after-extra-res.diff.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - # Step 1: guarded gotpl. Emits an ExtraResources requirement AND (once - # .extraResources is populated on the inner fetcher iteration) a composed - # resource. Stabilizes cleanly within the single render call. - - step: render-templates - functionRef: - name: function-go-templating - input: - apiVersion: gotemplating.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - --- - apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1 - kind: ExtraResources - requirements: - configmaps: - apiVersion: v1 - kind: ConfigMap - matchLabels: - app: test-app - environment: {{ .observed.composite.resource.spec.environment }} - {{- with .extraResources }} - {{ $configMaps := index . "configmaps" }} - {{- range $i, $configMap := $configMaps.items }} - --- - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ $.observed.composite.resource.metadata.name }} - namespace: {{ $.observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: {{ $.observed.composite.resource.spec.coolField }} - roleName: templated-{{ $configMap.resource.metadata.name }} - {{- end }} - {{- end }} - # Step 2: always fatals. `index .missing.path 0` — .missing is , - # indexing returns "index of untyped nil". This step does NOT emit - # requirements — so rsp.Requirements is nil for this step, but the render - # library has already accumulated step 1's requirements into its outputs. - # This produces the combination the PR's render-loop fix addresses: - # renderErr != nil AND len(output.Requirements) > 0. - - step: always-fatal - functionRef: - name: function-go-templating - input: - apiVersion: gotemplating.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - {{ index .missing.path 0 }} diff --git a/cmd/crossplane/project/testdata/diff/resources/nested/child-composition.yaml b/cmd/crossplane/project/testdata/diff/resources/nested/child-composition.yaml deleted file mode 100644 index 17ae8f7d..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/nested/child-composition.yaml +++ /dev/null @@ -1,33 +0,0 @@ -# Child composition - creates managed resources downstream -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xchildresources.nested.example.org -spec: - compositeTypeRef: - apiVersion: ns.nested.example.org/v1alpha1 - kind: XChildResource - mode: Pipeline - pipeline: - - step: generate-managed-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }}-managed - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: managed-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.childField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/nested/child-revision-templating-composition.yaml b/cmd/crossplane/project/testdata/diff/resources/nested/child-revision-templating-composition.yaml deleted file mode 100644 index 54b1faa3..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/nested/child-revision-templating-composition.yaml +++ /dev/null @@ -1,38 +0,0 @@ -# A child composition whose template reads the CompositionRevision's own name off the nested composite -# and propagates it into the composed resource — the nested counterpart of -# testdata/comp/resources/revision-templating-composition.yaml. A nested XR is rendered from its -# parent's output, which never carries compositionRevisionRef (Crossplane's composite reconciler writes -# that), so unless the render inherits the cluster copy's ref this template has nothing to read. -# Used in place of child-composition.yaml, never alongside it: both target XChildResource. -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xchildresources.nested.example.org -spec: - compositeTypeRef: - apiVersion: ns.nested.example.org/v1alpha1 - kind: XChildResource - mode: Pipeline - pipeline: - - step: generate-managed-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }}-managed - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: managed-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.crossplane.compositionRevisionRef.name }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/nested/child-xrd.yaml b/cmd/crossplane/project/testdata/diff/resources/nested/child-xrd.yaml deleted file mode 100644 index c8da4876..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/nested/child-xrd.yaml +++ /dev/null @@ -1,55 +0,0 @@ -# Child XRD - this will be created as a composed resource of the parent -# and then will itself create downstream managed resources -apiVersion: apiextensions.crossplane.io/v2 -kind: CompositeResourceDefinition -metadata: - name: xchildresources.ns.nested.example.org -spec: - group: ns.nested.example.org - names: - kind: XChildResource - plural: xchildresources - scope: Namespaced - versions: - - name: v1alpha1 - served: true - referenceable: true - schema: - openAPIV3Schema: - type: object - properties: - spec: - type: object - properties: - childField: - type: string - description: A field for the child resource - tier: - type: string - default: standard - description: Defaulted by the XRD; the parent composition never sets it - status: - type: object - properties: - conditions: - description: Conditions of the resource. - type: array - items: - type: object - required: - - lastTransitionTime - - reason - - status - - type - properties: - lastTransitionTime: - type: string - format: date-time - message: - type: string - reason: - type: string - status: - type: string - type: - type: string diff --git a/cmd/crossplane/project/testdata/diff/resources/nested/existing-child-xr-revision-ref.yaml b/cmd/crossplane/project/testdata/diff/resources/nested/existing-child-xr-revision-ref.yaml deleted file mode 100644 index 7004e4f1..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/nested/existing-child-xr-revision-ref.yaml +++ /dev/null @@ -1,18 +0,0 @@ -# existing-child-xr.yaml as Crossplane's composite reconciler leaves it once it has selected -# child-revision-templating-composition.yaml and its revision: carrying spec.crossplane.compositionRef -# and, from the harness, compositionRevisionRef. That composition reads the revision name into the -# managed resource, so a nested render that did not inherit the refs would report the managed resource -# as changing. -apiVersion: ns.nested.example.org/v1alpha1 -kind: XChildResource -metadata: - name: test-parent-child - annotations: - crossplane.io/composition-resource-name: child-xr - namespace: default -spec: - childField: "existing-value" - crossplane: - compositionRef: - name: xchildresources.nested.example.org - compositionUpdatePolicy: Automatic diff --git a/cmd/crossplane/project/testdata/diff/resources/nested/existing-child-xr-tier-other-manager.yaml b/cmd/crossplane/project/testdata/diff/resources/nested/existing-child-xr-tier-other-manager.yaml deleted file mode 100644 index 3cef855c..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/nested/existing-child-xr-tier-other-manager.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# A different field manager sets the XRD-defaulted spec.tier to a non-default value on the child XR, -# which the parent composition renders without it. -apiVersion: ns.nested.example.org/v1alpha1 -kind: XChildResource -metadata: - name: test-parent-child - namespace: default -spec: - tier: premium diff --git a/cmd/crossplane/project/testdata/diff/resources/nested/existing-child-xr.yaml b/cmd/crossplane/project/testdata/diff/resources/nested/existing-child-xr.yaml deleted file mode 100644 index fa857d35..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/nested/existing-child-xr.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# Existing child XR in the cluster (created by parent) -apiVersion: ns.nested.example.org/v1alpha1 -kind: XChildResource -metadata: - name: test-parent-child - annotations: - crossplane.io/composition-resource-name: child-xr - namespace: default -spec: - childField: "existing-value" - crossplane: - compositionRef: - name: xchildresources.nested.example.org diff --git a/cmd/crossplane/project/testdata/diff/resources/nested/existing-managed-resource-revision-ref.yaml b/cmd/crossplane/project/testdata/diff/resources/nested/existing-managed-resource-revision-ref.yaml deleted file mode 100644 index de74ec9e..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/nested/existing-managed-resource-revision-ref.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# The managed resource composed by existing-child-xr-revision-ref.yaml through -# child-revision-templating-composition.yaml, so configData holds the revision the child tracks. -# Carries crossplane.io/composition-resource-name (and, from the harness, the composite label) because -# the render adds both: a fixture missing them is reported as modified on every run, which would hide -# whether the revision name propagated. -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: test-parent-child-managed - namespace: default - annotations: - crossplane.io/composition-resource-name: managed-resource -spec: - forProvider: - configData: xchildresources.nested.example.org-96afc06 diff --git a/cmd/crossplane/project/testdata/diff/resources/nested/existing-managed-resource.yaml b/cmd/crossplane/project/testdata/diff/resources/nested/existing-managed-resource.yaml deleted file mode 100644 index 1211a7e4..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/nested/existing-managed-resource.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# Existing managed resource in the cluster (created by child XR) -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: test-parent-child-managed - namespace: default - annotations: - crossplane.io/composition-resource-name: managed-resource -spec: - forProvider: - configData: "existing-value" diff --git a/cmd/crossplane/project/testdata/diff/resources/nested/existing-parent-xr.yaml b/cmd/crossplane/project/testdata/diff/resources/nested/existing-parent-xr.yaml deleted file mode 100644 index 9ce1afe0..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/nested/existing-parent-xr.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# Existing parent XR in the cluster -apiVersion: ns.nested.example.org/v1alpha1 -kind: XParentResource -metadata: - name: test-parent - namespace: default -spec: - parentField: "existing-value" - crossplane: - compositionRef: - name: xparentresources.nested.example.org diff --git a/cmd/crossplane/project/testdata/diff/resources/nested/parent-composition.yaml b/cmd/crossplane/project/testdata/diff/resources/nested/parent-composition.yaml deleted file mode 100644 index 5a4ce994..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/nested/parent-composition.yaml +++ /dev/null @@ -1,32 +0,0 @@ -# Parent composition - creates a child XR as its composed resource -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xparentresources.nested.example.org -spec: - compositeTypeRef: - apiVersion: ns.nested.example.org/v1alpha1 - kind: XParentResource - mode: Pipeline - pipeline: - - step: generate-child-xr - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nested.example.org/v1alpha1 - kind: XChildResource - metadata: - name: {{ .observed.composite.resource.metadata.name }}-child - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: child-xr - spec: - childField: {{ .observed.composite.resource.spec.parentField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/nested/parent-xrd.yaml b/cmd/crossplane/project/testdata/diff/resources/nested/parent-xrd.yaml deleted file mode 100644 index 04de284e..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/nested/parent-xrd.yaml +++ /dev/null @@ -1,50 +0,0 @@ -# Parent XRD - this will create nested XRs as its composed resources -apiVersion: apiextensions.crossplane.io/v2 -kind: CompositeResourceDefinition -metadata: - name: xparentresources.ns.nested.example.org -spec: - group: ns.nested.example.org - names: - kind: XParentResource - plural: xparentresources - scope: Namespaced - versions: - - name: v1alpha1 - served: true - referenceable: true - schema: - openAPIV3Schema: - type: object - properties: - spec: - type: object - properties: - parentField: - type: string - description: A field that will be passed to child XR - status: - type: object - properties: - conditions: - description: Conditions of the resource. - type: array - items: - type: object - required: - - lastTransitionTime - - reason - - status - - type - properties: - lastTransitionTime: - type: string - format: date-time - message: - type: string - reason: - type: string - status: - type: string - type: - type: string diff --git a/cmd/crossplane/project/testdata/diff/resources/pinned-claim/definitions.yaml b/cmd/crossplane/project/testdata/diff/resources/pinned-claim/definitions.yaml deleted file mode 100644 index 34bff81d..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/pinned-claim/definitions.yaml +++ /dev/null @@ -1,96 +0,0 @@ -# A claim-capable XRD and a composition with two revisions, for the revision tests. Each revision's -# template writes "::" -# into the composed resource, so one value shows both which revision rendered and which ref the render -# saw. The composition itself is the newest revision (v2), as it is in a cluster, and the harness creates -# that revision from it; revision 1 is declared here. -apiVersion: apiextensions.crossplane.io/v1 -kind: CompositeResourceDefinition -metadata: - name: xpinneds.pinned.diff.example.org -spec: - group: pinned.diff.example.org - names: - kind: XPinned - plural: xpinneds - claimNames: - kind: Pinned - plural: pinneds - versions: - - name: v1alpha1 - served: true - referenceable: true - schema: - openAPIV3Schema: - type: object - properties: - spec: - type: object - properties: - coolField: - type: string ---- -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xpinneds.pinned.diff.example.org -spec: - compositeTypeRef: - apiVersion: pinned.diff.example.org/v1alpha1 - kind: XPinned - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: v2:{{ .observed.composite.resource.spec.compositionRevisionRef.name }}:{{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready ---- -apiVersion: apiextensions.crossplane.io/v1 -kind: CompositionRevision -metadata: - labels: - crossplane.io/composition-name: xpinneds.pinned.diff.example.org -spec: - revision: 1 - compositeTypeRef: - apiVersion: pinned.diff.example.org/v1alpha1 - kind: XPinned - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: v1:{{ .observed.composite.resource.spec.compositionRevisionRef.name }}:{{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-automatic-downstream.yaml b/cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-automatic-downstream.yaml deleted file mode 100644 index d0672a40..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-automatic-downstream.yaml +++ /dev/null @@ -1,10 +0,0 @@ -# The composed resource of existing-automatic-xr.yaml, as revision 2 rendered it. -apiVersion: nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: automatic-claim-a1b2c - annotations: - crossplane.io/composition-resource-name: nop-resource -spec: - forProvider: - configData: v2:xpinneds.pinned.diff.example.org-806289b:existing-value diff --git a/cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-automatic-xr.yaml b/cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-automatic-xr.yaml deleted file mode 100644 index ecd3a8a9..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-automatic-xr.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# The backing XR of existing-automatic.yaml, tracking revision 2, the latest. -apiVersion: pinned.diff.example.org/v1alpha1 -kind: XPinned -metadata: - name: automatic-claim-a1b2c -spec: - coolField: existing-value - compositionUpdatePolicy: Automatic - compositionRef: - name: xpinneds.pinned.diff.example.org - claimRef: - apiVersion: pinned.diff.example.org/v1alpha1 - kind: Pinned - name: automatic-claim - namespace: existing-namespace diff --git a/cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-automatic.yaml b/cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-automatic.yaml deleted file mode 100644 index 7d3eda85..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-automatic.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# An Automatic claim bound to a backing XR tracking revision 2, the latest: existing-automatic-xr.yaml, -# whose composed resource, as revision 2 rendered it, is existing-automatic-downstream.yaml. The claim -# carries the ref the claim syncer copies back from its XR under Automatic. -apiVersion: pinned.diff.example.org/v1alpha1 -kind: Pinned -metadata: - name: automatic-claim - namespace: existing-namespace -spec: - coolField: existing-value - compositionUpdatePolicy: Automatic - compositionRef: - name: xpinneds.pinned.diff.example.org - compositionRevisionRef: - name: xpinneds.pinned.diff.example.org-806289b - resourceRef: - apiVersion: pinned.diff.example.org/v1alpha1 - kind: XPinned - name: automatic-claim-a1b2c diff --git a/cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-lagging-automatic-downstream.yaml b/cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-lagging-automatic-downstream.yaml deleted file mode 100644 index 8fca08a8..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-lagging-automatic-downstream.yaml +++ /dev/null @@ -1,10 +0,0 @@ -# The composed resource of existing-lagging-automatic-xr.yaml, as revision 1 rendered it. -apiVersion: nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: lagging-xr - annotations: - crossplane.io/composition-resource-name: nop-resource -spec: - forProvider: - configData: v1:xpinneds.pinned.diff.example.org-3d33597:existing-value diff --git a/cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-lagging-automatic-xr.yaml b/cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-lagging-automatic-xr.yaml deleted file mode 100644 index 553bd228..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-lagging-automatic-xr.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# An Automatic XR, bound to no claim, whose compositionRevisionRef still names revision 1 although revision -# 2 is the latest. Its composed resource, as revision 1 rendered it, is -# existing-lagging-automatic-downstream.yaml. Crossplane re-points an Automatic composite at the latest -# revision on its next reconcile, so the ref lags until then: say, when the composite is diffed straight -# after its composition changed. -apiVersion: pinned.diff.example.org/v1alpha1 -kind: XPinned -metadata: - name: lagging-xr -spec: - coolField: existing-value - compositionUpdatePolicy: Automatic - compositionRef: - name: xpinneds.pinned.diff.example.org - compositionRevisionRef: - name: xpinneds.pinned.diff.example.org-3d33597 diff --git a/cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-manual-downstream.yaml b/cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-manual-downstream.yaml deleted file mode 100644 index f6d3a87d..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-manual-downstream.yaml +++ /dev/null @@ -1,10 +0,0 @@ -# The composed resource of existing-manual-xr.yaml, as revision 1 rendered it. -apiVersion: nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: manual-claim-x1y2z - annotations: - crossplane.io/composition-resource-name: nop-resource -spec: - forProvider: - configData: v1:xpinneds.pinned.diff.example.org-3d33597:existing-value diff --git a/cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-manual-xr.yaml b/cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-manual-xr.yaml deleted file mode 100644 index a5d186fa..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-manual-xr.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# The backing XR of existing-manual.yaml, pinned to revision 1. -apiVersion: pinned.diff.example.org/v1alpha1 -kind: XPinned -metadata: - name: manual-claim-x1y2z -spec: - coolField: existing-value - compositionUpdatePolicy: Manual - compositionRef: - name: xpinneds.pinned.diff.example.org - compositionRevisionRef: - name: xpinneds.pinned.diff.example.org-3d33597 - claimRef: - apiVersion: pinned.diff.example.org/v1alpha1 - kind: Pinned - name: manual-claim - namespace: existing-namespace diff --git a/cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-manual.yaml b/cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-manual.yaml deleted file mode 100644 index 8ece1692..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/pinned-claim/existing-manual.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# A Manual claim bound to a backing XR that is pinned to revision 1, the older of the composition's two -# revisions: existing-manual-xr.yaml, whose composed resource, as revision 1 rendered it, is -# existing-manual-downstream.yaml. The claim carries no compositionRevisionRef: the claim syncer copies -# the ref back to a claim only under Automatic, so a Manual claim has one only if its author set it. The -# pin therefore lives on the backing XR alone. -apiVersion: pinned.diff.example.org/v1alpha1 -kind: Pinned -metadata: - name: manual-claim - namespace: existing-namespace -spec: - coolField: existing-value - compositionUpdatePolicy: Manual - compositionRef: - name: xpinneds.pinned.diff.example.org - resourceRef: - apiVersion: pinned.diff.example.org/v1alpha1 - kind: XPinned - name: manual-claim-x1y2z diff --git a/cmd/crossplane/project/testdata/diff/resources/production-composition.yaml b/cmd/crossplane/project/testdata/diff/resources/production-composition.yaml deleted file mode 100644 index 1e0b06ba..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/production-composition.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: production-composition - labels: - provider: aws - environment: production -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-production-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: production-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - resourceTier: "production" - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/rejecting-admission-policy.yaml b/cmd/crossplane/project/testdata/diff/resources/rejecting-admission-policy.yaml deleted file mode 100644 index ff10e3eb..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/rejecting-admission-policy.yaml +++ /dev/null @@ -1,38 +0,0 @@ -# A ValidatingAdmissionPolicy that refuses XDownstreamResources carrying the configData values the -# diff fixtures render, so a dry-run against the apiserver is rejected (crossplane-diff#334). -# -# Why a VAP and not a CEL x-kubernetes-validations rule on the CRD: crossplane-diff's own schema -# validator evaluates CEL rules locally, before the diff calculator runs, so a CEL rule would reject -# the resource without the apiserver ever being consulted. The test would pass on code that never -# dry-ran anything — the exact false positive this instrument has to avoid. A VAP is invisible to the -# local validator, needs no webhook server, and has been GA since Kubernetes 1.30. -# -# CREATE covers the addition path (dry-run create); UPDATE covers the existing-resource path, where -# a server-side-apply dry-run reaches admission as an UPDATE. The same policy therefore drives the -# rejection tests for both, including the deliberate reclassification of an existing resource's -# rejection from a tool error to a validation failure. -apiVersion: admissionregistration.k8s.io/v1 -kind: ValidatingAdmissionPolicy -metadata: - name: reject-marked-downstream-resources -spec: - failurePolicy: Fail - matchConstraints: - resourceRules: - - apiGroups: ["ns.nop.example.org"] - apiVersions: ["v1alpha1"] - operations: ["CREATE", "UPDATE"] - resources: ["xdownstreamresources"] - validations: - - expression: >- - !has(object.spec) || !has(object.spec.forProvider) || !has(object.spec.forProvider.configData) - || !(object.spec.forProvider.configData in ['new-value', 'modified-value']) - message: "this cluster refuses that configData value" ---- -apiVersion: admissionregistration.k8s.io/v1 -kind: ValidatingAdmissionPolicyBinding -metadata: - name: reject-marked-downstream-resources -spec: - policyName: reject-marked-downstream-resources - validationActions: ["Deny"] diff --git a/cmd/crossplane/project/testdata/diff/resources/removal-test-cluster-composition.yaml b/cmd/crossplane/project/testdata/diff/resources/removal-test-cluster-composition.yaml deleted file mode 100644 index 766a06f8..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/removal-test-cluster-composition.yaml +++ /dev/null @@ -1,31 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: removal-test-composition -spec: - compositeTypeRef: - apiVersion: diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-remaining-resource - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: resource-to-be-kept - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: resource1 - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/removal-test-cluster-downstream-resource1.yaml b/cmd/crossplane/project/testdata/diff/resources/removal-test-cluster-downstream-resource1.yaml deleted file mode 100644 index 4d6bc8d1..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/removal-test-cluster-downstream-resource1.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - annotations: - crossplane.io/composition-resource-name: resource1 - name: resource-to-be-kept -spec: - forProvider: - configData: existing-value diff --git a/cmd/crossplane/project/testdata/diff/resources/removal-test-legacy-child-composition.yaml b/cmd/crossplane/project/testdata/diff/resources/removal-test-legacy-child-composition.yaml deleted file mode 100644 index 67995f9f..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/removal-test-legacy-child-composition.yaml +++ /dev/null @@ -1,31 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xlegacychildresources.legacycluster.diff.example.org -spec: - compositeTypeRef: - apiVersion: legacycluster.diff.example.org/v1alpha1 - kind: XLegacyChildResource - mode: Pipeline - pipeline: - - step: generate-managed-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: legacycluster.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }}-managed - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: managed-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.childField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/removal-test-legacy-child-xrd.yaml b/cmd/crossplane/project/testdata/diff/resources/removal-test-legacy-child-xrd.yaml deleted file mode 100644 index ce48cad4..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/removal-test-legacy-child-xrd.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# A legacy (v1) XR that a legacy XR composes, so a removal hierarchy at the v1 spec.resourceRefs path is -# two real XRs deep, as in a cluster: removal-test-legacy-nested-xr.yaml is one. -apiVersion: apiextensions.crossplane.io/v1 -kind: CompositeResourceDefinition -metadata: - name: xlegacychildresources.legacycluster.diff.example.org -spec: - group: legacycluster.diff.example.org - names: - kind: XLegacyChildResource - plural: xlegacychildresources - versions: - - name: v1alpha1 - served: true - referenceable: true - schema: - openAPIV3Schema: - type: object - properties: - spec: - type: object - properties: - childField: - type: string diff --git a/cmd/crossplane/project/testdata/diff/resources/removal-test-legacy-composition.yaml b/cmd/crossplane/project/testdata/diff/resources/removal-test-legacy-composition.yaml deleted file mode 100644 index e8fa8909..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/removal-test-legacy-composition.yaml +++ /dev/null @@ -1,31 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: removal-test-composition -spec: - compositeTypeRef: - apiVersion: legacycluster.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-remaining-resource - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: legacycluster.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: resource-to-be-kept - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: resource1 - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/removal-test-legacy-nested-xr-managed.yaml b/cmd/crossplane/project/testdata/diff/resources/removal-test-legacy-nested-xr-managed.yaml deleted file mode 100644 index 10d846bf..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/removal-test-legacy-nested-xr-managed.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# The managed resource removal-test-legacy-child-composition.yaml composed for -# removal-test-legacy-nested-xr.yaml. -apiVersion: legacycluster.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: resource-to-be-removed-managed - annotations: - crossplane.io/composition-resource-name: managed-resource -spec: - forProvider: - configData: child-value diff --git a/cmd/crossplane/project/testdata/diff/resources/removal-test-legacy-nested-xr.yaml b/cmd/crossplane/project/testdata/diff/resources/removal-test-legacy-nested-xr.yaml deleted file mode 100644 index e421b6f2..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/removal-test-legacy-nested-xr.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# removal-test-nested-xr.yaml at the legacy (v1) paths: the nested XR removal-test-legacy-composition -# once composed as resource2. It composes removal-test-legacy-nested-xr-managed.yaml in turn, through -# the revision the harness creates for removal-test-legacy-child-composition.yaml. -apiVersion: legacycluster.diff.example.org/v1alpha1 -kind: XLegacyChildResource -metadata: - name: resource-to-be-removed - annotations: - crossplane.io/composition-resource-name: resource2 -spec: - childField: child-value - compositionRef: - name: xlegacychildresources.legacycluster.diff.example.org diff --git a/cmd/crossplane/project/testdata/diff/resources/removal-test-legacycluster-downstream-resource1.yaml b/cmd/crossplane/project/testdata/diff/resources/removal-test-legacycluster-downstream-resource1.yaml deleted file mode 100644 index e03c4398..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/removal-test-legacycluster-downstream-resource1.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: legacycluster.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - annotations: - crossplane.io/composition-resource-name: resource1 - name: resource-to-be-kept -spec: - forProvider: - configData: existing-value diff --git a/cmd/crossplane/project/testdata/diff/resources/removal-test-nested-xr-managed.yaml b/cmd/crossplane/project/testdata/diff/resources/removal-test-nested-xr-managed.yaml deleted file mode 100644 index 0bde272b..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/removal-test-nested-xr-managed.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# The managed resource nested/child-composition.yaml composed for removal-test-nested-xr.yaml. -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: resource-to-be-removed-managed - namespace: default - annotations: - crossplane.io/composition-resource-name: managed-resource -spec: - forProvider: - configData: child-value diff --git a/cmd/crossplane/project/testdata/diff/resources/removal-test-nested-xr.yaml b/cmd/crossplane/project/testdata/diff/resources/removal-test-nested-xr.yaml deleted file mode 100644 index 6ff9ea7b..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/removal-test-nested-xr.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# The nested XR the removal-test compositions once composed as resource2, as Crossplane left it, with -# the composition of its own kind it was composed with. It composes removal-test-nested-xr-managed.yaml in -# turn, so removing it removes that too. -apiVersion: ns.nested.example.org/v1alpha1 -kind: XChildResource -metadata: - name: resource-to-be-removed - namespace: default - annotations: - crossplane.io/composition-resource-name: resource2 -spec: - childField: child-value - crossplane: - compositionRef: - name: xchildresources.nested.example.org diff --git a/cmd/crossplane/project/testdata/diff/resources/rendered-defaults/definitions.yaml b/cmd/crossplane/project/testdata/diff/resources/rendered-defaults/definitions.yaml deleted file mode 100644 index 5976be20..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/rendered-defaults/definitions.yaml +++ /dev/null @@ -1,129 +0,0 @@ -# XRDs whose defaults a composition reads, one modern (v2, machinery under spec.crossplane) and one -# legacy (v1, machinery directly under spec), each with a composition that writes the composite's -# defaulted fields into a composed resource. The CRDs are generated from these XRDs (upstream xcrd), so -# the XR CRDs default region from the XRD's schema and, for the legacy XR, spec.compositionUpdatePolicy -# from defaultCompositionUpdatePolicy, as in a real cluster. A render input that was not defaulted would -# write "" instead. -# -# The modern template does not read spec.crossplane.compositionUpdatePolicy: its CRD defaults the -# policy only inside an existing spec.crossplane, which a new XR's manifest does not have, so the tool -# renders a new modern XR without one. (In a cluster Crossplane writes spec.crossplane before it first -# composes, so the apiserver defaults the policy by then.) -apiVersion: apiextensions.crossplane.io/v2 -kind: CompositeResourceDefinition -metadata: - name: xregionaldatabases.rendered-defaults.diff.example.org -spec: - group: rendered-defaults.diff.example.org - names: - kind: XRegionalDatabase - plural: xregionaldatabases - scope: Namespaced - defaultCompositionUpdatePolicy: Automatic - versions: - - name: v1alpha1 - served: true - referenceable: true - schema: - openAPIV3Schema: - type: object - properties: - spec: - type: object - properties: - size: - type: string - region: - type: string - default: us-east-1 ---- -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xregionaldatabases.rendered-defaults.diff.example.org -spec: - compositeTypeRef: - apiVersion: rendered-defaults.diff.example.org/v1alpha1 - kind: XRegionalDatabase - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: database - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.region }}/{{ .observed.composite.resource.spec.size }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready ---- -apiVersion: apiextensions.crossplane.io/v1 -kind: CompositeResourceDefinition -metadata: - name: xlegacyregionaldatabases.rendered-defaults.diff.example.org -spec: - group: rendered-defaults.diff.example.org - names: - kind: XLegacyRegionalDatabase - plural: xlegacyregionaldatabases - versions: - - name: v1alpha1 - served: true - referenceable: true - schema: - openAPIV3Schema: - type: object - properties: - spec: - type: object - properties: - size: - type: string - region: - type: string - default: eu-west-1 ---- -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xlegacyregionaldatabases.rendered-defaults.diff.example.org -spec: - compositeTypeRef: - apiVersion: rendered-defaults.diff.example.org/v1alpha1 - kind: XLegacyRegionalDatabase - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: database - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.region }}/{{ .observed.composite.resource.spec.size }}/{{ .observed.composite.resource.spec.compositionUpdatePolicy }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/selector-bound/existing-downstream.yaml b/cmd/crossplane/project/testdata/diff/resources/selector-bound/existing-downstream.yaml deleted file mode 100644 index 8e54b1b0..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/selector-bound/existing-downstream.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# The composed resource of existing-xr.yaml, as composition selector-bound-a rendered it. -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - name: selector-bound - namespace: default - annotations: - crossplane.io/composition-resource-name: nop-resource -spec: - forProvider: - configData: a-existing-value diff --git a/cmd/crossplane/project/testdata/diff/resources/selector-bound/existing-xr.yaml b/cmd/crossplane/project/testdata/diff/resources/selector-bound/existing-xr.yaml deleted file mode 100644 index 9711b44c..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/selector-bound/existing-xr.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# A composite whose compositionSelector matches both compositions in setup.yaml, and which Crossplane -# bound to the first, selector-bound-a. -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: selector-bound - namespace: default -spec: - coolField: existing-value - crossplane: - compositionSelector: - matchLabels: - binding: shared - compositionRef: - name: selector-bound-a - compositionUpdatePolicy: Automatic diff --git a/cmd/crossplane/project/testdata/diff/resources/selector-bound/setup.yaml b/cmd/crossplane/project/testdata/diff/resources/selector-bound/setup.yaml deleted file mode 100644 index e33e1e16..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/selector-bound/setup.yaml +++ /dev/null @@ -1,74 +0,0 @@ -# Two compositions that both match one compositionSelector. existing-xr.yaml is a composite Crossplane -# has already bound to the first, and existing-downstream.yaml its composed resource. Crossplane selects -# a composition once, when the composite has no compositionRef, and never re-selects -# (composite.APILabelSelectorResolver), so the composite stays on composition A however many -# compositions its selector matches. -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: selector-bound-a - labels: - binding: shared -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: a-{{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready ---- -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: selector-bound-b - labels: - binding: shared -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: b-{{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/sequencer-composition.yaml b/cmd/crossplane/project/testdata/diff/resources/sequencer-composition.yaml deleted file mode 100644 index 3a56fdf9..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/sequencer-composition.yaml +++ /dev/null @@ -1,53 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: sequencer-composition -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-all-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: 0-stage0-resource - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: stage0-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - --- - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: 1-stage1-resource - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: stage1-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready - - step: sequence-resources - functionRef: - name: function-sequencer - input: - apiVersion: sequencer.fn.crossplane.io/v1beta1 - kind: Input - rules: - - sequence: - - ^stage0- # Stage 0 (composition-resource-name starts with stage0-) - - ^stage1- # Stage 1 (hidden until stage 0 is ready, unless it already exists) diff --git a/cmd/crossplane/project/testdata/diff/resources/sequencer-stage0-downstream.yaml b/cmd/crossplane/project/testdata/diff/resources/sequencer-stage0-downstream.yaml deleted file mode 100644 index fa1608b9..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/sequencer-stage0-downstream.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - annotations: - crossplane.io/composition-resource-name: stage0-resource - name: 0-stage0-resource - namespace: default -spec: - forProvider: - configData: test-value diff --git a/cmd/crossplane/project/testdata/diff/resources/sequencer-stage1-downstream.yaml b/cmd/crossplane/project/testdata/diff/resources/sequencer-stage1-downstream.yaml deleted file mode 100644 index 23e1180e..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/sequencer-stage1-downstream.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: ns.nop.example.org/v1alpha1 -kind: XDownstreamResource -metadata: - annotations: - crossplane.io/composition-resource-name: stage1-resource - name: 1-stage1-resource - namespace: default -spec: - forProvider: - configData: test-value diff --git a/cmd/crossplane/project/testdata/diff/resources/sequencer-with-envconfig-composition.yaml b/cmd/crossplane/project/testdata/diff/resources/sequencer-with-envconfig-composition.yaml deleted file mode 100644 index 706f8194..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/sequencer-with-envconfig-composition.yaml +++ /dev/null @@ -1,67 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: sequencer-with-envconfig-composition -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: get-environment-configs - functionRef: - name: function-environment-configs - input: - apiVersion: environmentconfigs.fn.crossplane.io/v1beta1 - kind: Input - spec: - environmentConfigs: - - type: Reference - ref: - name: test-env-config - - step: generate-all-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - {{ $envConfig := index .context "apiextensions.crossplane.io/environment" }} - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: 0-stage0-resource - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: stage0-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - resourceTier: {{ $envConfig.environment }} - --- - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: 1-stage1-resource - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: stage1-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - resourceTier: {{ $envConfig.environment }} - - step: sequence-resources - functionRef: - name: function-sequencer - input: - apiVersion: sequencer.fn.crossplane.io/v1beta1 - kind: Input - rules: - - sequence: - - ^0- # Stage 0 resources (rendered first) - - ^1- # Stage 1 resources (hidden until stage 0 is ready) - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/sequencer-with-envconfig-xr.yaml b/cmd/crossplane/project/testdata/diff/resources/sequencer-with-envconfig-xr.yaml deleted file mode 100644 index 52c724cd..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/sequencer-with-envconfig-xr.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: sequencer-envconfig-test - namespace: default -spec: - crossplane: - compositionRef: - name: sequencer-with-envconfig-composition - coolField: "test-value" diff --git a/cmd/crossplane/project/testdata/diff/resources/sequencer-xr.yaml b/cmd/crossplane/project/testdata/diff/resources/sequencer-xr.yaml deleted file mode 100644 index de0f52ec..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/sequencer-xr.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: sequencer-test - namespace: default -spec: - coolField: "test-value" diff --git a/cmd/crossplane/project/testdata/diff/resources/staging-composition.yaml b/cmd/crossplane/project/testdata/diff/resources/staging-composition.yaml deleted file mode 100644 index 5cf736f9..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/staging-composition.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: staging-composition - labels: - provider: aws - environment: staging -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-staging-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: staging-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - resourceTier: "staging" - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/unestablished-xrd.yaml b/cmd/crossplane/project/testdata/diff/resources/unestablished-xrd.yaml deleted file mode 100644 index 286e15ee..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/unestablished-xrd.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# An XRD unrelated to any input, applied with no generated CRD: one Crossplane has not established yet, -# so the apiserver serves neither its XR type nor its claim type. -apiVersion: apiextensions.crossplane.io/v2 -kind: CompositeResourceDefinition -metadata: - name: xunestablishedresources.unrelated.example.org -spec: - group: unrelated.example.org - names: - kind: XUnestablishedResource - plural: xunestablishedresources - scope: Namespaced - versions: - - name: v1alpha1 - served: true - referenceable: true - schema: - openAPIV3Schema: - type: object - properties: - spec: - type: object - properties: - coolField: - type: string diff --git a/cmd/crossplane/project/testdata/diff/resources/unguarded-external-res-gotpl-composition.yaml b/cmd/crossplane/project/testdata/diff/resources/unguarded-external-res-gotpl-composition.yaml deleted file mode 100644 index 26561167..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/unguarded-external-res-gotpl-composition.yaml +++ /dev/null @@ -1,44 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: unguarded-templated-extra-resources.diff.example.org -spec: - compositeTypeRef: - apiVersion: ns.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: render-templates - functionRef: - name: function-go-templating - input: - apiVersion: gotemplating.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - --- - apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1 - kind: ExtraResources - requirements: - configmaps: - apiVersion: v1 - kind: ConfigMap - matchLabels: - app: test-app - environment: {{ .observed.composite.resource.spec.environment }} - {{- $configMaps := index .extraResources "configmaps" }} - {{- range $i, $configMap := $configMaps.items }} - --- - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ $.observed.composite.resource.metadata.name }} - namespace: {{ $.observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: {{ $.observed.composite.resource.spec.coolField }} - roleName: templated-{{ $configMap.resource.metadata.name }} - {{- end }} diff --git a/cmd/crossplane/project/testdata/diff/resources/v2-xrd-with-v1-paths-composition.yaml b/cmd/crossplane/project/testdata/diff/resources/v2-xrd-with-v1-paths-composition.yaml deleted file mode 100644 index 380d66c5..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/v2-xrd-with-v1-paths-composition.yaml +++ /dev/null @@ -1,32 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: xnopresources.v2withv1paths.diff.example.org -spec: - compositeTypeRef: - apiVersion: v2withv1paths.diff.example.org/v1alpha1 - kind: XNopResource - mode: Pipeline - pipeline: - - step: generate-resources - functionRef: - name: function-go-templating - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: GoTemplate - source: Inline - inline: - template: | - apiVersion: ns.nop.example.org/v1alpha1 - kind: XDownstreamResource - metadata: - name: {{ .observed.composite.resource.metadata.name }} - namespace: {{ .observed.composite.resource.metadata.namespace }} - annotations: - gotemplating.fn.crossplane.io/composition-resource-name: nop-resource - spec: - forProvider: - configData: {{ .observed.composite.resource.spec.coolField }} - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/cmd/crossplane/project/testdata/diff/resources/v2-xrd-with-v1-paths.yaml b/cmd/crossplane/project/testdata/diff/resources/v2-xrd-with-v1-paths.yaml deleted file mode 100644 index e150c8a9..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/v2-xrd-with-v1-paths.yaml +++ /dev/null @@ -1,101 +0,0 @@ -# This is a v2 XRD that explicitly supports both v1-style and v2-style composition paths. -# Used to test issue #206: v2 XRs using v1-style composition paths are not recognized. -apiVersion: apiextensions.crossplane.io/v2 -kind: CompositeResourceDefinition -metadata: - name: xnopresources.v2withv1paths.diff.example.org -spec: - group: v2withv1paths.diff.example.org - names: - kind: XNopResource - plural: xnopresources - scope: Namespaced - versions: - - name: v1alpha1 - served: true - referenceable: true - schema: - openAPIV3Schema: - type: object - properties: - spec: - type: object - properties: - coolField: - type: string - environment: - type: string - # v1-style composition fields (for backwards compatibility) - compositionRef: - type: object - properties: - name: - type: string - compositionRevisionRef: - type: object - properties: - name: - type: string - compositionUpdatePolicy: - type: string - enum: - - Automatic - - Manual - compositionSelector: - type: object - properties: - matchLabels: - type: object - additionalProperties: - type: string - # v2-style composition fields (nested under crossplane) - crossplane: - type: object - properties: - compositionRef: - type: object - properties: - name: - type: string - compositionRevisionRef: - type: object - properties: - name: - type: string - compositionUpdatePolicy: - type: string - enum: - - Automatic - - Manual - compositionSelector: - type: object - properties: - matchLabels: - type: object - additionalProperties: - type: string - status: - type: object - properties: - conditions: - description: Conditions of the resource. - type: array - items: - type: object - required: - - lastTransitionTime - - reason - - status - - type - properties: - lastTransitionTime: - type: string - format: date-time - message: - type: string - reason: - type: string - status: - type: string - type: - type: string diff --git a/cmd/crossplane/project/testdata/diff/resources/xdownstreamenvresource-xrd.yaml b/cmd/crossplane/project/testdata/diff/resources/xdownstreamenvresource-xrd.yaml deleted file mode 100644 index 5e2954ba..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/xdownstreamenvresource-xrd.yaml +++ /dev/null @@ -1,31 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: CompositeResourceDefinition -metadata: - name: xdownstreamenvresources.nop.example.org -spec: - group: nop.example.org - names: - kind: XDownstreamEnvResource - plural: xdownstreamenvresources - versions: - - name: v1alpha1 - served: true - referenceable: true - schema: - openAPIV3Schema: - type: object - properties: - spec: - type: object - properties: - forProvider: - type: object - properties: - configData: - type: string - region: - type: string - environment: - type: string - serviceLevel: - type: string diff --git a/cmd/crossplane/project/testdata/diff/resources/xrd-with-defaults.yaml b/cmd/crossplane/project/testdata/diff/resources/xrd-with-defaults.yaml deleted file mode 100644 index bbd6eb8e..00000000 --- a/cmd/crossplane/project/testdata/diff/resources/xrd-with-defaults.yaml +++ /dev/null @@ -1,79 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v2 -kind: CompositeResourceDefinition -metadata: - name: xtestdefaultresources.ns.diff.example.org -spec: - group: ns.diff.example.org - names: - kind: XTestDefaultResource - plural: xtestdefaultresources - scope: Namespaced - versions: - - name: v1alpha1 - served: true - referenceable: true - schema: - openAPIV3Schema: - type: object - properties: - spec: - type: object - properties: - region: - type: string - default: "us-east-1" - description: "AWS region with default value" - size: - type: string - default: "small" - description: "Instance size with default" - settings: - type: object - default: - enabled: true - timeout: 30 - properties: - enabled: - type: boolean - default: true - timeout: - type: integer - default: 30 - retries: - type: integer - default: 3 - tags: - type: object - default: - environment: "development" - properties: - environment: - type: string - default: "development" - team: - type: string - status: - type: object - properties: - conditions: - description: Conditions of the resource. - type: array - items: - type: object - required: - - lastTransitionTime - - reason - - status - - type - properties: - lastTransitionTime: - type: string - format: date-time - message: - type: string - reason: - type: string - status: - type: string - type: - type: string \ No newline at end of file diff --git a/cmd/crossplane/project/testdata/diff/unchanged-automatic-pinned-claim.yaml b/cmd/crossplane/project/testdata/diff/unchanged-automatic-pinned-claim.yaml deleted file mode 100644 index c22ad2e2..00000000 --- a/cmd/crossplane/project/testdata/diff/unchanged-automatic-pinned-claim.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# resources/pinned-claim/existing-automatic.yaml's claim as its author keeps it: unchanged, but without -# the compositionRevisionRef the claim syncer copied onto the cluster copy (issue #498). -apiVersion: pinned.diff.example.org/v1alpha1 -kind: Pinned -metadata: - name: automatic-claim - namespace: existing-namespace -spec: - coolField: existing-value - compositionUpdatePolicy: Automatic - compositionRef: - name: xpinneds.pinned.diff.example.org diff --git a/cmd/crossplane/project/testdata/diff/unchanged-lagging-automatic-xr.yaml b/cmd/crossplane/project/testdata/diff/unchanged-lagging-automatic-xr.yaml deleted file mode 100644 index 10905951..00000000 --- a/cmd/crossplane/project/testdata/diff/unchanged-lagging-automatic-xr.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# resources/pinned-claim/existing-lagging-automatic-xr.yaml's XR as its author keeps it: unchanged, and -# without the compositionRevisionRef Crossplane's composite reconciler writes. -apiVersion: pinned.diff.example.org/v1alpha1 -kind: XPinned -metadata: - name: lagging-xr -spec: - coolField: existing-value - compositionUpdatePolicy: Automatic - compositionRef: - name: xpinneds.pinned.diff.example.org diff --git a/cmd/crossplane/project/testdata/diff/xr-with-ambiguous-selector.yaml b/cmd/crossplane/project/testdata/diff/xr-with-ambiguous-selector.yaml deleted file mode 100644 index 86c1d6c4..00000000 --- a/cmd/crossplane/project/testdata/diff/xr-with-ambiguous-selector.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-resource - namespace: default -spec: - coolField: "test-value" - crossplane: - compositionSelector: - matchLabels: - provider: aws diff --git a/cmd/crossplane/project/testdata/diff/xr-with-argocd-annotations.yaml b/cmd/crossplane/project/testdata/diff/xr-with-argocd-annotations.yaml deleted file mode 100644 index f9dcd869..00000000 --- a/cmd/crossplane/project/testdata/diff/xr-with-argocd-annotations.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-resource - namespace: default - annotations: - argocd.argoproj.io/tracking-id: "test-app:/XNopResource:default/test-resource" - kubectl.kubernetes.io/last-applied-configuration: | - {"apiVersion":"ns.diff.example.org/v1alpha1","kind":"XNopResource","metadata":{"name":"test-resource","namespace":"default"}} - labels: - argocd.argoproj.io/instance: test-app -spec: - coolField: "new-value" diff --git a/cmd/crossplane/project/testdata/diff/xr-with-argocd-mixed-changes.yaml b/cmd/crossplane/project/testdata/diff/xr-with-argocd-mixed-changes.yaml deleted file mode 100644 index 3a399e9f..00000000 --- a/cmd/crossplane/project/testdata/diff/xr-with-argocd-mixed-changes.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-resource - namespace: default - annotations: - argocd.argoproj.io/tracking-id: "test-app:/XNopResource:default/test-resource" - kubectl.kubernetes.io/last-applied-configuration: | - {"apiVersion":"ns.diff.example.org/v1alpha1","kind":"XNopResource","metadata":{"name":"test-resource","namespace":"default"}} - labels: - argocd.argoproj.io/instance: test-app -spec: - coolField: "modified-value" diff --git a/cmd/crossplane/project/testdata/diff/xr-with-composition-ref.yaml b/cmd/crossplane/project/testdata/diff/xr-with-composition-ref.yaml deleted file mode 100644 index e417e325..00000000 --- a/cmd/crossplane/project/testdata/diff/xr-with-composition-ref.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-resource - namespace: default -spec: - coolField: "test-value" - crossplane: - compositionRef: - name: production-composition diff --git a/cmd/crossplane/project/testdata/diff/xr-with-composition-selector.yaml b/cmd/crossplane/project/testdata/diff/xr-with-composition-selector.yaml deleted file mode 100644 index bf5bdc13..00000000 --- a/cmd/crossplane/project/testdata/diff/xr-with-composition-selector.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XNopResource -metadata: - name: test-resource - namespace: default -spec: - coolField: "test-value" - crossplane: - compositionSelector: - matchLabels: - environment: staging - provider: aws diff --git a/cmd/crossplane/project/testdata/diff/xr-with-missing-defaults.yaml b/cmd/crossplane/project/testdata/diff/xr-with-missing-defaults.yaml deleted file mode 100644 index d1d9007d..00000000 --- a/cmd/crossplane/project/testdata/diff/xr-with-missing-defaults.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XTestDefaultResource -metadata: - name: test-resource-with-defaults - namespace: default -spec: - # Only specify one field, leaving others to use defaults from XRD - size: "large" - # Note: region, settings, and tags are not specified and should get defaults \ No newline at end of file diff --git a/cmd/crossplane/project/testdata/diff/xr-with-overridden-defaults.yaml b/cmd/crossplane/project/testdata/diff/xr-with-overridden-defaults.yaml deleted file mode 100644 index 53f5dfc2..00000000 --- a/cmd/crossplane/project/testdata/diff/xr-with-overridden-defaults.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: ns.diff.example.org/v1alpha1 -kind: XTestDefaultResource -metadata: - name: test-resource-with-overrides - namespace: default -spec: - # Override the default region (default: us-east-1) - region: "us-west-2" - # Override the default size (default: small) - size: "xlarge" - # Partially override settings (default: {enabled: true, timeout: 30}) - settings: - enabled: false # Override default true - timeout: 60 # Override default 30 - # retries should get default value of 3 - # Override default tags (default: {environment: "development"}) - tags: - environment: "production" # Override default "development" - team: "platform" # New field, no default to override \ No newline at end of file diff --git a/internal/testutils/integration/simharness/cases.go b/internal/testutils/integration/simharness/cases.go new file mode 100644 index 00000000..1718c570 --- /dev/null +++ b/internal/testutils/integration/simharness/cases.go @@ -0,0 +1,397 @@ +//go:build integration + +/* +Copyright 2026 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package simharness + +import ( + "bytes" + "fmt" + "strconv" + "strings" + "testing" + "time" + + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/client-go/rest" + + tu "github.com/crossplane/cli/v2/internal/diff/testutils" + "github.com/crossplane/cli/v2/internal/simulate/simtest" + "github.com/crossplane/cli/v2/internal/testutils/integration" +) + +// TestdataDir is the shared testdata, relative to a suite's package directory under cmd/crossplane. +const TestdataDir = "../../../internal/testutils/integration/testdata" + +// TestType is the command a case runs against: its testdata subdirectory, and the simulate command its +// crossplane-diff args translate to. +type TestType string + +// The test types: XRTest runs `xr simulate`, CompositionTest `composition simulate`. +const ( + XRTest TestType = "diff" + CompositionTest TestType = "comp" +) + +// IntegrationTestCase represents a common test case structure for both XR and composition diff tests. +type IntegrationTestCase struct { + Reason string // Description of what this test validates + SetupFiles []string + DeleteAfterSetup []string // Files whose resources are deleted after setup; with a finalizer this leaves them Terminating + // crossplaneManagedResources are the XRs that exist in the cluster, with what they compose. Every + // composed resource is server-side applied under Crossplane's composed field manager and given a + // controller ownerReference to its XR's real UID, and each XR lists its composed resources in its + // resourceRefs, as Crossplane leaves them. Use integration.Composes() for an XR without nested XRs. Put a + // composed resource in setupFiles instead only when it deliberately has no controlling XR. + CrossplaneManagedResources []integration.HierarchicalOwnershipRelation + // fieldManagerApplies are server-side applied, in order, after setupFiles and + // crossplaneManagedResources, each under its own + // field manager. Use it when a test depends on which manager owns a field: a plain Create + // records only the test client as owner, and the apiserver drops managedFields supplied on Create. + FieldManagerApplies []integration.FieldManagerApply + // unestablishedXRDs are XRD manifests applied after setupFiles but, unlike an XRD in setupFiles, given + // no generated CRDs: an XRD Crossplane has not established yet, so its XR and claim types are not served. + UnestablishedXRDs []string + // rbacRules, when set, makes the case run the command twice: as the cluster admin, then as a user + // bound to a ClusterRole granting only these rules (plus the discovery and self-review access the + // apiserver's bootstrap RBAC grants every authenticated user). The two runs must print identical + // output, and the usual assertions apply to the second. + RbacRules []rbacv1.PolicyRule + InputFiles []string // Input files to diff (XR YAML files or Composition YAML files) + ExpectedOutput string + ExpectedError bool + ExpectedErrorContains string + ExpectedStderrContains []string // substrings that must appear on stderr (warnings, error lines) + ExpectedExitCode int // Expected exit code (0=success, 1=tool error, 2=schema validation, 3=diff detected) + NoColor bool + Namespace string // For composition tests (optional) + XrdAPIVersion integration.XRDAPIVersion // For XR tests (optional) + IgnorePaths []string // Paths to ignore in diffs + FunctionCredentials string // Path to function credentials file (optional) + EventualState bool // Enable eventual state simulation for XR or composition tests (optional) + Timeout time.Duration // Custom timeout for this test (0 = use default) + Resources []string // For composition tests: --resource values; each entry passed as one --resource flag + ResourcesCSV string // For composition tests: alternative single --resource=a,b style invocation + IncludeManual bool // For composition tests: pass --include-manual flag + AnalyzeUnchanged bool // For composition tests: pass the deprecated --analyze-unchanged flag + AnalyzeOn string // For composition tests: pass --analyze-on= (empty = rely on the default) + // dryRunOn passes --dry-run-on= (empty = rely on the default, which is "all"). + // The flag lives on CommonCmdFields, so it applies to both `xr` and `comp`. + DryRunOn string + // awaitAdmissionDenialFor names a manifest whose resources a correctly-installed + // ValidatingAdmissionPolicy must refuse. Before running the diff, the harness dry-run + // creates each of them until the apiserver rejects it. + // + // This is not belt-and-braces: the apiserver loads admission policies through an informer, + // so a diff issued immediately after the policy is created races the plugin and observes a + // cluster that accepts everything. Measured on envtest 1.32, the policy took ~600ms and four + // attempts to take effect — long enough that without this gate the test would pass or fail + // depending on machine load, and a "pass" would prove nothing. + AwaitAdmissionDenialFor string + // assertDeterministicAcrossRuns runs the command twice against the same cluster and requires + // byte-identical stdout, then applies the usual assertions to the second run. Needed for + // server-generated identity: the apiserver runs names.Generator before the dry-run + // short-circuit, so a generateName addition whose identity was not restored would emit a + // different random name on each run — which a single-run test cannot see. + AssertDeterministicAcrossRuns bool + Skip bool + SkipReason string + // JSON output support: set outputFormat to "json" to use structured assertions. + OutputFormat string // "json" or "" (default=visual diff) + // expectedStructured is asserted against the JSON output in either mode. It is typed as the + // Expectation interface, which every builder level implements, so a chain can end where it ends. + ExpectedStructured simtest.Expectation +} + +// RunIntegrationTest runs a single integration test case for both XR and composition diff commands. +// +//nolint:gocognit // One flow every case funnels through; splitting it would scatter the case semantics. +func RunIntegrationTest(t *testing.T, testType TestType, tt IntegrationTestCase) { + t.Helper() + + t.Parallel() // Enable parallel test execution + + // Resolve a local crossplane binary if one is present. When non-empty, + // it's threaded into the kong arg slice below as + // --crossplane-render-binary= so each parallel subtest has its own + // copy with no shared process state. When empty, the diff command falls + // through to the docker render engine — slower but works wherever Docker + // does (including the Earthfile's go-test target's WITH DOCKER block). + crossplaneBin := LocalCrossplaneBinary(t) + + // Skip test if requested + if tt.Skip { + t.Skip(tt.SkipReason) + return + } + + // Use custom timeout if specified, otherwise use default + testTimeout := DefaultTimeout + if tt.Timeout > 0 { + testTimeout = tt.Timeout + } + + // Set up the test files + testFiles := make([]string, 0, len(tt.InputFiles)) + + // Handle any additional input files + // Note: NewCompositeLoader handles both individual files and directories, + // so we can pass paths directly without special handling + testFiles = append(testFiles, tt.InputFiles...) + + // Capture stdout and stderr into separate buffers. The diff command + // writes structured output (JSON / YAML / text diff) to stdout and + // human-readable error lines to stderr; mixing them defeats JSON + // parsing for tests that drive the structured assertion path. + var stdout, stderr bytes.Buffer + + // Create command line args that match your pre-populated struct + args := []string{ + fmt.Sprintf("--timeout=%s", testTimeout.String()), + } + + // Only thread --crossplane-render-binary when a local binary exists; an + // empty value would still parse but waste cycles, and a missing flag + // lets the diff command pick the docker render engine cleanly. + if crossplaneBin != "" { + args = append(args, fmt.Sprintf("--crossplane-render-binary=%s", crossplaneBin)) + } + + // Add namespace if specified (for composition tests only) + if tt.Namespace != "" && testType == CompositionTest { + args = append(args, fmt.Sprintf("--namespace=%s", tt.Namespace)) + } + + // Add no-color flag if true + if tt.NoColor { + args = append(args, "--no-color") + } + + // Add JSON output format if specified + if tt.OutputFormat == "json" { + args = append(args, "--output=json") + } + + // Add ignore-paths if specified + if len(tt.IgnorePaths) > 0 { + for _, path := range tt.IgnorePaths { + args = append(args, fmt.Sprintf("--ignore-paths=%s", path)) + } + } + + // Add function-credentials if specified + if tt.FunctionCredentials != "" { + args = append(args, fmt.Sprintf("--function-credentials=%s", tt.FunctionCredentials)) + } + + // Add eventual-state flag if specified (supported by both `xr` and `comp`). + if tt.EventualState { + args = append(args, "--eventual-state") + } + + // --dry-run-on lives on CommonCmdFields, so unlike --analyze-on it is threaded for both + // command types. An empty value relies on the flag's own default. + if tt.DryRunOn != "" { + args = append(args, "--dry-run-on="+tt.DryRunOn) + } + + // Add --resource flags (composition tests only) + if testType == CompositionTest { + for _, r := range tt.Resources { + args = append(args, fmt.Sprintf("--resource=%s", r)) + } + + if tt.ResourcesCSV != "" { + args = append(args, fmt.Sprintf("--resource=%s", tt.ResourcesCSV)) + } + + if tt.IncludeManual { + args = append(args, "--include-manual") + } + + if tt.AnalyzeUnchanged { + args = append(args, "--analyze-unchanged") + } + + if tt.AnalyzeOn != "" { + args = append(args, "--analyze-on="+tt.AnalyzeOn) + } + } + + // Add files as positional arguments + args = append(args, testFiles...) + + // The render engine runs functions as Docker containers. Without a Docker daemon no rendering + // case can run; cases that would not render anyway cannot be told apart upfront, so they skip. + if !DockerAvailable() { + t.Skip("env: no Docker daemon for the function render engine") + return + } + + // Start fails, rather than skips, a run without envtest's binaries or Crossplane's CRDs. + cluster := integration.Start(t, integration.Setup{ + CRDDirs: []string{TestdataDir + "/" + string(testType) + "/crds"}, + SetupFiles: tt.SetupFiles, + UnestablishedXRDs: tt.UnestablishedXRDs, + CrossplaneManagedResources: tt.CrossplaneManagedResources, + XRDAPIVersion: tt.XrdAPIVersion, + FieldManagerApplies: tt.FieldManagerApplies, + DeleteAfterSetup: tt.DeleteAfterSetup, + RBACRules: tt.RbacRules, + }) + + // Block until any ValidatingAdmissionPolicy in the setup is actually enforcing, so the diff + // cannot race the apiserver's policy informer. See the field's doc comment. + if err := integration.AwaitAdmissionDenial(cluster.Context, cluster.Client, tt.AwaitAdmissionDenialFor); err != nil { + t.Fatalf("admission policy never took effect: %v", err) + } + + // runOnce executes the command against the running cluster, leaving its output in stdout / + // stderr. Everything the command layer touches is rebuilt per call, so a second run starts + // with cold client caches just as a second invocation of the CLI would. + runOnce := func(runCfg *rest.Config) (int, error) { + stdout.Reset() + stderr.Reset() + + return runSimulate(t, runCfg, testType, args, &stdout, &stderr) + } + + exitCode, err := runOnce(cluster.Config) + + if tt.AssertDeterministicAcrossRuns { + first := stdout.String() + + exitCode, err = runOnce(cluster.Config) + + if second := stdout.String(); second != first { + t.Errorf("output differs between two runs against the same cluster; a value the apiserver generated has leaked into the diff.\nfirst run:\n%s\nsecond run:\n%s", first, second) + } + } + + if cluster.UserConfig != nil { + admin, adminErr := stdout.String(), err + + exitCode, err = runOnce(cluster.UserConfig) + + if restricted := stdout.String(); restricted != admin { + t.Errorf("output with only the case's RBAC rules differs from the admin's; the run needs access the rules don't grant.\nadmin (error: %v):\n%s\nrestricted (error: %v, stderr below):\n%s\n%s", + adminErr, admin, err, restricted, stderr.String()) + } + } + + // Check exit code matches expected + if exitCode != tt.ExpectedExitCode { + t.Errorf("expected exit code %d, got %d", tt.ExpectedExitCode, exitCode) + } + + // Assert stderr expectations before any of the early returns below, so they apply in every + // output mode (human-readable, JSON, and expected-error cases alike). + for _, want := range tt.ExpectedStderrContains { + if !strings.Contains(stderr.String(), want) { + t.Errorf("expected stderr to contain %q, got:\n%s", want, stderr.String()) + } + } + + if tt.ExpectedError && err == nil { + t.Fatal("expected error but got none") + } + + if !tt.ExpectedError && err != nil { + t.Fatalf("expected no error but got: %v", err) + } + + // Check for specific error message if expected. + // + // Three modes: + // 1. expectedError + expectedErrorContains: substring-check the error. + // 2. expectedError + structured assertion: any error is fine here; + // the structured assertion below validates errors[]. + // 3. unexpected error: already caught by the t.Fatalf above. + if err != nil && tt.ExpectedError { + hasStructuredAssertion := tt.ExpectedStructured != nil + + switch { + case tt.ExpectedErrorContains != "": + if strings.Contains(err.Error(), tt.ExpectedErrorContains) { + t.Logf("Got expected error containing: %s", tt.ExpectedErrorContains) + } else { + t.Errorf("Expected error containing %q, got: %v", tt.ExpectedErrorContains, err) + } + case hasStructuredAssertion: + t.Logf("Got expected error; structured assertion will validate errors[]") + default: + t.Errorf("expectedError set without expectedErrorContains or structured assertion; got: %v", err) + } + } + + // Skip stdout check for expected-error cases that use the legacy + // substring assertion. Cases that pin a structured assertion fall + // through so the JSON path is exercised against errors[] / changes. + if tt.ExpectedError && tt.ExpectedErrorContains != "" && + tt.ExpectedStructured == nil { + return + } + + // Check the output + outputStr := stdout.String() + + // Handle JSON output format with structured assertions + if tt.OutputFormat == "json" && tt.ExpectedStructured != nil { + simtest.AssertStructured(t, outputStr, tt.ExpectedStructured) + return + } + + // Using TrimSpace because the output might have trailing newlines + if !strings.Contains(strings.TrimSpace(outputStr), strings.TrimSpace(tt.ExpectedOutput)) { + // Strings aren't equal, *including* ansi. but we can compare ignoring ansi to determine what output to + // show for the failure. if the difference is only in color codes, we'll show escaped ansi codes. + out := outputStr + + expect := tt.ExpectedOutput + if tu.CompareIgnoringAnsi(strings.TrimSpace(outputStr), strings.TrimSpace(tt.ExpectedOutput)) { + out = strconv.QuoteToASCII(outputStr) + expect = strconv.QuoteToASCII(tt.ExpectedOutput) + } + + t.Fatalf("expected output to contain:\n%s\n\nbut got:\n%s", expect, out) + } +} + +// LeastPrivilegeRules is the access a diff of an XNopResource composing XDownstreamResources needs, the +// types the testdata XRDs, compositions and CRDs use. It grants Crossplane's own types only get, except a +// list of CompositionRevisions, which are found by their composition's label. It grants list only where a +// lookup is scoped by a label. xrVerbs are the verbs on +// the XR type: get to read the existing XR, patch and create to dry-run it, and list only where the +// command finds composites by listing them. +// +//nolint:goconst // Verbs read as RBAC rules, not as shared constants. +func LeastPrivilegeRules(xrVerbs ...string) []rbacv1.PolicyRule { + return []rbacv1.PolicyRule{ + // Fetched by name as the input needs them: the XR's XRD and CRD, its Composition, the + // pipeline's Functions, and any EnvironmentConfig a function requires by name. + {APIGroups: []string{"apiextensions.crossplane.io"}, Resources: []string{"compositeresourcedefinitions", "compositions", "environmentconfigs"}, Verbs: []string{"get"}}, + {APIGroups: []string{"apiextensions.k8s.io"}, Resources: []string{"customresourcedefinitions"}, Verbs: []string{"get"}}, + {APIGroups: []string{"pkg.crossplane.io"}, Resources: []string{"functions"}, Verbs: []string{"get"}}, + // A composition's revisions are listed by its crossplane.io/composition-name label. + {APIGroups: []string{"apiextensions.crossplane.io"}, Resources: []string{"compositionrevisions"}, Verbs: []string{"get", "list"}}, + // The XR type. + {APIGroups: []string{"ns.diff.example.org"}, Resources: []string{"xnopresources"}, Verbs: xrVerbs}, + // Composed resources: read from the XR's resource tree, listed by their crossplane.io/composite + // label to find a new XR's existing ones, and dry-run created or applied. + {APIGroups: []string{"ns.nop.example.org"}, Resources: []string{"xdownstreamresources"}, Verbs: []string{"get", "list", "create", "patch"}}, + } +} diff --git a/cmd/crossplane/project/integration_simulate_harness_test.go b/internal/testutils/integration/simharness/harness.go similarity index 86% rename from cmd/crossplane/project/integration_simulate_harness_test.go rename to internal/testutils/integration/simharness/harness.go index 9e7347de..2b54d56d 100644 --- a/cmd/crossplane/project/integration_simulate_harness_test.go +++ b/internal/testutils/integration/simharness/harness.go @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// This file is the simulate harness: the one function every integration +// This package is the simulate harness: the one function every integration // case funnels through. The cases are crossplane-diff's (origin/main // https://github.com/crossplane-contrib/crossplane-diff/commit/6a140b65a6bdb428add78680e6caee9f08b7f04b, plus later upstream additions through // https://github.com/crossplane-contrib/crossplane-diff/commit/d93eb87c6315cb2b44e4790e369eef3590c31dff), written as its comp and xr commands' CLI args plus an expected @@ -32,10 +32,11 @@ limitations under the License. // without a Docker daemon no rendering case can run; cases that would not // render cannot be told apart upfront, so they skip too. // -// testdata/README.md describes how a case runs, the skips, and how to run -// the suite and read its coverage. +// Each suite's testdata/README.md describes how its cases run, the skips, +// and how to run the suite and read its coverage. -package project +// Package simharness is the simulate harness shared by the integration suites of `composition`, `xr` and `project`. +package simharness import ( "context" @@ -54,6 +55,7 @@ import ( "github.com/crossplane/crossplane-runtime/v2/pkg/logging" "github.com/crossplane/cli/v2/cmd/crossplane/composition" + "github.com/crossplane/cli/v2/cmd/crossplane/project" "github.com/crossplane/cli/v2/cmd/crossplane/xr" "github.com/crossplane/cli/v2/internal/config" dp "github.com/crossplane/cli/v2/internal/diff/diffprocessor" @@ -65,8 +67,12 @@ import ( clixpkg "github.com/crossplane/cli/v2/internal/xpkg" ) -// defaultTimeout is the --timeout the cases pass to simulate. -const defaultTimeout = 60 * time.Second +// DefaultTimeout is the --timeout the cases pass to simulate. +const DefaultTimeout = 60 * time.Second + +// LongTimeout is the --timeout for cases with multiple iterations, such as +// eventual state. +const LongTimeout = 180 * time.Second // simArgs mirrors the crossplane-diff flags the integration cases pass, // plus each command's positional files, so runSimulate can translate @@ -90,7 +96,7 @@ type simArgs struct { // parseSimulateArgs parses one case's CLI-style args. A parse failure is // fatal, as it was when kong parsed crossplane-diff's own commands. -func parseSimulateArgs(t *testing.T, testType DiffTestType, args []string) *simArgs { +func parseSimulateArgs(t *testing.T, testType TestType, args []string) *simArgs { t.Helper() var a simArgs @@ -112,14 +118,14 @@ func parseSimulateArgs(t *testing.T, testType DiffTestType, args []string) *simA // --analyze-unchanged as its replacement --analyze-on=always. The output // is passed explicitly: crossplane-diff's default diff, since simulate // defaults to -o plan, and json or yaml as simulate's -o json/yaml result type. -func simulateArgv(t *testing.T, testType DiffTestType, a *simArgs) []string { +func simulateArgv(t *testing.T, testType TestType, a *simArgs) []string { t.Helper() var cmd string switch testType { - case CompositionDiffTest: + case CompositionTest: cmd = "composition" - case XRDiffTest: + case XRTest: cmd = "xr" default: t.Fatalf("simulateArgv: no simulate command for test type %q", testType) @@ -179,39 +185,39 @@ func simulateArgv(t *testing.T, testType DiffTestType, a *simArgs) []string { // Each call parses a fresh command, so a second run starts with cold // client caches just as a second CLI invocation would; the determinism // re-run and the restricted RBAC rerun rely on it. -func runSimulate(t *testing.T, cfg *rest.Config, testType DiffTestType, args []string, stdout, stderr io.Writer) (int, error) { +func runSimulate(t *testing.T, cfg *rest.Config, testType TestType, args []string, stdout, stderr io.Writer) (int, error) { t.Helper() a := parseSimulateArgs(t, testType, args) - return runSimulateArgv(t, cfg, simulateArgv(t, testType, a), a.CrossplaneRenderBinary, nil, stdout, stderr) + return RunSimulateArgv(t, cfg, simulateArgv(t, testType, a), a.CrossplaneRenderBinary, nil, stdout, stderr) } -// runSimulateArgv runs `crossplane simulate` with argv against cfg, +// RunSimulateArgv runs `crossplane simulate` with argv against cfg, // rendering with renderBinary when it is set, as runSimulate does, and routing provider // plans through router, when it is non-nil, instead of started diff servers. -func runSimulateArgv(t *testing.T, cfg *rest.Config, argv []string, renderBinary string, router simulate.Router, stdout, stderr io.Writer) (int, error) { +func RunSimulateArgv(t *testing.T, cfg *rest.Config, argv []string, renderBinary string, router simulate.Router, stdout, stderr io.Writer) (int, error) { t.Helper() - return runSimulateArgvWith(t, cfg, withRenderBinary(argv, renderBinary), &simulate.Hooks{Router: router}, stdout, stderr) + return RunSimulateArgvWith(t, cfg, WithRenderBinary(argv, renderBinary), &simulate.Hooks{Router: router}, stdout, stderr) } -// withRenderBinary returns argv rendering with the local crossplane binary +// WithRenderBinary returns argv rendering with the local crossplane binary // bin, or argv itself, rendering with the render image, when bin is empty. -func withRenderBinary(argv []string, bin string) []string { +func WithRenderBinary(argv []string, bin string) []string { if bin == "" { return argv } return append(slices.Clip(argv), "--crossplane-binary="+bin) } -// runSimulateArgvWith runs `crossplane simulate` with argv against +// RunSimulateArgvWith runs `crossplane simulate` with argv against // cfg and hooks, whose RESTConfig and Terminal it sets. -func runSimulateArgvWith(t *testing.T, cfg *rest.Config, argv []string, hooks *simulate.Hooks, stdout, stderr io.Writer) (int, error) { +func RunSimulateArgvWith(t *testing.T, cfg *rest.Config, argv []string, hooks *simulate.Hooks, stdout, stderr io.Writer) (int, error) { t.Helper() var cli struct { - Project Cmd `cmd:""` + Project project.Cmd `cmd:""` Composition composition.Cmd `cmd:""` XR xr.Cmd `cmd:""` } @@ -273,16 +279,18 @@ func runSimulateArgvWith(t *testing.T, cfg *rest.Config, argv []string, hooks *s } } -// dockerAvailable reports whether the local Docker daemon answers, which +// DockerAvailable reports whether the local Docker daemon answers, which // the render engine needs to run functions as containers. Probed once per // test binary. -var dockerAvailable = sync.OnceValue(func() bool { +// +//nolint:gochecknoglobals // Memoizes one daemon probe for every case. +var DockerAvailable = sync.OnceValue(func() bool { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() return docker.Check(ctx) == nil }) -// localCrossplaneBinary returns the absolute path to a locally built +// LocalCrossplaneBinary returns the absolute path to a locally built // `crossplane` binary at _output/bin/crossplane in the repo root, or "" when // there is none. With a binary, the render engine runs `crossplane internal // render` directly, which is faster; without one, callers pass "" and it @@ -297,7 +305,7 @@ var dockerAvailable = sync.OnceValue(func() bool { // Callers pass the path to the command as --crossplane-binary= rather // than through a process-global env var, so parallel tests don't race on // shared state. -func localCrossplaneBinary(t *testing.T) string { +func LocalCrossplaneBinary(t *testing.T) string { t.Helper() if override := os.Getenv("CROSSPLANE_RENDER_BINARY"); override != "" { diff --git a/cmd/crossplane/project/testdata/comp/crds/xapimigrateresource-crd.yaml b/internal/testutils/integration/testdata/comp/crds/xapimigrateresource-crd.yaml similarity index 100% rename from cmd/crossplane/project/testdata/comp/crds/xapimigrateresource-crd.yaml rename to internal/testutils/integration/testdata/comp/crds/xapimigrateresource-crd.yaml diff --git a/cmd/crossplane/project/testdata/comp/crds/xdownstreamresource-cluster-crd.yaml b/internal/testutils/integration/testdata/comp/crds/xdownstreamresource-cluster-crd.yaml similarity index 100% rename from cmd/crossplane/project/testdata/comp/crds/xdownstreamresource-cluster-crd.yaml rename to internal/testutils/integration/testdata/comp/crds/xdownstreamresource-cluster-crd.yaml diff --git a/cmd/crossplane/project/testdata/comp/crds/xdownstreamresource-legacycluster-crd.yaml b/internal/testutils/integration/testdata/comp/crds/xdownstreamresource-legacycluster-crd.yaml similarity index 100% rename from cmd/crossplane/project/testdata/comp/crds/xdownstreamresource-legacycluster-crd.yaml rename to internal/testutils/integration/testdata/comp/crds/xdownstreamresource-legacycluster-crd.yaml diff --git a/cmd/crossplane/project/testdata/comp/crds/xdownstreamresource-ns-crd.yaml b/internal/testutils/integration/testdata/comp/crds/xdownstreamresource-ns-crd.yaml similarity index 100% rename from cmd/crossplane/project/testdata/comp/crds/xdownstreamresource-ns-crd.yaml rename to internal/testutils/integration/testdata/comp/crds/xdownstreamresource-ns-crd.yaml diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-downstream-1.yaml b/internal/testutils/integration/testdata/comp/resources/existing-downstream-1.yaml similarity index 100% rename from cmd/crossplane/project/testdata/comp/resources/existing-downstream-1.yaml rename to internal/testutils/integration/testdata/comp/resources/existing-downstream-1.yaml diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-downstream-2.yaml b/internal/testutils/integration/testdata/comp/resources/existing-downstream-2.yaml similarity index 100% rename from cmd/crossplane/project/testdata/comp/resources/existing-downstream-2.yaml rename to internal/testutils/integration/testdata/comp/resources/existing-downstream-2.yaml diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-downstream-manual.yaml b/internal/testutils/integration/testdata/comp/resources/existing-downstream-manual.yaml similarity index 100% rename from cmd/crossplane/project/testdata/comp/resources/existing-downstream-manual.yaml rename to internal/testutils/integration/testdata/comp/resources/existing-downstream-manual.yaml diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-downstream-revision-ref.yaml b/internal/testutils/integration/testdata/comp/resources/existing-downstream-revision-ref.yaml similarity index 100% rename from cmd/crossplane/project/testdata/comp/resources/existing-downstream-revision-ref.yaml rename to internal/testutils/integration/testdata/comp/resources/existing-downstream-revision-ref.yaml diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-xr-1.yaml b/internal/testutils/integration/testdata/comp/resources/existing-xr-1.yaml similarity index 100% rename from cmd/crossplane/project/testdata/comp/resources/existing-xr-1.yaml rename to internal/testutils/integration/testdata/comp/resources/existing-xr-1.yaml diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-xr-2.yaml b/internal/testutils/integration/testdata/comp/resources/existing-xr-2.yaml similarity index 100% rename from cmd/crossplane/project/testdata/comp/resources/existing-xr-2.yaml rename to internal/testutils/integration/testdata/comp/resources/existing-xr-2.yaml diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-xr-manual.yaml b/internal/testutils/integration/testdata/comp/resources/existing-xr-manual.yaml similarity index 100% rename from cmd/crossplane/project/testdata/comp/resources/existing-xr-manual.yaml rename to internal/testutils/integration/testdata/comp/resources/existing-xr-manual.yaml diff --git a/cmd/crossplane/project/testdata/comp/resources/existing-xr-revision-ref.yaml b/internal/testutils/integration/testdata/comp/resources/existing-xr-revision-ref.yaml similarity index 100% rename from cmd/crossplane/project/testdata/comp/resources/existing-xr-revision-ref.yaml rename to internal/testutils/integration/testdata/comp/resources/existing-xr-revision-ref.yaml diff --git a/cmd/crossplane/project/testdata/comp/resources/functions.yaml b/internal/testutils/integration/testdata/comp/resources/functions.yaml similarity index 100% rename from cmd/crossplane/project/testdata/comp/resources/functions.yaml rename to internal/testutils/integration/testdata/comp/resources/functions.yaml diff --git a/cmd/crossplane/project/testdata/comp/resources/original-composition.yaml b/internal/testutils/integration/testdata/comp/resources/original-composition.yaml similarity index 100% rename from cmd/crossplane/project/testdata/comp/resources/original-composition.yaml rename to internal/testutils/integration/testdata/comp/resources/original-composition.yaml diff --git a/cmd/crossplane/project/testdata/comp/resources/revision-templating-composition-revision.yaml b/internal/testutils/integration/testdata/comp/resources/revision-templating-composition-revision.yaml similarity index 100% rename from cmd/crossplane/project/testdata/comp/resources/revision-templating-composition-revision.yaml rename to internal/testutils/integration/testdata/comp/resources/revision-templating-composition-revision.yaml diff --git a/cmd/crossplane/project/testdata/comp/resources/revision-templating-composition.yaml b/internal/testutils/integration/testdata/comp/resources/revision-templating-composition.yaml similarity index 100% rename from cmd/crossplane/project/testdata/comp/resources/revision-templating-composition.yaml rename to internal/testutils/integration/testdata/comp/resources/revision-templating-composition.yaml diff --git a/cmd/crossplane/project/testdata/comp/resources/xrd.yaml b/internal/testutils/integration/testdata/comp/resources/xrd.yaml similarity index 100% rename from cmd/crossplane/project/testdata/comp/resources/xrd.yaml rename to internal/testutils/integration/testdata/comp/resources/xrd.yaml diff --git a/cmd/crossplane/project/testdata/comp/updated-composition.yaml b/internal/testutils/integration/testdata/comp/updated-composition.yaml similarity index 100% rename from cmd/crossplane/project/testdata/comp/updated-composition.yaml rename to internal/testutils/integration/testdata/comp/updated-composition.yaml diff --git a/cmd/crossplane/project/testdata/diff/crds/clusternopresources.nop.crossplane.io.yaml b/internal/testutils/integration/testdata/diff/crds/clusternopresources.nop.crossplane.io.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/crds/clusternopresources.nop.crossplane.io.yaml rename to internal/testutils/integration/testdata/diff/crds/clusternopresources.nop.crossplane.io.yaml diff --git a/cmd/crossplane/project/testdata/diff/crds/xapimigrateresource-crd.yaml b/internal/testutils/integration/testdata/diff/crds/xapimigrateresource-crd.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/crds/xapimigrateresource-crd.yaml rename to internal/testutils/integration/testdata/diff/crds/xapimigrateresource-crd.yaml diff --git a/cmd/crossplane/project/testdata/diff/crds/xdefaulteddownstreamresource-ns-crd.yaml b/internal/testutils/integration/testdata/diff/crds/xdefaulteddownstreamresource-ns-crd.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/crds/xdefaulteddownstreamresource-ns-crd.yaml rename to internal/testutils/integration/testdata/diff/crds/xdefaulteddownstreamresource-ns-crd.yaml diff --git a/cmd/crossplane/project/testdata/diff/crds/xdownstreamresource-cluster-crd.yaml b/internal/testutils/integration/testdata/diff/crds/xdownstreamresource-cluster-crd.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/crds/xdownstreamresource-cluster-crd.yaml rename to internal/testutils/integration/testdata/diff/crds/xdownstreamresource-cluster-crd.yaml diff --git a/cmd/crossplane/project/testdata/diff/crds/xdownstreamresource-legacycluster-crd.yaml b/internal/testutils/integration/testdata/diff/crds/xdownstreamresource-legacycluster-crd.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/crds/xdownstreamresource-legacycluster-crd.yaml rename to internal/testutils/integration/testdata/diff/crds/xdownstreamresource-legacycluster-crd.yaml diff --git a/cmd/crossplane/project/testdata/diff/crds/xdownstreamresource-ns-crd.yaml b/internal/testutils/integration/testdata/diff/crds/xdownstreamresource-ns-crd.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/crds/xdownstreamresource-ns-crd.yaml rename to internal/testutils/integration/testdata/diff/crds/xdownstreamresource-ns-crd.yaml diff --git a/cmd/crossplane/project/testdata/diff/crds/xstatusresource-ns-crd.yaml b/internal/testutils/integration/testdata/diff/crds/xstatusresource-ns-crd.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/crds/xstatusresource-ns-crd.yaml rename to internal/testutils/integration/testdata/diff/crds/xstatusresource-ns-crd.yaml diff --git a/cmd/crossplane/project/testdata/diff/crds/xversioneddownstreamresource-ns-crd.yaml b/internal/testutils/integration/testdata/diff/crds/xversioneddownstreamresource-ns-crd.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/crds/xversioneddownstreamresource-ns-crd.yaml rename to internal/testutils/integration/testdata/diff/crds/xversioneddownstreamresource-ns-crd.yaml diff --git a/cmd/crossplane/project/testdata/diff/modified-xr.yaml b/internal/testutils/integration/testdata/diff/modified-xr.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/modified-xr.yaml rename to internal/testutils/integration/testdata/diff/modified-xr.yaml diff --git a/cmd/crossplane/project/testdata/diff/new-xr.yaml b/internal/testutils/integration/testdata/diff/new-xr.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/new-xr.yaml rename to internal/testutils/integration/testdata/diff/new-xr.yaml diff --git a/cmd/crossplane/project/testdata/diff/resources/cluster-xrd.yaml b/internal/testutils/integration/testdata/diff/resources/cluster-xrd.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/resources/cluster-xrd.yaml rename to internal/testutils/integration/testdata/diff/resources/cluster-xrd.yaml diff --git a/cmd/crossplane/project/testdata/diff/resources/composition.yaml b/internal/testutils/integration/testdata/diff/resources/composition.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/resources/composition.yaml rename to internal/testutils/integration/testdata/diff/resources/composition.yaml diff --git a/cmd/crossplane/project/testdata/diff/resources/conditional-gating-composition.yaml b/internal/testutils/integration/testdata/diff/resources/conditional-gating-composition.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/resources/conditional-gating-composition.yaml rename to internal/testutils/integration/testdata/diff/resources/conditional-gating-composition.yaml diff --git a/cmd/crossplane/project/testdata/diff/resources/cycle/compositions.yaml b/internal/testutils/integration/testdata/diff/resources/cycle/compositions.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/resources/cycle/compositions.yaml rename to internal/testutils/integration/testdata/diff/resources/cycle/compositions.yaml diff --git a/cmd/crossplane/project/testdata/diff/resources/cycle/xrds.yaml b/internal/testutils/integration/testdata/diff/resources/cycle/xrds.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/resources/cycle/xrds.yaml rename to internal/testutils/integration/testdata/diff/resources/cycle/xrds.yaml diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-downstream-resource.yaml b/internal/testutils/integration/testdata/diff/resources/existing-downstream-resource.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/resources/existing-downstream-resource.yaml rename to internal/testutils/integration/testdata/diff/resources/existing-downstream-resource.yaml diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-other-xr-downstream.yaml b/internal/testutils/integration/testdata/diff/resources/existing-other-xr-downstream.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/resources/existing-other-xr-downstream.yaml rename to internal/testutils/integration/testdata/diff/resources/existing-other-xr-downstream.yaml diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-other-xr.yaml b/internal/testutils/integration/testdata/diff/resources/existing-other-xr.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/resources/existing-other-xr.yaml rename to internal/testutils/integration/testdata/diff/resources/existing-other-xr.yaml diff --git a/cmd/crossplane/project/testdata/diff/resources/existing-xr.yaml b/internal/testutils/integration/testdata/diff/resources/existing-xr.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/resources/existing-xr.yaml rename to internal/testutils/integration/testdata/diff/resources/existing-xr.yaml diff --git a/cmd/crossplane/project/testdata/diff/resources/functions.yaml b/internal/testutils/integration/testdata/diff/resources/functions.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/resources/functions.yaml rename to internal/testutils/integration/testdata/diff/resources/functions.yaml diff --git a/cmd/crossplane/project/testdata/diff/resources/removal-test-composition.yaml b/internal/testutils/integration/testdata/diff/resources/removal-test-composition.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/resources/removal-test-composition.yaml rename to internal/testutils/integration/testdata/diff/resources/removal-test-composition.yaml diff --git a/cmd/crossplane/project/testdata/diff/resources/removal-test-ns-downstream-resource1.yaml b/internal/testutils/integration/testdata/diff/resources/removal-test-ns-downstream-resource1.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/resources/removal-test-ns-downstream-resource1.yaml rename to internal/testutils/integration/testdata/diff/resources/removal-test-ns-downstream-resource1.yaml diff --git a/cmd/crossplane/project/testdata/diff/resources/removal-test-ns-downstream-resource2.yaml b/internal/testutils/integration/testdata/diff/resources/removal-test-ns-downstream-resource2.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/resources/removal-test-ns-downstream-resource2.yaml rename to internal/testutils/integration/testdata/diff/resources/removal-test-ns-downstream-resource2.yaml diff --git a/cmd/crossplane/project/testdata/diff/resources/removal-test-ns-downstream-without-controller.yaml b/internal/testutils/integration/testdata/diff/resources/removal-test-ns-downstream-without-controller.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/resources/removal-test-ns-downstream-without-controller.yaml rename to internal/testutils/integration/testdata/diff/resources/removal-test-ns-downstream-without-controller.yaml diff --git a/cmd/crossplane/project/testdata/diff/resources/sequencer-gating-composition.yaml b/internal/testutils/integration/testdata/diff/resources/sequencer-gating-composition.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/resources/sequencer-gating-composition.yaml rename to internal/testutils/integration/testdata/diff/resources/sequencer-gating-composition.yaml diff --git a/cmd/crossplane/project/testdata/diff/resources/shared-child-composition.yaml b/internal/testutils/integration/testdata/diff/resources/shared-child-composition.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/resources/shared-child-composition.yaml rename to internal/testutils/integration/testdata/diff/resources/shared-child-composition.yaml diff --git a/cmd/crossplane/project/testdata/diff/resources/xrd.yaml b/internal/testutils/integration/testdata/diff/resources/xrd.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/resources/xrd.yaml rename to internal/testutils/integration/testdata/diff/resources/xrd.yaml diff --git a/cmd/crossplane/project/testdata/diff/shared-child-xrs.yaml b/internal/testutils/integration/testdata/diff/shared-child-xrs.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/shared-child-xrs.yaml rename to internal/testutils/integration/testdata/diff/shared-child-xrs.yaml diff --git a/cmd/crossplane/project/testdata/diff/unmodified-xr.yaml b/internal/testutils/integration/testdata/diff/unmodified-xr.yaml similarity index 100% rename from cmd/crossplane/project/testdata/diff/unmodified-xr.yaml rename to internal/testutils/integration/testdata/diff/unmodified-xr.yaml From cd3b8c83d07d8c0cd925f2a0fe9f3603eb7902ea Mon Sep 17 00:00:00 2001 From: Jonathan Ogilvie Date: Fri, 6 Nov 2026 18:02:42 -0500 Subject: [PATCH 2/6] test(composition): own the composition integration suite Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Jonathan Ogilvie --- .../composition/integration_main_test.go | 23 + .../composition/integration_test.go | 2521 +++++++++++++++++ cmd/crossplane/composition/testdata/README.md | 12 + .../comp/api-version-updated-composition.yaml | 32 + ...vision-templating-updated-composition.yaml | 37 + .../testdata/comp/composition-no-changes.yaml | 33 + .../field-removal-updated-composition.yaml | 32 + .../mixed-status-updated-composition.yaml | 59 + .../testdata/comp/net-new-composition.yaml | 37 + .../api-version-original-composition.yaml | 32 + .../comp/resources/claim-composition.yaml | 32 + ...emplating-composition-kubectl-applied.yaml | 41 + ...claim-revision-templating-composition.yaml | 39 + .../testdata/comp/resources/claim-xrd.yaml | 27 + .../comp/resources/custom-namespace.yaml | 4 + .../resources/cycle/existing-cycle-xr.yaml | 12 + ...isting-api-version-downstream-v1beta1.yaml | 10 + .../resources/existing-api-version-xr.yaml | 11 + .../comp/resources/existing-claim-1-xr.yaml | 21 + .../comp/resources/existing-claim-1.yaml | 18 + .../comp/resources/existing-claim-2-xr.yaml | 21 + .../comp/resources/existing-claim-2.yaml | 18 + .../existing-claim-downstream-1.yaml | 10 + .../existing-claim-downstream-2.yaml | 10 + ...xisting-claim-revision-ref-downstream.yaml | 14 + ...vision-ref-kubectl-applied-downstream.yaml | 12 + ...claim-revision-ref-kubectl-applied-xr.yaml | 15 + ...ng-claim-revision-ref-kubectl-applied.yaml | 20 + .../existing-claim-revision-ref-xr.yaml | 15 + .../existing-claim-revision-ref.yaml | 20 + .../existing-conditional-gating-stage0.yaml | 12 + .../existing-conditional-gating-xr.yaml | 10 + .../existing-custom-ns-downstream.yaml | 11 + .../comp/resources/existing-custom-ns-xr.yaml | 10 + .../existing-downstream-deleting.yaml | 11 + .../existing-downstream-drifted.yaml | 14 + ...isting-downstream-pinned-revision-ref.yaml | 15 + .../resources/existing-downstream-v2.yaml | 13 + .../existing-downstream-with-argocd.yaml | 14 + .../existing-sequencer-gating-stage0.yaml | 12 + .../existing-sequencer-gating-xr.yaml | 10 + .../resources/existing-sha256-downstream.yaml | 11 + .../comp/resources/existing-sha256-xr.yaml | 10 + .../existing-unverified-cluster-xr.yaml | 11 + ...existing-xr-1-kubectl-applied-current.yaml | 13 + .../existing-xr-1-kubectl-applied.yaml | 13 + .../comp/resources/existing-xr-deleting.yaml | 14 + ...ing-xr-listing-uncontrolled-resources.yaml | 22 + .../existing-xr-pinned-revision-ref.yaml | 18 + .../resources/existing-xr-selector-match.yaml | 18 + .../existing-xr-selector-mismatch.yaml | 18 + .../comp/resources/existing-xr-v2.yaml | 12 + .../resources/existing-xr-with-argocd.yaml | 14 + .../field-removal/composition-with-field.yaml | 33 + .../field-removal/existing-downstream.yaml | 11 + .../resources/field-removal/existing-xr.yaml | 10 + .../comp/resources/functions-sha256.yaml | 17 + .../comp/resources/mixed-downstream-db-1.yaml | 11 + .../comp/resources/mixed-downstream-db-2.yaml | 11 + .../comp/resources/mixed-downstream-db-3.yaml | 12 + .../resources/mixed-downstream-network-1.yaml | 11 + .../resources/mixed-downstream-network-2.yaml | 11 + .../resources/mixed-downstream-network-3.yaml | 11 + .../resources/mixed-downstream-storage-1.yaml | 11 + .../resources/mixed-downstream-storage-2.yaml | 11 + .../resources/mixed-downstream-storage-3.yaml | 11 + .../resources/mixed-status-composition.yaml | 58 + .../testdata/comp/resources/mixed-xr-1.yaml | 10 + .../testdata/comp/resources/mixed-xr-2.yaml | 10 + .../testdata/comp/resources/mixed-xr-3.yaml | 10 + .../collision-composition-revision.yaml | 56 + .../collision-composition.yaml | 52 + .../namespace-collision/configmap-ns-a.yaml | 8 + .../namespace-collision/configmap-ns-b.yaml | 8 + .../existing-downstream-ns-a.yaml | 14 + .../existing-downstream-ns-b.yaml | 14 + .../namespace-collision/existing-xr-ns-a.yaml | 15 + .../namespace-collision/existing-xr-ns-b.yaml | 15 + .../namespace-collision/namespace-ns-a.yaml | 4 + .../namespace-collision/namespace-ns-b.yaml | 4 + .../nested-xr/existing-child-downstream.yaml | 16 + .../nested-xr/existing-nested-child-xr.yaml | 14 + .../existing-parent-direct-downstream.yaml | 12 + .../nested-xr/existing-parent-xr.yaml | 12 + .../nested-xr/parent-composition.yaml | 49 + .../comp/resources/nested-xr/parent-xrd.yaml | 50 + .../resources/original-composition-2.yaml | 33 + ...l-composition-kubectl-applied-current.yaml | 41 + .../original-composition-kubectl-applied.yaml | 40 + .../removal-test-original-composition.yaml | 45 + ...templating-composition-older-revision.yaml | 40 + .../comp/resources/sha256-composition.yaml | 33 + .../resources/shared-child-xrs-differing.yaml | 23 + .../comp/resources/status-downstream-1.yaml | 11 + .../comp/resources/status-downstream-2.yaml | 11 + .../status-indicator-composition.yaml | 33 + .../testdata/comp/resources/status-xr-1.yaml | 10 + .../testdata/comp/resources/status-xr-2.yaml | 10 + .../comp/resources/test-namespace.yaml | 4 + .../unverified-cluster-composition.yaml | 15 + ...vision-templating-updated-composition.yaml | 43 + .../status-indicator-updated-composition.yaml | 35 + .../comp/updated-claim-composition.yaml | 32 + .../comp/updated-collision-composition.yaml | 50 + .../comp/updated-composition-2-changed.yaml | 36 + .../testdata/comp/updated-composition-2.yaml | 33 + .../comp/updated-composition-labeled.yaml | 35 + ...pdated-conditional-gating-composition.yaml | 57 + .../comp/updated-cycle-composition.yaml | 35 + .../comp/updated-parent-composition.yaml | 50 + .../updated-sequencer-gating-composition.yaml | 53 + .../comp/updated-sha256-composition.yaml | 33 + .../updated-shared-child-composition.yaml | 36 + ...pdated-unverified-cluster-composition.yaml | 47 + 114 files changed, 5020 insertions(+) create mode 100644 cmd/crossplane/composition/integration_main_test.go create mode 100644 cmd/crossplane/composition/integration_test.go create mode 100644 cmd/crossplane/composition/testdata/README.md create mode 100644 cmd/crossplane/composition/testdata/comp/api-version-updated-composition.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/claim-revision-templating-updated-composition.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/composition-no-changes.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/field-removal-updated-composition.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/mixed-status-updated-composition.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/net-new-composition.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/api-version-original-composition.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/claim-composition.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/claim-revision-templating-composition-kubectl-applied.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/claim-revision-templating-composition.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/claim-xrd.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/custom-namespace.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/cycle/existing-cycle-xr.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-api-version-downstream-v1beta1.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-api-version-xr.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-claim-1-xr.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-claim-1.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-claim-2-xr.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-claim-2.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-claim-downstream-1.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-claim-downstream-2.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-claim-revision-ref-downstream.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-claim-revision-ref-kubectl-applied-downstream.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-claim-revision-ref-kubectl-applied-xr.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-claim-revision-ref-kubectl-applied.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-claim-revision-ref-xr.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-claim-revision-ref.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-conditional-gating-stage0.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-conditional-gating-xr.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-custom-ns-downstream.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-custom-ns-xr.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-downstream-deleting.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-downstream-drifted.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-downstream-pinned-revision-ref.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-downstream-v2.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-downstream-with-argocd.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-sequencer-gating-stage0.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-sequencer-gating-xr.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-sha256-downstream.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-sha256-xr.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-unverified-cluster-xr.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-xr-1-kubectl-applied-current.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-xr-1-kubectl-applied.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-xr-deleting.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-xr-listing-uncontrolled-resources.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-xr-pinned-revision-ref.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-xr-selector-match.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-xr-selector-mismatch.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-xr-v2.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/existing-xr-with-argocd.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/field-removal/composition-with-field.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/field-removal/existing-downstream.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/field-removal/existing-xr.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/functions-sha256.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-db-1.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-db-2.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-db-3.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-network-1.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-network-2.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-network-3.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-storage-1.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-storage-2.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-storage-3.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/mixed-status-composition.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/mixed-xr-1.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/mixed-xr-2.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/mixed-xr-3.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/namespace-collision/collision-composition-revision.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/namespace-collision/collision-composition.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/namespace-collision/configmap-ns-a.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/namespace-collision/configmap-ns-b.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/namespace-collision/existing-downstream-ns-a.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/namespace-collision/existing-downstream-ns-b.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/namespace-collision/existing-xr-ns-a.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/namespace-collision/existing-xr-ns-b.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/namespace-collision/namespace-ns-a.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/namespace-collision/namespace-ns-b.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/nested-xr/existing-child-downstream.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/nested-xr/existing-nested-child-xr.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/nested-xr/existing-parent-direct-downstream.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/nested-xr/existing-parent-xr.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/nested-xr/parent-composition.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/nested-xr/parent-xrd.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/original-composition-2.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/original-composition-kubectl-applied-current.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/original-composition-kubectl-applied.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/removal-test-original-composition.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/revision-templating-composition-older-revision.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/sha256-composition.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/shared-child-xrs-differing.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/status-downstream-1.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/status-downstream-2.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/status-indicator-composition.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/status-xr-1.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/status-xr-2.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/test-namespace.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/resources/unverified-cluster-composition.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/revision-templating-updated-composition.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/status-indicator-updated-composition.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/updated-claim-composition.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/updated-collision-composition.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/updated-composition-2-changed.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/updated-composition-2.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/updated-composition-labeled.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/updated-conditional-gating-composition.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/updated-cycle-composition.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/updated-parent-composition.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/updated-sequencer-gating-composition.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/updated-sha256-composition.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/updated-shared-child-composition.yaml create mode 100644 cmd/crossplane/composition/testdata/comp/updated-unverified-cluster-composition.yaml diff --git a/cmd/crossplane/composition/integration_main_test.go b/cmd/crossplane/composition/integration_main_test.go new file mode 100644 index 00000000..58bc78d3 --- /dev/null +++ b/cmd/crossplane/composition/integration_main_test.go @@ -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) +} diff --git a/cmd/crossplane/composition/integration_test.go b/cmd/crossplane/composition/integration_test.go new file mode 100644 index 00000000..6b7368fa --- /dev/null +++ b/cmd/crossplane/composition/integration_test.go @@ -0,0 +1,2521 @@ +//go:build integration + +/* +Copyright 2026 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package composition_test + +import ( + "strings" + "testing" + + dp "github.com/crossplane/cli/v2/internal/diff/diffprocessor" + tu "github.com/crossplane/cli/v2/internal/diff/testutils" + "github.com/crossplane/cli/v2/internal/simulate/simtest" + "github.com/crossplane/cli/v2/internal/testutils/integration" + "github.com/crossplane/cli/v2/internal/testutils/integration/simharness" +) + +// TestCompDiffIntegration runs an integration test for the composition diff command. +// +//nolint:tparallel // simharness.RunIntegrationTest calls t.Parallel in each subtest. +func TestCompDiffIntegration(t *testing.T) { + t.Parallel() + + tests := map[string]simharness.IntegrationTestCase{ + "CompositionChangeImpactsXRs": { + Reason: "Validates composition change impacts existing XRs", + // Set up existing XRs that use the original composition + SetupFiles: []string{ + "../../../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", + }, + // Add existing XRs that use the composition + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-1.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-1.yaml", + ), + integration.Composes( + "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-2.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-2.yaml", + ), + }, + // New composition files that will be diffed + InputFiles: []string{"../../../internal/testutils/integration/testdata/comp/updated-composition.yaml"}, + Namespace: "default", + ExpectedOutput: ` +=== Composition Changes === + +~~~ Composition/xnopresources.diff.example.org + apiVersion: apiextensions.crossplane.io/v1 + kind: Composition + metadata: + name: xnopresources.diff.example.org + spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: +- configData: {{ .observed.composite.resource.spec.coolField }} +- resourceTier: basic ++ configData: updated-{{ .observed.composite.resource.spec.coolField }} ++ resourceTier: premium + kind: GoTemplate + source: Inline + step: generate-resources + - functionRef: + name: function-auto-ready + step: automatically-detect-ready-composed-resources + +--- + +Summary: 1 modified + +Applying this composition creates a new CompositionRevision, which 2 composites would adopt. + +=== Affected Composite Resources === + + ⚠ XNopResource/another-resource (namespace: default) + ⚠ XNopResource/test-resource (namespace: default) + +Summary: 2 resources with changes + +=== Impact Analysis === + +~~~ XDownstreamResource/another-resource + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + annotations: + crossplane.io/composition-resource-name: nop-resource + labels: + crossplane.io/composite: another-resource + name: another-resource + namespace: default + spec: + forProvider: +- configData: another-existing-value +- resourceTier: basic ++ configData: updated-another-existing-value ++ resourceTier: premium + +--- +~~~ XDownstreamResource/test-resource + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + annotations: + crossplane.io/composition-resource-name: nop-resource + labels: + crossplane.io/composite: test-resource + name: test-resource + namespace: default + spec: + forProvider: +- configData: existing-value +- resourceTier: basic ++ configData: updated-existing-value ++ resourceTier: premium + +--- + +Summary: 2 modified`, + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + NoColor: true, + }, + // Issue #452: an XR with a deletionTimestamp is on Crossplane's teardown path and will never + // adopt the diffed composition's revision. Rendering it yields no composed resources (the + // composite reconciler takes its deletion path), so including it produces a meaningless + // impact analysis. It must be excluded and counted, leaving only the live XR evaluated. + "CompositionDiffExcludesDeletingXRs": { + Reason: "A deleting XR is excluded from impact analysis and counted as filteredByDeletion; the live XR is still evaluated", + SetupFiles: []string{ + "../../../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{ + { + OwnerFile: "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-1.yaml", + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-1.yaml": nil, + }, + }, + { + OwnerFile: "testdata/comp/resources/existing-xr-deleting.yaml", + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + "testdata/comp/resources/existing-downstream-deleting.yaml": nil, + }, + }, + }, + DeleteAfterSetup: []string{"testdata/comp/resources/existing-xr-deleting.yaml"}, + InputFiles: []string{"../../../internal/testutils/integration/testdata/comp/updated-composition.yaml"}, + Namespace: "default", + OutputFormat: "json", + ExpectedExitCode: dp.ExitCodeDiffDetected, // the composition itself changed + // Total counts both discovered XRs; only the live one is evaluated (1 with changes), and + // the deleting one is reported via filteredByDeletion. In default-discovery mode filtered + // XRs are counted but not surfaced as impact entries, so only test-resource appears. + ExpectedStructured: simtest.Expect(). + WithComposition("xnopresources.diff.example.org"). + WithCompositionModified(). + WithAffectedResources(2, 1, 0, 0). + WithFilteredByDeletion(1). + WithXRImpact("XNopResource", "test-resource", "default", "changed"), + }, + // Issue #452, --resource mode: filtered XRs are surfaced explicitly there, so a user who names + // a deleting composite is told why it was skipped rather than getting silence. + "ResourceFilterSurfacesDeletingXR": { + Reason: "--resource matching a deleting composite surfaces it as filtered with reason deleting", + SetupFiles: []string{ + "../../../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{ + { + OwnerFile: "testdata/comp/resources/existing-xr-deleting.yaml", + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + "testdata/comp/resources/existing-downstream-deleting.yaml": nil, + }, + }, + }, + DeleteAfterSetup: []string{"testdata/comp/resources/existing-xr-deleting.yaml"}, + InputFiles: []string{"../../../internal/testutils/integration/testdata/comp/updated-composition.yaml"}, + Resources: []string{"default/deleting-resource"}, + OutputFormat: "json", + ExpectedExitCode: dp.ExitCodeDiffDetected, // the composition itself changed + ExpectedStructured: simtest.Expect(). + WithComposition("xnopresources.diff.example.org"). + WithCompositionModified(). + WithFilteredByDeletion(1). + WithXRImpact("XNopResource", "deleting-resource", "default", "filtered"). + WithFilterReason("deleting"), + }, + // Issue #495, `comp` side: impact analysis renders each affected XR through the same observed- + // resource fetch as `xr`, so a composed resource deleted out of band is tolerated with the same + // warning. Being genuinely absent, it is then reported as something the XR will create. + "CompositionDiffComposedResourceDeletedOutOfBandWarns": { + Reason: "comp against an XR whose resourceRefs name a deleted composed resource warns and reports it as an addition", + SetupFiles: []string{ + "../../../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{ + { + OwnerFile: "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-1.yaml", + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-1.yaml": nil, + }, + }, + }, + DeleteAfterSetup: []string{"../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-1.yaml"}, + InputFiles: []string{"../../../internal/testutils/integration/testdata/comp/updated-composition.yaml"}, + Namespace: "default", + OutputFormat: "json", + ExpectedExitCode: dp.ExitCodeDiffDetected, + ExpectedStructured: simtest.Expect(). + WithWarning("was not found in the cluster"). + WithWarningContext(map[string]string{ + "resource": "XDownstreamResource/test-resource", + "namespace": "default", + }). + And(). + WithComposition("xnopresources.diff.example.org"). + WithCompositionModified(). + WithAffectedResources(1, 1, 0, 0). + WithXRImpact("XNopResource", "test-resource", "default", "changed"). + WithDownstreamSummary(1, 0, 0). + WithDownstreamResource("added", "XDownstreamResource", "test-resource", "default"), + }, + // The comp side of TestDiffIntegration/ResourceRemovalSkipsResourcesTheXRDoesNotControl (#552): + // the updated composition stops rendering resource2, which the XR controls, so it is removed; + // the XR's resourceRefs also name a resource another XR controls and one with no controller, + // which the composition doesn't render either, and neither of those is removed. + "CompositionDropsResourceRemovingOnlyWhatTheXRControls": { + Reason: "A composition that stops rendering resources removes only those the XR controls", + // What this case pins: test-resource's resourceRefs still name two resources it doesn't control. + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/comp/resources/xrd.yaml", + "testdata/comp/resources/removal-test-original-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/composition.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/functions.yaml", + // Deliberately not in crossplaneManagedResources: its controller reference was removed, so + // no XR controls it, though test-resource's resourceRefs still name it. + "../../../internal/testutils/integration/testdata/diff/resources/removal-test-ns-downstream-without-controller.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/comp/resources/existing-xr-listing-uncontrolled-resources.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/removal-test-ns-downstream-resource1.yaml", // still rendered: kept + "../../../internal/testutils/integration/testdata/diff/resources/removal-test-ns-downstream-resource2.yaml", // no longer rendered: removed + ), + integration.Composes( + "../../../internal/testutils/integration/testdata/diff/resources/existing-other-xr.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/existing-other-xr-downstream.yaml", + ), + }, + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/resources/removal-test-composition.yaml"}, + Namespace: "default", + OutputFormat: "json", + ExpectedExitCode: dp.ExitCodeDiffDetected, + ExpectedStructured: simtest.Expect(). + WithComposition("removal-test-composition"). + WithCompositionModified(). + WithAffectedResources(1, 1, 0, 0). + WithXRImpact("XNopResource", "test-resource", "default", "changed"). + WithDownstreamSummary(0, 0, 1). + WithDownstreamResource("removed", "XDownstreamResource", "resource-to-be-removed", "default"), + }, + "CompositionDiffIgnorePaths": { + Reason: "Validates that ArgoCD annotations are ignored in composition diffs", + SetupFiles: []string{ + "../../../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", + }, + // Add existing XR with ArgoCD annotations + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/comp/resources/existing-xr-with-argocd.yaml", + "testdata/comp/resources/existing-downstream-with-argocd.yaml", + ), + }, + InputFiles: []string{"testdata/comp/composition-no-changes.yaml"}, + Namespace: "default", + // The composition is unchanged, so impact analysis would be skipped by default (issue + // #453). This case is about XR/downstream-level ignore paths, so it opts back in. + AnalyzeUnchanged: true, + IgnorePaths: []string{ + "metadata.annotations[argocd.argoproj.io/tracking-id]", + "metadata.labels[argocd.argoproj.io/instance]", + }, + ExpectedOutput: ` +=== Composition Changes === + +No changes detected in composition xnopresources.diff.example.org + +=== Affected Composite Resources === + + ✓ XNopResource/test-resource (namespace: default) + +Summary: 1 resource unchanged + +=== Impact Analysis === + +All composite resources are up-to-date. No downstream resource changes detected. + +`, + ExpectedError: false, + NoColor: true, + }, + // Issue #453: any revision an unchanged composition produced would carry the same spec, so no XR + // renders differently and the affected-XR / impact-analysis sections are replaced by a skip note. + // + // These fixtures are deliberately ones that DO produce a downstream delta when evaluated (see + // UnchangedCompositionAnalyzeUnchangedEvaluatesXRs, which asserts exactly that against the same + // setup). So this case proves two things at once: the skip suppresses a report that would + // otherwise be emitted, and the exit code is 0 rather than the 3 that report would have caused + // — a delta not attributable to any composition change must not gate a CI pipeline. + "UnchangedCompositionSkipsImpactAnalysis": { + Reason: "An unchanged composition skips impact analysis entirely and says so", + SetupFiles: []string{ + "../../../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( + "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-1.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-1.yaml", + ), + }, + InputFiles: []string{"testdata/comp/composition-no-changes.yaml"}, + Namespace: "default", + ExpectedOutput: ` +=== Composition Changes === + +No changes detected in composition xnopresources.diff.example.org + +Impact analysis skipped: this composition is identical to the cluster's, so applying it creates no new CompositionRevision and no composite resource could change as a result. Pass --analyze-on=always to evaluate them anyway. + +`, + ExpectedError: false, + // No composition change and no evaluated XRs, so nothing was detected. + ExpectedExitCode: dp.ExitCodeSuccess, + NoColor: true, + }, + // Issue #467: the cluster composition was applied with a client-side `kubectl apply`, so it + // carries kubectl.kubernetes.io/last-applied-configuration; the file being diffed never does. + // That annotation is suppressed from the rendered diff — it is a multi-KB serialization of the + // object, useless to show — but Crossplane's Composition.Hash() covers annotations, so applying + // this DOES create a new CompositionRevision that composites re-point to, and whose name a + // template can read off the XR's compositionRevisionRef. A suppression made for readability + // must not decide that away, so the composites are evaluated. Contrast the sibling above, which + // is byte-identical and needs --analyze-unchanged to evaluate anything. + // + // This runs the real CLI wiring, which the unit coverage in + // TestDefaultCompDiffProcessor_calculateCompositionDiff cannot reach: it pins that + // defaultProcessorOptions does not fold the annotation into --ignore-paths, and that the + // display-only suppression does not leak into the change verdict. + // + // The evaluated composite renders unchanged, because the new revision's spec is identical, so + // nothing renders differently and the exit code is 0 (as with AnalyzeOnSpecChangeSkipsMetadataOnlyChange). + // What proves evaluation is that the composite is counted, rather than impact analysis being skipped. + "CompositionAppliedWithKubectlEvaluatesXRs": { + Reason: "A composition differing only in kubectl's last-applied-configuration still counts as changed, because applying it creates a new CompositionRevision", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/comp/resources/xrd.yaml", + "testdata/comp/resources/original-composition-kubectl-applied.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/functions.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/comp/resources/existing-xr-1-kubectl-applied.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-1.yaml", + ), + }, + InputFiles: []string{"testdata/comp/composition-no-changes.yaml"}, + Namespace: "default", + OutputFormat: "json", + ExpectedExitCode: dp.ExitCodeSuccess, + ExpectedStderrContains: []string{ + // Issue #474: the composites are still evaluated, but they are rendered with their existing + // compositionRevisionRef, because the name of the revision this would create cannot be + // predicted for a client-side-applied composition. Saying so is the honest position: if this + // composition's template read the revision name, the resulting change would go undetected. + "Could not predict the name of the CompositionRevision", + }, + ExpectedStructured: simtest.Expect(). + // The same advisory reaches warnings[] for machine consumers, keyed by composition so that two + // compositions declining to predict stay two warnings rather than being merged as duplicates. + WithWarning("Could not predict the name of the CompositionRevision"). + WithWarningContext(map[string]string{"composition": "xnopresources.diff.example.org"}). + And(). + WithComposition("xnopresources.diff.example.org"). + // createsRevision stays true while the name is withheld: the annotation delta is real, and a + // stale annotation genuinely would be rewritten and mint a revision. Only the identity is + // unknowable, which is why the guard makes the weak claim rather than "no revision". + WithRevisionImpact("metadata", true, 1). + WithoutPredictedRevisionName(). + WithAffectedResources(1, 0, 1, 0). + WithXRImpact("XNopResource", "test-resource", "default", "unchanged"), + }, + // Issue #500: the same asymmetry, but the cluster's last-applied-configuration is the one a + // client-side `kubectl apply` of this very file would write. Re-applying it computes the same + // annotation, the patch is empty and kubectl writes nothing, so no CompositionRevision is created. + // The sibling above differs only in that its annotation is stale, which a re-apply does rewrite. + "CompositionReappliedWithKubectlCreatesNoRevision": { + Reason: "A composition whose live last-applied-configuration already describes the file is unchanged, because re-applying it writes nothing", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/comp/resources/xrd.yaml", + "testdata/comp/resources/original-composition-kubectl-applied-current.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/functions.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/comp/resources/existing-xr-1-kubectl-applied-current.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-1.yaml", + ), + }, + InputFiles: []string{"testdata/comp/composition-no-changes.yaml"}, + Namespace: "default", + OutputFormat: "json", + ExpectedExitCode: dp.ExitCodeSuccess, + ExpectedStructured: simtest.Expect(). + WithNoWarnings(). + WithComposition("xnopresources.diff.example.org"). + WithImpactAnalysisSkipped(). + WithRevisionImpact("none", false, 0), + }, + // Issue #474: the case that makes "creates a revision but renders identically" a claim to be + // verified rather than assumed. The composition edit is metadata-only — one added label, identical + // spec — so the CompositionRevision it mints carries a byte-identical spec. But the template reads + // the revision's own name off the composite and propagates it into the composed resource, so the + // rendered output genuinely changes. + // + // Before the fix, the composite was rendered with its EXISTING compositionRevisionRef: configData + // came out as the old revision's name, matched the cluster, and the tool reported unchanged with + // exit 0. Seeding the predicted ref is what turns that into the exit 3 below. This is the + // regression test for the whole feature — stop comp's composition provider resolving the predicted + // revision, or stop the processor writing the resolved revision to the ref, and this is what fails. + // + // Note also what is NOT in the output: the composite's own + // spec.crossplane.compositionRevisionRef diff. The predicted ref is written only to the render input, + // never to the composite's dry-run payload, which is why the exit code reflects rendering rather than + // bookkeeping; predictedRevisionName is what keeps the downstream change interpretable without it. + "RevisionNamePropagatesToComposedResource": { + Reason: "A metadata-only composition edit whose template reads the CompositionRevision name changes rendered output, and is detected rather than assumed away", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/comp/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/revision-templating-composition.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/revision-templating-composition-revision.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/functions.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-revision-ref.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-revision-ref.yaml", + ), + }, + InputFiles: []string{"testdata/comp/revision-templating-updated-composition.yaml"}, + Namespace: "default", + OutputFormat: "json", + ExpectedExitCode: dp.ExitCodeDiffDetected, + ExpectedStructured: simtest.Expect(). + // The name was predictable, so the could-not-predict advisory must stay silent. + WithNoWarnings(). + WithComposition("xrevisionrefs.diff.example.org"). + WithRevisionImpact("metadata", true, 1). + // Matched by pattern, not literally: the suffix is the first 7 hex digits of the + // composition's hash, so pinning it would make any edit to the fixture a test failure. The + // derivation is pinned exactly by TestRevisionIdentity, which is where that belongs. + WithPredictedRevisionNamePattern(`^xrevisionrefs\.diff\.example\.org-[0-9a-f]{7}$`). + WithAffectedResources(1, 1, 0, 0). + WithXRImpact("XNopResource", "revision-ref-resource", "default", "changed"). + WithDownstreamSummary(0, 1, 0). + WithDownstreamResource("modified", "XDownstreamResource", "revision-ref-resource", "default"). + // The old value is the revision the composite currently tracks; the new one is the predicted + // revision, i.e. the thing this whole feature exists to surface. Both are real revision names, + // so both match the pattern above: only the exact values tell them apart. + WithFieldChange("spec.forProvider.configData", + "xrevisionrefs.diff.example.org-e36719b", "xrevisionrefs.diff.example.org-ec5496c"), + }, + // Issue #536 / #479: the same edit with --include-manual, which keeps a Manual composite for analysis + // without making it adopt the new revision. Only the Automatic composite is rendered with the + // predicted revision name; the Manual one stays pinned, so its template-read name, and with it its + // composed resource, is unchanged. Giving every composite the name would report the pinned one as + // changing to a revision it never uses. + "RevisionNameReachesOnlyRepointingCompositesWithIncludeManual": { + Reason: "Under --include-manual, the predicted revision name reaches an Automatic composite but not a Manual one pinned to an older revision", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/comp/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/revision-templating-composition.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/revision-templating-composition-revision.yaml", + "testdata/comp/resources/revision-templating-composition-older-revision.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/functions.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-revision-ref.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-revision-ref.yaml", + ), + integration.Composes( + "testdata/comp/resources/existing-xr-pinned-revision-ref.yaml", + "testdata/comp/resources/existing-downstream-pinned-revision-ref.yaml", + ), + }, + InputFiles: []string{"testdata/comp/revision-templating-updated-composition.yaml"}, + Namespace: "default", + IncludeManual: true, + OutputFormat: "json", + ExpectedExitCode: dp.ExitCodeDiffDetected, + ExpectedStructured: simtest.Expect(). + WithNoWarnings(). + WithComposition("xrevisionrefs.diff.example.org"). + // One composite re-points; the Manual one is kept but adopts nothing. + WithRevisionImpact("metadata", true, 1). + WithPredictedRevisionNamePattern(`^xrevisionrefs\.diff\.example\.org-[0-9a-f]{7}$`). + WithAffectedResources(2, 1, 1, 0). + WithXRImpact("XNopResource", "pinned-revision-ref-resource", "default", "unchanged"). + WithDownstreamSummary(0, 0, 0). + AndComp(). + WithXRImpact("XNopResource", "revision-ref-resource", "default", "changed"). + WithDownstreamSummary(0, 1, 0). + WithDownstreamResource("modified", "XDownstreamResource", "revision-ref-resource", "default"). + WithFieldChange("spec.forProvider.configData", + "xrevisionrefs.diff.example.org-e36719b", "xrevisionrefs.diff.example.org-ec5496c"), + }, + // Issue #498, the claim variant of RevisionNamePropagatesToComposedResource. A claim is rendered from its backing XR with the + // claim's spec synced in. Under Automatic the claim's own ref is never propagated (the XR controller + // owns the backing XR's), so seeding the claim would reach nothing: comp's composition provider is + // handed the backing XR the render consumes, and resolves that to the predicted revision. Without it, + // the backing XR's stale ref renders and the change goes unreported. + "ClaimRevisionNamePropagatesToComposedResource": { + Reason: "A metadata-only composition edit is rendered for an Automatic claim with the revision its backing XR would re-point to", + SetupFiles: []string{ + "testdata/comp/resources/claim-xrd.yaml", + "testdata/comp/resources/claim-revision-templating-composition.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/functions.yaml", + "testdata/comp/resources/test-namespace.yaml", + "testdata/comp/resources/existing-claim-revision-ref.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/comp/resources/existing-claim-revision-ref-xr.yaml", + "testdata/comp/resources/existing-claim-revision-ref-downstream.yaml", + ), + }, + XrdAPIVersion: integration.XRDV1, // the XRD is v1, so Crossplane writes the XR's resourceRefs at spec.resourceRefs + InputFiles: []string{"testdata/comp/claim-revision-templating-updated-composition.yaml"}, + Namespace: "test-namespace", + OutputFormat: "json", + ExpectedExitCode: dp.ExitCodeDiffDetected, + ExpectedStructured: simtest.Expect(). + // The claim's composed resource is labelled with its backing XR's name, which is the claim's + // owner, so no "already belongs to another composite" advisory is raised (#534). + WithNoWarnings(). + WithComposition("xrevisionrefclaims.diff.example.org"). + WithRevisionImpact("metadata", true, 1). + WithPredictedRevisionNamePattern(`^xrevisionrefclaims\.diff\.example\.org-[0-9a-f]{7}$`). + WithAffectedResources(1, 1, 0, 0). + WithXRImpact("NopClaim", "revision-ref-claim", "test-namespace", "changed"). + WithDownstreamSummary(0, 1, 0). + WithDownstreamResource("modified", "XDownstreamResource", "revision-ref-claim-xr", ""). + // The old value is the revision the backing XR tracks now; the new one is the predicted name. + WithFieldChange("spec.forProvider.configData", + "xrevisionrefclaims.diff.example.org-98b484d", "xrevisionrefclaims.diff.example.org-55c3a3b"), + }, + // Issue #498, the other half: when nothing is seeded, an Automatic claim renders against the + // revision its backing XR tracks now, not against no revision at all. Nothing is seeded here + // because the composition differs from the cluster's only by a stale kubectl annotation, so the + // name of the revision that would mint is unknowable (see CompositionAppliedWithKubectlEvaluatesXRs); + // the claim is still rendered, so the backing XR's ref is what the template reads. + "ClaimRendersWithBackingXRRevisionWhenNothingIsSeeded": { + Reason: "An Automatic claim with nothing seeded renders with its backing XR's compositionRevisionRef, so a template reading the revision name shows no spurious change", + SetupFiles: []string{ + "testdata/comp/resources/claim-xrd.yaml", + "testdata/comp/resources/claim-revision-templating-composition-kubectl-applied.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/functions.yaml", + "testdata/comp/resources/test-namespace.yaml", + "testdata/comp/resources/existing-claim-revision-ref-kubectl-applied.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/comp/resources/existing-claim-revision-ref-kubectl-applied-xr.yaml", + "testdata/comp/resources/existing-claim-revision-ref-kubectl-applied-downstream.yaml", + ), + }, + XrdAPIVersion: integration.XRDV1, // the XRD is v1, so Crossplane writes the XR's resourceRefs at spec.resourceRefs + InputFiles: []string{"testdata/comp/resources/claim-revision-templating-composition.yaml"}, + Namespace: "test-namespace", + OutputFormat: "json", + ExpectedExitCode: dp.ExitCodeSuccess, + ExpectedStderrContains: []string{"Could not predict the name of the CompositionRevision"}, + ExpectedStructured: simtest.Expect(). + // The could-not-predict advisory is the only warning: the claim's composed resource is labelled + // with its backing XR's name, so it raises no ownership advisory (#534). + WithWarning("Could not predict the name of the CompositionRevision"). + WithWarningContext(map[string]string{"composition": "xrevisionrefclaims.diff.example.org"}). + And(). + WithComposition("xrevisionrefclaims.diff.example.org"). + WithRevisionImpact("metadata", true, 1). + WithoutPredictedRevisionName(). + WithAffectedResources(1, 0, 1, 0). + WithXRImpact("NopClaim", "revision-ref-claim", "test-namespace", "unchanged"). + WithDownstreamSummary(0, 0, 0), + }, + // Issue #472: the same metadata-only change, with the user opting out of paying a render per + // composite for it. The composites go unevaluated — but the mutative consequence is still + // reported, which is what keeps --analyze-on a cost knob rather than a correctness mode. So the + // skip is recorded, and revisionImpact says a CompositionRevision is created and one composite + // would adopt it. Exit code 0: nothing renders differently, and a GitOps loop re-applying the + // same manifests must not fail its gate forever. + "AnalyzeOnSpecChangeSkipsMetadataOnlyChange": { + Reason: "--analyze-on=spec-change leaves a metadata-only change unevaluated, while still reporting that it creates a CompositionRevision", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/comp/resources/xrd.yaml", + "testdata/comp/resources/original-composition-kubectl-applied.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/functions.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/comp/resources/existing-xr-1-kubectl-applied.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-1.yaml", + ), + }, + InputFiles: []string{"testdata/comp/composition-no-changes.yaml"}, + Namespace: "default", + AnalyzeOn: "spec-change", + OutputFormat: "json", + ExpectedExitCode: dp.ExitCodeSuccess, + ExpectedStructured: simtest.Expect(). + WithComposition("xnopresources.diff.example.org"). + WithImpactAnalysisSkipped(). + WithRevisionImpact("metadata", true, 1). + // Issue #474: no predicted name here, and that absence is the point. The compositions + // differ by nothing but kubectl's last-applied-configuration, whose post-apply value is a + // function of how the user applies rather than of the file — so the hash, and therefore the + // revision's name, is unknowable. createsRevision stays true; only the identity is withheld. + WithoutPredictedRevisionName(), + }, + // The same setup with --analyze-unchanged evaluates the XRs after all (the pre-edit + // convergence-baseline workflow). Here the composed resource has drifted out of band, so the + // evaluation reports a downstream modification even though the composition is byte-identical + // to the cluster's. That delta is not caused by this composition — it is exactly the class of + // finding the default skip keeps out of the "impact of your composition change" report, and why + // opting in is explicit. + "UnchangedCompositionAnalyzeUnchangedEvaluatesXRs": { + Reason: "--analyze-unchanged evaluates affected XRs even though the composition is unchanged, surfacing deltas not caused by it", + SetupFiles: []string{ + "../../../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( + "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-1.yaml", + "testdata/comp/resources/existing-downstream-drifted.yaml", + ), + }, + InputFiles: []string{"testdata/comp/composition-no-changes.yaml"}, + Namespace: "default", + AnalyzeUnchanged: true, + OutputFormat: "json", + ExpectedExitCode: dp.ExitCodeDiffDetected, + ExpectedStructured: simtest.Expect(). + WithComposition("xnopresources.diff.example.org"). + WithAffectedResources(1, 1, 0, 0). + WithXRImpact("XNopResource", "test-resource", "default", "changed"). + WithDownstreamSummary(0, 1, 0). + WithDownstreamResource("modified", "XDownstreamResource", "test-resource", "default"). + WithFieldChange("spec.forProvider.configData", "drifted-value", "existing-value"), + }, + "CompositionDiffCustomNamespace": { + Reason: "Validates composition diff with custom namespace", + // Set up existing XRs in both custom and default namespaces to validate filtering + SetupFiles: []string{ + "../../../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", + // Create the custom namespace first + "testdata/comp/resources/custom-namespace.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + // Add existing XRs in custom namespace (should be included in output) + integration.Composes( + "testdata/comp/resources/existing-custom-ns-xr.yaml", + "testdata/comp/resources/existing-custom-ns-downstream.yaml", + ), + // Add existing XRs in default namespace (should be filtered out) + integration.Composes( + "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-1.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-1.yaml", + ), + }, + // New composition files that will be diffed + InputFiles: []string{"../../../internal/testutils/integration/testdata/comp/updated-composition.yaml"}, + Namespace: "custom-namespace", + // Expected output should only show custom-namespace resources, NOT default namespace resources + // This validates that namespace filtering works correctly + ExpectedOutput: ` +=== Composition Changes === + +~~~ Composition/xnopresources.diff.example.org + apiVersion: apiextensions.crossplane.io/v1 + kind: Composition + metadata: + name: xnopresources.diff.example.org + spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: +- configData: {{ .observed.composite.resource.spec.coolField }} +- resourceTier: basic ++ configData: updated-{{ .observed.composite.resource.spec.coolField }} ++ resourceTier: premium + kind: GoTemplate + source: Inline + step: generate-resources + - functionRef: + name: function-auto-ready + step: automatically-detect-ready-composed-resources + +--- + +Summary: 1 modified + +Applying this composition creates a new CompositionRevision, which 1 composite would adopt. + +=== Affected Composite Resources === + + ⚠ XNopResource/custom-namespace-resource (namespace: custom-namespace) + +Summary: 1 resource with changes + +=== Impact Analysis === + +~~~ XDownstreamResource/custom-namespace-resource + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + annotations: + crossplane.io/composition-resource-name: nop-resource + labels: + crossplane.io/composite: custom-namespace-resource + name: custom-namespace-resource + namespace: custom-namespace + spec: + forProvider: +- configData: custom-ns-existing-value +- resourceTier: basic ++ configData: updated-custom-ns-existing-value ++ resourceTier: premium + +--- + +Summary: 1 modified`, + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + NoColor: true, + }, + // Two compositions that BOTH genuinely change, each with its own affected XR. This is the + // aggregation case: one CompositionDiff entry per input, each carrying its own impact analysis, + // with hasDiffs ORed across them. MultipleCompositionDiffImpact below covers the mixed + // changed/unchanged variant; before this case existed, nothing covered two compositions that + // both report impacts — the second composition there has no affected XR at all. + "MultipleChangedCompositionsBothReportImpact": { + Reason: "Two changed compositions each report their own affected XR's downstream changes", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/comp/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/original-composition.yaml", + "testdata/comp/resources/original-composition-2.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/functions.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + // XR using composition 1 + integration.Composes( + "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-1.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-1.yaml", + ), + // XR using composition 2 + integration.Composes( + "testdata/comp/resources/existing-xr-v2.yaml", + "testdata/comp/resources/existing-downstream-v2.yaml", + ), + }, + InputFiles: []string{ + "../../../internal/testutils/integration/testdata/comp/updated-composition.yaml", + "testdata/comp/updated-composition-2-changed.yaml", + }, + Namespace: "default", + OutputFormat: "json", + ExpectedExitCode: dp.ExitCodeDiffDetected, + ExpectedStructured: simtest.Expect(). + WithComposition("xnopresources.diff.example.org"). + WithCompositionModified(). + WithAffectedResources(1, 1, 0, 0). + WithXRImpact("XNopResource", "test-resource", "default", "changed"). + WithDownstreamSummary(0, 1, 0). + WithDownstreamResource("modified", "XDownstreamResource", "test-resource", "default"). + WithFieldChange("spec.forProvider.configData", "existing-value", "updated-existing-value"). + WithFieldChange("spec.forProvider.resourceTier", "basic", "premium"). + AndComposition("xnopresources-v2.diff.example.org"). + WithCompositionModified(). + WithAffectedResources(1, 1, 0, 0). + WithXRImpact("XNopResource", "v2-resource", "default", "changed"). + WithDownstreamSummary(0, 1, 0). + WithDownstreamResource("modified", "XDownstreamResource", "v2-resource", "default"). + WithFieldChange("spec.forProvider.configData", "v2-updated-v2-existing-value", "v2-changed-v2-existing-value"). + WithFieldChange("spec.forProvider.resourceTier", "enterprise", "premium-plus"), + }, + // Note the second input composition (xnopresources-v2.diff.example.org) is byte-identical to its + // in-cluster version, so this case doubles as the mixed changed/unchanged scenario: the changed + // composition reports its impact while the unchanged one is skipped per-composition (issue + // #453). It is not a broken fixture. + "MultipleCompositionDiffImpact": { + Reason: "Validates multiple compositions in one invocation: the changed one reports impact on existing XRs, the unchanged one is skipped", + // Set up existing XRs that use both compositions + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/comp/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/original-composition.yaml", + "testdata/comp/resources/original-composition-2.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/functions.yaml", + }, + // Add existing XRs that use the compositions + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-1.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-1.yaml", + ), + integration.Composes( + "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-2.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-2.yaml", + ), + }, + // Multiple composition files that will be diffed + InputFiles: []string{ + "../../../internal/testutils/integration/testdata/comp/updated-composition.yaml", + "testdata/comp/updated-composition-2.yaml", + }, + Namespace: "default", + ExpectedOutput: ` +=== Composition Changes === + +~~~ Composition/xnopresources.diff.example.org + apiVersion: apiextensions.crossplane.io/v1 + kind: Composition + metadata: + name: xnopresources.diff.example.org + spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: +- configData: {{ .observed.composite.resource.spec.coolField }} +- resourceTier: basic ++ configData: updated-{{ .observed.composite.resource.spec.coolField }} ++ resourceTier: premium + kind: GoTemplate + source: Inline + step: generate-resources + - functionRef: + name: function-auto-ready + step: automatically-detect-ready-composed-resources + +--- + +Summary: 1 modified + +Applying this composition creates a new CompositionRevision, which 2 composites would adopt. + +=== Affected Composite Resources === + + ⚠ XNopResource/another-resource (namespace: default) + ⚠ XNopResource/test-resource (namespace: default) + +Summary: 2 resources with changes + +=== Impact Analysis === + +~~~ XDownstreamResource/another-resource + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + annotations: + crossplane.io/composition-resource-name: nop-resource + labels: + crossplane.io/composite: another-resource + name: another-resource + namespace: default + spec: + forProvider: +- configData: another-existing-value +- resourceTier: basic ++ configData: updated-another-existing-value ++ resourceTier: premium + +--- +~~~ XDownstreamResource/test-resource + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + annotations: + crossplane.io/composition-resource-name: nop-resource + labels: + crossplane.io/composite: test-resource + name: test-resource + namespace: default + spec: + forProvider: +- configData: existing-value +- resourceTier: basic ++ configData: updated-existing-value ++ resourceTier: premium + +--- + +Summary: 2 modified + +================================================================================ + +=== Composition Changes === + +No changes detected in composition xnopresources-v2.diff.example.org + +Impact analysis skipped: this composition is identical to the cluster's, so applying it creates no new CompositionRevision and no composite resource could change as a result. Pass --analyze-on=always to evaluate them anyway. +`, + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + NoColor: true, + }, + "CompositionDiffFiltersManualXRs": { + Reason: "Validates composition diff filters Manual policy XRs by default", + // Set up existing XRs - one with Automatic policy and one with Manual policy + SetupFiles: []string{ + "../../../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{ + // Add existing XR with Automatic policy (should be included) + integration.Composes( + "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-1.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-1.yaml", + ), + // Add existing XR with Manual policy (should be filtered out by default) + integration.Composes( + "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-manual.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-manual.yaml", + ), + }, + // Updated composition + InputFiles: []string{"../../../internal/testutils/integration/testdata/comp/updated-composition.yaml"}, + Namespace: "default", + // Expected output should only show test-resource (Automatic), NOT manual-resource (Manual) + ExpectedOutput: ` +=== Composition Changes === + +~~~ Composition/xnopresources.diff.example.org + apiVersion: apiextensions.crossplane.io/v1 + kind: Composition + metadata: + name: xnopresources.diff.example.org + spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: +- configData: {{ .observed.composite.resource.spec.coolField }} +- resourceTier: basic ++ configData: updated-{{ .observed.composite.resource.spec.coolField }} ++ resourceTier: premium + kind: GoTemplate + source: Inline + step: generate-resources + - functionRef: + name: function-auto-ready + step: automatically-detect-ready-composed-resources + +--- + +Summary: 1 modified + +Applying this composition creates a new CompositionRevision, which 1 composite would adopt. + +=== Affected Composite Resources === + + ⚠ XNopResource/test-resource (namespace: default) + +Summary: 1 resource with changes + +=== Impact Analysis === + +~~~ XDownstreamResource/test-resource + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + annotations: + crossplane.io/composition-resource-name: nop-resource + labels: + crossplane.io/composite: test-resource + name: test-resource + namespace: default + spec: + forProvider: +- configData: existing-value +- resourceTier: basic ++ configData: updated-existing-value ++ resourceTier: premium + +--- + +Summary: 1 modified`, + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + NoColor: true, + }, + "CompositionUpgradesResourceAPIVersion": { + // NOTE: This test validates that resources are correctly matched across API version changes, + // avoiding delete/recreate. The composition template changes from v1beta1 to v1beta2, but + // Kubernetes automatically converts resources between served API versions. When we query for + // the v1beta2 resource, Kubernetes finds the v1beta1 resource and returns it auto-converted + // to v1beta2. From Kubernetes' perspective, the resource exists as both versions simultaneously, + // so there's no apiVersion field change to show in the diff. The important thing is that the + // resource is matched, preventing delete/recreate operations: since the stored object is + // identical at either version it renders unchanged, where an unmatched one would show as a + // +++ addition and a --- removal. The composition diff itself WILL show the template change + // from v1beta1 to v1beta2. + // + // As in TestDiffIntegration/CompositionRevisionUpgradesResourceAPIVersion, the XR composes the + // resource as Crossplane leaves it, named in its resourceRefs at v1beta1, so this also checks + // that it is not reported as removed (#552). + Reason: "Validates composition upgrade that changes resource API version shows as update not remove/add", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/comp/resources/xrd.yaml", + // NOTE: xapimigrate CRD is auto-loaded from testdata/comp/crds/ + // We apply no XRD for XApiMigrateResource because it is a regular managed resource + // (not a composite), and an XRD would make it composite, causing infinite recursion. + "testdata/comp/resources/api-version-original-composition.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/functions.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + { + OwnerFile: "testdata/comp/resources/existing-api-version-xr.yaml", + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + "testdata/comp/resources/existing-api-version-downstream-v1beta1.yaml": nil, + }, + }, + }, + InputFiles: []string{"testdata/comp/api-version-updated-composition.yaml"}, + Namespace: "default", + ExpectedOutput: ` +=== Composition Changes === + +~~~ Composition/xapimigrateresources.example.org + apiVersion: apiextensions.crossplane.io/v1 + kind: Composition + metadata: + name: xapimigrateresources.example.org + spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + inline: + template: | +- apiVersion: comp.example.org/v1beta1 ++ apiVersion: comp.example.org/v1beta2 + kind: XApiMigrateResource + metadata: + name: {{ .observed.composite.resource.metadata.name }}-api-resource + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: api-migrate-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + kind: GoTemplate + source: Inline + step: generate-api-versioned-resources + - functionRef: + name: function-auto-ready + step: automatically-detect-ready-composed-resources + +--- + +Summary: 1 modified + +Applying this composition creates a new CompositionRevision, which 1 composite would adopt. + +=== Affected Composite Resources === + + ✓ XNopResource/test-api-version (namespace: default) + +Summary: 1 resource unchanged + +=== Impact Analysis === + +All composite resources are up-to-date. No downstream resource changes detected. +`, + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + NoColor: true, + }, + "NetNewCompositionNoImpact": { + Reason: "Validates net-new composition with no downstream impact", + // Set up cluster with existing resources but no composition that matches the new one + SetupFiles: []string{ + "../../../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", + }, + // Add existing XRs that use different compositions (won't be affected) + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-1.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-1.yaml", + ), + }, + // Net-new composition file that doesn't exist in cluster and targets different XR type + InputFiles: []string{"testdata/comp/net-new-composition.yaml"}, + Namespace: "default", + ExpectedOutput: ` +=== Composition Changes === + ++++ Composition/xnewresources.diff.example.org ++ apiVersion: apiextensions.crossplane.io/v1 ++ kind: Composition ++ metadata: ++ labels: ++ environment: staging ++ provider: aws ++ name: xnewresources.diff.example.org ++ spec: ++ compositeTypeRef: ++ apiVersion: staging.diff.example.org/v1alpha1 ++ kind: XNewResource ++ mode: Pipeline ++ pipeline: ++ - functionRef: ++ name: function-go-templating ++ input: ++ apiVersion: template.fn.crossplane.io/v1beta1 ++ inline: ++ template: | ++ apiVersion: staging.nop.example.org/v1alpha1 ++ kind: XStagingResource ++ metadata: ++ name: {{ .observed.composite.resource.metadata.name }} ++ namespace: {{ .observed.composite.resource.metadata.namespace }} ++ annotations: ++ gotemplating.fn.crossplane.io/composition-resource-name: staging-resource ++ spec: ++ forProvider: ++ environment: staging ++ configData: new-{{ .observed.composite.resource.spec.newField }} ++ resourceTier: premium ++ kind: GoTemplate ++ source: Inline ++ step: generate-new-resources ++ - functionRef: ++ name: function-auto-ready ++ step: automatically-detect-ready-composed-resources + +--- + +Summary: 1 added + +No XRs found using composition xnewresources.diff.example.org`, + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + NoColor: true, + }, + "CompositionChangeWithUnchangedDownstreamResources": { + Reason: "Validates status indicators for unchanged downstream resources", + // Set up existing XRs that use the original composition + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/comp/resources/xrd.yaml", + "testdata/comp/resources/status-indicator-composition.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/functions.yaml", + }, + // Add existing XRs that use the composition + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/comp/resources/status-xr-1.yaml", + "testdata/comp/resources/status-downstream-1.yaml", + ), + integration.Composes( + "testdata/comp/resources/status-xr-2.yaml", + "testdata/comp/resources/status-downstream-2.yaml", + ), + }, + // Updated composition with only metadata change (no downstream impact) + InputFiles: []string{"testdata/comp/status-indicator-updated-composition.yaml"}, + Namespace: "default", + ExpectedOutput: ` +=== Composition Changes === + +~~~ Composition/xstatus.diff.example.org + apiVersion: apiextensions.crossplane.io/v1 + kind: Composition + metadata: ++ labels: ++ environment: production + name: xstatus.diff.example.org + spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: stable-value + resourceTier: standard + kind: GoTemplate + source: Inline + step: generate-resources + - functionRef: + name: function-auto-ready + step: automatically-detect-ready-composed-resources + +--- + +Summary: 1 modified + +Applying this composition creates a new CompositionRevision, which 2 composites would adopt. + +=== Affected Composite Resources === + + ✓ XNopResource/status-test-xr-1 (namespace: default) + ✓ XNopResource/status-test-xr-2 (namespace: default) + +Summary: 2 resources unchanged + +=== Impact Analysis === + +All composite resources are up-to-date. No downstream resource changes detected.`, + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + NoColor: true, + }, + "CompositionChangeWithMixedStatusAndColors": { + Reason: "Validates status indicators with colorization for mixed changed/unchanged XRs", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/comp/resources/xrd.yaml", + "testdata/comp/resources/mixed-status-composition.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/functions.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + // XR 1 with downstream resources (standard tier - will change) + integration.Composes( + "testdata/comp/resources/mixed-xr-1.yaml", + "testdata/comp/resources/mixed-downstream-db-1.yaml", + "testdata/comp/resources/mixed-downstream-storage-1.yaml", + "testdata/comp/resources/mixed-downstream-network-1.yaml", + ), + // XR 2 with downstream resources (standard tier - will change) + integration.Composes( + "testdata/comp/resources/mixed-xr-2.yaml", + "testdata/comp/resources/mixed-downstream-db-2.yaml", + "testdata/comp/resources/mixed-downstream-storage-2.yaml", + "testdata/comp/resources/mixed-downstream-network-2.yaml", + ), + // XR 3 with downstream resources (already premium tier - no change) + integration.Composes( + "testdata/comp/resources/mixed-xr-3.yaml", + "testdata/comp/resources/mixed-downstream-db-3.yaml", + "testdata/comp/resources/mixed-downstream-storage-3.yaml", + "testdata/comp/resources/mixed-downstream-network-3.yaml", + ), + }, + InputFiles: []string{"testdata/comp/mixed-status-updated-composition.yaml"}, + Namespace: "default", + ExpectedOutput: strings.Join([]string{ + ` +=== Composition Changes === + +~~~ Composition/xmixed.diff.example.org + apiVersion: apiextensions.crossplane.io/v1 + kind: Composition + metadata: + name: xmixed.diff.example.org + spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + inline: + template: | + --- + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }}-database + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: database + spec: + forProvider: + resourceType: database +`, tu.Red(`- tier: standard`), ` +`, tu.Green(`+ tier: premium`), ` +`, tu.Green(`+ backupEnabled: true`), ` + --- + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }}-storage + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: storage + spec: + forProvider: + resourceType: storage + tier: standard + --- + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }}-network + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: network + spec: + forProvider: + resourceType: network + tier: standard + kind: GoTemplate + source: Inline + step: generate-resources + - functionRef: + name: function-auto-ready + step: automatically-detect-ready-composed-resources + +--- + +Summary: 1 modified + +Applying this composition creates a new CompositionRevision, which 3 composites would adopt. + +=== Affected Composite Resources === + +`, tu.Yellow(` ⚠ XNopResource/mixed-test-xr-1 (namespace: default)`), ` +`, tu.Yellow(` ⚠ XNopResource/mixed-test-xr-2 (namespace: default)`), ` +`, tu.Green(` ✓ XNopResource/mixed-test-xr-3 (namespace: default)`), ` + +Summary: 2 resources with changes, 1 resource unchanged + +=== Impact Analysis === + +~~~ XDownstreamResource/mixed-test-xr-1-database + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + annotations: + crossplane.io/composition-resource-name: database + labels: + crossplane.io/composite: mixed-test-xr-1 + name: mixed-test-xr-1-database + namespace: default + spec: + forProvider: +`, tu.Red(`- resourceType: database`), ` +`, tu.Red(`- tier: standard`), ` +`, tu.Green(`+ backupEnabled: true`), ` +`, tu.Green(`+ resourceType: database`), ` +`, tu.Green(`+ tier: premium`), ` + + + +--- +~~~ XDownstreamResource/mixed-test-xr-2-database + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + annotations: + crossplane.io/composition-resource-name: database + labels: + crossplane.io/composite: mixed-test-xr-2 + name: mixed-test-xr-2-database + namespace: default + spec: + forProvider: +`, tu.Red(`- resourceType: database`), ` +`, tu.Red(`- tier: standard`), ` +`, tu.Green(`+ backupEnabled: true`), ` +`, tu.Green(`+ resourceType: database`), ` +`, tu.Green(`+ tier: premium`), ` + + + +--- + +Summary: 2 modified +`, + }, ""), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + NoColor: false, + }, + "CompositionChangeImpactsClaims": { + Reason: "Validates composition change impacts existing Claims (issue #120)", + // Set up existing Claims and their XRs that use the original composition + SetupFiles: []string{ + // XRD, composition, and functions + "testdata/comp/resources/claim-xrd.yaml", + "testdata/comp/resources/claim-composition.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/functions.yaml", + // Test namespace + "testdata/comp/resources/test-namespace.yaml", + // Existing Claims and their corresponding XRs + "testdata/comp/resources/existing-claim-1.yaml", + "testdata/comp/resources/existing-claim-2.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/comp/resources/existing-claim-1-xr.yaml", + "testdata/comp/resources/existing-claim-downstream-1.yaml", + ), + integration.Composes( + "testdata/comp/resources/existing-claim-2-xr.yaml", + "testdata/comp/resources/existing-claim-downstream-2.yaml", + ), + }, + XrdAPIVersion: integration.XRDV1, // the XRD is v1, so Crossplane writes the XR's resourceRefs at spec.resourceRefs + // Updated composition that will be diffed + InputFiles: []string{"testdata/comp/updated-claim-composition.yaml"}, + Namespace: "test-namespace", + ExpectedOutput: ` +=== Composition Changes === + +~~~ Composition/nopclaims.diff.example.org + apiVersion: apiextensions.crossplane.io/v1 + kind: Composition + metadata: + name: nopclaims.diff.example.org + spec: + compositeTypeRef: + apiVersion: diff.example.org/v1alpha1 + kind: XNop + mode: Pipeline + pipeline: + - functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + inline: + template: | + apiVersion: nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + name: {{ .observed.composite.resource.metadata.name }} + spec: + forProvider: +- configData: {{ .observed.composite.resource.spec.coolField }} +- resourceTier: basic ++ configData: updated-{{ .observed.composite.resource.spec.coolField }} ++ resourceTier: premium + kind: GoTemplate + source: Inline + step: generate-resources + - functionRef: + name: function-auto-ready + step: automatically-detect-ready-composed-resources + +--- + +Summary: 1 modified + +Applying this composition creates a new CompositionRevision, which 2 composites would adopt. + +=== Affected Composite Resources === + + ⚠ NopClaim/test-claim-1 (namespace: test-namespace) + ⚠ NopClaim/test-claim-2 (namespace: test-namespace) + +Summary: 2 resources with changes + +=== Impact Analysis === + +~~~ XDownstreamResource/test-claim-1-xr + apiVersion: nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + annotations: + crossplane.io/composition-resource-name: nop-resource + labels: + crossplane.io/claim-name: test-claim-1 + crossplane.io/claim-namespace: test-namespace + crossplane.io/composite: test-claim-1-xr + name: test-claim-1-xr + spec: + forProvider: +- configData: claim-value-1 +- resourceTier: basic ++ configData: updated-claim-value-1 ++ resourceTier: premium + +--- +~~~ XDownstreamResource/test-claim-2-xr + apiVersion: nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + annotations: + crossplane.io/composition-resource-name: nop-resource + labels: + crossplane.io/claim-name: test-claim-2 + crossplane.io/claim-namespace: test-namespace + crossplane.io/composite: test-claim-2-xr + name: test-claim-2-xr + spec: + forProvider: +- configData: claim-value-2 +- resourceTier: basic ++ configData: updated-claim-value-2 ++ resourceTier: premium + +--- + +Summary: 2 modified`, + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + NoColor: true, + }, + // Without -n every namespace is listed, so discovery finds each claim and its cluster-scoped + // backing XR. The claim renders through that XR, so the XR is dropped: each change is reported + // once, under the claim. + "CompositionChangeImpactsClaimsAllNamespaces": { + Reason: "A claim's backing XR discovered alongside it is not reported a second time", + SetupFiles: []string{ + "testdata/comp/resources/claim-xrd.yaml", + "testdata/comp/resources/claim-composition.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/functions.yaml", + "testdata/comp/resources/test-namespace.yaml", + "testdata/comp/resources/existing-claim-1.yaml", + "testdata/comp/resources/existing-claim-2.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/comp/resources/existing-claim-1-xr.yaml", + "testdata/comp/resources/existing-claim-downstream-1.yaml", + ), + integration.Composes( + "testdata/comp/resources/existing-claim-2-xr.yaml", + "testdata/comp/resources/existing-claim-downstream-2.yaml", + ), + }, + XrdAPIVersion: integration.XRDV1, + InputFiles: []string{"testdata/comp/updated-claim-composition.yaml"}, + OutputFormat: "json", + ExpectedExitCode: dp.ExitCodeDiffDetected, + ExpectedStructured: simtest.Expect(). + WithComposition("nopclaims.diff.example.org"). + WithCompositionModified(). + WithAffectedResources(2, 2, 0, 0). + WithXRImpact("NopClaim", "test-claim-1", "test-namespace", "changed").AndComp(). + WithXRImpact("NopClaim", "test-claim-2", "test-namespace", "changed"), + }, + // Composition mode checks the composites it renders for overlaps as xr mode checks its inputs: + // two XRs on one composition that render one fixed-name child differently would both control it. + // Contention is checked before differing renderings, so it is the error the run fails with. + "SharedChildDifferingRendersFail": { + Reason: "Two XRs a composition change renders one child for, differently, fail the run", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/shared-child-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + "testdata/comp/resources/shared-child-xrs-differing.yaml", + }, + InputFiles: []string{"testdata/comp/updated-shared-child-composition.yaml"}, + Namespace: "default", + ExpectedError: true, + ExpectedExitCode: dp.ExitCodeToolError, + ExpectedErrorContains: `resource "ns.nop.example.org/v1alpha1/XDownstreamResource/default/shared-child" would be ` + + `controlled by more than one XR (XNopResource/shared-child-owner-a, XNopResource/shared-child-owner-b)`, + }, + "SharedChildContendedEvenWhenIdentical": { + Reason: "Two XRs a composition change renders one child for, identically, still contend for it", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/shared-child-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + "../../../internal/testutils/integration/testdata/diff/shared-child-xrs.yaml", + }, + InputFiles: []string{"testdata/comp/updated-shared-child-composition.yaml"}, + Namespace: "default", + ExpectedError: true, + ExpectedExitCode: dp.ExitCodeToolError, + ExpectedErrorContains: `resource "ns.nop.example.org/v1alpha1/XDownstreamResource/default/shared-child" would be ` + + `controlled by more than one XR (XNopResource/shared-child-owner-a, XNopResource/shared-child-owner-b)`, + }, + "SSAFieldRemovalDetection": { + Reason: "Validates that field removals are correctly detected when resources are created via Server-Side Apply with Crossplane's field manager (issue #121)", + // Set up XRD, functions, and original composition with the removable field + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/comp/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/functions.yaml", + "testdata/comp/resources/field-removal/composition-with-field.yaml", + }, + // XR and downstream resource applied via SSA with Crossplane field manager + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + { + OwnerFile: "testdata/comp/resources/field-removal/existing-xr.yaml", + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + "testdata/comp/resources/field-removal/existing-downstream.yaml": nil, + }, + }, + }, + // Updated composition that removes the removableField + InputFiles: []string{"testdata/comp/field-removal-updated-composition.yaml"}, + Namespace: "default", + ExpectedOutput: ` +=== Composition Changes === + +~~~ Composition/xfieldremoval.diff.example.org + apiVersion: apiextensions.crossplane.io/v1 + kind: Composition + metadata: + name: xfieldremoval.diff.example.org + spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} +- removableField: will-be-removed + kind: GoTemplate + source: Inline + step: generate-resources + - functionRef: + name: function-auto-ready + step: automatically-detect-ready-composed-resources + +--- + +Summary: 1 modified + +Applying this composition creates a new CompositionRevision, which 1 composite would adopt. + +=== Affected Composite Resources === + + ⚠ XNopResource/field-removal-test (namespace: default) + +Summary: 1 resource with changes + +=== Impact Analysis === + +~~~ XDownstreamResource/field-removal-test + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + annotations: + crossplane.io/composition-resource-name: nop-resource + labels: + crossplane.io/composite: field-removal-test + name: field-removal-test + namespace: default + spec: + forProvider: + configData: test-value +- removableField: will-be-removed + +--- + +Summary: 1 modified`, + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + NoColor: true, + }, + "SHA256DigestFunctionReference": { + Reason: "Validates composition diff works with functions referenced by SHA256 digest instead of tag", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/comp/resources/xrd.yaml", + "testdata/comp/resources/sha256-composition.yaml", + "testdata/comp/resources/functions-sha256.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/comp/resources/existing-sha256-xr.yaml", + "testdata/comp/resources/existing-sha256-downstream.yaml", + ), + }, + InputFiles: []string{"testdata/comp/updated-sha256-composition.yaml"}, + Namespace: "default", + ExpectedOutput: ` +=== Composition Changes === + +~~~ Composition/xnopresources-sha256.diff.example.org + apiVersion: apiextensions.crossplane.io/v1 + kind: Composition + metadata: + name: xnopresources-sha256.diff.example.org + spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - functionRef: + name: function-go-templating-sha256 + input: + apiVersion: template.fn.crossplane.io/v1beta1 + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: +- configData: {{ .observed.composite.resource.spec.coolField }} +- resourceTier: basic ++ configData: updated-{{ .observed.composite.resource.spec.coolField }} ++ resourceTier: premium + kind: GoTemplate + source: Inline + step: generate-resources + - functionRef: + name: function-auto-ready + step: automatically-detect-ready-composed-resources + +--- + +Summary: 1 modified + +Applying this composition creates a new CompositionRevision, which 1 composite would adopt. + +=== Affected Composite Resources === + + ⚠ XNopResource/sha256-test-resource (namespace: default) + +Summary: 1 resource with changes + +=== Impact Analysis === + +~~~ XDownstreamResource/sha256-test-resource + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + annotations: + crossplane.io/composition-resource-name: nop-resource + labels: + crossplane.io/composite: sha256-test-resource + name: sha256-test-resource + namespace: default + spec: + forProvider: +- configData: sha256-value +- resourceTier: basic ++ configData: updated-sha256-value ++ resourceTier: premium + +--- + +Summary: 1 modified`, + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + NoColor: true, + }, + // Issue #485, comp's half. comp re-renders each discovered composite through DiffSingleResource, + // the same recursion xr uses, so one fix covers both — this pins that for comp rather than + // assuming it. comp's own error only counts the failures, so the assertion that matters is that + // the depth message reaches the failing composite's impact entry: without it, a user would be + // told something failed but not that the composition recurses into itself. + "CyclicCompositionStopsAtMaxNestedDepth": { + Reason: "comp shares xr's recursion path (it enters via DiffSingleResource), so a composition cycle must stop at --max-nested-depth here too, and say so on the failing composite (#485)", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/cycle/xrds.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/cycle/compositions.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/functions.yaml", + // Composed nothing: with a cyclic composition, Crossplane would nest XRs without end, so + // the case models the XR before its first compose rather than an endless tree. + "testdata/comp/resources/cycle/existing-cycle-xr.yaml", + }, + InputFiles: []string{"testdata/comp/updated-cycle-composition.yaml"}, + Namespace: "default", + ExpectedError: true, + ExpectedErrorContains: "impact analysis failed for 1 XR(s)", + ExpectedExitCode: dp.ExitCodeToolError, + ExpectedStructured: simtest.Expect(). + WithComposition("xcycleas.cycle.example.org"). + WithAffectedResources(1, 0, 0, 1). + WithXRImpact("XCycleA", "test-cycle", "default", "error"). + WithErrorContaining("maximum nesting depth exceeded"), + }, + // The same failure, asserting the composite's error is recorded against it in the top-level errors[]. + "CyclicCompositionStopsAtMaxNestedDepthV2": { + Reason: "A failing composite's error is a structured errors[] entry recorded against it", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/cycle/xrds.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/cycle/compositions.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/functions.yaml", + // Composed nothing, as in CyclicCompositionStopsAtMaxNestedDepth. + "testdata/comp/resources/cycle/existing-cycle-xr.yaml", + }, + InputFiles: []string{"testdata/comp/updated-cycle-composition.yaml"}, + Namespace: "default", + ExpectedError: true, + ExpectedErrorContains: "impact analysis failed for 1 XR(s)", + ExpectedExitCode: dp.ExitCodeToolError, + ExpectedStructured: simtest.Expect(). + WithErrorID("XCycleA/test-cycle"). + WithComposition("xcycleas.cycle.example.org"). + WithAffectedResources(1, 0, 0, 1). + WithXRImpact("XCycleA", "test-cycle", "default", "error"). + WithErrorContaining("maximum nesting depth exceeded"). + WithErrorResourceID("XCycleA/test-cycle"), + }, + "NestedXRUsesOwnComposition": { + Reason: "Validates that nested XRs use their own composition from the cluster, not the parent's CLI composition", + // Set up XRDs, compositions, and functions + SetupFiles: []string{ + // Child XRD and composition (XNopResource uses xnopresources.diff.example.org) + "../../../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", + // Parent XRD and composition + "testdata/comp/resources/nested-xr/parent-xrd.yaml", + "testdata/comp/resources/nested-xr/parent-composition.yaml", + }, + // Use crossplaneManagedResources to set up proper ownership hierarchy + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + { + // Parent XR owns both direct downstream and nested child XR + OwnerFile: "testdata/comp/resources/nested-xr/existing-parent-xr.yaml", + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + // Direct downstream owned by parent XR + "testdata/comp/resources/nested-xr/existing-parent-direct-downstream.yaml": nil, + // Nested child XR owned by parent XR, which owns its own downstream + "testdata/comp/resources/nested-xr/existing-nested-child-xr.yaml": { + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + // Child's downstream owned by nested child XR + "testdata/comp/resources/nested-xr/existing-child-downstream.yaml": nil, + }, + }, + }, + }, + }, + // Diff the updated parent composition + InputFiles: []string{"testdata/comp/updated-parent-composition.yaml"}, + Namespace: "default", + // Expected: Only the parent's direct downstream shows changes (tier change). + // The nested child's downstream should NOT change because: + // 1. The nested XR (XNopResource) uses its own composition (xnopresources.diff.example.org) + // 2. That child composition was NOT modified + // Bug scenario: If nested XR incorrectly uses parent composition, we'd see wrong changes to child's downstream. + // Expected output: Only the parent's direct downstream should show in the Impact Analysis. + // The child's downstream (XDownstreamResource/test-parent-nested) should NOT appear here. + // If it did, it would mean the nested XR incorrectly used the parent's CLI composition. + ExpectedOutput: `=== Impact Analysis === + +~~~ XDownstreamResource/test-parent-direct + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + annotations: + crossplane.io/composition-resource-name: direct-resource + labels: + crossplane.io/composite: test-parent + name: test-parent-direct + namespace: default + spec: + forProvider: + configData: parent-parent-value +- resourceTier: parent-tier ++ resourceTier: premium-tier + +--- + +Summary: 1 modified`, + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + NoColor: true, + }, + "CrossNamespaceResourceCollision": { + Reason: "Validates that resources with the same name in different namespaces are correctly distinguished", + Skip: true, + SkipReason: "Blocked on https://github.com/crossplane-contrib/function-extra-resources/issues/106. " + + "function-extra-resources (v0.2.0/v0.3.0) emits ResourceSelector{Namespace=\"\"} for " + + "by-name references that omit `ref.namespace` — it only forwards user-yaml verbatim, " + + "never inferring from the XR's namespace. Crossplane v2.3+'s render binary then does " + + "InMemoryClient.Get(name, namespace=\"\"), a strict (GVK, ns, name) match that doesn't " + + "find our resolved ConfigMap (stored at namespace=ns-{a,b}). By-label selectors aren't " + + "affected — List(InNamespace(\"\")) lists across all namespaces — which is why our " + + "other extra-resources tests still pass. Re-enable once a function release defaults " + + "selector.Namespace to the XR's namespace.", + Timeout: simharness.LongTimeout, // Test involves multiple namespaces and can be slow with Docker contention + // This test catches a bug where the cache key didn't include namespace, causing + // ConfigMaps with the same name in different namespaces to collide. + // The XR in ns-a should get ConfigMap from ns-a (value: from-ns-a) + // The XR in ns-b should get ConfigMap from ns-b (value: from-ns-b) + // Bug: Both XRs would get the same ConfigMap (whichever was cached first) + SetupFiles: []string{ + // Create namespaces first + "testdata/comp/resources/namespace-collision/namespace-ns-a.yaml", + "testdata/comp/resources/namespace-collision/namespace-ns-b.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/functions.yaml", + "testdata/comp/resources/namespace-collision/collision-composition.yaml", + "testdata/comp/resources/namespace-collision/collision-composition-revision.yaml", + // ConfigMaps with same name in different namespaces + "testdata/comp/resources/namespace-collision/configmap-ns-a.yaml", + "testdata/comp/resources/namespace-collision/configmap-ns-b.yaml", + }, + // XRs and their downstream resources in different namespaces + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + { + OwnerFile: "testdata/comp/resources/namespace-collision/existing-xr-ns-a.yaml", + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + "testdata/comp/resources/namespace-collision/existing-downstream-ns-a.yaml": nil, + }, + }, + { + OwnerFile: "testdata/comp/resources/namespace-collision/existing-xr-ns-b.yaml", + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + "testdata/comp/resources/namespace-collision/existing-downstream-ns-b.yaml": nil, + }, + }, + }, + InputFiles: []string{"testdata/comp/updated-collision-composition.yaml"}, + // Expected: Each XR's downstream should show the correct prefix applied to its namespace's ConfigMap value. + // XR in ns-a: from-ns-a → prefix-from-ns-a + // XR in ns-b: from-ns-b → prefix-from-ns-b + // Bug scenario: If namespace collision occurs, ns-b would incorrectly show: + // from-ns-b → prefix-from-ns-a (wrong ConfigMap cached from ns-a) + ExpectedOutput: `=== Impact Analysis === + +~~~ XDownstreamResource/xr-in-ns-a + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + annotations: + crossplane.io/composition-resource-name: collision-resource + labels: + crossplane.io/composite: xr-in-ns-a + name: xr-in-ns-a + namespace: ns-a + spec: + forProvider: +- configData: from-ns-a ++ configData: prefix-from-ns-a + +--- +~~~ XDownstreamResource/xr-in-ns-b + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + annotations: + crossplane.io/composition-resource-name: collision-resource + labels: + crossplane.io/composite: xr-in-ns-b + name: xr-in-ns-b + namespace: ns-b + spec: + forProvider: +- configData: from-ns-b ++ configData: prefix-from-ns-b + +--- + +Summary: 2 modified`, + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + NoColor: true, + }, + // Paired comp sequencer-gating tests — mirror of the XR pair. Verifies that + // --eventual-state on `comp` surfaces downstream resources hidden by + // function-sequencer in the impact analysis. + "CompSequencerGatesLaterStageWithoutEventualState": { + Reason: "`comp` without --eventual-state: sequencer-gated stage1 is absent from the impact analysis", + Timeout: simharness.LongTimeout, + OutputFormat: "json", + // eventualState deliberately false + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/sequencer-gating-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + // The XR references sequencer-gating-composition by name, so comp discovers it. It has + // composed stage0, which isn't Ready, so stage1 stays gated. + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes("testdata/comp/resources/existing-sequencer-gating-xr.yaml", "testdata/comp/resources/existing-sequencer-gating-stage0.yaml"), + }, + InputFiles: []string{"testdata/comp/updated-sequencer-gating-composition.yaml"}, + Namespace: "default", + ExpectedExitCode: dp.ExitCodeDiffDetected, + // Expect: impact shows stage0 modified (its configData changed); stage1 is absent from + // downstream impact because sequencer gates it. + ExpectedStructured: simtest.Expect(). + WithComposition("sequencer-gating-composition"). + WithCompositionModified(). + WithXRImpact("XNopResource", "sequencer-gating-test", "default", "changed"). + WithDownstreamSummary(0, 1, 0). + WithDownstreamResource("modified", "XDownstreamResource", "0-stage0-resource", "default"). + WithFieldChange("spec.forProvider.configData", "existing-value", "updated-existing-value"), + ExpectedError: false, + }, + "CompSequencerGatedStageAppearsWithEventualState": { + Reason: "`comp` with --eventual-state: sequencer-gated stage1 surfaces in the impact analysis", + Timeout: simharness.LongTimeout, // Multiple simulation iterations need more time + OutputFormat: "json", + EventualState: true, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/sequencer-gating-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + // The XR has composed stage0, which isn't Ready, so stage1 stays gated until eventual + // state synthesizes stage0's Ready (#564). + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes("testdata/comp/resources/existing-sequencer-gating-xr.yaml", "testdata/comp/resources/existing-sequencer-gating-stage0.yaml"), + }, + InputFiles: []string{"testdata/comp/updated-sequencer-gating-composition.yaml"}, + Namespace: "default", + ExpectedExitCode: dp.ExitCodeDiffDetected, + // Expect: impact shows stage0 modified and stage1 added (eventual-state unlocks stage1). + ExpectedStructured: simtest.Expect(). + WithComposition("sequencer-gating-composition"). + WithCompositionModified(). + WithXRImpact("XNopResource", "sequencer-gating-test", "default", "changed"). + WithDownstreamSummary(1, 1, 0). + WithDownstreamResource("modified", "XDownstreamResource", "0-stage0-resource", "default"). + WithFieldChange("spec.forProvider.configData", "existing-value", "updated-existing-value"). + AndXR(). + WithDownstreamResource("added", "XDownstreamResource", "1-stage1-resource", "default"). + // dryRun absence through `comp`: a downstream addition that reached the apiserver + // carries no dryRun object. Piggy-backed on an existing case rather than given its + // own envtest instance, since it is a pure additional assertion. + WithDryRunPerformed(). + WithField("spec.forProvider.configData", "updated-existing-value"), + ExpectedError: false, + }, + // Paired comp composition-driven gating tests — gating encoded in go-templating, + // no function-sequencer. Mirrors the XR composition-driven pair. + "CompConditionalGatingHidesLaterStageWithoutEventualState": { + Reason: "`comp` without --eventual-state: template-gated stage1 is absent from the impact analysis", + Timeout: simharness.LongTimeout, + OutputFormat: "json", + // eventualState deliberately false + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/conditional-gating-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + // The XR has composed stage0, which isn't Ready, so stage1 stays gated. + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes("testdata/comp/resources/existing-conditional-gating-xr.yaml", "testdata/comp/resources/existing-conditional-gating-stage0.yaml"), + }, + InputFiles: []string{"testdata/comp/updated-conditional-gating-composition.yaml"}, + Namespace: "default", + ExpectedExitCode: dp.ExitCodeDiffDetected, + ExpectedStructured: simtest.Expect(). + WithComposition("conditional-gating-composition"). + WithCompositionModified(). + WithXRImpact("XNopResource", "conditional-gating-test", "default", "changed"). + WithDownstreamSummary(0, 1, 0). + WithDownstreamResource("modified", "XDownstreamResource", "0-stage0-resource", "default"). + WithFieldChange("spec.forProvider.configData", "existing-value", "updated-existing-value"), + ExpectedError: false, + }, + "CompConditionalGatedStageAppearsWithEventualState": { + Reason: "`comp` with --eventual-state: template-gated stage1 surfaces in the impact analysis", + Timeout: simharness.LongTimeout, // Multiple simulation iterations need more time + OutputFormat: "json", + EventualState: true, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/conditional-gating-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + // The XR has composed stage0, which isn't Ready, so stage1 stays gated until eventual + // state synthesizes stage0's Ready (#564). + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes("testdata/comp/resources/existing-conditional-gating-xr.yaml", "testdata/comp/resources/existing-conditional-gating-stage0.yaml"), + }, + InputFiles: []string{"testdata/comp/updated-conditional-gating-composition.yaml"}, + Namespace: "default", + ExpectedExitCode: dp.ExitCodeDiffDetected, + ExpectedStructured: simtest.Expect(). + WithComposition("conditional-gating-composition"). + WithCompositionModified(). + WithXRImpact("XNopResource", "conditional-gating-test", "default", "changed"). + WithDownstreamSummary(1, 1, 0). + WithDownstreamResource("modified", "XDownstreamResource", "0-stage0-resource", "default"). + WithFieldChange("spec.forProvider.configData", "existing-value", "updated-existing-value"). + AndXR(). + WithDownstreamResource("added", "XDownstreamResource", "1-stage1-resource", "default"). + WithField("spec.forProvider.configData", "updated-existing-value"), + ExpectedError: false, + }, + // dryRun presence through `comp`. dryRun lives on ChangeDetail, which `xr` reaches via + // changes[] and `comp` via impactAnalysis[].downstreamChanges.changes[] — one shared wire + // shape, but only an assertion through the comp renderer proves comp actually populates it. + // The composed resource is deliberately absent from the setup so the impact is an ADDITION, + // which is the only kind of change the dryRun field applies to: the XR has its composition + // refs, but its first compose has not created its resource. + "CompDownstreamAdditionReportsDryRunSkippedWhenDryRunOnExisting": { + Reason: "`comp` structured output carries dryRun.skipReason on a downstream addition when --dry-run-on=existing", + OutputFormat: "json", + DryRunOn: "existing", + SetupFiles: []string{ + "../../../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", + // existing-downstream-1.yaml is intentionally NOT applied: with no composed resource + // in the cluster, the impact analysis reports an addition rather than a modification. + "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-1.yaml", + }, + InputFiles: []string{"../../../internal/testutils/integration/testdata/comp/updated-composition.yaml"}, + Namespace: "default", + ExpectedExitCode: dp.ExitCodeDiffDetected, + ExpectedStructured: simtest.Expect(). + WithComposition("xnopresources.diff.example.org"). + WithCompositionModified(). + WithXRImpact("XNopResource", "test-resource", "default", "changed"). + WithDownstreamSummary(1, 0, 0). + WithDownstreamResource("added", "XDownstreamResource", "test-resource", "default"). + WithDryRunSkipped("disabled", ""). + WithField("spec.forProvider.configData", "updated-existing-value"), + ExpectedError: false, + }, + // The unverified-addition warning through `comp`: one run-wide summary in the top-level + // warnings[], counting the downstream additions behind it, rather than one per composition. + // A cluster-scoped XR is what lets the composition reach a namespace that does not exist. + "CompDownstreamAdditionsInMissingNamespaceAreCountedInOneWarning": { + Reason: "`comp` summarises downstream additions it could not verify as one warning per GVK + namespace + cause, with a count", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/cluster-xrd.yaml", + "testdata/comp/resources/unverified-cluster-composition.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/functions.yaml", + "testdata/comp/resources/existing-unverified-cluster-xr.yaml", + }, + InputFiles: []string{"testdata/comp/updated-unverified-cluster-composition.yaml"}, + ExpectedExitCode: dp.ExitCodeDiffDetected, + ExpectedStderrContains: []string{ + `WARNING: skipped apiserver verification of added resources: their namespace does not exist yet, so their diffs omit server-side defaulting and admission ` + + `(cause=namespaces "nonexistent-namespace" not found, count=2, gvk=/v1, Kind=ConfigMap, namespace=nonexistent-namespace)`, + }, + ExpectedStructured: simtest.Expect(). + WithWarning("their namespace does not exist yet"). + WithWarningContext(map[string]string{ + "gvk": "/v1, Kind=ConfigMap", + "namespace": "nonexistent-namespace", + "cause": `namespaces "nonexistent-namespace" not found`, + "count": "2", + }). + And(). + WithComposition("xnopresources-unverified.diff.example.org"). + WithCompositionModified(). + WithXRImpact("XNopResource", "unverified-xr", "", "changed"). + WithDownstreamSummary(2, 0, 0). + WithDownstreamResource("added", "ConfigMap", "unverified-xr-first", "nonexistent-namespace"). + WithDryRunSkipped("namespaceNotFound", "nonexistent-namespace"). + AndXR(). + WithDownstreamResource("added", "ConfigMap", "unverified-xr-second", "nonexistent-namespace"). + WithDryRunSkipped("namespaceNotFound", "nonexistent-namespace"), + ExpectedError: false, + }, + // --resource flag tests (issue #321) + "ResourceFilterScopesAffectedXRs": { + Reason: "--resource limits impact analysis to the named composites and ignores the rest", + SetupFiles: []string{ + "../../../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( // test-resource (default ns) + "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-1.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-1.yaml", + ), + integration.Composes( // another-resource (default ns) + "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-2.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-2.yaml", + ), + }, + InputFiles: []string{"../../../internal/testutils/integration/testdata/comp/updated-composition.yaml"}, + Resources: []string{"default/test-resource"}, + OutputFormat: "json", + ExpectedExitCode: dp.ExitCodeDiffDetected, + ExpectedStructured: simtest.Expect(). + WithComposition("xnopresources.diff.example.org"). + WithCompositionModified(). + WithAffectedResources(1, 1, 0, 0). + WithXRImpact("XNopResource", "test-resource", "default", "changed"), + }, + "LeastPrivilegeCredentialsDiffSucceeds": { + Reason: "A user who can list only the composition's XR type, not Crossplane's own types, gets the admin's impact analysis", + SetupFiles: []string{ + "../../../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( + "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-1.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-1.yaml", + ), + integration.Composes( + "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-2.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-2.yaml", + ), + }, + InputFiles: []string{"../../../internal/testutils/integration/testdata/comp/updated-composition.yaml"}, + Namespace: "default", + // list on the XR type: without --resource, comp finds the composition's XRs by listing them. + RbacRules: simharness.LeastPrivilegeRules("get", "list", "create", "patch"), + OutputFormat: "json", + ExpectedExitCode: dp.ExitCodeDiffDetected, + ExpectedStructured: simtest.Expect(). + WithComposition("xnopresources.diff.example.org"). + WithCompositionModified(). + WithAffectedResources(2, 2, 0, 0). + WithXRImpact("XNopResource", "test-resource", "default", "changed"). + AndComp(). + WithXRImpact("XNopResource", "another-resource", "default", "changed"), + }, + "NameScopedRunSucceedsWithoutCompositeList": { + Reason: "--resource looks its composites up by name, so a user who cannot list the XR type gets the admin's impact analysis", + SetupFiles: []string{ + "../../../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( + "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-1.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-1.yaml", + ), + integration.Composes( + "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-2.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-2.yaml", + ), + }, + InputFiles: []string{"../../../internal/testutils/integration/testdata/comp/updated-composition.yaml"}, + Resources: []string{"default/test-resource"}, + RbacRules: simharness.LeastPrivilegeRules("get", "create", "patch"), + OutputFormat: "json", + ExpectedExitCode: dp.ExitCodeDiffDetected, + ExpectedStructured: simtest.Expect(). + WithComposition("xnopresources.diff.example.org"). + WithCompositionModified(). + WithAffectedResources(1, 1, 0, 0). + WithXRImpact("XNopResource", "test-resource", "default", "changed"), + }, + "ResourceFilterCommaSeparated": { + Reason: "--resource accepts comma-separated values via kong's auto-parsing", + SetupFiles: []string{ + "../../../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( + "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-1.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-1.yaml", + ), + integration.Composes( + "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-2.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-2.yaml", + ), + }, + InputFiles: []string{"../../../internal/testutils/integration/testdata/comp/updated-composition.yaml"}, + ResourcesCSV: "default/test-resource,default/another-resource", + OutputFormat: "json", + ExpectedExitCode: dp.ExitCodeDiffDetected, + ExpectedStructured: simtest.Expect(). + WithComposition("xnopresources.diff.example.org"). + WithCompositionModified(). + WithAffectedResources(2, 2, 0, 0). + WithXRImpact("XNopResource", "test-resource", "default", "changed"). + AndComp(). + WithXRImpact("XNopResource", "another-resource", "default", "changed"), + }, + "ResourceFilterUnmatched_FailsBeforeRendering": { + Reason: "--resource naming a non-existent composite fails fast with an error before any rendering", + SetupFiles: []string{ + "../../../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( + "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-1.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-1.yaml", + ), + }, + InputFiles: []string{"../../../internal/testutils/integration/testdata/comp/updated-composition.yaml"}, + Resources: []string{"default/does-not-exist"}, + ExpectedError: true, + ExpectedErrorContains: "does-not-exist", + ExpectedExitCode: dp.ExitCodeToolError, + }, + "ResourceFilterRespectsManualPolicy_WithoutIncludeManual": { + Reason: "--resource matching a Manual-policy composite surfaces it as filtered (reason manual_policy) when --include-manual is unset", + SetupFiles: []string{ + "../../../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( + "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-manual.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-manual.yaml", + ), + }, + InputFiles: []string{"../../../internal/testutils/integration/testdata/comp/updated-composition.yaml"}, + Resources: []string{"default/manual-resource"}, + OutputFormat: "json", + ExpectedExitCode: dp.ExitCodeDiffDetected, // composition itself changed + ExpectedStructured: simtest.Expect(). + WithComposition("xnopresources.diff.example.org"). + WithCompositionModified(). + WithXRImpact("XNopResource", "manual-resource", "default", "filtered"). + WithFilterReason("manualPolicy"), + }, + "ResourceFilterRespectsManualPolicy_WithIncludeManual": { + Reason: "--include-manual evaluates the Manual-policy composite normally instead of marking it filtered", + SetupFiles: []string{ + "../../../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( + "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-manual.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-manual.yaml", + ), + }, + InputFiles: []string{"../../../internal/testutils/integration/testdata/comp/updated-composition.yaml"}, + Resources: []string{"default/manual-resource"}, + IncludeManual: true, + OutputFormat: "json", + ExpectedExitCode: dp.ExitCodeDiffDetected, + ExpectedStructured: simtest.Expect(). + WithComposition("xnopresources.diff.example.org"). + WithCompositionModified(). + WithXRImpact("XNopResource", "manual-resource", "default", "changed"), + }, + // Issue #388 (Bug A): an Automatic-policy XR whose compositionRevisionSelector does not match + // the edited composition's labels would not adopt the resulting revision, so it must be + // surfaced as filtered (reason revision_selector_mismatch), NOT evaluated for changes. + "ResourceFilterRespectsRevisionSelectorMismatch": { + Reason: "--resource matching an Automatic XR whose compositionRevisionSelector does not match the composition's labels surfaces it as filtered (reason revision_selector_mismatch)", + SetupFiles: []string{ + "../../../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/comp/resources/existing-xr-selector-mismatch.yaml", + }, + InputFiles: []string{"testdata/comp/updated-composition-labeled.yaml"}, + Resources: []string{"default/selector-mismatch-resource"}, + OutputFormat: "json", + ExpectedExitCode: dp.ExitCodeDiffDetected, // composition itself changed + ExpectedStructured: simtest.Expect(). + WithComposition("xnopresources.diff.example.org"). + WithCompositionModified(). + WithXRImpact("XNopResource", "selector-mismatch-resource", "default", "filtered"). + WithFilterReason("revisionSelectorMismatch"), + // Note: that --include-manual does NOT rescue a selector-mismatched Automatic XR is + // proven at the unit level (TestDefaultCompDiffProcessor_partitionXRsByUpdatePolicy / + // IncludeManualTrue_StillDropsSelectorMismatchedAutomaticXRs). A second slow + // docker-render integration case for that flag interaction would be redundant. + }, + // Issue #388 (Bug A, positive): an Automatic XR whose compositionRevisionSelector DOES match + // the edited composition's labels is kept and evaluated for downstream changes (not filtered). + "ResourceFilterRespectsRevisionSelectorMatch": { + Reason: "--resource matching an Automatic XR whose compositionRevisionSelector matches the composition's labels keeps it and reports downstream changes", + SetupFiles: []string{ + "../../../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/comp/resources/existing-xr-selector-match.yaml", + }, + InputFiles: []string{"testdata/comp/updated-composition-labeled.yaml"}, + Resources: []string{"default/selector-match-resource"}, + OutputFormat: "json", + ExpectedExitCode: dp.ExitCodeDiffDetected, + ExpectedStructured: simtest.Expect(). + WithComposition("xnopresources.diff.example.org"). + WithCompositionModified(). + WithXRImpact("XNopResource", "selector-match-resource", "default", "changed"), + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + simharness.RunIntegrationTest(t, simharness.CompositionTest, tt) + }) + } +} diff --git a/cmd/crossplane/composition/testdata/README.md b/cmd/crossplane/composition/testdata/README.md new file mode 100644 index 00000000..18bb223d --- /dev/null +++ b/cmd/crossplane/composition/testdata/README.md @@ -0,0 +1,12 @@ +# composition simulate integration tests + +`integration_test.go` runs `TestCompDiffIntegration`: crossplane-diff's composition cases, run as +`crossplane composition simulate -f ` 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/`. diff --git a/cmd/crossplane/composition/testdata/comp/api-version-updated-composition.yaml b/cmd/crossplane/composition/testdata/comp/api-version-updated-composition.yaml new file mode 100644 index 00000000..02748913 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/api-version-updated-composition.yaml @@ -0,0 +1,32 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xapimigrateresources.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-api-versioned-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: comp.example.org/v1beta2 + kind: XApiMigrateResource + metadata: + name: {{ .observed.composite.resource.metadata.name }}-api-resource + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: api-migrate-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/claim-revision-templating-updated-composition.yaml b/cmd/crossplane/composition/testdata/comp/claim-revision-templating-updated-composition.yaml new file mode 100644 index 00000000..60bfc213 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/claim-revision-templating-updated-composition.yaml @@ -0,0 +1,37 @@ +# A metadata-only edit of resources/claim-revision-templating-composition.yaml: one added label, +# identical spec. Applying it mints a new CompositionRevision, which an Automatic claim's backing XR +# re-points to — and the template reads that revision's name, so the composed resource changes. +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xrevisionrefclaims.diff.example.org + labels: + channel: preview +spec: + compositeTypeRef: + apiVersion: diff.example.org/v1alpha1 + kind: XNop + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + name: {{ .observed.composite.resource.metadata.name }} + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.compositionRevisionRef.name }} + resourceTier: basic + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/composition-no-changes.yaml b/cmd/crossplane/composition/testdata/comp/composition-no-changes.yaml new file mode 100644 index 00000000..556cf7bf --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/composition-no-changes.yaml @@ -0,0 +1,33 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnopresources.diff.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + resourceTier: basic + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/field-removal-updated-composition.yaml b/cmd/crossplane/composition/testdata/comp/field-removal-updated-composition.yaml new file mode 100644 index 00000000..7cff974f --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/field-removal-updated-composition.yaml @@ -0,0 +1,32 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xfieldremoval.diff.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/mixed-status-updated-composition.yaml b/cmd/crossplane/composition/testdata/comp/mixed-status-updated-composition.yaml new file mode 100644 index 00000000..3e01b1e8 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/mixed-status-updated-composition.yaml @@ -0,0 +1,59 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xmixed.diff.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + --- + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }}-database + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: database + spec: + forProvider: + resourceType: database + tier: premium + backupEnabled: true + --- + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }}-storage + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: storage + spec: + forProvider: + resourceType: storage + tier: standard + --- + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }}-network + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: network + spec: + forProvider: + resourceType: network + tier: standard + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/net-new-composition.yaml b/cmd/crossplane/composition/testdata/comp/net-new-composition.yaml new file mode 100644 index 00000000..4bfa487f --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/net-new-composition.yaml @@ -0,0 +1,37 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnewresources.diff.example.org + labels: + environment: staging + provider: aws +spec: + compositeTypeRef: + apiVersion: staging.diff.example.org/v1alpha1 + kind: XNewResource + mode: Pipeline + pipeline: + - step: generate-new-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: staging.nop.example.org/v1alpha1 + kind: XStagingResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: staging-resource + spec: + forProvider: + environment: staging + configData: new-{{ .observed.composite.resource.spec.newField }} + resourceTier: premium + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready \ No newline at end of file diff --git a/cmd/crossplane/composition/testdata/comp/resources/api-version-original-composition.yaml b/cmd/crossplane/composition/testdata/comp/resources/api-version-original-composition.yaml new file mode 100644 index 00000000..00221d82 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/api-version-original-composition.yaml @@ -0,0 +1,32 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xapimigrateresources.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-api-versioned-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: comp.example.org/v1beta1 + kind: XApiMigrateResource + metadata: + name: {{ .observed.composite.resource.metadata.name }}-api-resource + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: api-migrate-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/resources/claim-composition.yaml b/cmd/crossplane/composition/testdata/comp/resources/claim-composition.yaml new file mode 100644 index 00000000..58219d58 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/claim-composition.yaml @@ -0,0 +1,32 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: nopclaims.diff.example.org +spec: + compositeTypeRef: + apiVersion: diff.example.org/v1alpha1 + kind: XNop + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + name: {{ .observed.composite.resource.metadata.name }} + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + resourceTier: basic + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/resources/claim-revision-templating-composition-kubectl-applied.yaml b/cmd/crossplane/composition/testdata/comp/resources/claim-revision-templating-composition-kubectl-applied.yaml new file mode 100644 index 00000000..f7a6b7c3 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/claim-revision-templating-composition-kubectl-applied.yaml @@ -0,0 +1,41 @@ +# claim-revision-templating-composition.yaml as the cluster holds it after a client-side +# `kubectl apply` of an older file: identical content, plus a last-applied-configuration annotation +# that no longer describes the file. Diffing claim-revision-templating-composition.yaml against this is +# a change whose only delta is that annotation, so the name of the revision it would mint cannot be +# predicted, nothing is seeded, and every composite renders against the revision it tracks now. +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xrevisionrefclaims.diff.example.org + annotations: + # Deliberately stale: it describes some earlier file, not the one diffed against this. + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"apiextensions.crossplane.io/v1","kind":"Composition","metadata":{"name":"xrevisionrefclaims.diff.example.org"},"spec":{"mode":"Pipeline"}} +spec: + compositeTypeRef: + apiVersion: diff.example.org/v1alpha1 + kind: XNop + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + name: {{ .observed.composite.resource.metadata.name }} + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.compositionRevisionRef.name }} + resourceTier: basic + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/resources/claim-revision-templating-composition.yaml b/cmd/crossplane/composition/testdata/comp/resources/claim-revision-templating-composition.yaml new file mode 100644 index 00000000..0f5cd92c --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/claim-revision-templating-composition.yaml @@ -0,0 +1,39 @@ +# The claim-side counterpart of revision-templating-composition.yaml: the in-cluster copy of a +# composition whose template reads the CompositionRevision's own name off the composite and propagates +# it into a composed resource. XNop is a legacy (claim-capable) XR, so the ref is at the v1 path, +# spec.compositionRevisionRef. +# +# Paired with ../claim-revision-templating-updated-composition.yaml (a label-only edit) and with +# claim-revision-templating-composition-kubectl-applied.yaml (this, plus a stale kubectl annotation). +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xrevisionrefclaims.diff.example.org +spec: + compositeTypeRef: + apiVersion: diff.example.org/v1alpha1 + kind: XNop + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + name: {{ .observed.composite.resource.metadata.name }} + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.compositionRevisionRef.name }} + resourceTier: basic + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/resources/claim-xrd.yaml b/cmd/crossplane/composition/testdata/comp/resources/claim-xrd.yaml new file mode 100644 index 00000000..b2b8dfa7 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/claim-xrd.yaml @@ -0,0 +1,27 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: CompositeResourceDefinition +metadata: + name: xnops.diff.example.org +spec: + group: diff.example.org + names: + kind: XNop + plural: xnops + claimNames: + kind: NopClaim + plural: nopclaims + versions: + - name: v1alpha1 + served: true + referenceable: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + coolField: + type: string + writeConnectionSecretsToNamespace: + type: string diff --git a/cmd/crossplane/composition/testdata/comp/resources/custom-namespace.yaml b/cmd/crossplane/composition/testdata/comp/resources/custom-namespace.yaml new file mode 100644 index 00000000..bb71c9a0 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/custom-namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: custom-namespace \ No newline at end of file diff --git a/cmd/crossplane/composition/testdata/comp/resources/cycle/existing-cycle-xr.yaml b/cmd/crossplane/composition/testdata/comp/resources/cycle/existing-cycle-xr.yaml new file mode 100644 index 00000000..38d622c3 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/cycle/existing-cycle-xr.yaml @@ -0,0 +1,12 @@ +# An existing XR on the XCycleA composition, which comp discovers and re-renders against the +# updated composition. Its composition starts the XCycleA -> XCycleB -> XCycleA cycle. +apiVersion: ns.cycle.example.org/v1alpha1 +kind: XCycleA +metadata: + name: test-cycle + namespace: default +spec: + cycleField: "cycle-value" + crossplane: + compositionRef: + name: xcycleas.cycle.example.org diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-api-version-downstream-v1beta1.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-api-version-downstream-v1beta1.yaml new file mode 100644 index 00000000..8b63f58b --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-api-version-downstream-v1beta1.yaml @@ -0,0 +1,10 @@ +apiVersion: comp.example.org/v1beta1 +kind: XApiMigrateResource +metadata: + name: test-api-version-api-resource + namespace: default + annotations: + crossplane.io/composition-resource-name: api-migrate-resource +spec: + forProvider: + configData: test-value diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-api-version-xr.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-api-version-xr.yaml new file mode 100644 index 00000000..05f4d7b2 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-api-version-xr.yaml @@ -0,0 +1,11 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-api-version + namespace: default +spec: + crossplane: + compositionRef: + name: xapimigrateresources.example.org + compositionUpdatePolicy: Automatic + coolField: test-value diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-claim-1-xr.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-claim-1-xr.yaml new file mode 100644 index 00000000..24de708a --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-claim-1-xr.yaml @@ -0,0 +1,21 @@ +# The backing XR Crossplane's claim syncer created for existing-claim-1.yaml. Its composed resource is +# existing-claim-downstream-1.yaml. +apiVersion: diff.example.org/v1alpha1 +kind: XNop +metadata: + name: test-claim-1-xr +spec: + claimRef: + apiVersion: diff.example.org/v1alpha1 + kind: NopClaim + name: test-claim-1 + namespace: test-namespace + coolField: claim-value-1 + compositeDeletePolicy: Background + compositionUpdatePolicy: Automatic + compositionRef: + name: nopclaims.diff.example.org + resourceRefs: + - apiVersion: nop.example.org/v1alpha1 + kind: XDownstreamResource + name: test-claim-1-xr diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-claim-1.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-claim-1.yaml new file mode 100644 index 00000000..dac0a2b9 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-claim-1.yaml @@ -0,0 +1,18 @@ +apiVersion: diff.example.org/v1alpha1 +kind: NopClaim +metadata: + name: test-claim-1 + namespace: test-namespace +spec: + coolField: claim-value-1 + compositeDeletePolicy: Background + compositionRef: + name: nopclaims.diff.example.org + # Copied back from the backing XR, as the claim syncer does under the XR's Automatic update policy. + compositionRevisionRef: + name: nopclaims.diff.example.org-e164b8b + # Bound to its backing XR, existing-claim-1-xr.yaml, as Crossplane's claim syncer binds it. + resourceRef: + apiVersion: diff.example.org/v1alpha1 + kind: XNop + name: test-claim-1-xr diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-claim-2-xr.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-claim-2-xr.yaml new file mode 100644 index 00000000..de2703cb --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-claim-2-xr.yaml @@ -0,0 +1,21 @@ +# The backing XR Crossplane's claim syncer created for existing-claim-2.yaml. Its composed resource is +# existing-claim-downstream-2.yaml. +apiVersion: diff.example.org/v1alpha1 +kind: XNop +metadata: + name: test-claim-2-xr +spec: + claimRef: + apiVersion: diff.example.org/v1alpha1 + kind: NopClaim + name: test-claim-2 + namespace: test-namespace + coolField: claim-value-2 + compositeDeletePolicy: Background + compositionUpdatePolicy: Automatic + compositionRef: + name: nopclaims.diff.example.org + resourceRefs: + - apiVersion: nop.example.org/v1alpha1 + kind: XDownstreamResource + name: test-claim-2-xr diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-claim-2.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-claim-2.yaml new file mode 100644 index 00000000..7f7b89e1 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-claim-2.yaml @@ -0,0 +1,18 @@ +apiVersion: diff.example.org/v1alpha1 +kind: NopClaim +metadata: + name: test-claim-2 + namespace: test-namespace +spec: + coolField: claim-value-2 + compositeDeletePolicy: Background + compositionRef: + name: nopclaims.diff.example.org + # Copied back from the backing XR, as the claim syncer does under the XR's Automatic update policy. + compositionRevisionRef: + name: nopclaims.diff.example.org-e164b8b + # Bound to its backing XR, existing-claim-2-xr.yaml, as Crossplane's claim syncer binds it. + resourceRef: + apiVersion: diff.example.org/v1alpha1 + kind: XNop + name: test-claim-2-xr diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-claim-downstream-1.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-claim-downstream-1.yaml new file mode 100644 index 00000000..23fd1f94 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-claim-downstream-1.yaml @@ -0,0 +1,10 @@ +apiVersion: nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + annotations: + crossplane.io/composition-resource-name: nop-resource + name: test-claim-1-xr +spec: + forProvider: + configData: claim-value-1 + resourceTier: basic diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-claim-downstream-2.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-claim-downstream-2.yaml new file mode 100644 index 00000000..f0848c7b --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-claim-downstream-2.yaml @@ -0,0 +1,10 @@ +apiVersion: nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + annotations: + crossplane.io/composition-resource-name: nop-resource + name: test-claim-2-xr +spec: + forProvider: + configData: claim-value-2 + resourceTier: basic diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-claim-revision-ref-downstream.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-claim-revision-ref-downstream.yaml new file mode 100644 index 00000000..4803e305 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-claim-revision-ref-downstream.yaml @@ -0,0 +1,14 @@ +# The composed resource of existing-claim-revision-ref-xr.yaml, rendered from the revision that XR +# tracks, so configData holds that revision's name. Carries crossplane.io/composition-resource-name (and, +# from the harness, the composite and claim labels) because the render adds all of them: a fixture missing +# any is reported as modified on every run, which would hide whether the revision name propagated. +apiVersion: nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: revision-ref-claim-xr + annotations: + crossplane.io/composition-resource-name: nop-resource +spec: + forProvider: + configData: xrevisionrefclaims.diff.example.org-98b484d + resourceTier: basic diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-claim-revision-ref-kubectl-applied-downstream.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-claim-revision-ref-kubectl-applied-downstream.yaml new file mode 100644 index 00000000..865fbace --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-claim-revision-ref-kubectl-applied-downstream.yaml @@ -0,0 +1,12 @@ +# existing-claim-revision-ref-downstream.yaml as rendered from the revision +# existing-claim-revision-ref-kubectl-applied-xr.yaml tracks, so configData holds that revision's name. +apiVersion: nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: revision-ref-claim-xr + annotations: + crossplane.io/composition-resource-name: nop-resource +spec: + forProvider: + configData: xrevisionrefclaims.diff.example.org-329a7cd + resourceTier: basic diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-claim-revision-ref-kubectl-applied-xr.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-claim-revision-ref-kubectl-applied-xr.yaml new file mode 100644 index 00000000..9aa0011f --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-claim-revision-ref-kubectl-applied-xr.yaml @@ -0,0 +1,15 @@ +# existing-claim-revision-ref-xr.yaml where the composition was applied with a client-side `kubectl +# apply`. Composition.Hash() covers annotations, so the revision it tracks has a different name. +apiVersion: diff.example.org/v1alpha1 +kind: XNop +metadata: + name: revision-ref-claim-xr +spec: + claimRef: + apiVersion: diff.example.org/v1alpha1 + kind: NopClaim + name: revision-ref-claim + namespace: test-namespace + coolField: revision-ref-claim-value + compositionRef: + name: xrevisionrefclaims.diff.example.org diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-claim-revision-ref-kubectl-applied.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-claim-revision-ref-kubectl-applied.yaml new file mode 100644 index 00000000..76a758b2 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-claim-revision-ref-kubectl-applied.yaml @@ -0,0 +1,20 @@ +# existing-claim-revision-ref.yaml where the composition was applied with a client-side `kubectl apply` +# (claim-revision-templating-composition-kubectl-applied.yaml). Composition.Hash() covers annotations, +# so that composition's revision has another name, which the claim carries like its backing XR, +# existing-claim-revision-ref-kubectl-applied-xr.yaml. +apiVersion: diff.example.org/v1alpha1 +kind: NopClaim +metadata: + name: revision-ref-claim + namespace: test-namespace +spec: + coolField: revision-ref-claim-value + compositeDeletePolicy: Background + compositionRef: + name: xrevisionrefclaims.diff.example.org + compositionRevisionRef: + name: xrevisionrefclaims.diff.example.org-329a7cd + resourceRef: + apiVersion: diff.example.org/v1alpha1 + kind: XNop + name: revision-ref-claim-xr diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-claim-revision-ref-xr.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-claim-revision-ref-xr.yaml new file mode 100644 index 00000000..09cb9326 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-claim-revision-ref-xr.yaml @@ -0,0 +1,15 @@ +# The backing XR of existing-claim-revision-ref.yaml, tracking the current revision of +# claim-revision-templating-composition.yaml as Crossplane's composite reconciler leaves it. +apiVersion: diff.example.org/v1alpha1 +kind: XNop +metadata: + name: revision-ref-claim-xr +spec: + claimRef: + apiVersion: diff.example.org/v1alpha1 + kind: NopClaim + name: revision-ref-claim + namespace: test-namespace + coolField: revision-ref-claim-value + compositionRef: + name: xrevisionrefclaims.diff.example.org diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-claim-revision-ref.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-claim-revision-ref.yaml new file mode 100644 index 00000000..6bb6c655 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-claim-revision-ref.yaml @@ -0,0 +1,20 @@ +# A Claim at the default (Automatic) update policy, bound to existing-claim-revision-ref-xr.yaml. It +# carries that XR's compositionRevisionRef, because under Automatic the claim syncer copies the XR's ref +# back onto the claim. Going the other way the claim's ref is not propagated: under Automatic the XR +# controller owns the backing XR's ref, so that is the one render must see (issue #498). +apiVersion: diff.example.org/v1alpha1 +kind: NopClaim +metadata: + name: revision-ref-claim + namespace: test-namespace +spec: + coolField: revision-ref-claim-value + compositeDeletePolicy: Background + compositionRef: + name: xrevisionrefclaims.diff.example.org + compositionRevisionRef: + name: xrevisionrefclaims.diff.example.org-98b484d + resourceRef: + apiVersion: diff.example.org/v1alpha1 + kind: XNop + name: revision-ref-claim-xr diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-conditional-gating-stage0.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-conditional-gating-stage0.yaml new file mode 100644 index 00000000..2e0c9b2c --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-conditional-gating-stage0.yaml @@ -0,0 +1,12 @@ +# The stage0 resource conditional-gating-composition.yaml composed for existing-conditional-gating-xr.yaml. +# It has no Ready condition yet, so stage1 stays gated until --eventual-state synthesizes one. +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + annotations: + crossplane.io/composition-resource-name: stage0-resource + name: 0-stage0-resource + namespace: default +spec: + forProvider: + configData: existing-value diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-conditional-gating-xr.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-conditional-gating-xr.yaml new file mode 100644 index 00000000..0a9b0167 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-conditional-gating-xr.yaml @@ -0,0 +1,10 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: conditional-gating-test + namespace: default +spec: + coolField: existing-value + crossplane: + compositionRef: + name: conditional-gating-composition diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-custom-ns-downstream.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-custom-ns-downstream.yaml new file mode 100644 index 00000000..f22ee1bf --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-custom-ns-downstream.yaml @@ -0,0 +1,11 @@ +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: custom-namespace-resource + namespace: custom-namespace + annotations: + crossplane.io/composition-resource-name: nop-resource +spec: + forProvider: + configData: custom-ns-existing-value + resourceTier: basic \ No newline at end of file diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-custom-ns-xr.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-custom-ns-xr.yaml new file mode 100644 index 00000000..c8c5d766 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-custom-ns-xr.yaml @@ -0,0 +1,10 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: custom-namespace-resource + namespace: custom-namespace +spec: + coolField: custom-ns-existing-value + crossplane: + compositionRef: + name: xnopresources.diff.example.org diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-downstream-deleting.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-downstream-deleting.yaml new file mode 100644 index 00000000..3dd7c99d --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-downstream-deleting.yaml @@ -0,0 +1,11 @@ +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: deleting-resource + namespace: default + annotations: + crossplane.io/composition-resource-name: nop-resource +spec: + forProvider: + configData: existing-value + resourceTier: basic diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-downstream-drifted.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-downstream-drifted.yaml new file mode 100644 index 00000000..347c4221 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-downstream-drifted.yaml @@ -0,0 +1,14 @@ +# existing-downstream-1.yaml after an out-of-band edit: someone changed configData in the cluster, so +# it no longer matches what the composition renders for existing-xr-1.yaml. Re-rendering an unchanged +# composition therefore surfaces a delta the composition did not cause. +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: test-resource + namespace: default + annotations: + crossplane.io/composition-resource-name: nop-resource +spec: + forProvider: + configData: drifted-value + resourceTier: basic diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-downstream-pinned-revision-ref.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-downstream-pinned-revision-ref.yaml new file mode 100644 index 00000000..eea3c1cc --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-downstream-pinned-revision-ref.yaml @@ -0,0 +1,15 @@ +# The composed resource of existing-xr-pinned-revision-ref.yaml as its pinned revision rendered it, so +# configData holds that revision's name. It carries crossplane.io/composition-resource-name as well as the +# gotemplating annotation, as a rendered resource does; see existing-downstream-revision-ref.yaml for why a +# fixture without it would show a diff on every run. +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: pinned-revision-ref-resource + namespace: default + annotations: + crossplane.io/composition-resource-name: nop-resource +spec: + forProvider: + configData: xrevisionrefs.diff.example.org-0d65896 + resourceTier: basic diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-downstream-v2.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-downstream-v2.yaml new file mode 100644 index 00000000..709ba671 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-downstream-v2.yaml @@ -0,0 +1,13 @@ +# What original-composition-2.yaml currently renders for existing-xr-v2, so the diff against the +# changed composition shows only the fields that composition actually changed. +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: v2-resource + namespace: default + annotations: + crossplane.io/composition-resource-name: nop-resource-v2 +spec: + forProvider: + configData: v2-updated-v2-existing-value + resourceTier: enterprise diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-downstream-with-argocd.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-downstream-with-argocd.yaml new file mode 100644 index 00000000..73dcfbfc --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-downstream-with-argocd.yaml @@ -0,0 +1,14 @@ +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + annotations: + crossplane.io/composition-resource-name: nop-resource + argocd.argoproj.io/tracking-id: "test-app:/XDownstreamResource:default/test-resource" + labels: + argocd.argoproj.io/instance: test-app + name: test-resource + namespace: default +spec: + forProvider: + configData: existing-value + resourceTier: basic diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-sequencer-gating-stage0.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-sequencer-gating-stage0.yaml new file mode 100644 index 00000000..9f520639 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-sequencer-gating-stage0.yaml @@ -0,0 +1,12 @@ +# The stage0 resource sequencer-gating-composition.yaml composed for existing-sequencer-gating-xr.yaml. +# It has no Ready condition yet, so stage1 stays gated until --eventual-state synthesizes one. +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + annotations: + crossplane.io/composition-resource-name: stage0-resource + name: 0-stage0-resource + namespace: default +spec: + forProvider: + configData: existing-value diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-sequencer-gating-xr.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-sequencer-gating-xr.yaml new file mode 100644 index 00000000..bf00981f --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-sequencer-gating-xr.yaml @@ -0,0 +1,10 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: sequencer-gating-test + namespace: default +spec: + coolField: existing-value + crossplane: + compositionRef: + name: sequencer-gating-composition diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-sha256-downstream.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-sha256-downstream.yaml new file mode 100644 index 00000000..c5c48f16 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-sha256-downstream.yaml @@ -0,0 +1,11 @@ +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: sha256-test-resource + namespace: default + annotations: + crossplane.io/composition-resource-name: nop-resource +spec: + forProvider: + configData: sha256-value + resourceTier: basic diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-sha256-xr.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-sha256-xr.yaml new file mode 100644 index 00000000..3409f380 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-sha256-xr.yaml @@ -0,0 +1,10 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: sha256-test-resource + namespace: default +spec: + coolField: sha256-value + crossplane: + compositionRef: + name: xnopresources-sha256.diff.example.org diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-unverified-cluster-xr.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-unverified-cluster-xr.yaml new file mode 100644 index 00000000..4b75574e --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-unverified-cluster-xr.yaml @@ -0,0 +1,11 @@ +# A cluster-scoped XR using unverified-cluster-composition.yaml. It has composed nothing: the +# composition renders ConfigMaps into a namespace that does not exist, so Crossplane cannot create them. +apiVersion: diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: unverified-xr +spec: + coolField: existing-value + crossplane: + compositionRef: + name: xnopresources-unverified.diff.example.org diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-xr-1-kubectl-applied-current.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-xr-1-kubectl-applied-current.yaml new file mode 100644 index 00000000..b7ff7ef3 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-xr-1-kubectl-applied-current.yaml @@ -0,0 +1,13 @@ +# existing-xr-1.yaml where the composition was applied with a client-side `kubectl apply` of the +# file being diffed (original-composition-kubectl-applied-current.yaml). Composition.Hash() covers +# annotations, so that composition's revision has a different name, and this is the one the XR tracks. +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-resource + namespace: default +spec: + coolField: existing-value + crossplane: + compositionRef: + name: xnopresources.diff.example.org diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-xr-1-kubectl-applied.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-xr-1-kubectl-applied.yaml new file mode 100644 index 00000000..ea3e88fe --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-xr-1-kubectl-applied.yaml @@ -0,0 +1,13 @@ +# existing-xr-1.yaml where the composition was applied with a client-side `kubectl apply` +# (original-composition-kubectl-applied.yaml). Composition.Hash() covers annotations, so that +# composition's revision has a different name, and this is the one the XR tracks. +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-resource + namespace: default +spec: + coolField: existing-value + crossplane: + compositionRef: + name: xnopresources.diff.example.org diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-xr-deleting.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-xr-deleting.yaml new file mode 100644 index 00000000..c4d717a5 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-xr-deleting.yaml @@ -0,0 +1,14 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: deleting-resource + namespace: default + # The finalizer keeps the object around after a delete is issued, so envtest (which runs no + # controllers) leaves it with metadata.deletionTimestamp set — a resource mid-deletion. + finalizers: + - composite.apiextensions.crossplane.io +spec: + coolField: existing-value + crossplane: + compositionRef: + name: xnopresources.diff.example.org diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-xr-listing-uncontrolled-resources.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-xr-listing-uncontrolled-resources.yaml new file mode 100644 index 00000000..c03ffe8e --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-xr-listing-uncontrolled-resources.yaml @@ -0,0 +1,22 @@ +# ../../diff/resources/existing-xr-listing-uncontrolled-resources.yaml, tracking the in-cluster +# removal-test-original-composition.yaml instead. Its resourceRefs still name two composed resources it +# no longer controls: one ../../diff/resources/existing-other-xr.yaml now controls, and one whose +# controller reference was removed. The harness appends the resources this XR does control. +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-resource + namespace: default +spec: + coolField: "existing-value" + crossplane: + compositionRef: + name: removal-test-composition + compositionUpdatePolicy: Automatic + resourceRefs: + - apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + name: other-resource + - apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + name: resource-without-controller diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-xr-pinned-revision-ref.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-xr-pinned-revision-ref.yaml new file mode 100644 index 00000000..ff2586ee --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-xr-pinned-revision-ref.yaml @@ -0,0 +1,18 @@ +# A Manual composite of the revision-templating composition, pinned to an older CompositionRevision than +# the one existing-xr-revision-ref.yaml tracks: revision 1, +# revision-templating-composition-older-revision.yaml. A Manual composite never re-points, so applying +# an edit of its composition leaves it on this revision, and a template reading the revision name must +# keep reading this one. Kept for analysis only under --include-pinned. +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: pinned-revision-ref-resource + namespace: default +spec: + coolField: pinned-revision-ref-existing-value + crossplane: + compositionRef: + name: xrevisionrefs.diff.example.org + compositionUpdatePolicy: Manual + compositionRevisionRef: + name: xrevisionrefs.diff.example.org-0d65896 diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-xr-selector-match.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-xr-selector-match.yaml new file mode 100644 index 00000000..ef9c030d --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-xr-selector-match.yaml @@ -0,0 +1,18 @@ +# Deliberately no compositionRevisionRef. The selector matches no revision in the cluster: +# original-composition.yaml has no version label, so neither has its one revision. Crossplane cannot +# select a revision for this XR, so it never writes the ref, and the XR waits for a composition +# labelled version=0.0.2, like updated-composition-labeled.yaml. +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: selector-match-resource + namespace: default +spec: + coolField: selector-match-existing-value + crossplane: + compositionRef: + name: xnopresources.diff.example.org + compositionUpdatePolicy: Automatic + compositionRevisionSelector: + matchLabels: + version: 0.0.2 diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-xr-selector-mismatch.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-xr-selector-mismatch.yaml new file mode 100644 index 00000000..a7f18cdc --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-xr-selector-mismatch.yaml @@ -0,0 +1,18 @@ +# Deliberately no compositionRevisionRef. The selector matches no revision in the cluster: +# original-composition.yaml has no version label, so neither has its one revision. Crossplane cannot +# select a revision for this XR, so it never writes the ref, and the XR waits for a composition +# labelled version=0.0.1 (updated-composition-labeled.yaml is 0.0.2, so it is not that one). +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: selector-mismatch-resource + namespace: default +spec: + coolField: selector-existing-value + crossplane: + compositionRef: + name: xnopresources.diff.example.org + compositionUpdatePolicy: Automatic + compositionRevisionSelector: + matchLabels: + version: 0.0.1 diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-xr-v2.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-xr-v2.yaml new file mode 100644 index 00000000..10b2dad7 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-xr-v2.yaml @@ -0,0 +1,12 @@ +# An XR that uses the SECOND composition (xnopresources-v2), so a multi-composition run can have +# affected XRs under both compositions rather than only the first. +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: v2-resource + namespace: default +spec: + coolField: v2-existing-value + crossplane: + compositionRef: + name: xnopresources-v2.diff.example.org diff --git a/cmd/crossplane/composition/testdata/comp/resources/existing-xr-with-argocd.yaml b/cmd/crossplane/composition/testdata/comp/resources/existing-xr-with-argocd.yaml new file mode 100644 index 00000000..2b4fce63 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/existing-xr-with-argocd.yaml @@ -0,0 +1,14 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-resource + namespace: default + annotations: + argocd.argoproj.io/tracking-id: "test-app:/XNopResource:default/test-resource" + labels: + argocd.argoproj.io/instance: test-app +spec: + coolField: "existing-value" + crossplane: + compositionRef: + name: xnopresources.diff.example.org diff --git a/cmd/crossplane/composition/testdata/comp/resources/field-removal/composition-with-field.yaml b/cmd/crossplane/composition/testdata/comp/resources/field-removal/composition-with-field.yaml new file mode 100644 index 00000000..78a14146 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/field-removal/composition-with-field.yaml @@ -0,0 +1,33 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xfieldremoval.diff.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + removableField: will-be-removed + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/resources/field-removal/existing-downstream.yaml b/cmd/crossplane/composition/testdata/comp/resources/field-removal/existing-downstream.yaml new file mode 100644 index 00000000..0c1491f2 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/field-removal/existing-downstream.yaml @@ -0,0 +1,11 @@ +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: field-removal-test + namespace: default + annotations: + crossplane.io/composition-resource-name: nop-resource +spec: + forProvider: + configData: test-value + removableField: will-be-removed diff --git a/cmd/crossplane/composition/testdata/comp/resources/field-removal/existing-xr.yaml b/cmd/crossplane/composition/testdata/comp/resources/field-removal/existing-xr.yaml new file mode 100644 index 00000000..831e9b86 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/field-removal/existing-xr.yaml @@ -0,0 +1,10 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: field-removal-test + namespace: default +spec: + coolField: test-value + crossplane: + compositionRef: + name: xfieldremoval.diff.example.org diff --git a/cmd/crossplane/composition/testdata/comp/resources/functions-sha256.yaml b/cmd/crossplane/composition/testdata/comp/resources/functions-sha256.yaml new file mode 100644 index 00000000..0fda179d --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/functions-sha256.yaml @@ -0,0 +1,17 @@ +--- +# Function referenced by SHA256 digest instead of tag. +# This tests that a function whose package is a digest reference renders. +# The digest is for function-go-templating:v0.11.0 - same image, different reference format. +apiVersion: pkg.crossplane.io/v1beta1 +kind: Function +metadata: + name: function-go-templating-sha256 +spec: + package: xpkg.crossplane.io/crossplane-contrib/function-go-templating@sha256:34f1ca0ff2e1702ae0a5088a96f31e992eb9b15356af3b0f1a2068603e767753 +--- +apiVersion: pkg.crossplane.io/v1beta1 +kind: Function +metadata: + name: function-auto-ready +spec: + package: xpkg.crossplane.io/crossplane-contrib/function-auto-ready:v0.4.2 diff --git a/cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-db-1.yaml b/cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-db-1.yaml new file mode 100644 index 00000000..02c75e6e --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-db-1.yaml @@ -0,0 +1,11 @@ +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: mixed-test-xr-1-database + namespace: default + annotations: + crossplane.io/composition-resource-name: database +spec: + forProvider: + resourceType: database + tier: standard diff --git a/cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-db-2.yaml b/cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-db-2.yaml new file mode 100644 index 00000000..55cbbba5 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-db-2.yaml @@ -0,0 +1,11 @@ +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: mixed-test-xr-2-database + namespace: default + annotations: + crossplane.io/composition-resource-name: database +spec: + forProvider: + resourceType: database + tier: standard diff --git a/cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-db-3.yaml b/cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-db-3.yaml new file mode 100644 index 00000000..25ddf6f4 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-db-3.yaml @@ -0,0 +1,12 @@ +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: mixed-test-xr-3-database + namespace: default + annotations: + crossplane.io/composition-resource-name: database +spec: + forProvider: + resourceType: database + tier: premium + backupEnabled: true diff --git a/cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-network-1.yaml b/cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-network-1.yaml new file mode 100644 index 00000000..f9672d09 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-network-1.yaml @@ -0,0 +1,11 @@ +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: mixed-test-xr-1-network + namespace: default + annotations: + crossplane.io/composition-resource-name: network +spec: + forProvider: + resourceType: network + tier: standard diff --git a/cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-network-2.yaml b/cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-network-2.yaml new file mode 100644 index 00000000..e368615b --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-network-2.yaml @@ -0,0 +1,11 @@ +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: mixed-test-xr-2-network + namespace: default + annotations: + crossplane.io/composition-resource-name: network +spec: + forProvider: + resourceType: network + tier: standard diff --git a/cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-network-3.yaml b/cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-network-3.yaml new file mode 100644 index 00000000..7186308e --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-network-3.yaml @@ -0,0 +1,11 @@ +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: mixed-test-xr-3-network + namespace: default + annotations: + crossplane.io/composition-resource-name: network +spec: + forProvider: + resourceType: network + tier: standard diff --git a/cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-storage-1.yaml b/cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-storage-1.yaml new file mode 100644 index 00000000..1ef2aeff --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-storage-1.yaml @@ -0,0 +1,11 @@ +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: mixed-test-xr-1-storage + namespace: default + annotations: + crossplane.io/composition-resource-name: storage +spec: + forProvider: + resourceType: storage + tier: standard diff --git a/cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-storage-2.yaml b/cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-storage-2.yaml new file mode 100644 index 00000000..d6bcfd9d --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-storage-2.yaml @@ -0,0 +1,11 @@ +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: mixed-test-xr-2-storage + namespace: default + annotations: + crossplane.io/composition-resource-name: storage +spec: + forProvider: + resourceType: storage + tier: standard diff --git a/cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-storage-3.yaml b/cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-storage-3.yaml new file mode 100644 index 00000000..af025860 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/mixed-downstream-storage-3.yaml @@ -0,0 +1,11 @@ +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: mixed-test-xr-3-storage + namespace: default + annotations: + crossplane.io/composition-resource-name: storage +spec: + forProvider: + resourceType: storage + tier: standard diff --git a/cmd/crossplane/composition/testdata/comp/resources/mixed-status-composition.yaml b/cmd/crossplane/composition/testdata/comp/resources/mixed-status-composition.yaml new file mode 100644 index 00000000..f3c367ee --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/mixed-status-composition.yaml @@ -0,0 +1,58 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xmixed.diff.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + --- + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }}-database + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: database + spec: + forProvider: + resourceType: database + tier: standard + --- + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }}-storage + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: storage + spec: + forProvider: + resourceType: storage + tier: standard + --- + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }}-network + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: network + spec: + forProvider: + resourceType: network + tier: standard + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/resources/mixed-xr-1.yaml b/cmd/crossplane/composition/testdata/comp/resources/mixed-xr-1.yaml new file mode 100644 index 00000000..8a44ded7 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/mixed-xr-1.yaml @@ -0,0 +1,10 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: mixed-test-xr-1 + namespace: default +spec: + coolField: mixed-value-1 + crossplane: + compositionRef: + name: xmixed.diff.example.org diff --git a/cmd/crossplane/composition/testdata/comp/resources/mixed-xr-2.yaml b/cmd/crossplane/composition/testdata/comp/resources/mixed-xr-2.yaml new file mode 100644 index 00000000..413db4d2 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/mixed-xr-2.yaml @@ -0,0 +1,10 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: mixed-test-xr-2 + namespace: default +spec: + coolField: mixed-value-2 + crossplane: + compositionRef: + name: xmixed.diff.example.org diff --git a/cmd/crossplane/composition/testdata/comp/resources/mixed-xr-3.yaml b/cmd/crossplane/composition/testdata/comp/resources/mixed-xr-3.yaml new file mode 100644 index 00000000..a6fb9cd6 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/mixed-xr-3.yaml @@ -0,0 +1,10 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: mixed-test-xr-3 + namespace: default +spec: + coolField: mixed-value-3 + crossplane: + compositionRef: + name: xmixed.diff.example.org diff --git a/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/collision-composition-revision.yaml b/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/collision-composition-revision.yaml new file mode 100644 index 00000000..12a756b2 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/collision-composition-revision.yaml @@ -0,0 +1,56 @@ +# The CompositionRevision Crossplane creates for collision-composition.yaml, +# named and labelled from its hash as Crossplane names it. envtest runs no Crossplane controllers, +# so it must be declared. The XRs in existing-xr-ns-{a,b}.yaml track it. +apiVersion: apiextensions.crossplane.io/v1 +kind: CompositionRevision +metadata: + name: xnopresources.collision.example.org-163b210 + labels: + crossplane.io/composition-name: xnopresources.collision.example.org + crossplane.io/composition-hash: 163b2101da346b4554697172116dfe489afe2605a36a3af8122f643c3f13e81 +spec: + revision: 1 + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: fetch-external-resources + functionRef: + name: function-extra-resources + input: + apiVersion: extra-resources.fn.crossplane.io/v1beta1 + kind: Input + spec: + extraResources: + # Fetch ConfigMap by name - uses XR's namespace since no namespace specified + - apiVersion: v1 + kind: ConfigMap + into: nsConfig + type: Reference + ref: + name: collision-config + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + {{- $config := index .context "apiextensions.crossplane.io/extra-resources" "nsConfig" 0 -}} + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: collision-resource + spec: + forProvider: + # This should contain the ConfigMap data from the XR's namespace + configData: {{ index $config "data" "value" }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/collision-composition.yaml b/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/collision-composition.yaml new file mode 100644 index 00000000..eff808b4 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/collision-composition.yaml @@ -0,0 +1,52 @@ +# Composition that fetches a ConfigMap by name from the XR's namespace. +# This is used to test the namespace collision bug where resources with +# the same name in different namespaces collide in the cache. +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnopresources.collision.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: fetch-external-resources + functionRef: + name: function-extra-resources + input: + apiVersion: extra-resources.fn.crossplane.io/v1beta1 + kind: Input + spec: + extraResources: + # Fetch ConfigMap by name - uses XR's namespace since no namespace specified + - apiVersion: v1 + kind: ConfigMap + into: nsConfig + type: Reference + ref: + name: collision-config + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + {{- $config := index .context "apiextensions.crossplane.io/extra-resources" "nsConfig" 0 -}} + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: collision-resource + spec: + forProvider: + # This should contain the ConfigMap data from the XR's namespace + configData: {{ index $config "data" "value" }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/configmap-ns-a.yaml b/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/configmap-ns-a.yaml new file mode 100644 index 00000000..bc1a9387 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/configmap-ns-a.yaml @@ -0,0 +1,8 @@ +# ConfigMap in namespace ns-a - will be fetched by XR in ns-a +apiVersion: v1 +kind: ConfigMap +metadata: + name: collision-config + namespace: ns-a +data: + value: from-ns-a diff --git a/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/configmap-ns-b.yaml b/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/configmap-ns-b.yaml new file mode 100644 index 00000000..3f5ba205 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/configmap-ns-b.yaml @@ -0,0 +1,8 @@ +# ConfigMap in namespace ns-b - will be fetched by XR in ns-b +apiVersion: v1 +kind: ConfigMap +metadata: + name: collision-config + namespace: ns-b +data: + value: from-ns-b diff --git a/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/existing-downstream-ns-a.yaml b/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/existing-downstream-ns-a.yaml new file mode 100644 index 00000000..eccd4905 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/existing-downstream-ns-a.yaml @@ -0,0 +1,14 @@ +# Downstream resource for XR in ns-a +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: xr-in-ns-a + namespace: ns-a + annotations: + crossplane.io/composition-resource-name: collision-resource + labels: + crossplane.io/composite: xr-in-ns-a +spec: + forProvider: + # Current value from the ConfigMap in ns-a + configData: from-ns-a diff --git a/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/existing-downstream-ns-b.yaml b/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/existing-downstream-ns-b.yaml new file mode 100644 index 00000000..75a16cdf --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/existing-downstream-ns-b.yaml @@ -0,0 +1,14 @@ +# Downstream resource for XR in ns-b +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: xr-in-ns-b + namespace: ns-b + annotations: + crossplane.io/composition-resource-name: collision-resource + labels: + crossplane.io/composite: xr-in-ns-b +spec: + forProvider: + # Current value from the ConfigMap in ns-b + configData: from-ns-b diff --git a/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/existing-xr-ns-a.yaml b/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/existing-xr-ns-a.yaml new file mode 100644 index 00000000..6e1aad43 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/existing-xr-ns-a.yaml @@ -0,0 +1,15 @@ +# XR in namespace ns-a +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: xr-in-ns-a + labels: + crossplane.io/composite: xr-in-ns-a + namespace: ns-a +spec: + coolField: test-value-a + crossplane: + compositionRef: + name: xnopresources.collision.example.org + compositionRevisionRef: + name: xnopresources.collision.example.org-163b210 diff --git a/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/existing-xr-ns-b.yaml b/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/existing-xr-ns-b.yaml new file mode 100644 index 00000000..a7e9c5a0 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/existing-xr-ns-b.yaml @@ -0,0 +1,15 @@ +# XR in namespace ns-b +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: xr-in-ns-b + labels: + crossplane.io/composite: xr-in-ns-b + namespace: ns-b +spec: + coolField: test-value-b + crossplane: + compositionRef: + name: xnopresources.collision.example.org + compositionRevisionRef: + name: xnopresources.collision.example.org-163b210 diff --git a/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/namespace-ns-a.yaml b/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/namespace-ns-a.yaml new file mode 100644 index 00000000..cbf261f5 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/namespace-ns-a.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: ns-a diff --git a/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/namespace-ns-b.yaml b/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/namespace-ns-b.yaml new file mode 100644 index 00000000..0dc47b53 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/namespace-collision/namespace-ns-b.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: ns-b diff --git a/cmd/crossplane/composition/testdata/comp/resources/nested-xr/existing-child-downstream.yaml b/cmd/crossplane/composition/testdata/comp/resources/nested-xr/existing-child-downstream.yaml new file mode 100644 index 00000000..3094b7df --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/nested-xr/existing-child-downstream.yaml @@ -0,0 +1,16 @@ +# Downstream resource created by the CHILD'S composition (xnopresources.diff.example.org). +# This resource should NOT be affected when we diff the parent composition, +# because the nested XR should use its own composition. +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: test-parent-nested + namespace: default + annotations: + crossplane.io/composition-resource-name: nop-resource +spec: + forProvider: + # This value comes from the CHILD's composition (xnopresources.diff.example.org) + # which uses: configData: {{ .observed.composite.resource.spec.coolField }} + configData: child-config-value + resourceTier: basic diff --git a/cmd/crossplane/composition/testdata/comp/resources/nested-xr/existing-nested-child-xr.yaml b/cmd/crossplane/composition/testdata/comp/resources/nested-xr/existing-nested-child-xr.yaml new file mode 100644 index 00000000..e9ff65cb --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/nested-xr/existing-nested-child-xr.yaml @@ -0,0 +1,14 @@ +# Nested XNopResource created by the parent composition. +# This XR uses its OWN composition (xnopresources.diff.example.org) to create downstream resources. +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-parent-nested + namespace: default + annotations: + crossplane.io/composition-resource-name: nested-xr +spec: + coolField: child-config-value + crossplane: + compositionRef: + name: xnopresources.diff.example.org diff --git a/cmd/crossplane/composition/testdata/comp/resources/nested-xr/existing-parent-direct-downstream.yaml b/cmd/crossplane/composition/testdata/comp/resources/nested-xr/existing-parent-direct-downstream.yaml new file mode 100644 index 00000000..7d6b8b19 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/nested-xr/existing-parent-direct-downstream.yaml @@ -0,0 +1,12 @@ +# Direct downstream resource created by the parent composition +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: test-parent-direct + namespace: default + annotations: + crossplane.io/composition-resource-name: direct-resource +spec: + forProvider: + configData: parent-parent-value + resourceTier: parent-tier diff --git a/cmd/crossplane/composition/testdata/comp/resources/nested-xr/existing-parent-xr.yaml b/cmd/crossplane/composition/testdata/comp/resources/nested-xr/existing-parent-xr.yaml new file mode 100644 index 00000000..6b88e567 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/nested-xr/existing-parent-xr.yaml @@ -0,0 +1,12 @@ +# Existing parent XR that uses the parent composition +apiVersion: parent.diff.example.org/v1alpha1 +kind: XParentResource +metadata: + name: test-parent + namespace: default +spec: + parentField: parent-value + childConfig: child-config-value + crossplane: + compositionRef: + name: xparentresources.parent.diff.example.org diff --git a/cmd/crossplane/composition/testdata/comp/resources/nested-xr/parent-composition.yaml b/cmd/crossplane/composition/testdata/comp/resources/nested-xr/parent-composition.yaml new file mode 100644 index 00000000..066be354 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/nested-xr/parent-composition.yaml @@ -0,0 +1,49 @@ +# Parent composition that creates a nested XNopResource. +# The nested XNopResource uses its OWN composition (xnopresources.diff.example.org) +# to create its downstream resources. +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xparentresources.parent.diff.example.org +spec: + compositeTypeRef: + apiVersion: parent.diff.example.org/v1alpha1 + kind: XParentResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + --- + # Direct downstream resource owned by the parent XR + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }}-direct + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: direct-resource + spec: + forProvider: + configData: parent-{{ .observed.composite.resource.spec.parentField }} + resourceTier: parent-tier + --- + # Nested XR of type XNopResource - this should use its OWN composition + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + metadata: + name: {{ .observed.composite.resource.metadata.name }}-nested + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nested-xr + spec: + coolField: {{ .observed.composite.resource.spec.childConfig }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/resources/nested-xr/parent-xrd.yaml b/cmd/crossplane/composition/testdata/comp/resources/nested-xr/parent-xrd.yaml new file mode 100644 index 00000000..ffe6e36d --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/nested-xr/parent-xrd.yaml @@ -0,0 +1,50 @@ +# Parent XRD that creates nested XRs of type XNopResource +apiVersion: apiextensions.crossplane.io/v2 +kind: CompositeResourceDefinition +metadata: + name: xparentresources.parent.diff.example.org +spec: + group: parent.diff.example.org + names: + kind: XParentResource + plural: xparentresources + scope: Namespaced + versions: + - name: v1alpha1 + served: true + referenceable: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + parentField: + type: string + childConfig: + type: string + status: + type: object + properties: + conditions: + type: array + items: + type: object + required: + - lastTransitionTime + - reason + - status + - type + properties: + lastTransitionTime: + type: string + format: date-time + message: + type: string + reason: + type: string + status: + type: string + type: + type: string diff --git a/cmd/crossplane/composition/testdata/comp/resources/original-composition-2.yaml b/cmd/crossplane/composition/testdata/comp/resources/original-composition-2.yaml new file mode 100644 index 00000000..a6605cec --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/original-composition-2.yaml @@ -0,0 +1,33 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnopresources-v2.diff.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource-v2 + spec: + forProvider: + configData: v2-updated-{{ .observed.composite.resource.spec.coolField }} + resourceTier: enterprise + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready \ No newline at end of file diff --git a/cmd/crossplane/composition/testdata/comp/resources/original-composition-kubectl-applied-current.yaml b/cmd/crossplane/composition/testdata/comp/resources/original-composition-kubectl-applied-current.yaml new file mode 100644 index 00000000..0b2fe1dd --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/original-composition-kubectl-applied-current.yaml @@ -0,0 +1,41 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnopresources.diff.example.org + annotations: + # Content-identical to original-composition.yaml apart from this annotation, whose value is what a + # client-side `kubectl apply` of composition-no-changes.yaml writes: that file minus this annotation, + # JSON-encoded (kubectl keeps the emptied annotations map, hence "annotations":{}). Unlike + # original-composition-kubectl-applied.yaml, whose value is stale, re-applying that file over this + # one is a no-op: kubectl recomputes the same annotation, the patch is empty, and nothing is written. + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"apiextensions.crossplane.io/v1","kind":"Composition","metadata":{"name":"xnopresources.diff.example.org","annotations":{}},"spec":{"compositeTypeRef":{"apiVersion":"ns.diff.example.org/v1alpha1","kind":"XNopResource"},"mode":"Pipeline","pipeline":[{"step":"generate-resources","functionRef":{"name":"function-go-templating"},"input":{"apiVersion":"template.fn.crossplane.io/v1beta1","kind":"GoTemplate","source":"Inline","inline":{"template":"apiVersion: ns.nop.example.org/v1alpha1\nkind: XDownstreamResource\nmetadata:\n name: {{ .observed.composite.resource.metadata.name }}\n namespace: {{ .observed.composite.resource.metadata.namespace }}\n annotations:\n gotemplating.fn.crossplane.io/composition-resource-name: nop-resource\nspec:\n forProvider:\n configData: {{ .observed.composite.resource.spec.coolField }}\n resourceTier: basic\n"}}},{"step":"automatically-detect-ready-composed-resources","functionRef":{"name":"function-auto-ready"}}]}} +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + resourceTier: basic + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/resources/original-composition-kubectl-applied.yaml b/cmd/crossplane/composition/testdata/comp/resources/original-composition-kubectl-applied.yaml new file mode 100644 index 00000000..f13c7064 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/original-composition-kubectl-applied.yaml @@ -0,0 +1,40 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnopresources.diff.example.org + annotations: + # Content-identical to original-composition.yaml apart from this annotation, which is what a + # client-side `kubectl apply` stamps on the cluster copy. The file a user diffs never carries it, + # so this is the asymmetry the tool must not mistake for an edit. The value below is abridged — + # only its presence matters, never its content. + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"apiextensions.crossplane.io/v1","kind":"Composition","metadata":{"name":"xnopresources.diff.example.org"},"spec":{"compositeTypeRef":{"apiVersion":"ns.diff.example.org/v1alpha1","kind":"XNopResource"},"mode":"Pipeline"}} +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + resourceTier: basic + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/resources/removal-test-original-composition.yaml b/cmd/crossplane/composition/testdata/comp/resources/removal-test-original-composition.yaml new file mode 100644 index 00000000..1c07815c --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/removal-test-original-composition.yaml @@ -0,0 +1,45 @@ +# The in-cluster version of removal-test-composition, which still renders resource2. Diffing +# ../../diff/resources/removal-test-composition.yaml, which no longer does, against it removes resource2. +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: removal-test-composition +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: resource-to-be-kept + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: resource1 + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + --- + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: resource-to-be-removed + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: resource2 + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/resources/revision-templating-composition-older-revision.yaml b/cmd/crossplane/composition/testdata/comp/resources/revision-templating-composition-older-revision.yaml new file mode 100644 index 00000000..a6a9dff3 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/revision-templating-composition-older-revision.yaml @@ -0,0 +1,40 @@ +# An earlier revision of revision-templating-composition.yaml, from when the composition carried a +# channel=stable label it has since dropped: same spec, so only its name differs from revision 2's. +# The harness names and labels it from the hash of the composition it was cut from, as Crossplane does. +# The Manual XR in existing-xr-pinned-revision-ref.yaml is pinned to it. +apiVersion: apiextensions.crossplane.io/v1 +kind: CompositionRevision +metadata: + labels: + channel: "stable" + crossplane.io/composition-name: xrevisionrefs.diff.example.org +spec: + revision: 1 + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.crossplane.compositionRevisionRef.name }} + resourceTier: basic + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/resources/sha256-composition.yaml b/cmd/crossplane/composition/testdata/comp/resources/sha256-composition.yaml new file mode 100644 index 00000000..e08a7433 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/sha256-composition.yaml @@ -0,0 +1,33 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnopresources-sha256.diff.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating-sha256 + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + resourceTier: basic + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/resources/shared-child-xrs-differing.yaml b/cmd/crossplane/composition/testdata/comp/resources/shared-child-xrs-differing.yaml new file mode 100644 index 00000000..c9421c59 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/shared-child-xrs-differing.yaml @@ -0,0 +1,23 @@ +# Two XRs on the shared-child composition, in one namespace, with DIFFERENT specs, so a change to that +# composition renders its fixed-name child differently for each. No one diff is correct for both. +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: shared-child-owner-a + namespace: default +spec: + coolField: value-a + crossplane: + compositionRef: + name: xnopresources-shared-child.diff.example.org +--- +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: shared-child-owner-b + namespace: default +spec: + coolField: value-b + crossplane: + compositionRef: + name: xnopresources-shared-child.diff.example.org diff --git a/cmd/crossplane/composition/testdata/comp/resources/status-downstream-1.yaml b/cmd/crossplane/composition/testdata/comp/resources/status-downstream-1.yaml new file mode 100644 index 00000000..a84d5a67 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/status-downstream-1.yaml @@ -0,0 +1,11 @@ +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: status-test-xr-1 + namespace: default + annotations: + crossplane.io/composition-resource-name: nop-resource +spec: + forProvider: + configData: stable-value + resourceTier: standard diff --git a/cmd/crossplane/composition/testdata/comp/resources/status-downstream-2.yaml b/cmd/crossplane/composition/testdata/comp/resources/status-downstream-2.yaml new file mode 100644 index 00000000..1fee2a23 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/status-downstream-2.yaml @@ -0,0 +1,11 @@ +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: status-test-xr-2 + namespace: default + annotations: + crossplane.io/composition-resource-name: nop-resource +spec: + forProvider: + configData: stable-value + resourceTier: standard diff --git a/cmd/crossplane/composition/testdata/comp/resources/status-indicator-composition.yaml b/cmd/crossplane/composition/testdata/comp/resources/status-indicator-composition.yaml new file mode 100644 index 00000000..08fea7b5 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/status-indicator-composition.yaml @@ -0,0 +1,33 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xstatus.diff.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: stable-value + resourceTier: standard + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/resources/status-xr-1.yaml b/cmd/crossplane/composition/testdata/comp/resources/status-xr-1.yaml new file mode 100644 index 00000000..d22c27e2 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/status-xr-1.yaml @@ -0,0 +1,10 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: status-test-xr-1 + namespace: default +spec: + coolField: test-value-1 + crossplane: + compositionRef: + name: xstatus.diff.example.org diff --git a/cmd/crossplane/composition/testdata/comp/resources/status-xr-2.yaml b/cmd/crossplane/composition/testdata/comp/resources/status-xr-2.yaml new file mode 100644 index 00000000..2ef34578 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/status-xr-2.yaml @@ -0,0 +1,10 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: status-test-xr-2 + namespace: default +spec: + coolField: test-value-2 + crossplane: + compositionRef: + name: xstatus.diff.example.org diff --git a/cmd/crossplane/composition/testdata/comp/resources/test-namespace.yaml b/cmd/crossplane/composition/testdata/comp/resources/test-namespace.yaml new file mode 100644 index 00000000..f8db044d --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/test-namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: test-namespace diff --git a/cmd/crossplane/composition/testdata/comp/resources/unverified-cluster-composition.yaml b/cmd/crossplane/composition/testdata/comp/resources/unverified-cluster-composition.yaml new file mode 100644 index 00000000..5c757c43 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/resources/unverified-cluster-composition.yaml @@ -0,0 +1,15 @@ +# The in-cluster version of updated-unverified-cluster-composition.yaml: a composition for the +# cluster-scoped XNopResource that composes nothing yet. +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnopresources-unverified.diff.example.org +spec: + compositeTypeRef: + apiVersion: diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/revision-templating-updated-composition.yaml b/cmd/crossplane/composition/testdata/comp/revision-templating-updated-composition.yaml new file mode 100644 index 00000000..263ad44c --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/revision-templating-updated-composition.yaml @@ -0,0 +1,43 @@ +# A metadata-only edit of resources/revision-templating-composition.yaml: one added label, identical +# spec. Crossplane's Composition.Hash() covers labels, so applying this mints a new CompositionRevision +# with a byte-identical spec — and because the template below reads the revision's own name, the +# composed resource genuinely changes. +# +# Before issue #474 the tool rendered this composite with its *existing* compositionRevisionRef, saw no +# downstream delta, and exited 0. The label is the only difference; everything else must stay identical +# to the cluster copy or the test stops exercising the metadata-only path. +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xrevisionrefs.diff.example.org + labels: + channel: preview +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.crossplane.compositionRevisionRef.name }} + resourceTier: basic + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/status-indicator-updated-composition.yaml b/cmd/crossplane/composition/testdata/comp/status-indicator-updated-composition.yaml new file mode 100644 index 00000000..a6a6a6a7 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/status-indicator-updated-composition.yaml @@ -0,0 +1,35 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xstatus.diff.example.org + labels: + environment: production +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: stable-value + resourceTier: standard + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/updated-claim-composition.yaml b/cmd/crossplane/composition/testdata/comp/updated-claim-composition.yaml new file mode 100644 index 00000000..f619a369 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/updated-claim-composition.yaml @@ -0,0 +1,32 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: nopclaims.diff.example.org +spec: + compositeTypeRef: + apiVersion: diff.example.org/v1alpha1 + kind: XNop + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + name: {{ .observed.composite.resource.metadata.name }} + spec: + forProvider: + configData: updated-{{ .observed.composite.resource.spec.coolField }} + resourceTier: premium + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/updated-collision-composition.yaml b/cmd/crossplane/composition/testdata/comp/updated-collision-composition.yaml new file mode 100644 index 00000000..88a356ef --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/updated-collision-composition.yaml @@ -0,0 +1,50 @@ +# Updated composition - adds a prefix to trigger a diff +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnopresources.collision.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: fetch-external-resources + functionRef: + name: function-extra-resources + input: + apiVersion: extra-resources.fn.crossplane.io/v1beta1 + kind: Input + spec: + extraResources: + # Fetch ConfigMap by name - uses XR's namespace since no namespace specified + - apiVersion: v1 + kind: ConfigMap + into: nsConfig + type: Reference + ref: + name: collision-config + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + {{- $config := index .context "apiextensions.crossplane.io/extra-resources" "nsConfig" 0 -}} + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: collision-resource + spec: + forProvider: + # CHANGED: Added prefix to configData + configData: prefix-{{ index $config "data" "value" }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/updated-composition-2-changed.yaml b/cmd/crossplane/composition/testdata/comp/updated-composition-2-changed.yaml new file mode 100644 index 00000000..83dfeec3 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/updated-composition-2-changed.yaml @@ -0,0 +1,36 @@ +# A genuinely-changed version of original-composition-2.yaml (configData prefix and resourceTier +# differ). Distinct from updated-composition-2.yaml, which is byte-identical to the original and is +# used for the mixed changed/unchanged case. +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnopresources-v2.diff.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource-v2 + spec: + forProvider: + configData: v2-changed-{{ .observed.composite.resource.spec.coolField }} + resourceTier: premium-plus + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/updated-composition-2.yaml b/cmd/crossplane/composition/testdata/comp/updated-composition-2.yaml new file mode 100644 index 00000000..a6605cec --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/updated-composition-2.yaml @@ -0,0 +1,33 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnopresources-v2.diff.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource-v2 + spec: + forProvider: + configData: v2-updated-{{ .observed.composite.resource.spec.coolField }} + resourceTier: enterprise + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready \ No newline at end of file diff --git a/cmd/crossplane/composition/testdata/comp/updated-composition-labeled.yaml b/cmd/crossplane/composition/testdata/comp/updated-composition-labeled.yaml new file mode 100644 index 00000000..6768f115 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/updated-composition-labeled.yaml @@ -0,0 +1,35 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnopresources.diff.example.org + labels: + version: 0.0.2 +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: updated-{{ .observed.composite.resource.spec.coolField }} + resourceTier: premium + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/updated-conditional-gating-composition.yaml b/cmd/crossplane/composition/testdata/comp/updated-conditional-gating-composition.yaml new file mode 100644 index 00000000..6d5a3ed9 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/updated-conditional-gating-composition.yaml @@ -0,0 +1,57 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: conditional-gating-composition +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: 0-stage0-resource + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: stage0-resource + spec: + forProvider: + configData: updated-{{ .observed.composite.resource.spec.coolField }} + {{- $ready := false -}} + {{- with .observed.resources -}} + {{- with (index . "stage0-resource") -}} + {{- with .resource.status.conditions -}} + {{- range . -}} + {{- if and (eq .type "Ready") (eq .status "True") -}} + {{- $ready = true -}} + {{- end -}} + {{- end -}} + {{- end -}} + {{- end -}} + {{- end -}} + {{- if $ready }} + --- + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: 1-stage1-resource + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: stage1-resource + spec: + forProvider: + configData: updated-{{ .observed.composite.resource.spec.coolField }} + {{- end }} + - step: mark-ready-from-observed + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/updated-cycle-composition.yaml b/cmd/crossplane/composition/testdata/comp/updated-cycle-composition.yaml new file mode 100644 index 00000000..a1807a2d --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/updated-cycle-composition.yaml @@ -0,0 +1,35 @@ +# testdata/diff/resources/cycle/compositions.yaml's XCycleA composition with a spec change (a label +# on the composed XCycleB), so comp evaluates its composites at every --analyze-on setting. +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xcycleas.cycle.example.org +spec: + compositeTypeRef: + apiVersion: ns.cycle.example.org/v1alpha1 + kind: XCycleA + mode: Pipeline + pipeline: + - step: generate-cycle-b + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.cycle.example.org/v1alpha1 + kind: XCycleB + metadata: + name: {{ .observed.composite.resource.metadata.name }}-c + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: cycle-b + labels: + cycle.example.org/edited: "true" + spec: + cycleField: {{ .observed.composite.resource.spec.cycleField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/updated-parent-composition.yaml b/cmd/crossplane/composition/testdata/comp/updated-parent-composition.yaml new file mode 100644 index 00000000..b4e7093b --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/updated-parent-composition.yaml @@ -0,0 +1,50 @@ +# Updated parent composition - changes the direct downstream resource tier from parent-tier to premium-tier. +# The nested XNopResource and its downstream should NOT be affected because: +# 1. The nested XR uses its own composition (xnopresources.diff.example.org) +# 2. That composition is NOT being diffed here +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xparentresources.parent.diff.example.org +spec: + compositeTypeRef: + apiVersion: parent.diff.example.org/v1alpha1 + kind: XParentResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + --- + # Direct downstream resource owned by the parent XR - CHANGED tier + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }}-direct + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: direct-resource + spec: + forProvider: + configData: parent-{{ .observed.composite.resource.spec.parentField }} + resourceTier: premium-tier + --- + # Nested XR of type XNopResource - unchanged + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + metadata: + name: {{ .observed.composite.resource.metadata.name }}-nested + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nested-xr + spec: + coolField: {{ .observed.composite.resource.spec.childConfig }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/updated-sequencer-gating-composition.yaml b/cmd/crossplane/composition/testdata/comp/updated-sequencer-gating-composition.yaml new file mode 100644 index 00000000..603c3ec0 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/updated-sequencer-gating-composition.yaml @@ -0,0 +1,53 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: sequencer-gating-composition +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-all-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: 0-stage0-resource + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: stage0-resource + spec: + forProvider: + configData: updated-{{ .observed.composite.resource.spec.coolField }} + --- + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: 1-stage1-resource + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: stage1-resource + spec: + forProvider: + configData: updated-{{ .observed.composite.resource.spec.coolField }} + - step: mark-ready-from-observed + functionRef: + name: function-auto-ready + - step: sequence-resources + functionRef: + name: function-sequencer + input: + apiVersion: sequencer.fn.crossplane.io/v1beta1 + kind: Input + rules: + - sequence: + - ^stage0- + - ^stage1- diff --git a/cmd/crossplane/composition/testdata/comp/updated-sha256-composition.yaml b/cmd/crossplane/composition/testdata/comp/updated-sha256-composition.yaml new file mode 100644 index 00000000..de89c21e --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/updated-sha256-composition.yaml @@ -0,0 +1,33 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnopresources-sha256.diff.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating-sha256 + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: updated-{{ .observed.composite.resource.spec.coolField }} + resourceTier: premium + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/updated-shared-child-composition.yaml b/cmd/crossplane/composition/testdata/comp/updated-shared-child-composition.yaml new file mode 100644 index 00000000..e0751a68 --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/updated-shared-child-composition.yaml @@ -0,0 +1,36 @@ +# The shared-child composition (testdata/diff/resources/shared-child-composition.yaml) with a label and +# a changed template, so diffing it analyses the XRs that use it. +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnopresources-shared-child.diff.example.org + labels: + version: 0.0.2 +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-shared-child + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: shared-child + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: shared-child + spec: + forProvider: + configData: updated-{{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/composition/testdata/comp/updated-unverified-cluster-composition.yaml b/cmd/crossplane/composition/testdata/comp/updated-unverified-cluster-composition.yaml new file mode 100644 index 00000000..0f80e57d --- /dev/null +++ b/cmd/crossplane/composition/testdata/comp/updated-unverified-cluster-composition.yaml @@ -0,0 +1,47 @@ +# Adds two ConfigMaps in a namespace that does not exist in the cluster. +# +# A cluster-scoped XR may compose resources in any namespace (a namespaced one is confined to its +# own), which is what lets `comp` reach a missing namespace from an XR that already exists. Both +# additions' dry-run creates are refused for want of the namespace, so both are diffed unverified, +# and the warning saying so counts them. +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnopresources-unverified.diff.example.org +spec: + compositeTypeRef: + apiVersion: diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ .observed.composite.resource.metadata.name }}-first + namespace: nonexistent-namespace + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: first-config + data: + configData: {{ .observed.composite.resource.spec.coolField }} + --- + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ .observed.composite.resource.metadata.name }}-second + namespace: nonexistent-namespace + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: second-config + data: + configData: {{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready From 6d897fe11c66051cfd7d4f52d7edebbd0f0254fa Mon Sep 17 00:00:00 2001 From: Jonathan Ogilvie Date: Fri, 6 Nov 2026 18:02:43 -0500 Subject: [PATCH 3/6] test(xr): own the xr integration suite Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Jonathan Ogilvie --- cmd/crossplane/xr/integration_main_test.go | 23 + cmd/crossplane/xr/integration_test.go | 3131 +++++++++++++++++ cmd/crossplane/xr/testdata/README.md | 12 + .../existing-xr-revision-ref-omitted.yaml | 16 + .../diff/first-xr-with-composition-ref.yaml | 11 + cmd/crossplane/xr/testdata/diff/first-xr.yaml | 7 + .../xr/testdata/diff/generated-name-xr.yaml | 7 + .../xr/testdata/diff/invalid-schema-xr.yaml | 8 + ...ied-api-version-xr-rev2-changed-field.yaml | 13 + .../diff/modified-api-version-xr-rev2.yaml | 13 + .../testdata/diff/modified-claim-nested.yaml | 12 + ...ified-claim-omitting-defaulted-fields.yaml | 11 + .../xr/testdata/diff/modified-claim.yaml | 10 + .../xr/testdata/diff/modified-cluster-xr.yaml | 6 + .../testdata/diff/modified-cross-ns-xr.yaml | 8 + .../xr/testdata/diff/modified-env-xr.yaml | 7 + ...dified-legacy-xr-manual-upgrade-to-v2.yaml | 11 + ...egacy-xr-manual-v1-omits-revision-ref.yaml | 12 + .../xr/testdata/diff/modified-legacy-xr.yaml | 7 + .../diff/modified-manual-pinned-claim.yaml | 12 + .../xr/testdata/diff/modified-nested-xr.yaml | 8 + .../testdata/diff/modified-xr-automatic.yaml | 11 + .../modified-xr-manual-upgrade-to-v2.yaml | 13 + ...ified-xr-manual-v1-omits-revision-ref.yaml | 16 + .../testdata/diff/modified-xr-manual-v1.yaml | 13 + .../diff/modified-xr-selector-bound.yaml | 13 + .../diff/modified-xr-selector-nomatch.yaml | 14 + .../diff/modified-xr-selector-stable.yaml | 14 + .../diff/modified-xr-switch-to-automatic.yaml | 11 + .../diff/modified-xr-with-external-dep.yaml | 8 + .../new-claim-with-claimref-composition.yaml | 10 + .../xr/testdata/diff/new-claim.yaml | 11 + .../xr/testdata/diff/new-cycle-xr.yaml | 8 + .../testdata/diff/new-manual-default-xr.yaml | 15 + .../xr/testdata/diff/new-nested-child-xr.yaml | 9 + .../xr/testdata/diff/new-nested-xr.yaml | 8 + ...arent-claim-omitting-defaulted-fields.yaml | 11 + .../diff/new-rendered-defaults-xrs.yaml | 16 + .../testdata/diff/new-xr-manual-no-ref.yaml | 11 + .../diff/new-xr-missing-namespace-second.yaml | 10 + .../diff/new-xr-missing-namespace.yaml | 13 + .../xr/testdata/diff/new-xr-second.yaml | 7 + .../xr/testdata/diff/new-xr-with-creds.yaml | 10 + .../new-xr-with-cross-ns-external-dep.yaml | 11 + .../resources/admission-policy-canary.yaml | 15 + .../api-version-composition-revision-v1.yaml | 34 + .../resources/api-version-composition.yaml | 34 + .../claim-composition-with-claimref.yaml | 35 + .../diff/resources/claim-composition.yaml | 31 + .../claim-nested/child-composition.yaml | 34 + .../claim-nested/child-definition.yaml | 29 + .../claim-nested/existing-child-xr.yaml | 11 + .../claim-nested/existing-claim.yaml | 18 + .../existing-managed-resource.yaml | 14 + .../claim-nested/existing-parent-xr.yaml | 24 + .../claim-nested/parent-composition.yaml | 31 + .../parent-defaults-composition.yaml | 35 + .../claim-nested/parent-definition.yaml | 35 + .../xr/testdata/diff/resources/claim-xrd.yaml | 30 + .../composition-no-namespace-patch.yaml | 32 + .../composition-revision-preview.yaml | 38 + .../composition-revision-stable.yaml | 38 + .../resources/composition-revision-v1.yaml | 34 + .../diff/resources/composition-v2.yaml | 32 + .../composition-with-builtin-defaults.yaml | 61 + .../resources/composition-with-configmap.yaml | 48 + ...osition-with-crd-defaulted-downstream.yaml | 33 + .../resources/composition-with-defaults.yaml | 16 + .../composition-with-missing-function.yaml | 17 + ...n-with-non-storage-version-downstream.yaml | 33 + .../resources/composition-with-status.yaml | 41 + .../composition-with-unserved-k8s-group.yaml | 42 + .../diff/resources/conditional-gating-xr.yaml | 7 + .../credentials/cli-credentials.yaml | 9 + .../credentials/composition-with-creds.yaml | 39 + .../function-credentials-secret.yaml | 14 + .../resources/cross-namespace-configmap.yaml | 21 + .../cross-namespace-fn-composition.yaml | 72 + .../diff/resources/default-composition.yaml | 36 + .../diff/resources/env-composition.yaml | 46 + .../xr/testdata/diff/resources/env-xrd.yaml | 22 + .../resources/environment-config-v1beta1.yaml | 8 + ...isting-api-version-downstream-v1beta1.yaml | 10 + .../existing-api-version-xr-rev1.yaml | 13 + ...-claim-defaulted-fields-other-manager.yaml | 10 + .../existing-claim-downstream-resource.yaml | 9 + .../diff/resources/existing-claim-xr.yaml | 21 + .../diff/resources/existing-claim.yaml | 18 + .../diff/resources/existing-cluster-xr.yaml | 10 + .../existing-cross-ns-downstream.yaml | 10 + .../diff/resources/existing-cross-ns-xr.yaml | 11 + ...xisting-defaulted-downstream-composed.yaml | 14 + ...ng-defaulted-downstream-other-manager.yaml | 10 + .../existing-downstream-automatic.yaml | 10 + .../existing-downstream-manual-v1.yaml | 10 + ...sting-downstream-resource-with-argocd.yaml | 15 + .../existing-downstream-selector-stable.yaml | 10 + ...existing-downstream-with-external-dep.yaml | 11 + ...isting-downstream-with-generated-name.yaml | 13 + .../existing-env-downstream-resource.yaml | 16 + .../diff/resources/existing-env-xr.yaml | 9 + .../resources/existing-generate-name-xr.yaml | 13 + .../existing-legacy-downstream-manual-v1.yaml | 9 + .../existing-legacy-xr-manual-v1.yaml | 11 + .../diff/resources/existing-legacy-xr.yaml | 9 + .../diff/resources/existing-namespace.yaml | 4 + .../resources/existing-removal-test-xr.yaml | 13 + ...xisting-sequencer-gating-stage0-ready.yaml | 19 + .../existing-sequencer-gating-stage0.yaml | 19 + ...xisting-sequencer-gating-stage1-ready.yaml | 18 + .../existing-sequencer-gating-xr.yaml | 13 + .../diff/resources/existing-sequencer-xr.yaml | 12 + .../existing-v2xrd-v1paths-downstream.yaml | 10 + .../resources/existing-v2xrd-v1paths-xr.yaml | 14 + .../diff/resources/existing-xr-automatic.yaml | 13 + .../diff/resources/existing-xr-deleting.yaml | 14 + ...ing-xr-listing-uncontrolled-resources.yaml | 23 + .../diff/resources/existing-xr-manual-v1.yaml | 13 + .../existing-xr-selector-stable.yaml | 16 + .../resources/existing-xr-with-argocd.yaml | 19 + ...isting-xr-with-defaults-other-manager.yaml | 9 + .../existing-xr-with-defaults-reconciled.yaml | 16 + ...-xr-with-external-dep-multistep-fatal.yaml | 14 + ...isting-xr-with-external-dep-templated.yaml | 13 + ...isting-xr-with-external-dep-unguarded.yaml | 14 + .../existing-xr-with-external-dep.yaml | 11 + .../resources/external-named-clusterrole.yaml | 8 + .../external-res-fn-composition.yaml | 64 + .../external-res-gotpl-composition.yaml | 46 + ...ernal-res-matchname-gotpl-composition.yaml | 46 + ...al-resource-configmap-other-namespace.yaml | 22 + .../external-resource-configmap.yaml | 21 + .../resources/generated-name-composition.yaml | 32 + .../legacy-composition-revision-v1.yaml | 33 + .../diff/resources/legacy-composition-v2.yaml | 31 + .../testdata/diff/resources/legacy-xrd.yaml | 49 + .../resources/manual-default/definitions.yaml | 96 + .../resources/modified-v2xrd-v1paths-xr.yaml | 13 + ...tep-fatal-after-extra-res-composition.yaml | 65 + .../resources/nested/child-composition.yaml | 33 + ...child-revision-templating-composition.yaml | 38 + .../diff/resources/nested/child-xrd.yaml | 55 + .../existing-child-xr-revision-ref.yaml | 18 + .../existing-child-xr-tier-other-manager.yaml | 9 + .../resources/nested/existing-child-xr.yaml | 13 + ...xisting-managed-resource-revision-ref.yaml | 15 + .../nested/existing-managed-resource.yaml | 11 + .../resources/nested/existing-parent-xr.yaml | 11 + .../resources/nested/parent-composition.yaml | 32 + .../diff/resources/nested/parent-xrd.yaml | 50 + .../resources/pinned-claim/definitions.yaml | 96 + .../existing-automatic-downstream.yaml | 10 + .../pinned-claim/existing-automatic-xr.yaml | 15 + .../pinned-claim/existing-automatic.yaml | 19 + ...existing-lagging-automatic-downstream.yaml | 10 + .../existing-lagging-automatic-xr.yaml | 16 + .../existing-manual-downstream.yaml | 10 + .../pinned-claim/existing-manual-xr.yaml | 17 + .../pinned-claim/existing-manual.yaml | 19 + .../resources/production-composition.yaml | 36 + .../resources/rejecting-admission-policy.yaml | 38 + .../removal-test-cluster-composition.yaml | 31 + ...val-test-cluster-downstream-resource1.yaml | 9 + ...removal-test-legacy-child-composition.yaml | 31 + .../removal-test-legacy-child-xrd.yaml | 24 + .../removal-test-legacy-composition.yaml | 31 + ...removal-test-legacy-nested-xr-managed.yaml | 11 + .../removal-test-legacy-nested-xr.yaml | 13 + ...st-legacycluster-downstream-resource1.yaml | 9 + .../removal-test-nested-xr-managed.yaml | 11 + .../resources/removal-test-nested-xr.yaml | 15 + .../rendered-defaults/definitions.yaml | 129 + .../selector-bound/existing-downstream.yaml | 11 + .../resources/selector-bound/existing-xr.yaml | 16 + .../diff/resources/selector-bound/setup.yaml | 74 + .../diff/resources/sequencer-composition.yaml | 53 + .../sequencer-stage0-downstream.yaml | 10 + .../sequencer-stage1-downstream.yaml | 10 + .../sequencer-with-envconfig-composition.yaml | 67 + .../sequencer-with-envconfig-xr.yaml | 10 + .../testdata/diff/resources/sequencer-xr.yaml | 7 + .../diff/resources/staging-composition.yaml | 36 + .../diff/resources/unestablished-xrd.yaml | 25 + ...uarded-external-res-gotpl-composition.yaml | 44 + .../v2-xrd-with-v1-paths-composition.yaml | 32 + .../diff/resources/v2-xrd-with-v1-paths.yaml | 101 + .../resources/xdownstreamenvresource-xrd.yaml | 31 + .../diff/resources/xrd-with-defaults.yaml | 79 + .../unchanged-automatic-pinned-claim.yaml | 12 + .../diff/unchanged-lagging-automatic-xr.yaml | 11 + .../diff/xr-with-ambiguous-selector.yaml | 11 + .../diff/xr-with-argocd-annotations.yaml | 13 + .../diff/xr-with-argocd-mixed-changes.yaml | 13 + .../diff/xr-with-composition-ref.yaml | 10 + .../diff/xr-with-composition-selector.yaml | 12 + .../diff/xr-with-missing-defaults.yaml | 9 + .../diff/xr-with-overridden-defaults.yaml | 19 + 197 files changed, 7453 insertions(+) create mode 100644 cmd/crossplane/xr/integration_main_test.go create mode 100644 cmd/crossplane/xr/integration_test.go create mode 100644 cmd/crossplane/xr/testdata/README.md create mode 100644 cmd/crossplane/xr/testdata/diff/existing-xr-revision-ref-omitted.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/first-xr-with-composition-ref.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/first-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/generated-name-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/invalid-schema-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/modified-api-version-xr-rev2-changed-field.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/modified-api-version-xr-rev2.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/modified-claim-nested.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/modified-claim-omitting-defaulted-fields.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/modified-claim.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/modified-cluster-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/modified-cross-ns-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/modified-env-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/modified-legacy-xr-manual-upgrade-to-v2.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/modified-legacy-xr-manual-v1-omits-revision-ref.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/modified-legacy-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/modified-manual-pinned-claim.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/modified-nested-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/modified-xr-automatic.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/modified-xr-manual-upgrade-to-v2.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/modified-xr-manual-v1-omits-revision-ref.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/modified-xr-manual-v1.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/modified-xr-selector-bound.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/modified-xr-selector-nomatch.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/modified-xr-selector-stable.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/modified-xr-switch-to-automatic.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/modified-xr-with-external-dep.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/new-claim-with-claimref-composition.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/new-claim.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/new-cycle-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/new-manual-default-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/new-nested-child-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/new-nested-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/new-parent-claim-omitting-defaulted-fields.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/new-rendered-defaults-xrs.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/new-xr-manual-no-ref.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/new-xr-missing-namespace-second.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/new-xr-missing-namespace.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/new-xr-second.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/new-xr-with-creds.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/new-xr-with-cross-ns-external-dep.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/admission-policy-canary.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/api-version-composition-revision-v1.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/api-version-composition.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/claim-composition-with-claimref.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/claim-composition.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/claim-nested/child-composition.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/claim-nested/child-definition.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/claim-nested/existing-child-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/claim-nested/existing-claim.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/claim-nested/existing-managed-resource.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/claim-nested/existing-parent-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/claim-nested/parent-composition.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/claim-nested/parent-defaults-composition.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/claim-nested/parent-definition.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/claim-xrd.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/composition-no-namespace-patch.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/composition-revision-preview.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/composition-revision-stable.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/composition-revision-v1.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/composition-v2.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/composition-with-builtin-defaults.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/composition-with-configmap.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/composition-with-crd-defaulted-downstream.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/composition-with-defaults.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/composition-with-missing-function.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/composition-with-non-storage-version-downstream.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/composition-with-status.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/composition-with-unserved-k8s-group.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/conditional-gating-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/credentials/cli-credentials.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/credentials/composition-with-creds.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/credentials/function-credentials-secret.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/cross-namespace-configmap.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/cross-namespace-fn-composition.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/default-composition.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/env-composition.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/env-xrd.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/environment-config-v1beta1.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-api-version-downstream-v1beta1.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-api-version-xr-rev1.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-claim-defaulted-fields-other-manager.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-claim-downstream-resource.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-claim-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-claim.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-cluster-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-cross-ns-downstream.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-cross-ns-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-defaulted-downstream-composed.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-defaulted-downstream-other-manager.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-downstream-automatic.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-downstream-manual-v1.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-downstream-resource-with-argocd.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-downstream-selector-stable.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-downstream-with-external-dep.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-downstream-with-generated-name.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-env-downstream-resource.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-env-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-generate-name-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-legacy-downstream-manual-v1.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-legacy-xr-manual-v1.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-legacy-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-namespace.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-removal-test-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-sequencer-gating-stage0-ready.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-sequencer-gating-stage0.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-sequencer-gating-stage1-ready.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-sequencer-gating-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-sequencer-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-v2xrd-v1paths-downstream.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-v2xrd-v1paths-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-xr-automatic.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-xr-deleting.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-xr-listing-uncontrolled-resources.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-xr-manual-v1.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-xr-selector-stable.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-xr-with-argocd.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-xr-with-defaults-other-manager.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-xr-with-defaults-reconciled.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-xr-with-external-dep-multistep-fatal.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-xr-with-external-dep-templated.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-xr-with-external-dep-unguarded.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/existing-xr-with-external-dep.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/external-named-clusterrole.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/external-res-fn-composition.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/external-res-gotpl-composition.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/external-res-matchname-gotpl-composition.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/external-resource-configmap-other-namespace.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/external-resource-configmap.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/generated-name-composition.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/legacy-composition-revision-v1.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/legacy-composition-v2.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/legacy-xrd.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/manual-default/definitions.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/modified-v2xrd-v1paths-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/multistep-fatal-after-extra-res-composition.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/nested/child-composition.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/nested/child-revision-templating-composition.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/nested/child-xrd.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/nested/existing-child-xr-revision-ref.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/nested/existing-child-xr-tier-other-manager.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/nested/existing-child-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/nested/existing-managed-resource-revision-ref.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/nested/existing-managed-resource.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/nested/existing-parent-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/nested/parent-composition.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/nested/parent-xrd.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/pinned-claim/definitions.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-automatic-downstream.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-automatic-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-automatic.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-lagging-automatic-downstream.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-lagging-automatic-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-manual-downstream.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-manual-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-manual.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/production-composition.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/rejecting-admission-policy.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/removal-test-cluster-composition.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/removal-test-cluster-downstream-resource1.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/removal-test-legacy-child-composition.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/removal-test-legacy-child-xrd.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/removal-test-legacy-composition.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/removal-test-legacy-nested-xr-managed.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/removal-test-legacy-nested-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/removal-test-legacycluster-downstream-resource1.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/removal-test-nested-xr-managed.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/removal-test-nested-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/rendered-defaults/definitions.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/selector-bound/existing-downstream.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/selector-bound/existing-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/selector-bound/setup.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/sequencer-composition.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/sequencer-stage0-downstream.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/sequencer-stage1-downstream.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/sequencer-with-envconfig-composition.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/sequencer-with-envconfig-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/sequencer-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/staging-composition.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/unestablished-xrd.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/unguarded-external-res-gotpl-composition.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/v2-xrd-with-v1-paths-composition.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/v2-xrd-with-v1-paths.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/xdownstreamenvresource-xrd.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/resources/xrd-with-defaults.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/unchanged-automatic-pinned-claim.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/unchanged-lagging-automatic-xr.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/xr-with-ambiguous-selector.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/xr-with-argocd-annotations.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/xr-with-argocd-mixed-changes.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/xr-with-composition-ref.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/xr-with-composition-selector.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/xr-with-missing-defaults.yaml create mode 100644 cmd/crossplane/xr/testdata/diff/xr-with-overridden-defaults.yaml diff --git a/cmd/crossplane/xr/integration_main_test.go b/cmd/crossplane/xr/integration_main_test.go new file mode 100644 index 00000000..18f843b8 --- /dev/null +++ b/cmd/crossplane/xr/integration_main_test.go @@ -0,0 +1,23 @@ +//go:build integration + +package xr_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) +} diff --git a/cmd/crossplane/xr/integration_test.go b/cmd/crossplane/xr/integration_test.go new file mode 100644 index 00000000..c60bc0d3 --- /dev/null +++ b/cmd/crossplane/xr/integration_test.go @@ -0,0 +1,3131 @@ +//go:build integration + +/* +Copyright 2026 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package xr_test + +import ( + "strings" + "testing" + + dp "github.com/crossplane/cli/v2/internal/diff/diffprocessor" + tu "github.com/crossplane/cli/v2/internal/diff/testutils" + "github.com/crossplane/cli/v2/internal/simulate/simtest" + "github.com/crossplane/cli/v2/internal/testutils/integration" + "github.com/crossplane/cli/v2/internal/testutils/integration/simharness" +) + +// TestDiffIntegration runs an integration test for the diff command. +// +//nolint:tparallel // simharness.RunIntegrationTest calls t.Parallel in each subtest. +func TestDiffIntegration(t *testing.T) { + t.Parallel() + + tests := map[string]simharness.IntegrationTestCase{ + "NewResourceDiff": { + Reason: "Shows color diff for new resources", + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/new-xr.yaml"}, + SetupFiles: []string{ + // TODO: For v2, we need to upgrade the XRD apiversion to v2 + put the xr in a namespace. + // this might be better served as a separate test(s). + // since we aren't actually creating resources (there's no crossplane actually running in unit tests), + // we don't need to worry about upgrading providers or anything. + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + ExpectedOutput: strings.Join([]string{ + `+++ XDownstreamResource/test-resource +`, tu.Green(`+ apiVersion: ns.nop.example.org/v1alpha1 ++ kind: XDownstreamResource ++ metadata: ++ annotations: ++ crossplane.io/composition-resource-name: nop-resource ++ labels: ++ crossplane.io/composite: test-resource ++ name: test-resource ++ namespace: default ++ spec: ++ forProvider: ++ configData: new-value +`), ` +--- ++++ XNopResource/test-resource +`, tu.Green(`+ apiVersion: ns.diff.example.org/v1alpha1 ++ kind: XNopResource ++ metadata: ++ name: test-resource ++ namespace: default ++ spec: ++ coolField: new-value +`), ` +--- +`, + }, ""), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "AutomaticNamespacePropagation": { + Reason: "Validates automatic namespace propagation for namespaced managed resources", + OutputFormat: "json", + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/new-xr.yaml"}, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "testdata/diff/resources/composition-no-namespace-patch.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + ExpectedStructured: simtest.Expect(). + WithSummary(2, 0, 0). + WithAddedResource("XDownstreamResource", "test-resource", "default"). + WithField("spec.forProvider.configData", "new-value"). + And(). + WithAddedResource("XNopResource", "test-resource", "default"). + WithField("spec.coolField", "new-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // A built-in Kubernetes type has no CRD, so this exercises the paths + // that would otherwise read scope and schema from one. Before scope + // came from discovery this failed outright, with "cannot determine + // scope for resource ConfigMap/... CRD not found". + "RendersBuiltInResourceWithoutCRD": { + Reason: "A composition rendering a built-in Kubernetes resource (no CRD) produces a diff for it", + OutputFormat: "json", + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/new-xr.yaml"}, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "testdata/diff/resources/composition-with-configmap.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + ExpectedStructured: simtest.Expect(). + WithSummary(3, 0, 0). + WithAddedResource("ConfigMap", "test-resource-config", "default"). + WithField("data.configData", "new-value"). + And(). + WithAddedResource("XDownstreamResource", "test-resource", "default"). + WithField("spec.forProvider.configData", "new-value"). + And(). + WithAddedResource("XNopResource", "test-resource", "default"). + WithField("spec.coolField", "new-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "UnservedGroupIsReportedAsAnUnknownTypeNotAMissingNamespace": { + // From the PR #502 review. A resource whose group/version the cluster does not serve must be + // reported as an unknown type; blaming its namespace would send the user after the wrong root + // cause entirely, since the namespace here exists and is irrelevant. + // + // Worth being precise about what this covers, because it is NOT the sentinel branch in + // dryRunCreateAddition. For a *.k8s.io group, two earlier guards let the resource through — + // IsCRDRequired skips discovery for that suffix, and FetchCurrentObject swallows NotFound as + // "this is new" — but removeNamespacesFromClusterScopedResources then fails first, because + // scope determination cannot resolve the GVK either. So this pins the outer behaviour (an + // accurate, type-naming error rather than a namespace one) without depending on which layer + // produces it. + // + // The sentinel branch is what covers the case this CANNOT reach: scope determination falls + // back to a CRD lookup when discovery fails, whereas GVKToGVR is discovery-only, so a CRD + // whose version is not `served` passes the scope check and fails inside DryRunCreate. That + // route is unit-covered in diff_calculator_test.go, since arranging a served:false CRD whose + // scope still resolves is far more setup than the classification warrants. + Reason: "An addition whose group/version the cluster does not serve is reported as an unknown type, not as a missing namespace", + OutputFormat: "json", + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/new-xr.yaml"}, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "testdata/diff/resources/composition-with-unserved-k8s-group.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + ExpectedError: true, + ExpectedErrorContains: "totallynotserved.k8s.io/v1", + ExpectedExitCode: dp.ExitCodeToolError, + }, + "AdditionInMissingNamespaceDegradesRatherThanFailing": { + // Regression test for the e2e failure the first cut of this feature caused + // (TestDiffConcurrentDirectory diffs 21 XRs into a namespace that is never created). + // + // NamespaceLifecycle admission refuses a dry-run create into a namespace that does not exist. + // That must degrade, not fail the run: a Namespace and its contents are routinely applied + // together, so the namespace's absence at diff time says nothing about whether the apply will + // succeed — which is what separates it from a quota or webhook refusal, where the verdict + // describes the object and will still hold. + // + // The exit code is the real assertion. Before the fix this was exit 1 with no diff at all. + Reason: "An addition whose namespace does not exist yet still produces a diff, marked as unverified", + OutputFormat: "json", + InputFiles: []string{"testdata/diff/new-xr-missing-namespace.yaml"}, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + ExpectedStructured: simtest.Expect(). + WithSummary(2, 0, 0). + WithAddedResource("XNopResource", "test-resource", "nonexistent-namespace"). + WithField("spec.coolField", "new-value"). + WithDryRunSkipped("namespaceNotFound", "nonexistent-namespace"). + And(). + WithAddedResource("XDownstreamResource", "test-resource", "nonexistent-namespace"). + WithDryRunSkipped("namespaceNotFound", "nonexistent-namespace"). + And(). + // One warning per GVK, and each cause exactly the apiserver's message. Through the REAL + // ApplyClient, whose error is wrapped with "failed to dry-run create resource /": + // if that wrapper leaked into the cause, every resource would be a distinct warning. The unit + // test mirrors the wrapping by hand; this pins it against the client that actually produces it. + WithWarning("their namespace does not exist yet"). + WithWarningContext(map[string]string{ + "gvk": "ns.diff.example.org/v1alpha1, Kind=XNopResource", + "namespace": "nonexistent-namespace", + "cause": `namespaces "nonexistent-namespace" not found`, + "count": "1", + }). + And(). + WithWarning("their namespace does not exist yet"). + WithWarningContext(map[string]string{ + "gvk": "ns.nop.example.org/v1alpha1, Kind=XDownstreamResource", + "namespace": "nonexistent-namespace", + "cause": `namespaces "nonexistent-namespace" not found`, + "count": "1", + }), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // Human-readable output shows no dryRun at all, so stderr is the only place a person learns that + // additions went unverified, and how many. Two inputs into one missing namespace share each GVK's + // warning rather than raising one per resource. + "UnverifiedAdditionsAcrossInputsAreCountedInOneWarning": { + Reason: "In text mode, additions that could not be verified are summarised on stderr once per GVK + namespace + cause, with a count of the resources behind it", + InputFiles: []string{ + "testdata/diff/new-xr-missing-namespace.yaml", + "testdata/diff/new-xr-missing-namespace-second.yaml", + }, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + NoColor: true, + ExpectedStderrContains: []string{ + `WARNING: skipped apiserver verification of added resources: their namespace does not exist yet, so their diffs omit server-side defaulting and admission ` + + `(cause=namespaces "nonexistent-namespace" not found, count=2, gvk=ns.diff.example.org/v1alpha1, Kind=XNopResource, namespace=nonexistent-namespace)`, + `WARNING: skipped apiserver verification of added resources: their namespace does not exist yet, so their diffs omit server-side defaulting and admission ` + + `(cause=namespaces "nonexistent-namespace" not found, count=2, gvk=ns.nop.example.org/v1alpha1, Kind=XDownstreamResource, namespace=nonexistent-namespace)`, + }, + ExpectedOutput: "+++ XDownstreamResource/second-resource", + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "BuiltInResourceAdditionPicksUpApiserverDefaults": { + // The choice of a built-in type keeps this test non-vacuous whatever crossplane-diff defaults + // locally. The lenient Defaulter (defaulter.go) predicts CRD-derived `default:` values for an + // addition that gets no apiserver result, and passes anything without a CRD through unchanged. + // Deployment has no CRD, so IsCRDRequired is false, and the fields asserted below can ONLY have + // come from round-tripping the addition through the apiserver. + // + // Every asserted default is string-valued on purpose: assertChangeFields compares with + // reflect.DeepEqual against JSON-decoded values, so a numeric default (revisionHistoryLimit: 10) + // would arrive as float64 and an int literal here would fail for the wrong reason. + Reason: "An added built-in resource is dry-run created against the apiserver, so apiserver-side defaults appear in its addition diff", + OutputFormat: "json", + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/new-xr.yaml"}, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "testdata/diff/resources/composition-with-builtin-defaults.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + ExpectedStructured: simtest.Expect(). + WithSummary(2, 0, 0). + WithAddedResource("Deployment", "test-resource-deploy", "default"). + // Absence of dryRun is the machine-readable claim that this desired state was + // verified against the apiserver. Asserted alongside the defaults so that a + // regression which stopped dry-running additions fails on the contract as well as + // on whichever fields happen to depend on server-side defaulting. + WithDryRunPerformed(). + WithField("spec.strategy.type", "RollingUpdate"). + WithField("spec.template.spec.restartPolicy", "Always"). + WithField("spec.template.spec.dnsPolicy", "ClusterFirst"). + And(). + WithAddedResource("XNopResource", "test-resource", "default"). + WithDryRunPerformed(). + WithField("spec.coolField", "new-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // The inverse of the case above, and the pair is what makes either meaningful: on its own, + // "the defaults appear" could be satisfied by something other than the apiserver round-trip, + // and "the defaults are absent" could be satisfied by the feature being broken. Together they + // pin the flag as the thing that decides. + // + // This also pins that `existing` reproduces the behaviour from before additions were dry-run + // (#334), which is why the fields are asserted absent rather than merely unasserted. + "BuiltInResourceAdditionSkipsApiserverWhenDryRunOnExisting": { + Reason: "With --dry-run-on=existing an added built-in resource is not sent to the apiserver, so its diff carries no server-side defaults and reports skipReason disabled", + OutputFormat: "json", + DryRunOn: "existing", + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/new-xr.yaml"}, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "testdata/diff/resources/composition-with-builtin-defaults.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + ExpectedStructured: simtest.Expect(). + WithSummary(2, 0, 0). + WithAddedResource("Deployment", "test-resource-deploy", "default"). + // No detail substring: "disabled" is the one skip reason the user asked for, so + // there is no cluster explanation to propagate. + WithDryRunSkipped("disabled", ""). + WithFieldAbsent("spec.strategy.type"). + WithFieldAbsent("spec.template.spec.restartPolicy"). + WithFieldAbsent("spec.template.spec.dnsPolicy"). + // The rendered fields must still be there — `existing` degrades fidelity, it does + // not degrade the diff. + WithField("spec.template.spec.containers[0].image", "nginx:1.27"). + And(). + WithAddedResource("XNopResource", "test-resource", "default"). + WithDryRunSkipped("disabled", ""). + WithField("spec.coolField", "new-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // A status authored by the composition pipeline must survive the apiserver round-trip. + // XStatusResource declares the status subresource, so the apiserver strips status from the + // dry-run create and returns an object without one; the value below can therefore only come + // from the calculator re-attaching what was rendered. Every other composed kind in this + // testdata tree omits the subresource, which would have made this assertion vacuous. + // + // SKIPPED, and the reason is a finding rather than an environment problem: the re-attach + // works, but nothing renders it. See skipReason. + "AddedResourceRetainsCompositionAuthoredStatus": { + Skip: true, + SkipReason: "The re-attached status cannot be observed in any output, so this assertion " + + "cannot pass and could not be made to fail by breaking the production code it targets. " + + "cleanupForDiff (compare/compare.go) deletes metadata.status unconditionally from " + + "both sides of every diff, in the same block that deletes ownerReferences — the strip whose " + + "identical consequence for ownerReferences this feature's own spec documented (§1a) and then " + + "missed for status. ResourceViews.Clean is the only view any renderer reads" + + "; ResourceViews.Desired.Raw, which does carry the " + + "re-attached status, has no reader at all. Verified empirically: the apiserver's dry-run " + + "create response for XStatusResource carries no status, the object handed to cleanupForDiff " + + "carries status.phase=authored-by-composition, and the object cleanupForDiff returns does " + + "not. Deciding whether status belongs in diff output is a rendering-design question well " + + "outside a test task, so this is left failing-by-skip with its instrument intact rather " + + "than quietly reworded into an assertion that passes.", + Reason: "A status written by the composition pipeline survives the dry-run create of an added resource", + OutputFormat: "json", + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/new-xr.yaml"}, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "testdata/diff/resources/composition-with-status.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + ExpectedStructured: simtest.Expect(). + WithSummary(2, 0, 0). + WithAddedResource("XStatusResource", "test-resource", "default"). + WithDryRunPerformed(). + WithField("status.phase", "authored-by-composition"). + WithField("spec.forProvider.configData", "new-value"). + And(). + WithAddedResource("XNopResource", "test-resource", "default"). + WithField("spec.coolField", "new-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // The apiserver runs names.Generator in rest.BeforeCreate, ahead of the dry-run + // short-circuit at the storage layer, so it mints a real random name even for a request it + // never persists. Unless that identity is restored, a generateName addition's diff differs on + // every invocation — which only a repeated run can detect, hence assertDeterministicAcrossRuns. + "GenerateNameAdditionIsDeterministicAcrossRuns": { + Reason: "An addition using generateName renders identically across two runs, i.e. no server-generated name leaks into the diff", + OutputFormat: "json", + DryRunOn: "all", + AssertDeterministicAcrossRuns: true, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "testdata/diff/resources/generated-name-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + InputFiles: []string{"testdata/diff/generated-name-xr.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(2, 0, 0). + // Both the XR and its composed resource use generateName, so both exercise the + // identity restore. The pattern is the renderer's display form for a + // not-yet-named object; a leaked server name would be a random suffix instead. + WithAddedResource("XDownstreamResource", "", "default"). + WithDryRunPerformed(). + WithNamePattern(`^test-resource-\(generated\)$`). + WithField("spec.forProvider.configData", "new-value"). + And(). + WithAddedResource("XNopResource", "", "default"). + WithDryRunPerformed(). + WithNamePattern(`^generated-xr-\(generated\)$`). + WithField("spec.coolField", "new-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // Cluster rejection of an ADDITION. A ValidatingAdmissionPolicy is the instrument rather than a CEL + // x-kubernetes-validations rule on the CRD, because crossplane-diff evaluates CEL rules + // locally before the diff calculator runs: a CEL rule would reject the resource without the + // apiserver ever being consulted, so the test would pass on code that never dry-ran anything. + "AddedResourceRejectedByAdmissionPolicyIsAValidationFailure": { + Reason: "An apiserver rejection of an added resource is exit 2 with a typed admission validation failure, not a bare tool error", + OutputFormat: "json", + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/new-xr.yaml"}, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + "testdata/diff/resources/rejecting-admission-policy.yaml", + }, + AwaitAdmissionDenialFor: "testdata/diff/resources/admission-policy-canary.yaml", + ExpectedError: true, + ExpectedExitCode: dp.ExitCodeSchemaValidation, + ExpectedStructured: simtest.Expect(). + // errors[].resourceID names the input the user supplied, so the rejected composed + // resource has to be identifiable from the message and the validationFailures entry. + WithError("XNopResource/test-resource"). + WithMessageContaining("the cluster rejected XDownstreamResource/test-resource"). + WithValidationFailure("ns.nop.example.org/v1alpha1", "XDownstreamResource", "test-resource", "default"). + WithStatus("invalid"). + // The "admission" type is what distinguishes a cluster rejection from a local schema + // rejection; field is empty because an admission refusal is not localized to one field. + WithFieldError("admission", ""). + WithMessageContaining("this cluster refuses that configData value"), + }, + // Cluster rejection of a MODIFICATION — the deliberate behaviour change. The identical cluster fact used to + // surface as a plain tool error (exit 1) for an existing resource while an addition's rejection + // would have been exit 2, i.e. the same rejection reported in two different tiers depending + // only on whether the resource happened to exist. This pins the reclassification. + "ModifiedResourceRejectedByAdmissionPolicyIsAValidationFailure": { + Reason: "An apiserver rejection of an EXISTING resource's dry-run apply is reported in the same tier as a rejected addition", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + "testdata/diff/resources/rejecting-admission-policy.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "../../../internal/testutils/integration/testdata/diff/resources/existing-xr.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/existing-downstream-resource.yaml", + ), + }, + AwaitAdmissionDenialFor: "testdata/diff/resources/admission-policy-canary.yaml", + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/modified-xr.yaml"}, + ExpectedError: true, + ExpectedExitCode: dp.ExitCodeSchemaValidation, + ExpectedStructured: simtest.Expect(). + WithError("XNopResource/test-resource"). + WithMessageContaining("the cluster rejected XDownstreamResource/test-resource"). + WithValidationFailure("ns.nop.example.org/v1alpha1", "XDownstreamResource", "test-resource", "default"). + WithStatus("invalid"). + WithFieldError("admission", ""). + WithMessageContaining("this cluster refuses that configData value"), + }, + "MultipleXRsGroupedByInputXR": { + Reason: "Two input XRs in one invocation are grouped per input XR in the xrs[] structured view", + OutputFormat: "json", + InputFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/new-xr.yaml", + "testdata/diff/new-xr-second.yaml", + }, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + ExpectedStructured: simtest.Expect(). + // Aggregate flat view still reports the merged total (2 adds per XR). + WithSummary(4, 0, 0). + // Grouped view: one XR(...) sub-expression per input XR, each with + // its own changed tree. + WithXRs( + simtest.XR("XNopResource", "test-resource", "default"). + Status("changed"). + Summary(2, 0, 0). + Change("added", "XNopResource", "test-resource", "default"). + WithField("spec.coolField", "new-value"). + Change("added", "XDownstreamResource", "test-resource", "default"). + WithField("spec.forProvider.configData", "new-value"), + simtest.XR("XNopResource", "second-resource", "default"). + Status("changed"). + Summary(2, 0, 0). + Change("added", "XNopResource", "second-resource", "default"). + WithField("spec.coolField", "second-value"). + Change("added", "XDownstreamResource", "second-resource", "default"). + WithField("spec.forProvider.configData", "second-value"), + ). + Build(), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "MultipleXRsGroupedByInputXRV2": { + Reason: "The composites[] entries group each input's changes", + OutputFormat: "json", + InputFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/new-xr.yaml", + "testdata/diff/new-xr-second.yaml", + }, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + ExpectedStructured: simtest.Expect(). + // The top-level summary counts distinct resources. + WithSummary(4, 0, 0). + WithXRs( + simtest.XR("XNopResource", "test-resource", "default"). + Status("changed"). + Summary(2, 0, 0). + Change("added", "XNopResource", "test-resource", "default"). + WithField("spec.coolField", "new-value"). + Change("added", "XDownstreamResource", "test-resource", "default"). + WithField("spec.forProvider.configData", "new-value"), + simtest.XR("XNopResource", "second-resource", "default"). + Status("changed"). + Summary(2, 0, 0). + Change("added", "XNopResource", "second-resource", "default"). + WithField("spec.coolField", "second-value"). + Change("added", "XDownstreamResource", "second-resource", "default"). + WithField("spec.forProvider.configData", "second-value"), + ), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "ModifiedResourceDiff": { + Reason: "Shows color diff for modified resources", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + // put an existing XR in the cluster to diff against + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "../../../internal/testutils/integration/testdata/diff/resources/existing-xr.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/existing-downstream-resource.yaml", + ), + }, + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/modified-xr.yaml"}, + ExpectedOutput: ` +~~~ XDownstreamResource/test-resource + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + annotations: + crossplane.io/composition-resource-name: nop-resource + labels: + crossplane.io/composite: test-resource + name: test-resource + namespace: default + spec: + forProvider: +` + tu.Red("- configData: existing-value") + ` +` + tu.Green("+ configData: modified-value") + ` + +--- +~~~ XNopResource/test-resource + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + metadata: + labels: + crossplane.io/composite: test-resource + name: test-resource + namespace: default + spec: +` + tu.Red("- coolField: existing-value") + ` +` + tu.Green("+ coolField: modified-value") + ` + crossplane: + compositionRef: + name: xnopresources.diff.example.org + compositionRevisionRef: + name: xnopresources.diff.example.org-b616215 + compositionUpdatePolicy: Automatic + +--- + +Summary: 2 modified`, + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "IgnorePathsArgoCD": { + Reason: "Ignores ArgoCD annotations and labels when --ignore-paths is specified", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + // put an existing resource with different ArgoCD annotations + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/diff/resources/existing-xr-with-argocd.yaml", + "testdata/diff/resources/existing-downstream-resource-with-argocd.yaml", + ), + }, + InputFiles: []string{"testdata/diff/xr-with-argocd-annotations.yaml"}, + IgnorePaths: []string{ + "metadata.annotations[argocd.argoproj.io/tracking-id]", + "metadata.labels[argocd.argoproj.io/instance]", + }, + OutputFormat: "json", + ExpectedStructured: simtest.Expect().WithSummary(0, 0, 0), + ExpectedError: false, + }, + "IgnorePathsMixedChanges": { + // Regression test: when a resource has BOTH ignored path changes + // AND non-ignored spec changes, the summary must count the + // resource as modified once and the emitted diff bodies must not + // leak the ignored paths. The renderer-level unit test + // TestStructuredDiffRenderer_RespectsIgnorePaths asserts the + // absence of ignored fields in diff.old / diff.new; this test + // pins the summary counts and the visible non-ignored changes + // through the full CLI stack. + Reason: "Ignores ArgoCD annotations/labels while still reporting a non-ignored spec change", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/diff/resources/existing-xr-with-argocd.yaml", + "testdata/diff/resources/existing-downstream-resource-with-argocd.yaml", + ), + }, + InputFiles: []string{"testdata/diff/xr-with-argocd-mixed-changes.yaml"}, + IgnorePaths: []string{ + "metadata.annotations[argocd.argoproj.io/tracking-id]", + "metadata.labels[argocd.argoproj.io/instance]", + }, + OutputFormat: "json", + ExpectedStructured: simtest.Expect(). + WithSummary(0, 2, 0). + WithModifiedResource("XDownstreamResource", "test-resource", "default"). + WithFieldChange("spec.forProvider.configData", "new-value", "modified-value"). + And(). + WithModifiedResource("XNopResource", "test-resource", "default"). + WithFieldChange("spec.coolField", "new-value", "modified-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "ModifiedXRCreatesDownstream": { + Reason: "Shows diff when modified XR creates new downstream resource", + OutputFormat: "json", + // The XR has its composition refs but no composed resource yet: Crossplane selects the + // composition and revision before it composes, so this is an XR whose first compose has not + // created its resource (a function error, say). That is what makes the resource an addition. + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/existing-xr.yaml", + }, + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/modified-xr.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(1, 1, 0). + WithAddedResource("XDownstreamResource", "test-resource", "default"). + WithField("spec.forProvider.configData", "modified-value"). + And(). + WithModifiedResource("XNopResource", "test-resource", "default"). + WithFieldChange("spec.coolField", "existing-value", "modified-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // Issue #452, `xr` side, delivered through the warning channel added for #459. Unlike `comp`, + // which excludes deleting XRs from impact analysis, `xr` diffs exactly the resource the user + // named — so it still emits the diff and raises an advisory. This asserts BOTH halves of the + // dual emission: the stderr line for humans and the warnings[] entry for machines. + "DeletingXRWarnsButStillDiffs": { + Reason: "xr against an XR whose cluster copy is being deleted still emits the diff, plus a warning on stderr and in warnings[]", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + // A Terminating XR still has the resource it composed until garbage collection removes it. + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes("testdata/diff/resources/existing-xr-deleting.yaml", "../../../internal/testutils/integration/testdata/diff/resources/existing-downstream-resource.yaml"), + }, + DeleteAfterSetup: []string{"testdata/diff/resources/existing-xr-deleting.yaml"}, + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/modified-xr.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(0, 2, 0). + WithModifiedResource("XDownstreamResource", "test-resource", "default"). + WithFieldChange("spec.forProvider.configData", "existing-value", "modified-value"). + And(). + WithModifiedResource("XNopResource", "test-resource", "default"). + WithFieldChange("spec.coolField", "existing-value", "modified-value"). + And(). + WithWarning("being deleted in the cluster"), + ExpectedStderrContains: []string{ + "WARNING: The resource being diffed is being deleted in the cluster", + "resource=XNopResource/test-resource", + }, + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // Issue #495: the XR still references a composed resource that was deleted out of band. The + // resource tree records that child as NotFound, which is tolerated: the resource really is + // gone, so it is neither observed nor a removal — but the user is told. The other per-node + // errors (Forbidden, transport) are fatal; envtest grants the harness everything, so those are + // covered by the FetchObservedResources unit tests instead. + "ComposedResourceDeletedOutOfBandWarns": { + Reason: "xr against an XR whose resourceRefs name a deleted composed resource omits it from observed state and warns", + OutputFormat: "json", + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + { + OwnerFile: "testdata/diff/resources/existing-removal-test-xr.yaml", + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + "../../../internal/testutils/integration/testdata/diff/resources/removal-test-ns-downstream-resource1.yaml": nil, + "../../../internal/testutils/integration/testdata/diff/resources/removal-test-ns-downstream-resource2.yaml": nil, + }, + }, + }, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/removal-test-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + DeleteAfterSetup: []string{"../../../internal/testutils/integration/testdata/diff/resources/removal-test-ns-downstream-resource2.yaml"}, + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/unmodified-xr.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(0, 0, 0). + WithWarning("was not found in the cluster"). + WithWarningContext(map[string]string{ + "resource": "XDownstreamResource/resource-to-be-removed", + "namespace": "default", + }), + ExpectedStderrContains: []string{ + "WARNING: A composed resource referenced by its XR was not found in the cluster", + "resource=XDownstreamResource/resource-to-be-removed", + }, + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeSuccess, + }, + "EnvironmentConfigIncorporation": { + Reason: "Validates EnvironmentConfig (v1beta1) incorporation in diff", + OutputFormat: "json", + SetupFiles: []string{ + "testdata/diff/resources/xdownstreamenvresource-xrd.yaml", + "testdata/diff/resources/env-xrd.yaml", + "testdata/diff/resources/env-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + "testdata/diff/resources/environment-config-v1beta1.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/diff/resources/existing-env-xr.yaml", + "testdata/diff/resources/existing-env-downstream-resource.yaml", + ), + }, + XrdAPIVersion: integration.XRDV1, // the XRD is v1, so Crossplane writes the XR's resourceRefs at spec.resourceRefs + InputFiles: []string{"testdata/diff/modified-env-xr.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(0, 2, 0). + WithModifiedResource("XDownstreamEnvResource", "test-env-resource", ""). + WithFieldChange("spec.forProvider.configData", "existing-config-value", "modified-config-value"). + And(). + WithModifiedResource("XEnvResource", "test-env-resource", ""). + WithFieldChange("spec.configKey", "existing-config-value", "modified-config-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "ExternalResourceDependencies": { + Reason: "Validates diff with external resource dependencies via fn-external-resources", + OutputFormat: "json", + // this test does a weird thing where it changes the XR but all the downstream changes come from external + // resources, including a field path from the XR itself. + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + "testdata/diff/resources/external-resource-configmap.yaml", + "testdata/diff/resources/external-res-fn-composition.yaml", + "testdata/diff/resources/external-named-clusterrole.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/diff/resources/existing-xr-with-external-dep.yaml", + "testdata/diff/resources/existing-downstream-with-external-dep.yaml", + ), + }, + InputFiles: []string{"testdata/diff/modified-xr-with-external-dep.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(0, 2, 0). + WithModifiedResource("XDownstreamResource", "test-resource", "default"). + WithFieldChange("spec.forProvider.configData", "existing-value", "testing-external-resource-data"). + WithFieldChange("spec.forProvider.roleName", "old-role-name", "external-named-clusterrole"). + And(). + WithModifiedResource("XNopResource", "test-resource", "default"). + WithFieldChange("spec.coolField", "existing-value", "modified-with-external-dep"). + WithFieldChange("spec.environment", "staging", "testing"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // Same-namespace case: the matchLabels ExtraResources selector omits the + // namespace, and the matched ConfigMap lives in "default" — the SAME + // namespace as the XR. This passed even before the #376 fix, because the + // old resolveNamespace defaulted an empty selector namespace to the XR + // namespace, which happened to be where the ConfigMap was. See + // "TemplatedExtraResourcesCrossNamespace" for the cross-namespace case + // that requires the all-namespaces lookup. + "TemplatedExtraResourcesSameNamespace": { + Reason: "Validates diff with templated ExtraResources (matchLabels, no namespace) resolving a resource in the XR's own namespace", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + "testdata/diff/resources/external-resource-configmap.yaml", + "testdata/diff/resources/external-res-gotpl-composition.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/diff/resources/existing-xr-with-external-dep-templated.yaml", + "testdata/diff/resources/existing-downstream-with-external-dep.yaml", + ), + }, + InputFiles: []string{"testdata/diff/modified-xr-with-external-dep.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(0, 2, 0). + WithModifiedResource("XDownstreamResource", "test-resource", "default"). + WithFieldChange("spec.forProvider.configData", "existing-value", "modified-with-external-dep"). + WithFieldChange("spec.forProvider.roleName", "old-role-name", "templated-external-resource-testing"). + And(). + WithModifiedResource("XNopResource", "test-resource", "default"). + WithFieldChange("spec.coolField", "existing-value", "modified-with-external-dep"). + WithFieldChange("spec.environment", "staging", "testing"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // Reproduction for crossplane-contrib/crossplane-diff#376: a matchLabels + // ExtraResources selector that omits the namespace is a cluster-wide + // (all-namespaces) lookup in Crossplane (internal/xfn/required_resources.go + // lists with client.InNamespace(rs.GetNamespace()); empty = all + // namespaces). Here the matched ConfigMap lives in "other-namespace" while + // the XR is in "default". Pre-fix, resolveNamespace defaulted the empty + // selector namespace to the XR namespace ("default"), so the dynamic + // client listed only "default", found nothing, and the go-templating + // requirement came back empty — the downstream XDownstreamResource would + // not render at all (or a real template reading the value would fatal with + // ValueError). Post-fix the lookup spans all namespaces, finds the + // ConfigMap, and the downstream renders with roleName derived from it. + "TemplatedExtraResourcesCrossNamespace": { + Reason: "matchLabels ExtraResources (no namespace) must resolve a resource in a DIFFERENT namespace than the XR via an all-namespaces lookup (issue #376)", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + "testdata/diff/resources/external-resource-configmap-other-namespace.yaml", + "testdata/diff/resources/external-res-gotpl-composition.yaml", + }, + InputFiles: []string{"testdata/diff/new-xr-with-cross-ns-external-dep.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(2, 0, 0). + WithAddedResource("XDownstreamResource", "test-resource", "default"). + WithField("spec.forProvider.configData", "new-value"). + // roleName is templated from the matched ConfigMap's name, proving + // the cross-namespace resource was resolved. + WithField("spec.forProvider.roleName", "templated-external-resource-crossns"). + And(). + WithAddedResource("XNopResource", "test-resource", "default"). + WithField("spec.coolField", "new-value"). + WithField("spec.environment", "crossns"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "TemplatedExtraResourcesMatchNameNotFound": { + // Regression test for crossplane-contrib/crossplane-diff#355: a + // go-templating ExtraResources block using matchName for a + // resource that does not exist in the cluster must NOT abort the + // diff. Pre-fix, the iterative render loop received an error + // from requirements_provider.processNameSelector and surfaced it + // as a per-XR failure. Post-fix, the unmet requirement is silently + // skipped (mirroring upstream xfn.required_resources.go) and the + // composition renders its downstream resource normally. + Reason: "matchName requirement for a missing resource must not abort the diff (issue #355)", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + "testdata/diff/resources/external-res-matchname-gotpl-composition.yaml", + }, + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/new-xr.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(2, 0, 0). + WithAddedResource("XDownstreamResource", "test-resource", "default"). + WithField("spec.forProvider.configData", "new-value"). + WithField("spec.forProvider.roleName", "static-role"). + And(). + WithAddedResource("XNopResource", "test-resource", "default"). + WithField("spec.coolField", "new-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "MultiStepFatalAfterExtraResources": { + // Reproduces the PR #295 bug #1 scenario: a multi-step pipeline where + // step 1 successfully emits ExtraResources requirements and composed + // resources, and a LATER step fatals. Per crossplane/cmd/crank/render/ + // render.go, a fatal in step N still returns the accumulated + // requirements from step 0 + // + // On iteration 2 (after requirements are cached) newReqCount == 0, + // and the OLD condition + // renderErr != nil && newReqCount == 0 && len(output.Requirements) == 0 + // is false — the error falls through to checkStability which declares + // the pipeline "stable" on an Outputs with nil CompositeResource, + // causing a SIGSEGV in CalculateNonRemovalDiffs. + // + // Expected behavior (with PR #295's render-loop fix): a clean fatal + // error bubbles up instead of a segfault. The diff is NOT produced + // because the pipeline legitimately failed. + Reason: "Multi-step pipeline with step-1 requirements + later-step fatal should surface the error cleanly, not SIGSEGV", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + "testdata/diff/resources/external-resource-configmap.yaml", + "testdata/diff/resources/multistep-fatal-after-extra-res-composition.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/diff/resources/existing-xr-with-external-dep-multistep-fatal.yaml", + "testdata/diff/resources/existing-downstream-with-external-dep.yaml", + ), + }, + InputFiles: []string{"testdata/diff/modified-xr-with-external-dep.yaml"}, + ExpectedError: true, + ExpectedErrorContains: `"always-fatal" returned a fatal result`, + ExpectedExitCode: dp.ExitCodeToolError, + }, + "UnguardedTemplatedExtraResources": { + // Regression guard: a user template that accesses .extraResources + // directly (e.g. `index .extraResources "configmaps"`) without a + // `{{- with .extraResources }}` guard fails fatally on the first + // function invocation. function-go-templating's template data has no + // "extraResources" entry on that first call — crossplane's + // FetchingFunctionRunner only populates req.ExtraResources AFTER a + // function returns a non-fatal rsp.Requirements, and the Fatal from + // this template short-circuits before that ever happens. + // + // Verified empirically (2026-04-29) against a kind cluster with + // Crossplane + function-go-templating v0.11.0: a live controller + // produces the identical error, and the XR stays Synced=False with + // ReconcileError. So crossplane-diff must surface the same fatal + // error — not mask it with a diff — to match production behavior. + // + // This test also guards against a hypothetical "fixed" prefetch that + // pre-populated .extraResources for the template: that would make + // crossplane-diff report a successful diff for a composition that + // actually fails in-cluster, which is a false positive. If this test + // ever starts passing with an unexpected exit code or producing a + // diff, something has diverged from prod behavior. + Reason: "Unguarded .extraResources access must surface a fatal error matching what a live Crossplane controller produces (verified empirically against kind cluster on 2026-04-29)", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + "testdata/diff/resources/external-resource-configmap.yaml", + "testdata/diff/resources/unguarded-external-res-gotpl-composition.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/diff/resources/existing-xr-with-external-dep-unguarded.yaml", + "testdata/diff/resources/existing-downstream-with-external-dep.yaml", + ), + }, + InputFiles: []string{"testdata/diff/modified-xr-with-external-dep.yaml"}, + ExpectedError: true, + ExpectedErrorContains: `: error calling index: index of untyped nil`, + ExpectedExitCode: dp.ExitCodeToolError, + }, + "CrossNamespaceResourceDependencies": { + Reason: "Validates cross-namespace resource dependencies via fn-external-resources", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + "testdata/diff/resources/cross-namespace-configmap.yaml", + "testdata/diff/resources/cross-namespace-fn-composition.yaml", + "testdata/diff/resources/external-named-clusterrole.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/diff/resources/existing-cross-ns-xr.yaml", + "testdata/diff/resources/existing-cross-ns-downstream.yaml", + ), + }, + InputFiles: []string{"testdata/diff/modified-cross-ns-xr.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(0, 2, 0). + WithModifiedResource("XDownstreamResource", "test-cross-ns-resource", "default"). + WithFieldChange("spec.forProvider.configData", "existing-cross-ns-data-existing-named-data-old-cross-ns-role", "cross-namespace-data-another-cross-namespace-data-external-named-clusterrole"). + And(). + WithModifiedResource("XNopResource", "test-cross-ns-resource", "default"). + WithFieldChange("spec.coolField", "existing-cross-ns-value", "modified-cross-ns-value"). + WithFieldChange("spec.environment", "staging", "production"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "ResourceRemovalHierarchyV1ClusterScoped": { + Reason: "Validates resource removal detection with hierarchy using v1 style resourceRefs and cluster scoped downstreams", + XrdAPIVersion: integration.XRDV1, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + { + OwnerFile: "testdata/diff/resources/existing-legacy-xr.yaml", + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + "testdata/diff/resources/removal-test-legacycluster-downstream-resource1.yaml": nil, // Will be kept + "testdata/diff/resources/removal-test-legacy-nested-xr.yaml": { + // A nested XR, no longer rendered: removed, with what it composes + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + "testdata/diff/resources/removal-test-legacy-nested-xr-managed.yaml": nil, // Child will also be removed + }, + }, + }, + }, + }, + SetupFiles: []string{ + "testdata/diff/resources/legacy-xrd.yaml", + "testdata/diff/resources/removal-test-legacy-composition.yaml", + "testdata/diff/resources/removal-test-legacy-child-xrd.yaml", + "testdata/diff/resources/removal-test-legacy-child-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + InputFiles: []string{"testdata/diff/modified-legacy-xr.yaml"}, + ExpectedOutput: ` +~~~ XDownstreamResource/resource-to-be-kept + apiVersion: legacycluster.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + annotations: + crossplane.io/composition-resource-name: resource1 + labels: + crossplane.io/composite: test-resource + name: resource-to-be-kept + spec: + forProvider: +- configData: existing-value ++ configData: modified-value + +--- +--- XDownstreamResource/resource-to-be-removed-managed +- apiVersion: legacycluster.nop.example.org/v1alpha1 +- kind: XDownstreamResource +- metadata: +- annotations: +- crossplane.io/composition-resource-name: managed-resource +- labels: +- crossplane.io/composite: test-resource +- name: resource-to-be-removed-managed +- spec: +- forProvider: +- configData: child-value + +--- +--- XLegacyChildResource/resource-to-be-removed +- apiVersion: legacycluster.diff.example.org/v1alpha1 +- kind: XLegacyChildResource +- metadata: +- annotations: +- crossplane.io/composition-resource-name: resource2 +- labels: +- crossplane.io/composite: test-resource +- name: resource-to-be-removed +- spec: +- childField: child-value +- compositionRef: +- name: xlegacychildresources.legacycluster.diff.example.org +- compositionRevisionRef: +- name: xlegacychildresources.legacycluster.diff.example.org-bf2c887 +- compositionUpdatePolicy: Automatic + +--- +~~~ XNopResource/test-resource + apiVersion: legacycluster.diff.example.org/v1alpha1 + kind: XNopResource + metadata: + labels: + crossplane.io/composite: test-resource + name: test-resource + spec: + compositionRef: + name: removal-test-composition + compositionRevisionRef: + name: removal-test-composition-4b6f9f1 + compositionUpdatePolicy: Automatic +- coolField: existing-value ++ coolField: modified-value + +--- + +Summary: 2 modified, 2 removed`, + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + NoColor: true, + }, + // Issue #259: function-sequencer hides resources from later stages, but those + // resources should be shown as removed if they exist in the cluster. + // https://github.com/crossplane-contrib/crossplane-diff/issues/259 + "FunctionSequencerPreservesExistingResources": { + Reason: "Function-sequencer should NOT hide resources that already exist in the cluster (fix for issue #259)", + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + { + OwnerFile: "testdata/diff/resources/existing-sequencer-xr.yaml", + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + // Stage 0 resource - exists in cluster + "testdata/diff/resources/sequencer-stage0-downstream.yaml": nil, + // Stage 1 resource - exists in cluster, should NOT be hidden by sequencer + // because observed resources are passed correctly to the function pipeline. + // Stage 0 has no Ready condition, so the sequencer would gate stage 1 if it + // were not observed: this case depends on the XR's own composed resources + // reaching render as observed state. + "testdata/diff/resources/sequencer-stage1-downstream.yaml": nil, + }, + }, + }, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "testdata/diff/resources/sequencer-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + InputFiles: []string{"testdata/diff/resources/sequencer-xr.yaml"}, + OutputFormat: "json", + // Expected: With the fix, observed resources ARE passed to function-sequencer. + // Function-sequencer sees both resources exist and does NOT remove them from desired state. + // Since existing resources match rendered resources, there should be no changes. + // XR diff outputs nothing when there are no changes (unlike comp diff which says "No changes detected"). + ExpectedStructured: simtest.Expect().WithSummary(0, 0, 0), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeSuccess, + }, + // Issue #259: Validates that removal detection works even when the XR has no changes. + // This is the core fix for the function-sequencer bug where existingXR was nil because + // the XR diff wasn't stored when there were no changes. + // https://github.com/crossplane-contrib/crossplane-diff/issues/259 + "ResourceRemovalWithUnmodifiedXR": { + Reason: "Validates resource removal detection works when XR has no changes (uses existingXRFromCluster fallback)", + OutputFormat: "json", + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + { + OwnerFile: "testdata/diff/resources/existing-removal-test-xr.yaml", + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + "../../../internal/testutils/integration/testdata/diff/resources/removal-test-ns-downstream-resource1.yaml": nil, // Will be kept + "testdata/diff/resources/removal-test-nested-xr.yaml": { + // A nested XR, no longer rendered: removed, with what it composes + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + "testdata/diff/resources/removal-test-nested-xr-managed.yaml": nil, // Child will also be removed + }, + }, + }, + }, + }, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/removal-test-composition.yaml", + "testdata/diff/resources/nested/child-xrd.yaml", + "testdata/diff/resources/nested/child-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/unmodified-xr.yaml"}, // XR has NO changes + // Expected: the nested XR resource2 and what it composes are removed, but XR and resource1 have no changes + ExpectedStructured: simtest.Expect(). + WithSummary(0, 0, 2). + WithRemovedResource("XChildResource", "resource-to-be-removed", "default"). + // dryRun must be absent on a removal. A removal has no desired state to preview, so it can + // never carry a dryRun object, and one appearing would wrongly mark it unverified; WithDryRunPerformed asserts exactly that absence. The method + // name reads oddly here — nothing was dry run — but the claim it makes ("no dryRun + // object is present") is the right one, and having a second spelling for the same + // assertion would be worse. + WithDryRunPerformed(). + WithField("spec.childField", "child-value"). + And(). + WithRemovedResource("XDownstreamResource", "resource-to-be-removed-managed", "default"). + WithDryRunPerformed(). + WithField("spec.forProvider.configData", "child-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // #552: removal mirrors Crossplane's garbage collector, which deletes a composed resource only if + // the composite controls it. The XR's resourceRefs name four resources, and its render desires only + // the first: one it controls and renders, one it controls and no longer renders, one another XR + // controls, and one with no controller reference at all. Only the second is removed. + "ResourceRemovalSkipsResourcesTheXRDoesNotControl": { + Reason: "A composed resource the XR's resourceRefs name but the XR does not control is not reported removed", + // What this case pins: test-resource's resourceRefs still name two resources it doesn't control. + OutputFormat: "json", + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/diff/resources/existing-xr-listing-uncontrolled-resources.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/removal-test-ns-downstream-resource1.yaml", // still rendered: kept + "../../../internal/testutils/integration/testdata/diff/resources/removal-test-ns-downstream-resource2.yaml", // no longer rendered: removed + ), + integration.Composes( + "../../../internal/testutils/integration/testdata/diff/resources/existing-other-xr.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/existing-other-xr-downstream.yaml", + ), + }, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/removal-test-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + // Deliberately not in crossplaneManagedResources: its controller reference was removed, so + // no XR controls it, though test-resource's resourceRefs still name it. + "../../../internal/testutils/integration/testdata/diff/resources/removal-test-ns-downstream-without-controller.yaml", + }, + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/unmodified-xr.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(0, 0, 1). + WithRemovedResource("XDownstreamResource", "resource-to-be-removed", "default"). + WithField("spec.forProvider.configData", "existing-value"), + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "ResourceRemovalHierarchyV2Namespaced": { + Reason: "Validates resource removal detection with hierarchy using v2 style resourceRefs and namespaced downstreams", + OutputFormat: "json", + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + { + OwnerFile: "testdata/diff/resources/existing-removal-test-xr.yaml", + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + "../../../internal/testutils/integration/testdata/diff/resources/removal-test-ns-downstream-resource1.yaml": nil, // Will be kept + "testdata/diff/resources/removal-test-nested-xr.yaml": { + // A nested XR, no longer rendered: removed, with what it composes + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + "testdata/diff/resources/removal-test-nested-xr-managed.yaml": nil, // Child will also be removed + }, + }, + }, + }, + }, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/removal-test-composition.yaml", + "testdata/diff/resources/nested/child-xrd.yaml", + "testdata/diff/resources/nested/child-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/modified-xr.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(0, 2, 2). + WithModifiedResource("XDownstreamResource", "resource-to-be-kept", "default"). + WithFieldChange("spec.forProvider.configData", "existing-value", "modified-value"). + And(). + WithRemovedResource("XChildResource", "resource-to-be-removed", "default"). + WithField("spec.childField", "child-value"). + And(). + WithRemovedResource("XDownstreamResource", "resource-to-be-removed-managed", "default"). + WithField("spec.forProvider.configData", "child-value"). + And(). + WithModifiedResource("XNopResource", "test-resource", "default"). + WithFieldChange("spec.coolField", "existing-value", "modified-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "ResourceRemovalHierarchyV2ClusterScoped": { + Reason: "Validates resource removal detection with hierarchy using v2 style resourceRefs and cluster scoped downstreams", + OutputFormat: "json", + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + { + OwnerFile: "testdata/diff/resources/existing-cluster-xr.yaml", + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + "testdata/diff/resources/removal-test-cluster-downstream-resource1.yaml": nil, // Will be kept + "testdata/diff/resources/removal-test-nested-xr.yaml": { + // A nested XR, no longer rendered: removed, with what it composes + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + "testdata/diff/resources/removal-test-nested-xr-managed.yaml": nil, // Child will also be removed + }, + }, + }, + }, + }, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/cluster-xrd.yaml", + "testdata/diff/resources/removal-test-cluster-composition.yaml", + "testdata/diff/resources/nested/child-xrd.yaml", + "testdata/diff/resources/nested/child-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + InputFiles: []string{"testdata/diff/modified-cluster-xr.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(0, 2, 2). + WithModifiedResource("XDownstreamResource", "resource-to-be-kept", ""). + WithFieldChange("spec.forProvider.configData", "existing-value", "modified-value"). + And(). + WithRemovedResource("XChildResource", "resource-to-be-removed", "default"). + WithField("spec.childField", "child-value"). + And(). + WithRemovedResource("XDownstreamResource", "resource-to-be-removed-managed", "default"). + WithField("spec.forProvider.configData", "child-value"). + And(). + WithModifiedResource("XNopResource", "test-resource", ""). + WithFieldChange("spec.coolField", "existing-value", "modified-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "ResourceWithGenerateName": { + Reason: "Validates handling of resources with generateName", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + "testdata/diff/resources/generated-name-composition.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + { + // Set up the XR as the owner of the generated composed resource + OwnerFile: "testdata/diff/resources/existing-generate-name-xr.yaml", + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + // This file has a generated name and is owned by the XR + "testdata/diff/resources/existing-downstream-with-generated-name.yaml": nil, + }, + }, + }, + // Use a composition that uses generateName for composed resources + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/new-xr.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(0, 2, 0). + WithModifiedResource("XDownstreamResource", "test-resource-abc123", "default"). + WithFieldChange("spec.forProvider.configData", "existing-value", "new-value"). + And(). + WithModifiedResource("XNopResource", "test-resource", "default"). + WithFieldChange("spec.coolField", "existing-value", "new-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // The generateName counterpart of NewXRWithExistingComposedResource. A new XR renders its composed + // resource with only a generateName, and nothing is observed for it, so no name identifies the + // existing resource: only its crossplane.io/composite label and composition-resource-name + // annotation can. ResourceWithGenerateName cannot reach that lookup, because there the XR exists and + // render names the resource after the observed one. + "NewXRAdoptsOrphanedGenerateNamedComposedResource": { + Reason: "A new XR's generateName composed resource is matched to an existing orphan by its composite label", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + "testdata/diff/resources/generated-name-composition.yaml", + // Deliberately not in crossplaneManagedResources: an orphan has no controller + // ownerReference (the garbage collector strips it when the XR is deleted with + // --cascade=orphan) and no XR lists it, but it keeps its labels and annotations. + "testdata/diff/resources/existing-downstream-with-generated-name.yaml", + }, + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/new-xr.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(1, 1, 0). + WithAddedResource("XNopResource", "test-resource", "default"). + And(). + WithModifiedResource("XDownstreamResource", "test-resource-abc123", "default"). + WithFieldChange("spec.forProvider.configData", "existing-value", "new-value"), + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // A generateName-only input next to an ordinary one is an ordinary invocation. The unnamed input + // has no identity, so input validation must not mistake it for the named XR's claim: an XR with + // no claimRef has an empty reference identity too, and the two must not match. + "GenerateNameInputAlongsideANamedInput": { + Reason: "A generateName-only input beside a named one is valid; both are diffed", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/new-xr.yaml", "testdata/diff/generated-name-xr.yaml"}, + ExpectedExitCode: dp.ExitCodeDiffDetected, + ExpectedStructured: simtest.Expect(). + WithSummary(4, 0, 0). + WithXRs( + simtest.XR("XNopResource", "test-resource", "default").Status("changed").Summary(2, 0, 0), + simtest.XR("XNopResource", "generated-xr-(generated)", "default").Status("changed").Summary(2, 0, 0), + ), + }, + "NewXRWithGenerateName": { + Reason: "Shows diff for new XR with generateName", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + // We don't add any existing XR, as we're testing creation of a new one + }, + InputFiles: []string{"testdata/diff/generated-name-xr.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(2, 0, 0). + WithAddedResource("XDownstreamResource", "", "default"). + WithNamePattern(`generated-xr-\(generated\)`). + WithField("spec.forProvider.configData", "new-value"). + And(). + WithAddedResource("XNopResource", "", "default"). + WithNamePattern(`generated-xr-\(generated\)`). + WithField("spec.coolField", "new-value"). + And(). + // Issue #477: the grouped view must attribute these changes to a + // named XR. metadata.name is empty on the input, so the identity + // carries the effective (synthesized) name — the same one the + // changes above are keyed by. + WithXRs( + simtest.XR("XNopResource", "generated-xr-(generated)", "default"). + Status("changed"). + Summary(2, 0, 0), + ), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // Issue #476. The same file passed twice — a fat-fingered command line, or CI enumerating one + // file twice — has one clear intent. It is diffed once, with a warning, and the run succeeds. + "IdenticalDuplicateInputIsDeduplicated": { + Reason: "An input identical to an earlier one is diffed once, with a warning, not rejected", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + InputFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/new-xr.yaml", + "../../../internal/testutils/integration/testdata/diff/new-xr.yaml", + }, + ExpectedExitCode: dp.ExitCodeDiffDetected, + ExpectedStderrContains: []string{"Ignoring a duplicate input: it is identical to an earlier one"}, + // The warning channel's contract is both halves: stderr above, and warnings[] here. + ExpectedStructured: simtest.Expect(). + WithSummary(2, 0, 0). + WithXRs(simtest.XR("XNopResource", "test-resource", "default").Status("changed").Summary(2, 0, 0)). + WithWarning("Ignoring a duplicate input: it is identical to an earlier one"). + WithWarningContext(map[string]string{"resource": "XNopResource/test-resource", "input": "2", "duplicateOf": "1"}), + }, + // The same object twice with different content fails loudly: applying both would leave whichever + // is applied last, and the order inputs are given in is not a statement of intent. + "SameObjectTwiceWithDifferentContentFails": { + Reason: "One object given twice with different content is an input error on both inputs", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + InputFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/new-xr.yaml", + "../../../internal/testutils/integration/testdata/diff/modified-xr.yaml", + }, + ExpectedError: true, + ExpectedErrorContains: "input 2 defines XNopResource/test-resource differently from input 1", + ExpectedExitCode: dp.ExitCodeToolError, + }, + // A child XR passed alongside the parent whose composition renders it: the parent's composition + // writes that child, so the child input would be a second writer. Rejected on the child; the + // parent's own diff is unaffected. + "InputComposedByAnotherInputIsRejected": { + Reason: "An XR supplied alongside the input whose composition composes it is an input error", + SetupFiles: []string{ + "testdata/diff/resources/nested/parent-xrd.yaml", + "testdata/diff/resources/nested/child-xrd.yaml", + "testdata/diff/resources/nested/parent-composition.yaml", + "testdata/diff/resources/nested/child-composition.yaml", + "testdata/diff/resources/xdownstreamenvresource-xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + InputFiles: []string{ + "testdata/diff/new-nested-xr.yaml", + "testdata/diff/new-nested-child-xr.yaml", + }, + ExpectedError: true, + ExpectedErrorContains: "XChildResource/test-parent-child is composed by input XParentResource/test-parent, " + + "whose composition writes it", + ExpectedExitCode: dp.ExitCodeToolError, + }, + // Issue #476. Two distinct XRs whose composition gives one child a fixed name render the same + // object — here with byte-identical content. Crossplane gives it to whichever XR creates it first + // and the other fails to reconcile it, so the run must fail however alike the renderings are. + // This depends on real renders carrying the controller reference; unit tests build that by hand. + "TwoXRsComposingOneObjectContend": { + Reason: "Two XRs that would both control one object fail, even when their renderings are identical", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/shared-child-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/shared-child-xrs.yaml"}, + ExpectedError: true, + ExpectedExitCode: dp.ExitCodeToolError, + ExpectedErrorContains: `resource "ns.nop.example.org/v1alpha1/XDownstreamResource/default/shared-child" would be ` + + `controlled by more than one XR (XNopResource/shared-child-owner-a, XNopResource/shared-child-owner-b)`, + }, + // Reproduces the PR #294 scenario: a net-new XR is diffed while the + // composed resource it would manage already exists in the cluster + // (e.g. after the prior XR was deleted with --cascade=orphan, or while + // importing existing resources under Crossplane management). The render + // pipeline emits ownerReferences with an empty UID for new XRs; + // without the fallback in CalculateNonRemovalDiffs, UpdateOwnerRefs + // runs as a no-op and the real apiserver rejects the dry-run apply + // with "metadata.ownerReferences.uid: must not be empty". + "NewXRWithExistingComposedResource": { + Reason: "Shows diff for new XR whose composed resource already exists in the cluster (PR #294)", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + // Pre-existing composed resource; no backing XR in the cluster. Deliberately not in + // crossplaneManagedResources: an orphaned resource has no controller ownerReference (the + // garbage collector strips it when the XR is deleted with --cascade=orphan), and adopting + // exactly such a resource is what this case tests. + "../../../internal/testutils/integration/testdata/diff/resources/existing-downstream-resource.yaml", + }, + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/new-xr.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(1, 1, 0). + WithAddedResource("XNopResource", "test-resource", "default"). + WithField("spec.coolField", "new-value"). + And(). + WithModifiedResource("XDownstreamResource", "test-resource", "default"). + WithFieldChange("spec.forProvider.configData", "existing-value", "new-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "MultipleXRs": { + Reason: "Validates diff for multiple XRs", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + // Add an existing XR and downstream resource to test modification + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "../../../internal/testutils/integration/testdata/diff/resources/existing-xr.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/existing-downstream-resource.yaml", + ), + }, + InputFiles: []string{ + "testdata/diff/first-xr.yaml", + "../../../internal/testutils/integration/testdata/diff/modified-xr.yaml", + }, + ExpectedStructured: simtest.Expect(). + WithSummary(2, 2, 0). + WithAddedResource("XDownstreamResource", "first-resource", "default"). + WithField("spec.forProvider.configData", "first-value"). + And(). + WithModifiedResource("XDownstreamResource", "test-resource", "default"). + WithFieldChange("spec.forProvider.configData", "existing-value", "modified-value"). + And(). + WithAddedResource("XNopResource", "first-resource", "default"). + WithField("spec.coolField", "first-value"). + And(). + WithModifiedResource("XNopResource", "test-resource", "default"). + WithFieldChange("spec.coolField", "existing-value", "modified-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "LeastPrivilegeCredentialsDiffSucceeds": { + Reason: "A user who can only get Crossplane's types, not list them, gets the admin's diff of a new and a modified XR", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "../../../internal/testutils/integration/testdata/diff/resources/existing-xr.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/existing-downstream-resource.yaml", + ), + }, + // The new XR names its composition: one that names none is matched against every Composition, + // which takes a list of them. + InputFiles: []string{"testdata/diff/first-xr-with-composition-ref.yaml", "../../../internal/testutils/integration/testdata/diff/modified-xr.yaml"}, + RbacRules: simharness.LeastPrivilegeRules("get", "create", "patch"), + OutputFormat: "json", + ExpectedStructured: simtest.Expect(). + WithSummary(2, 2, 0). + WithAddedResource("XDownstreamResource", "first-resource", "default"). + WithField("spec.forProvider.configData", "first-value"). + And(). + WithModifiedResource("XDownstreamResource", "test-resource", "default"). + WithFieldChange("spec.forProvider.configData", "existing-value", "modified-value"). + And(). + WithAddedResource("XNopResource", "first-resource", "default"). + WithField("spec.coolField", "first-value"). + And(). + WithModifiedResource("XNopResource", "test-resource", "default"). + WithFieldChange("spec.coolField", "existing-value", "modified-value"), + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "UnrelatedXRDWithoutCRDDoesNotFailRun": { + Reason: "An XRD unrelated to the input whose CRD does not exist yet does not affect the diff", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + UnestablishedXRDs: []string{"testdata/diff/resources/unestablished-xrd.yaml"}, + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/new-xr.yaml"}, + OutputFormat: "json", + // The same diff NewResourceDiff shows, which has no such XRD. + ExpectedStructured: simtest.Expect(). + WithSummary(2, 0, 0). + WithAddedResource("XDownstreamResource", "test-resource", "default"). + WithField("spec.forProvider.configData", "new-value"). + And(). + WithAddedResource("XNopResource", "test-resource", "default"). + WithField("spec.coolField", "new-value"), + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "MissingFunctionFailsWithSameError": { + Reason: "A pipeline naming a Function the cluster does not have fails, naming the function and its step", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "testdata/diff/resources/composition-with-missing-function.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/new-xr.yaml"}, + ExpectedError: true, + ExpectedErrorContains: `cannot get functions from pipeline: function "function-not-installed" referenced in pipeline step "generate-resources" not found`, + ExpectedExitCode: dp.ExitCodeToolError, + }, + "SelectCompositionByDirectReference": { + Reason: "Validates composition selection by direct reference", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + // Add multiple compositions for the same XR type + "testdata/diff/resources/default-composition.yaml", + "testdata/diff/resources/production-composition.yaml", + "testdata/diff/resources/staging-composition.yaml", + }, + InputFiles: []string{ + "testdata/diff/xr-with-composition-ref.yaml", + }, + ExpectedStructured: simtest.Expect(). + WithSummary(2, 0, 0). + WithAddedResource("XDownstreamResource", "test-resource", "default"). + WithField("spec.forProvider.configData", "test-value"). + WithField("spec.forProvider.resourceTier", "production"). + And(). + WithAddedResource("XNopResource", "test-resource", "default"). + WithField("spec.coolField", "test-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "SelectCompositionByLabelSelector": { + Reason: "Validates composition selection by label selector", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + // Add multiple compositions for the same XR type + "testdata/diff/resources/default-composition.yaml", + "testdata/diff/resources/production-composition.yaml", + "testdata/diff/resources/staging-composition.yaml", + }, + InputFiles: []string{ + "testdata/diff/xr-with-composition-selector.yaml", + }, + ExpectedStructured: simtest.Expect(). + WithSummary(2, 0, 0). + WithAddedResource("XDownstreamResource", "test-resource", "default"). + WithField("spec.forProvider.configData", "test-value"). + WithField("spec.forProvider.resourceTier", "staging"). + And(). + WithAddedResource("XNopResource", "test-resource", "default"). + WithField("spec.coolField", "test-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "AmbiguousCompositionSelection": { + Reason: "Validates error on ambiguous composition selection", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + // Add multiple compositions for the same XR type + "testdata/diff/resources/default-composition.yaml", + "testdata/diff/resources/production-composition.yaml", + "testdata/diff/resources/staging-composition.yaml", + }, + InputFiles: []string{ + "testdata/diff/xr-with-ambiguous-selector.yaml", + }, + ExpectedError: true, + ExpectedExitCode: dp.ExitCodeToolError, + ExpectedErrorContains: "ambiguous composition selection: multiple compositions match", + NoColor: true, + }, + "SchemaValidationError": { + Reason: "Validates exit code 2 for schema validation errors and structured per-resource failure detail in JSON output", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + InputFiles: []string{ + "testdata/diff/invalid-schema-xr.yaml", + }, + OutputFormat: "json", + ExpectedError: true, + ExpectedExitCode: dp.ExitCodeSchemaValidation, + // Structured assertion: lock down the typed + // validationFailures payload for both the failing XR and + // its composed resource. Field paths and error types are + // ours to assert; the embedded k8s message wording is + // left unasserted (apimachinery upgrades can shift its + // phrasing without changing our output contract). + ExpectedStructured: simtest.Expect(). + WithError("XNopResource/invalid-schema-xr"). + WithValidationFailure("ns.diff.example.org/v1alpha1", "XNopResource", "invalid-schema-xr", "default"). + WithStatus("invalid"). + WithFieldError("schema", "spec.coolField"). + AndFieldError(). + AndValidationFailure(). + WithValidationFailure("ns.nop.example.org/v1alpha1", "XDownstreamResource", "invalid-schema-xr", "default"). + WithStatus("invalid"). + WithFieldError("schema", "spec.forProvider.configData"). + AndFieldError(). + AndValidationFailure(). + AndError(), + NoColor: true, + }, + "NewClaimShowsDiff": { + Reason: "Shows diff for new claim", + OutputFormat: "json", + SetupFiles: []string{ + "testdata/diff/resources/existing-namespace.yaml", + // Add the necessary CRDs and compositions for claim diffing + "testdata/diff/resources/claim-xrd.yaml", + "testdata/diff/resources/claim-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + InputFiles: []string{"testdata/diff/new-claim.yaml"}, + // The claim is defaulted with its own CRD (generated from claim-xrd.yaml), which shares the XRD's + // user default spec.tier with the XR's CRD but, unlike it, does not default compositionUpdatePolicy. + ExpectedStructured: simtest.Expect(). + WithSummary(2, 0, 0). + WithAddedResource("NopClaim", "test-claim", "existing-namespace"). + WithField("spec.coolField", "new-value"). + WithField("spec.tier", "standard"). + WithFieldAbsent("spec.compositionUpdatePolicy"). + And(). + WithAddedResource("XDownstreamResource", "test-claim", ""). + WithField("spec.forProvider.configData", "new-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "NewClaimWithClaimRefComposition": { + Reason: "Shows diff for new claim when composition uses spec.claimRef - claimRef is synthesized for new claims", + OutputFormat: "json", + SetupFiles: []string{ + "testdata/diff/resources/existing-namespace.yaml", + "testdata/diff/resources/claim-xrd.yaml", + "testdata/diff/resources/claim-composition-with-claimref.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + InputFiles: []string{"testdata/diff/new-claim-with-claimref-composition.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(2, 0, 0). + WithAddedResource("NopClaim", "test-claim", "existing-namespace"). + WithField("spec.coolField", "new-value"). + And(). + WithAddedResource("XDownstreamResource", "test-claim", ""). + WithField("spec.forProvider.configData", "existing-namespace/test-claim"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "ModifiedClaimShowsDiff": { + Reason: "Shows diff for modified claim", + OutputFormat: "json", + SetupFiles: []string{ + "testdata/diff/resources/existing-namespace.yaml", + // Add necessary CRDs and composition + "testdata/diff/resources/claim-xrd.yaml", + "testdata/diff/resources/claim-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + // Add existing resources for comparison + "testdata/diff/resources/existing-claim.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/diff/resources/existing-claim-xr.yaml", + "testdata/diff/resources/existing-claim-downstream-resource.yaml", + ), + }, + XrdAPIVersion: integration.XRDV1, // the XRD is v1, so Crossplane writes the XR's resourceRefs at spec.resourceRefs + InputFiles: []string{"testdata/diff/modified-claim.yaml"}, + ExpectedStructured: simtest.Expect(). + // The composed resource is labelled with the backing XR the claim's resourceRef names, so it + // already belongs to this claim: no ownership advisory (#534). + WithNoWarnings(). + WithSummary(0, 2, 0). + WithModifiedResource("NopClaim", "test-claim", "existing-namespace"). + WithFieldChange("spec.coolField", "existing-value", "modified-value"). + And(). + WithModifiedResource("XDownstreamResource", "test-claim-82crv", ""). + WithFieldChange("spec.forProvider.configData", "existing-value", "modified-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // #503, for a claim. A claim is diffed as the claim itself, so its dry-run payload must carry + // none of the defaults its CRD declares for fields the manifest omits. + "XRDDefaultedFieldOnClaimOwnedByAnotherManagerIsNotReportedChanged": { + Reason: "An XRD-defaulted field the claim manifest omits keeps another manager's non-default value in the diff", + OutputFormat: "json", + SetupFiles: []string{ + "testdata/diff/resources/existing-namespace.yaml", + "testdata/diff/resources/claim-xrd.yaml", + "testdata/diff/resources/claim-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + "testdata/diff/resources/existing-claim.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/diff/resources/existing-claim-xr.yaml", + "testdata/diff/resources/existing-claim-downstream-resource.yaml", + ), + }, + XrdAPIVersion: integration.XRDV1, // the XRD is v1, so Crossplane writes the XR's resourceRefs at spec.resourceRefs + FieldManagerApplies: []integration.FieldManagerApply{ + {File: "testdata/diff/resources/existing-claim-defaulted-fields-other-manager.yaml", FieldManager: "platform-operator"}, + }, + InputFiles: []string{"testdata/diff/modified-claim-omitting-defaulted-fields.yaml"}, + ExpectedStructured: simtest.Expect(). + // As in ModifiedClaimShowsDiff: the composed resource belongs to the claim's backing XR (#534). + WithNoWarnings(). + WithSummary(0, 2, 0). + WithModifiedResource("NopClaim", "test-claim", "existing-namespace"). + WithFieldChange("spec.coolField", "existing-value", "modified-value"). + WithFieldChange("spec.compositeDeletePolicy", "Foreground", "Foreground"). + WithFieldChange("spec.compositionUpdatePolicy", "Manual", "Manual"). + And(). + WithModifiedResource("XDownstreamResource", "test-claim-82crv", ""). + WithFieldChange("spec.forProvider.configData", "existing-value", "modified-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // A claim is defaulted with its own CRD, never its XR's; only the XR that render consumes gets + // the XR CRD's defaults. The claimnested CRDs mirror what Crossplane generates: both carry the + // XRD's user default spec.tier, only the claim's defaults spec.compositeDeletePolicy, and only + // the XR's defaults spec.compositionUpdatePolicy. The composition writes the render input's + // tier and policy into the composed resource, so each case pins all three views at once: the + // claim's payload, its predicted or apiserver-defaulted form, and the render input. + "NewClaimIsDefaultedWithItsOwnCRDWhenDryRunCreated": { + Reason: "A new claim shows its own CRD's defaults, from the apiserver, and render sees its XR's", + OutputFormat: "json", + SetupFiles: []string{ + "testdata/diff/resources/claim-nested/parent-definition.yaml", + "testdata/diff/resources/claim-nested/parent-defaults-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + InputFiles: []string{"testdata/diff/new-parent-claim-omitting-defaulted-fields.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(2, 0, 0). + WithAddedResource("ParentNopClaim", "new-parent-claim", "default"). + WithDryRunPerformed(). + WithField("spec.tier", "standard"). + WithField("spec.compositeDeletePolicy", "Background"). + WithFieldAbsent("spec.compositionUpdatePolicy"). + And(). + WithAddedResource("XDownstreamResource", "new-parent-claim-defaults", ""). + WithField("spec.forProvider.configData", "standard/Automatic"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "NewClaimIsPredictedWithItsOwnCRDsDefaultsWithoutDryRun": { + Reason: "With --dry-run-on=existing a new claim is predicted with its own CRD's defaults, not its XR's", + OutputFormat: "json", + DryRunOn: "existing", + SetupFiles: []string{ + "testdata/diff/resources/claim-nested/parent-definition.yaml", + "testdata/diff/resources/claim-nested/parent-defaults-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + InputFiles: []string{"testdata/diff/new-parent-claim-omitting-defaulted-fields.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(2, 0, 0). + WithAddedResource("ParentNopClaim", "new-parent-claim", "default"). + WithDryRunSkipped("disabled", ""). + WithField("spec.tier", "standard"). + WithField("spec.compositeDeletePolicy", "Background"). + WithFieldAbsent("spec.compositionUpdatePolicy"). + And(). + WithAddedResource("XDownstreamResource", "new-parent-claim-defaults", ""). + WithDryRunSkipped("disabled", ""). + WithField("spec.forProvider.configData", "standard/Automatic"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "XRDDefaultsAppliedBeforeRendering": { + Reason: "Validates that XRD defaults are applied to XR before rendering", + OutputFormat: "json", + InputFiles: []string{"testdata/diff/xr-with-missing-defaults.yaml"}, + SetupFiles: []string{ + "testdata/diff/resources/xrd-with-defaults.yaml", + "testdata/diff/resources/composition-with-defaults.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + ExpectedStructured: simtest.Expect(). + WithSummary(1, 0, 0). + WithAddedResource("XTestDefaultResource", "test-resource-with-defaults", "default"). + WithField("spec.region", "us-east-1"). + WithField("spec.size", "large"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // The sibling of XRDDefaultsAppliedBeforeRendering, whose composition renders nothing, so it checks + // only the XR's dry run. Here a composition reads the composite's defaulted fields into a composed + // resource: a user default (spec.region) for a v2 and a legacy XR, and for the legacy XR also the + // machinery default spec.compositionUpdatePolicy. Rendering an undefaulted XR writes "". + "XRDDefaultsReachTheRender": { + Reason: "Validates that a composition observes an XR's CRD defaults, for both a v2 and a legacy XR", + OutputFormat: "json", + SetupFiles: []string{ + "testdata/diff/resources/rendered-defaults/definitions.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + InputFiles: []string{"testdata/diff/new-rendered-defaults-xrs.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(4, 0, 0). + WithAddedResource("XRegionalDatabase", "modern-database", "default"). + WithField("spec.region", "us-east-1"). + And(). + WithAddedResource("XLegacyRegionalDatabase", "legacy-database", ""). + WithField("spec.region", "eu-west-1"). + And(). + WithAddedResource("XDownstreamResource", "modern-database", "default"). + WithField("spec.forProvider.configData", "us-east-1/large"). + And(). + WithAddedResource("XDownstreamResource", "legacy-database", ""). + WithField("spec.forProvider.configData", "eu-west-1/small/Automatic"), + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // #503: a composed resource's dry-run apply payload must be exactly what the composition + // rendered. If it carried a locally-defaulted spec.forProvider.size: small, the apply would + // claim the field away from the manager that set it to large, predicting a change real + // Crossplane (which never sends the field) would not make. The composition renders + // everything else identically, so any modification reported for the composed resource could + // only have come from the defaulted field. + "CRDDefaultedFieldOwnedByAnotherManagerIsNotReportedChanged": { + Reason: "A CRD-defaulted field the composition does not render keeps another manager's non-default value in the diff", + OutputFormat: "json", + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/new-xr.yaml"}, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "testdata/diff/resources/composition-with-crd-defaulted-downstream.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + // The composed resource deliberately has no controlling XR, as after its XR was deleted with + // --cascade=orphan: the XR is new, and what this case pins is the composed resource's field + // managers, which an orphan keeps. Giving it an XR would make the XR modified rather than new. + FieldManagerApplies: []integration.FieldManagerApply{ + { + File: "testdata/diff/resources/existing-defaulted-downstream-composed.yaml", + FieldManager: "apiextensions.crossplane.io/composed/2b9d3c4e-0000-4000-8000-000000000503", + }, + { + File: "testdata/diff/resources/existing-defaulted-downstream-other-manager.yaml", + FieldManager: "platform-operator", + }, + }, + ExpectedStructured: simtest.Expect(). + WithSummary(1, 0, 0). + WithAddedResource("XNopResource", "test-resource", "default"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // #503, for the XR itself. The XR is defaulted before rendering, and must be: the composition + // has to see the spec Crossplane would. But the XR's own dry-run apply payload must not carry + // those defaults, or it claims spec.region from the manager that set it to eu-west-1 and reports + // a change applying the manifest would not make. Nothing is composed, so the XR is the only + // thing that could be reported changed. + "XRDDefaultedFieldOwnedByAnotherManagerIsNotReportedChanged": { + Reason: "An XRD-defaulted field the XR manifest omits keeps another manager's non-default value in the diff", + OutputFormat: "json", + InputFiles: []string{"testdata/diff/xr-with-missing-defaults.yaml"}, + SetupFiles: []string{ + "testdata/diff/resources/xrd-with-defaults.yaml", + "testdata/diff/resources/composition-with-defaults.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + FieldManagerApplies: []integration.FieldManagerApply{ + {File: "testdata/diff/xr-with-missing-defaults.yaml", FieldManager: "kubectl"}, + {File: "testdata/diff/resources/existing-xr-with-defaults-reconciled.yaml", FieldManager: integration.CompositeReconcilerFieldManager}, + {File: "testdata/diff/resources/existing-xr-with-defaults-other-manager.yaml", FieldManager: "platform-operator"}, + }, + ExpectedStructured: simtest.Expect().WithSummary(0, 0, 0), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeSuccess, + }, + // With no apiserver result for an addition, its defaults have to be predicted locally. These + // pin that keeping local defaults out of dry-run payloads did not also take them out of the + // additions that are never sent: an XRD default on the XR, and a CRD default on a composed + // resource. + "XRDDefaultsShownForAddedXRWithoutDryRun": { + Reason: "With --dry-run-on=existing an added XR still shows the defaults its XRD declares", + OutputFormat: "json", + DryRunOn: "existing", + InputFiles: []string{"testdata/diff/xr-with-missing-defaults.yaml"}, + SetupFiles: []string{ + "testdata/diff/resources/xrd-with-defaults.yaml", + "testdata/diff/resources/composition-with-defaults.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + ExpectedStructured: simtest.Expect(). + WithSummary(1, 0, 0). + WithAddedResource("XTestDefaultResource", "test-resource-with-defaults", "default"). + WithDryRunSkipped("disabled", ""). + WithField("spec.region", "us-east-1"). + WithField("spec.size", "large"). + WithField("spec.tags.environment", "development"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "CRDDefaultsShownForAddedComposedResourceWithoutDryRun": { + Reason: "With --dry-run-on=existing an added composed resource still shows the defaults its CRD declares", + OutputFormat: "json", + DryRunOn: "existing", + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/new-xr.yaml"}, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "testdata/diff/resources/composition-with-crd-defaulted-downstream.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + ExpectedStructured: simtest.Expect(). + WithSummary(2, 0, 0). + WithAddedResource("XDefaultedDownstreamResource", "test-resource-defaulted", "default"). + WithDryRunSkipped("disabled", ""). + WithField("spec.forProvider.size", "small"). + WithField("spec.forProvider.configData", "new-value"). + And(). + WithAddedResource("XNopResource", "test-resource", "default"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // The local prediction defaults an addition against the version it was requested at. Its CRD + // stores another version, whose defaults (and conversion) the cluster may also apply, so the + // prediction cannot be trusted to match and the advisory says so — even under + // --dry-run-on=existing, whose unverified additions are otherwise not warned about. Defaulting + // itself is unchanged: size is the requested version's default, not the storage version's. + // The XR's own XRD has one version, so it raises no warning, which the count pins. + "AdditionAtANonStorageVersionWarnsThatItsDiffOmitsTheStorageVersion": { + Reason: "With --dry-run-on=existing an addition requested at a version its CRD does not store warns that the cluster stores it at another version", + OutputFormat: "json", + DryRunOn: "existing", + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/new-xr.yaml"}, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "testdata/diff/resources/composition-with-non-storage-version-downstream.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + ExpectedStructured: simtest.Expect(). + WithSummary(2, 0, 0). + WithAddedResource("XVersionedDownstreamResource", "test-resource-versioned", "default"). + WithDryRunSkipped("disabled", ""). + WithDryRunStorageVersion("v1"). + WithField("spec.forProvider.size", "small"). + And(). + WithAddedResource("XNopResource", "test-resource", "default"). + WithDryRunSkipped("disabled", ""). + And(). + WithWarning("they are stored at a version other than the one requested"). + WithWarningContext(map[string]string{ + "gvk": "ns.nop.example.org/v1alpha1, Kind=XVersionedDownstreamResource", + "namespace": "default", + "versions": "requested v1alpha1, stored v1", + "count": "1", + }), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "AdditionAtANonStorageVersionWarnsOnStderr": { + Reason: "In text mode the storage-version note reaches stderr, the only place a person sees it", + DryRunOn: "existing", + InputFiles: []string{"../../../internal/testutils/integration/testdata/diff/new-xr.yaml"}, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "testdata/diff/resources/composition-with-non-storage-version-downstream.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + NoColor: true, + ExpectedStderrContains: []string{ + `WARNING: did not verify added resources against the apiserver (--dry-run-on=existing); they are stored at a version other than the one requested, ` + + `whose defaults and conversion the cluster may apply but their diffs do not ` + + `(count=1, gvk=ns.nop.example.org/v1alpha1, Kind=XVersionedDownstreamResource, namespace=default, versions=requested v1alpha1, stored v1)`, + }, + ExpectedOutput: "+++ XVersionedDownstreamResource/test-resource-versioned", + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "XRDDefaultsNoOverride": { + Reason: "Validates that XRD defaults do not override user-specified values", + OutputFormat: "json", + InputFiles: []string{"testdata/diff/xr-with-overridden-defaults.yaml"}, + SetupFiles: []string{ + "testdata/diff/resources/xrd-with-defaults.yaml", + "testdata/diff/resources/composition-with-defaults.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + ExpectedStructured: simtest.Expect(). + WithSummary(1, 0, 0). + WithAddedResource("XTestDefaultResource", "test-resource-with-overrides", "default"). + WithField("spec.region", "us-west-2"). + WithField("spec.size", "xlarge"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "NewNestedXRCreatesChildren": { + Reason: "Validates that new nested XR creates child XR and downstream resources", + OutputFormat: "json", + SetupFiles: []string{ + // XRDs for parent and child + "testdata/diff/resources/nested/parent-xrd.yaml", + "testdata/diff/resources/nested/child-xrd.yaml", + // Compositions for parent and child + "testdata/diff/resources/nested/parent-composition.yaml", + "testdata/diff/resources/nested/child-composition.yaml", + // XRD for downstream managed resource + "testdata/diff/resources/xdownstreamenvresource-xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + InputFiles: []string{"testdata/diff/new-nested-xr.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(3, 0, 0). + WithAddedResource("XChildResource", "test-parent-child", "default"). + WithField("spec.childField", "parent-value"). + And(). + WithAddedResource("XDownstreamResource", "test-parent-child-managed", "default"). + WithField("spec.forProvider.configData", "parent-value"). + And(). + WithAddedResource("XParentResource", "test-parent", "default"). + WithField("spec.parentField", "parent-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // Issue #485. The unit test for the depth guard uses a render stub that caps its own recursion, + // so it cannot show what a real cycle does across real renders: real templates, real names that + // grow at every level (kept under the generated CRDs' 63-character limit by a short suffix), real + // schema validation and dry-run at each level. This drives the whole path. It fails at exactly one level past the + // default --max-nested-depth of 10, attributed to the XR the user named. + "CyclicCompositionStopsAtMaxNestedDepth": { + Reason: "A composition cycle (XCycleA -> XCycleB -> XCycleA ...) must stop at --max-nested-depth with a clear error, not recurse until the stack overflows (#485)", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/cycle/xrds.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/cycle/compositions.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + InputFiles: []string{"testdata/diff/new-cycle-xr.yaml"}, + ExpectedError: true, + ExpectedErrorContains: "maximum nesting depth exceeded: XCycleB/test-cycle-c-c-c-c-c-c-c-c-c-c-c (nested depth 11) is nested 11 levels deep, but --max-nested-depth is 10", + ExpectedExitCode: dp.ExitCodeToolError, + ExpectedStructured: simtest.Expect(). + WithError("XCycleA/test-cycle"). + WithMessageContaining("maximum nesting depth exceeded"), + }, + "ModifiedNestedXRPropagatesChanges": { + Reason: "Validates that modified nested XR propagates changes through child XR to downstream resources", + OutputFormat: "json", + SetupFiles: []string{ + // XRDs for parent and child + "testdata/diff/resources/nested/parent-xrd.yaml", + "testdata/diff/resources/nested/child-xrd.yaml", + // Compositions for parent and child + "testdata/diff/resources/nested/parent-composition.yaml", + "testdata/diff/resources/nested/child-composition.yaml", + // XRD for downstream managed resource + "testdata/diff/resources/xdownstreamenvresource-xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + // Existing resources: the parent XR composes the child XR, which composes the managed resource + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + { + OwnerFile: "testdata/diff/resources/nested/existing-parent-xr.yaml", + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + "testdata/diff/resources/nested/existing-child-xr.yaml": { + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + "testdata/diff/resources/nested/existing-managed-resource.yaml": nil, + }, + }, + }, + }, + }, + InputFiles: []string{"testdata/diff/modified-nested-xr.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(0, 3, 0). + WithModifiedResource("XChildResource", "test-parent-child", "default"). + WithFieldChange("spec.childField", "existing-value", "modified-value"). + And(). + WithModifiedResource("XDownstreamResource", "test-parent-child-managed", "default"). + WithFieldChange("spec.forProvider.configData", "existing-value", "modified-value"). + And(). + WithModifiedResource("XParentResource", "test-parent", "default"). + WithFieldChange("spec.parentField", "existing-value", "modified-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // #503, for a nested XR. It is defaulted before its own render, so that render input carries + // spec.tier: standard although the parent's composition never sets it. Sending it would claim + // spec.tier from the manager that set it to premium. + "XRDDefaultedFieldOnNestedXROwnedByAnotherManagerIsNotReportedChanged": { + Reason: "An XRD-defaulted field a parent composition omits from a nested XR keeps another manager's non-default value in the diff", + OutputFormat: "json", + SetupFiles: []string{ + "testdata/diff/resources/nested/parent-xrd.yaml", + "testdata/diff/resources/nested/child-xrd.yaml", + "testdata/diff/resources/nested/parent-composition.yaml", + "testdata/diff/resources/nested/child-composition.yaml", + "testdata/diff/resources/xdownstreamenvresource-xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + { + OwnerFile: "testdata/diff/resources/nested/existing-parent-xr.yaml", + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + "testdata/diff/resources/nested/existing-child-xr.yaml": { + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + "testdata/diff/resources/nested/existing-managed-resource.yaml": nil, + }, + }, + }, + }, + }, + FieldManagerApplies: []integration.FieldManagerApply{ + {File: "testdata/diff/resources/nested/existing-child-xr-tier-other-manager.yaml", FieldManager: "platform-operator"}, + }, + InputFiles: []string{"testdata/diff/modified-nested-xr.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(0, 3, 0). + WithModifiedResource("XChildResource", "test-parent-child", "default"). + WithFieldChange("spec.childField", "existing-value", "modified-value"). + WithFieldChange("spec.tier", "premium", "premium"). + And(). + WithModifiedResource("XDownstreamResource", "test-parent-child-managed", "default"). + WithFieldChange("spec.forProvider.configData", "existing-value", "modified-value"). + And(). + WithModifiedResource("XParentResource", "test-parent", "default"). + WithFieldChange("spec.parentField", "existing-value", "modified-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // Composition Revision tests for v2 XRDs + "V2ManualPolicyPinnedRevision": { + Reason: "Validates v2 XR with Manual update policy stays on pinned revision", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "testdata/diff/resources/composition-revision-v1.yaml", + "testdata/diff/resources/composition-v2.yaml", // Current composition is v2 + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/diff/resources/existing-xr-manual-v1.yaml", + "testdata/diff/resources/existing-downstream-manual-v1.yaml", + ), + }, + InputFiles: []string{"testdata/diff/modified-xr-manual-v1.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(0, 2, 0). + WithModifiedResource("XDownstreamResource", "test-manual-v1", "default"). + WithFieldChange("spec.forProvider.configData", "v1-existing-value", "v1-modified-value"). + And(). + WithModifiedResource("XNopResource", "test-manual-v1", "default"). + WithFieldChange("spec.coolField", "existing-value", "modified-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "V2AutomaticPolicyLatestRevision": { + Reason: "Validates v2 XR with Automatic update policy uses latest revision", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "testdata/diff/resources/composition-revision-v1.yaml", + "testdata/diff/resources/composition-v2.yaml", // Current composition is v2 + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + // The XR is still on v1, and its composed resource holds v1's data. + integration.Composes( + "testdata/diff/resources/existing-xr-automatic.yaml", + "testdata/diff/resources/existing-downstream-automatic.yaml", + ), + }, + InputFiles: []string{"testdata/diff/modified-xr-automatic.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(0, 2, 0). + WithModifiedResource("XDownstreamResource", "test-automatic", "default"). + WithFieldChange("spec.forProvider.configData", "v1-existing-value", "v2-modified-value"). + And(). + WithModifiedResource("XNopResource", "test-automatic", "default"). + WithFieldChange("spec.coolField", "existing-value", "modified-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "V2AutomaticPolicyRevisionSelectorPinsMatchingRevision": { + // Issue #388 (Bug B): under Automatic policy WITH a compositionRevisionSelector, the XR + // resolves to the latest revision MATCHING the selector, not the latest overall. Here the + // selector pins channel=stable (revision 1) even though channel=preview (revision 2) is + // newer — so the diff must render against the stable template (stable-*), not preview-*. + Reason: "Validates v2 Automatic XR with compositionRevisionSelector resolves to the selector-matching revision, not the newest overall", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "testdata/diff/resources/composition-revision-stable.yaml", // revision 1, channel=stable + "testdata/diff/resources/composition-revision-preview.yaml", // revision 2, channel=preview (newest) + "testdata/diff/resources/composition-v2.yaml", // current composition + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/diff/resources/existing-xr-selector-stable.yaml", + "testdata/diff/resources/existing-downstream-selector-stable.yaml", + ), + }, + InputFiles: []string{"testdata/diff/modified-xr-selector-stable.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(0, 2, 0). + WithModifiedResource("XDownstreamResource", "test-selector", "default"). + // stable-* (not preview-*) proves the selector, not "latest overall", chose the revision. + WithFieldChange("spec.forProvider.configData", "stable-existing-value", "stable-modified-value"). + And(). + WithModifiedResource("XNopResource", "test-selector", "default"). + WithFieldChange("spec.coolField", "existing-value", "modified-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "V2AutomaticPolicyRevisionSelectorNoMatchErrors": { + // The mirror of the above: a selector matching NO revision must fail the diff rather than + // silently rendering against a non-matching revision (accuracy over guessing). + Reason: "Validates v2 Automatic XR whose compositionRevisionSelector matches no revision fails the diff", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "testdata/diff/resources/composition-revision-stable.yaml", + "testdata/diff/resources/composition-revision-preview.yaml", + "testdata/diff/resources/composition-v2.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + InputFiles: []string{"testdata/diff/modified-xr-selector-nomatch.yaml"}, + ExpectedError: true, + ExpectedErrorContains: "match selector", + ExpectedExitCode: dp.ExitCodeToolError, + }, + "V2ManualRevisionUpgradeDiff": { + Reason: "Validates v2 XR changing revision in Manual mode shows upgrade diff", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "testdata/diff/resources/composition-revision-v1.yaml", + "testdata/diff/resources/composition-v2.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/diff/resources/existing-xr-manual-v1.yaml", + "testdata/diff/resources/existing-downstream-manual-v1.yaml", + ), + }, + InputFiles: []string{"testdata/diff/modified-xr-manual-upgrade-to-v2.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(0, 2, 0). + WithModifiedResource("XDownstreamResource", "test-manual-v1", "default"). + WithFieldChange("spec.forProvider.configData", "v1-existing-value", "v2-modified-value"). + And(). + WithModifiedResource("XNopResource", "test-manual-v1", "default"). + WithFieldChange("spec.coolField", "existing-value", "modified-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "CompositionRevisionUpgradesResourceAPIVersion": { + // NOTE: This test validates that resources are correctly matched across API version changes, + // avoiding delete/recreate. The composition template changes from v1beta1 to v1beta2, but + // Kubernetes automatically converts resources between served API versions. When we query for + // the v1beta2 resource, Kubernetes finds the v1beta1 resource and returns it auto-converted + // to v1beta2. From Kubernetes' perspective, the resource exists as both versions simultaneously, + // so there's no apiVersion field change to show in the diff. The important thing is that the + // resource is matched (unchanged, not ---/+++), preventing delete/recreate operations. + // + // The XR composes the resource as Crossplane leaves it: controlled by the XR, and named in the + // XR's resourceRefs at v1beta1, the version it was composed at. Crossplane's garbage collector + // matches what an XR composed to what it renders by composition resource name, so it keeps the + // resource; removal detection must too, rather than report the v1beta1 reference unmatched by + // the v1beta2 render (#552). + Reason: "Validates XR upgrading composition revision that changes resource API version shows as update not remove/add", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + // NOTE: xapimigrate CRD is auto-loaded from testdata/diff/crds/ + // We apply no XRD for XApiMigrateResource because it is a regular managed resource + // (not a composite), and an XRD would make it composite. + "testdata/diff/resources/api-version-composition-revision-v1.yaml", + "testdata/diff/resources/api-version-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + { + OwnerFile: "testdata/diff/resources/existing-api-version-xr-rev1.yaml", + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + "testdata/diff/resources/existing-api-version-downstream-v1beta1.yaml": nil, + }, + }, + }, + InputFiles: []string{"testdata/diff/modified-api-version-xr-rev2.yaml"}, + // Key assertion: nothing is added or removed, proving API version migration works. The + // XApiMigrateResource is stored identically at either version, so it renders unchanged; an + // unmatched one would be a +++ addition plus a --- removal. Only the XR's revision ref changes. + ExpectedStructured: simtest.Expect(). + WithSummary(0, 1, 0). + WithModifiedResource("XNopResource", "test-api-version-xr", "default"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "CompositionRevisionUpgradesResourceAPIVersionAndChangesIt": { + // The same upgrade, with a change to the XR that the new revision renders into the resource too. + // The resource is one object, modified once at the version it is rendered at. Matched by API + // version, the v1beta1 reference was a removal contradicting that modification, and the xr + // command refused to combine the two diffs of one object (#552). + Reason: "Validates an API version change plus a field change is one modification, not a removal and a modification", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "testdata/diff/resources/api-version-composition-revision-v1.yaml", + "testdata/diff/resources/api-version-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + { + OwnerFile: "testdata/diff/resources/existing-api-version-xr-rev1.yaml", + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + "testdata/diff/resources/existing-api-version-downstream-v1beta1.yaml": nil, + }, + }, + }, + InputFiles: []string{"testdata/diff/modified-api-version-xr-rev2-changed-field.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(0, 2, 0). + WithModifiedResource("XApiMigrateResource", "test-api-version-xr-api-resource", "default"). + WithFieldChange("spec.forProvider.configData", "test-value", "changed-value"). + And(). + WithModifiedResource("XNopResource", "test-api-version-xr", "default"). + WithFieldChange("spec.coolField", "test-value", "changed-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "V2SwitchManualToAutomatic": { + Reason: "Validates v2 XR switching from Manual to Automatic mode uses latest revision", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "testdata/diff/resources/composition-revision-v1.yaml", + "testdata/diff/resources/composition-v2.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/diff/resources/existing-xr-manual-v1.yaml", + "testdata/diff/resources/existing-downstream-manual-v1.yaml", + ), + }, + InputFiles: []string{"testdata/diff/modified-xr-switch-to-automatic.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(0, 2, 0). + WithModifiedResource("XDownstreamResource", "test-manual-v1", "default"). + WithFieldChange("spec.forProvider.configData", "v1-existing-value", "v2-modified-value"). + And(). + WithModifiedResource("XNopResource", "test-manual-v1", "default"). + WithFieldChange("spec.coolField", "existing-value", "modified-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "V2NetNewManualNoRevRef": { + Reason: "Validates v2 net new XR with Manual policy but no revision ref uses latest revision", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "testdata/diff/resources/composition-revision-v1.yaml", + "testdata/diff/resources/composition-v2.yaml", // Current composition is v2 + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + InputFiles: []string{"testdata/diff/new-xr-manual-no-ref.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(2, 0, 0). + WithAddedResource("XDownstreamResource", "test-manual-no-ref", "default"). + WithField("spec.forProvider.configData", "v2-new-value"). + And(). + WithAddedResource("XNopResource", "test-manual-no-ref", "default"). + WithField("spec.coolField", "new-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // Composition Revision tests for v1 XRDs (Crossplane 1.20 compatibility) + "V1ManualRevisionUpgradeDiff": { + Reason: "Validates v1 XR with Manual update policy changing revision shows upgrade diff", + OutputFormat: "json", + XrdAPIVersion: integration.XRDV1, + SetupFiles: []string{ + "testdata/diff/resources/legacy-xrd.yaml", + "testdata/diff/resources/legacy-composition-revision-v1.yaml", + "testdata/diff/resources/legacy-composition-v2.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/diff/resources/existing-legacy-xr-manual-v1.yaml", + "testdata/diff/resources/existing-legacy-downstream-manual-v1.yaml", + ), + }, + InputFiles: []string{"testdata/diff/modified-legacy-xr-manual-upgrade-to-v2.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(0, 2, 0). + WithModifiedResource("XDownstreamResource", "test-legacy-manual-v1", ""). + WithFieldChange("spec.forProvider.configData", "v1-existing-value", "v2-modified-value"). + And(). + WithModifiedResource("XNopResource", "test-legacy-manual-v1", ""). + WithFieldChange("spec.coolField", "existing-value", "modified-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // Issue #499: Crossplane's composite reconciler writes compositionRevisionRef, so a hand-written + // manifest omits it — and an apply that omits a field leaves it where it is. The render must + // therefore see the cluster copy's ref. Here the composition propagates the revision name into the + // composed resource, so a render that dropped the ref reported configData changing from the real + // revision name to empty for an input identical to what is deployed. + "ExistingXROmittingRevisionRefRendersWithClusterRef": { + Reason: "An existing XR whose input omits compositionRevisionRef renders with the cluster's ref, so a template reading the revision name shows no spurious change", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/revision-templating-composition.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/revision-templating-composition-revision.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "../../../internal/testutils/integration/testdata/comp/resources/existing-xr-revision-ref.yaml", + "../../../internal/testutils/integration/testdata/comp/resources/existing-downstream-revision-ref.yaml", + ), + }, + InputFiles: []string{"testdata/diff/existing-xr-revision-ref-omitted.yaml"}, + ExpectedStructured: simtest.Expect().WithSummary(0, 0, 0), + ExpectedExitCode: dp.ExitCodeSuccess, + }, + // Issue #499, the consequence for which revision renders. The cluster's ref is inherited before the + // composition is resolved, so a Manual composite whose input omits the ref stays on the revision it + // is pinned to (-911318d, the v1 template), as it would in the cluster, instead of being rendered + // against the latest one (-430f3f3, v2). Compare V2ManualPolicyPinnedRevision, the same change with + // the ref spelled out. + "V2ManualPolicyOmittedRevisionRefStaysPinned": { + Reason: "Validates v2 XR with Manual update policy whose input omits compositionRevisionRef stays on the cluster's pinned revision", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "testdata/diff/resources/composition-revision-v1.yaml", + "testdata/diff/resources/composition-v2.yaml", // Current composition is v2 + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/diff/resources/existing-xr-manual-v1.yaml", + "testdata/diff/resources/existing-downstream-manual-v1.yaml", + ), + }, + InputFiles: []string{"testdata/diff/modified-xr-manual-v1-omits-revision-ref.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(0, 2, 0). + WithModifiedResource("XDownstreamResource", "test-manual-v1", "default"). + // v1-* (not v2-*) proves the pinned revision, not the latest, rendered. + WithFieldChange("spec.forProvider.configData", "v1-existing-value", "v1-modified-value"). + And(). + WithModifiedResource("XNopResource", "test-manual-v1", "default"). + WithFieldChange("spec.coolField", "existing-value", "modified-value"), + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // The v1-path (spec.compositionRevisionRef) twin of the above. + "V1ManualPolicyOmittedRevisionRefStaysPinned": { + Reason: "Validates v1 XR with Manual update policy whose input omits compositionRevisionRef stays on the cluster's pinned revision", + OutputFormat: "json", + XrdAPIVersion: integration.XRDV1, + SetupFiles: []string{ + "testdata/diff/resources/legacy-xrd.yaml", + "testdata/diff/resources/legacy-composition-revision-v1.yaml", + "testdata/diff/resources/legacy-composition-v2.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/diff/resources/existing-legacy-xr-manual-v1.yaml", + "testdata/diff/resources/existing-legacy-downstream-manual-v1.yaml", + ), + }, + InputFiles: []string{"testdata/diff/modified-legacy-xr-manual-v1-omits-revision-ref.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(0, 2, 0). + WithModifiedResource("XDownstreamResource", "test-legacy-manual-v1", ""). + WithFieldChange("spec.forProvider.configData", "v1-existing-value", "v1-modified-value"). + And(). + WithModifiedResource("XNopResource", "test-legacy-manual-v1", ""). + WithFieldChange("spec.coolField", "existing-value", "modified-value"), + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // Issue #499 for a nested XR. It is rendered from its parent's output, which never carries + // compositionRevisionRef, so it must inherit the cluster copy's, which ProcessNestedXRs hands it + // rather than fetching it again. The child's composition propagates the revision name into the + // managed resource; the parent's change reaches the child's childField but not that name, so the + // managed resource must not be reported. + "NestedXRInheritsClusterRevisionRef": { + Reason: "A nested XR renders with its cluster copy's compositionRevisionRef, so a template reading the revision name shows no spurious change", + OutputFormat: "json", + SetupFiles: []string{ + "testdata/diff/resources/nested/parent-xrd.yaml", + "testdata/diff/resources/nested/child-xrd.yaml", + "testdata/diff/resources/nested/parent-composition.yaml", + "testdata/diff/resources/nested/child-revision-templating-composition.yaml", + "testdata/diff/resources/xdownstreamenvresource-xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + { + OwnerFile: "testdata/diff/resources/nested/existing-parent-xr.yaml", + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + "testdata/diff/resources/nested/existing-child-xr-revision-ref.yaml": { + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + "testdata/diff/resources/nested/existing-managed-resource-revision-ref.yaml": nil, + }, + }, + }, + }, + }, + InputFiles: []string{"testdata/diff/modified-nested-xr.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(0, 2, 0). + WithModifiedResource("XChildResource", "test-parent-child", "default"). + WithFieldChange("spec.childField", "existing-value", "modified-value"). + // The nested XR's own reconciler wrote the ref, not the parent's composed apply, so the + // parent's render, which omits it, leaves it in place. + WithFieldChange("spec.crossplane.compositionRevisionRef.name", + "xchildresources.nested.example.org-96afc06", "xchildresources.nested.example.org-96afc06"). + And(). + WithModifiedResource("XParentResource", "test-parent", "default"). + WithFieldChange("spec.parentField", "existing-value", "modified-value"), + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // Issue #498, as a user meets it: an Automatic claim's manifest omits the compositionRevisionRef the + // claim syncer copied onto the cluster copy. Under Automatic the claim's ref is never propagated to + // the backing XR anyway; the XR controller owns the XR's own, which is what render must see. Before, + // the backing XR's ref was dropped, so the template read no revision name and configData was + // reported changing to "v2::existing-value" for an unchanged claim. + "AutomaticClaimOmittingRevisionRefRendersWithBackingXRRef": { + Reason: "An Automatic claim renders with its backing XR's compositionRevisionRef, so a template reading the revision name shows no spurious change", + OutputFormat: "json", + SetupFiles: []string{ + "testdata/diff/resources/existing-namespace.yaml", + "testdata/diff/resources/pinned-claim/definitions.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + "testdata/diff/resources/pinned-claim/existing-automatic.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/diff/resources/pinned-claim/existing-automatic-xr.yaml", + "testdata/diff/resources/pinned-claim/existing-automatic-downstream.yaml", + ), + }, + XrdAPIVersion: integration.XRDV1, // the XRD is v1, so Crossplane writes the XR's resourceRefs at spec.resourceRefs + InputFiles: []string{"testdata/diff/unchanged-automatic-pinned-claim.yaml"}, + // No warnings either: the composed resource belongs to the claim's backing XR (#534). + ExpectedStructured: simtest.Expect().WithNoWarnings().WithSummary(0, 0, 0), + ExpectedExitCode: dp.ExitCodeSuccess, + }, + // A Manual claim's pin lives on its backing XR: the claim syncer copies the ref back to a claim only + // under Automatic. Composition resolution and render must both see that pin. Before, the composition + // was resolved from the claim, which names no revision, so the latest one (v2) was chosen, while the + // render read the backing XR's ref (revision 1): configData became "v2:...-3d33597:...", a revision's template + // paired with another revision's name. + "ManualClaimRendersAgainstItsBackingXRsPinnedRevision": { + Reason: "A Manual claim whose backing XR is pinned to an older revision renders that revision, with that revision's name", + OutputFormat: "json", + SetupFiles: []string{ + "testdata/diff/resources/existing-namespace.yaml", + "testdata/diff/resources/pinned-claim/definitions.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + "testdata/diff/resources/pinned-claim/existing-manual.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/diff/resources/pinned-claim/existing-manual-xr.yaml", + "testdata/diff/resources/pinned-claim/existing-manual-downstream.yaml", + ), + }, + XrdAPIVersion: integration.XRDV1, // the XRD is v1, so Crossplane writes the XR's resourceRefs at spec.resourceRefs + InputFiles: []string{"testdata/diff/modified-manual-pinned-claim.yaml"}, + ExpectedStructured: simtest.Expect(). + // The composed resource is labelled with the backing XR's name, which the claim's resourceRef + // names, so it already belongs to this claim: no ownership advisory (#534). + WithNoWarnings(). + WithSummary(0, 2, 0). + WithModifiedResource("Pinned", "manual-claim", "existing-namespace"). + WithFieldChange("spec.coolField", "existing-value", "modified-value"). + And(). + WithModifiedResource("XDownstreamResource", "manual-claim-x1y2z", ""). + WithFieldChange("spec.forProvider.configData", + "v1:xpinneds.pinned.diff.example.org-3d33597:existing-value", + "v1:xpinneds.pinned.diff.example.org-3d33597:modified-value"), + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // Issue #536. Crossplane's composite reconciler resolves the revision, writes it to + // compositionRevisionRef, and then composes, so an Automatic composite whose ref lags behind the + // latest revision is composed from the latest one AND sees its name. Before, the latest revision + // rendered while the template read the stale ref: configData became "v2:...-3d33597:...", a revision's + // template paired with another revision's name. + "AutomaticXRWithLaggingRevisionRefRendersLatestRevisionAndName": { + Reason: "An Automatic XR whose cluster ref names an older revision renders the latest revision, with that revision's name", + OutputFormat: "json", + SetupFiles: []string{ + "testdata/diff/resources/pinned-claim/definitions.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/diff/resources/pinned-claim/existing-lagging-automatic-xr.yaml", + "testdata/diff/resources/pinned-claim/existing-lagging-automatic-downstream.yaml", + ), + }, + XrdAPIVersion: integration.XRDV1, // the XRD is v1, so Crossplane writes the XR's resourceRefs at spec.resourceRefs + InputFiles: []string{"testdata/diff/unchanged-lagging-automatic-xr.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(0, 1, 0). + WithModifiedResource("XDownstreamResource", "lagging-xr", ""). + WithFieldChange("spec.forProvider.configData", + "v1:xpinneds.pinned.diff.example.org-3d33597:existing-value", + "v2:xpinneds.pinned.diff.example.org-806289b:existing-value"), + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // An XRD's defaultCompositionUpdatePolicy applies to a new composite before Crossplane resolves its + // revision, so a new composite that names a revision and leaves the policy to an XRD defaulting to + // Manual renders the revision it names. Before, the composition was resolved before defaulting, as + // Automatic, so the latest revision (v2) rendered. + "NewXRUnderManualDefaultingXRDRendersTheRevisionItNames": { + Reason: "A new XR whose XRD defaults its update policy to Manual is resolved as Manual", + OutputFormat: "json", + SetupFiles: []string{ + "testdata/diff/resources/manual-default/definitions.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + InputFiles: []string{"testdata/diff/new-manual-default-xr.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(2, 0, 0). + WithAddedResource("XManualDefault", "new-manual-default", "default"). + And(). + WithAddedResource("XDownstreamResource", "new-manual-default", "default"). + WithField("spec.forProvider.configData", "v1-new-value"), + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // Crossplane selects a composition only for a composite with no compositionRef, and never + // re-selects. A composite whose input relies on a selector therefore keeps the compositionRef + // Crossplane wrote, here composition A, even though the selector now matches two compositions. + // Before, the selector was re-evaluated against the input, and the diff failed as ambiguous. + "ExistingXRSelectingItsCompositionKeepsTheClusterCompositionRef": { + Reason: "An existing XR whose input selects its composition by label renders with the composition Crossplane already bound it to", + OutputFormat: "json", + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + "testdata/diff/resources/selector-bound/setup.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/diff/resources/selector-bound/existing-xr.yaml", + "testdata/diff/resources/selector-bound/existing-downstream.yaml", + ), + }, + InputFiles: []string{"testdata/diff/modified-xr-selector-bound.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(0, 2, 0). + WithModifiedResource("XDownstreamResource", "selector-bound", "default"). + WithFieldChange("spec.forProvider.configData", "a-existing-value", "a-modified-value"). + And(). + WithModifiedResource("XNopResource", "selector-bound", "default"). + WithFieldChange("spec.coolField", "existing-value", "modified-value"), + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // v2 XRD with v1-style composition paths (issue #206) + "V2XRDWithV1StyleCompositionPaths": { + Reason: "Validates v2 XRD using v1-style spec.compositionRef paths are correctly recognized (issue #206)", + OutputFormat: "json", + SetupFiles: []string{ + "testdata/diff/resources/v2-xrd-with-v1-paths.yaml", + "testdata/diff/resources/v2-xrd-with-v1-paths-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes( + "testdata/diff/resources/existing-v2xrd-v1paths-xr.yaml", + "testdata/diff/resources/existing-v2xrd-v1paths-downstream.yaml", + ), + }, + InputFiles: []string{"testdata/diff/resources/modified-v2xrd-v1paths-xr.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(0, 2, 0). + WithModifiedResource("XDownstreamResource", "test-v2xrd-v1paths", "default"). + WithFieldChange("spec.forProvider.configData", "existing-value", "modified-value"). + And(). + WithModifiedResource("XNopResource", "test-v2xrd-v1paths", "default"). + WithFieldChange("spec.coolField", "existing-value", "modified-value"), + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + // A claim passed alongside its backing XR: Crossplane's claim controller writes that XR (a + // server-side apply with forced ownership, stamping claimRef and the claim labels), so the XR + // input would be a second writer. Rejected on the XR; the claim's diff still runs. The binding + // is read from the claim's cluster copy (spec.resourceRef) and from the XR's claim labels. + "ClaimPassedWithItsBackingXRIsRejected": { + Reason: "A claim and the XR bound to it, passed together, is an input error on the XR", + SetupFiles: []string{ + "testdata/diff/resources/existing-namespace.yaml", + "testdata/diff/resources/claim-nested/parent-definition.yaml", + "testdata/diff/resources/claim-nested/child-definition.yaml", + "testdata/diff/resources/claim-nested/parent-composition.yaml", + "testdata/diff/resources/claim-nested/child-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + "testdata/diff/resources/claim-nested/existing-claim.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + { + OwnerFile: "testdata/diff/resources/claim-nested/existing-parent-xr.yaml", + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + "testdata/diff/resources/claim-nested/existing-child-xr.yaml": { + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + "testdata/diff/resources/claim-nested/existing-managed-resource.yaml": nil, + }, + }, + }, + }, + }, + XrdAPIVersion: integration.XRDV1, // the XRDs are v1, so Crossplane writes the XRs' resourceRefs at spec.resourceRefs + InputFiles: []string{ + "testdata/diff/modified-claim-nested.yaml", + "testdata/diff/resources/claim-nested/existing-parent-xr.yaml", + }, + ExpectedError: true, + ExpectedErrorContains: "XNopParent/existing-parent-claim-82crv is the XR bound to claim " + + "ParentNopClaim/existing-parent-claim, which Crossplane's claim controller writes", + ExpectedExitCode: dp.ExitCodeToolError, + }, + "ModifiedClaimWithNestedXRsShowsDiff": { + Reason: "Validates that modified Claims with nested XRs show proper diff (3 modified resources)", + OutputFormat: "json", + SetupFiles: []string{ + "testdata/diff/resources/existing-namespace.yaml", + // XRDs for parent and child Claims (the harness generates their XR and Claim CRDs) + "testdata/diff/resources/claim-nested/parent-definition.yaml", + "testdata/diff/resources/claim-nested/child-definition.yaml", + // Compositions for parent and child + "testdata/diff/resources/claim-nested/parent-composition.yaml", + "testdata/diff/resources/claim-nested/child-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + // Claim is set up separately (not via owner refs - it uses spec.resourceRef to link to backing XR) + "testdata/diff/resources/claim-nested/existing-claim.yaml", + }, + // Owner ref hierarchy matches Crossplane's actual ownership model: + // - Backing XR is the root of the owner ref tree (no owner refs) + // - Nested XR has owner ref to backing XR + // - Managed resource has owner ref to nested XR + // Note: Claim is NOT in this hierarchy - it links to backing XR via spec.resourceRef, not owner refs + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + { + // Backing XR is the root of owner ref tree + OwnerFile: "testdata/diff/resources/claim-nested/existing-parent-xr.yaml", + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + // Nested XR owned by backing XR + "testdata/diff/resources/claim-nested/existing-child-xr.yaml": { + // Managed resource owned by nested XR + OwnedFiles: map[string]*integration.HierarchicalOwnershipRelation{ + "testdata/diff/resources/claim-nested/existing-managed-resource.yaml": nil, + }, + }, + }, + }, + }, + InputFiles: []string{"testdata/diff/modified-claim-nested.yaml"}, + ExpectedStructured: simtest.Expect(). + WithSummary(0, 3, 0). + WithModifiedResource("ClusterNopResource", "existing-parent-claim-82crv-nop", ""). + And(). + WithModifiedResource("ParentNopClaim", "existing-parent-claim", "default"). + WithFieldChange("spec.parentField", "existing-parent-value", "modified-parent-value"). + And(). + WithModifiedResource("XNopChild", "existing-parent-claim-82crv-child", ""). + WithFieldChange("spec.childField", "existing-parent-value", "modified-parent-value"). + // The nested XR's own reconciler wrote its composition refs, not the parent's composed + // apply, so the parent's render, which omits them, leaves them in place. + WithFieldChange("spec.compositionRef.name", "child-nop-claim-composition", "child-nop-claim-composition"). + WithFieldChange("spec.compositionRevisionRef.name", "child-nop-claim-composition-db22ad5", "child-nop-claim-composition-db22ad5"), + XrdAPIVersion: integration.XRDV1, // Use V1 style resourceRefs since XRDs have claims + ExpectedError: false, + ExpectedExitCode: dp.ExitCodeDiffDetected, + }, + "FunctionCredentialsAutoFetch": { + Reason: "Successfully renders XR when composition references credentials that exist in cluster", + OutputFormat: "json", + InputFiles: []string{"testdata/diff/new-xr-with-creds.yaml"}, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "testdata/diff/resources/credentials/composition-with-creds.yaml", + "testdata/diff/resources/credentials/function-credentials-secret.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + ExpectedExitCode: dp.ExitCodeDiffDetected, + ExpectedStructured: simtest.Expect(). + WithSummary(2, 0, 0). + WithAddedResource("XDownstreamResource", "test-resource-with-creds", "default"). + WithField("spec.forProvider.configData", "test-value-creds"). + And(). + WithAddedResource("XNopResource", "test-resource-with-creds", "default"). + WithField("spec.coolField", "test-value-creds"), + ExpectedError: false, + }, + "FunctionCredentialsFromCLI": { + Reason: "Successfully renders XR when credentials provided via --function-credentials flag", + OutputFormat: "json", + InputFiles: []string{"testdata/diff/new-xr-with-creds.yaml"}, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "testdata/diff/resources/credentials/composition-with-creds.yaml", + // Note: NOT setting up the secret in cluster - testing CLI override + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + // Credentials loaded from CLI flag file + FunctionCredentials: "testdata/diff/resources/credentials/cli-credentials.yaml", + ExpectedExitCode: dp.ExitCodeDiffDetected, + // WithNoWarnings is the point of this case for issue #481: the secret the composition + // references is deliberately absent from the cluster and supplied by --function-credentials + // instead, so the shortfall advisory must NOT fire. It used to, because the advisory was + // raised inside the credential client, which runs before the CLI-supplied secrets are merged + // in — telling a user who had just solved the problem to go and solve it. + ExpectedStructured: simtest.Expect(). + WithSummary(2, 0, 0). + WithNoWarnings(). + WithAddedResource("XDownstreamResource", "test-resource-with-creds", "default"). + WithField("spec.forProvider.configData", "test-value-creds"). + And(). + WithAddedResource("XNopResource", "test-resource-with-creds", "default"). + WithField("spec.coolField", "test-value-creds"), + ExpectedError: false, + }, + // Paired sequencer gating tests — the fixture sequencer-gating-composition.yaml + // is deliberately correct (rules match composition-resource-names stage0-/stage1- + // AND auto-ready runs BEFORE sequencer so DesiredComposed.Ready is populated + // before sequencer's gating check). Together these two cases discriminate + // --eventual-state from default behavior: only eventual-state surfaces stage1. + "SequencerGatesLaterStageWithoutEventualState": { + Reason: "Without --eventual-state, function-sequencer hides stage1 until stage0 is ready", + Timeout: simharness.LongTimeout, + OutputFormat: "json", + // eventualState deliberately false + InputFiles: []string{"testdata/diff/resources/sequencer-xr.yaml"}, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/sequencer-gating-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + ExpectedExitCode: dp.ExitCodeDiffDetected, + // Expect: XR + stage0 added; stage1 NOT present (gated). + ExpectedStructured: simtest.Expect(). + WithSummary(2, 0, 0). + WithAddedResource("XNopResource", "sequencer-test", "default"). + WithField("spec.coolField", "test-value"). + And(). + WithAddedResource("XDownstreamResource", "0-stage0-resource", "default"). + WithField("spec.forProvider.configData", "test-value"), + ExpectedError: false, + }, + "SequencerGatedStageAppearsWithEventualState": { + Reason: "With --eventual-state, sequencer-gated stage1 surfaces via synthesize-Ready iteration", + Timeout: simharness.LongTimeout, // Multiple simulation iterations need more time + OutputFormat: "json", + EventualState: true, + InputFiles: []string{"testdata/diff/resources/sequencer-xr.yaml"}, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/sequencer-gating-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + ExpectedExitCode: dp.ExitCodeDiffDetected, + // Expect: XR + stage0 + stage1 all added (stage1 unlocked via eventual-state). + ExpectedStructured: simtest.Expect(). + WithSummary(3, 0, 0). + WithAddedResource("XNopResource", "sequencer-test", "default"). + WithField("spec.coolField", "test-value"). + And(). + WithAddedResource("XDownstreamResource", "0-stage0-resource", "default"). + WithField("spec.forProvider.configData", "test-value"). + And(). + WithAddedResource("XDownstreamResource", "1-stage1-resource", "default"). + WithField("spec.forProvider.configData", "test-value"), + ExpectedError: false, + }, + // Issue #564: an existing XR whose stage0 exists but is not Ready yet, the normal state of an + // XR whose first stage is still provisioning. Nothing renders that is not already observed, so + // eventual state used to stop at once and never synthesize stage0's Ready; but Crossplane + // composes stage1 as soon as stage0 becomes Ready. + "SequencerGatedStageAppearsForExistingXRWithNotReadyStage0WithEventualState": { + Reason: "With --eventual-state, stage1 surfaces when stage0 already exists but is not Ready", + Timeout: simharness.LongTimeout, // Multiple simulation iterations need more time + OutputFormat: "json", + EventualState: true, + InputFiles: []string{"testdata/diff/resources/sequencer-xr.yaml"}, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/sequencer-gating-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes("testdata/diff/resources/existing-sequencer-gating-xr.yaml", "testdata/diff/resources/existing-sequencer-gating-stage0.yaml"), + }, + ExpectedExitCode: dp.ExitCodeDiffDetected, + // Expect: XR and stage0 modified, stage1 added (unlocked by stage0's synthesized Ready). + ExpectedStructured: simtest.Expect(). + WithSummary(1, 2, 0). + WithModifiedResource("XNopResource", "sequencer-test", "default"). + WithFieldChange("spec.coolField", "existing-value", "test-value"). + And(). + WithModifiedResource("XDownstreamResource", "0-stage0-resource", "default"). + WithFieldChange("spec.forProvider.configData", "existing-value", "test-value"). + And(). + WithAddedResource("XDownstreamResource", "1-stage1-resource", "default"). + WithField("spec.forProvider.configData", "test-value"), + ExpectedError: false, + }, + // The converged counterpart: both stages exist and are Ready, so synthesizing Ready changes + // nothing and eventual state is the first render. Only the spec changes the input makes show. + "EventualStateAddsNothingWhenEveryStageIsReady": { + Reason: "With --eventual-state, an XR whose composed resources are all Ready shows only its real changes", + Timeout: simharness.LongTimeout, + OutputFormat: "json", + EventualState: true, + InputFiles: []string{"testdata/diff/resources/sequencer-xr.yaml"}, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/sequencer-gating-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{ + integration.Composes("testdata/diff/resources/existing-sequencer-gating-xr.yaml", + "testdata/diff/resources/existing-sequencer-gating-stage0-ready.yaml", + "testdata/diff/resources/existing-sequencer-gating-stage1-ready.yaml"), + }, + ExpectedExitCode: dp.ExitCodeDiffDetected, + ExpectedStructured: simtest.Expect(). + WithSummary(0, 3, 0). + WithModifiedResource("XNopResource", "sequencer-test", "default"). + WithFieldChange("spec.coolField", "existing-value", "test-value"). + And(). + WithModifiedResource("XDownstreamResource", "0-stage0-resource", "default"). + WithFieldChange("spec.forProvider.configData", "existing-value", "test-value"). + And(). + WithModifiedResource("XDownstreamResource", "1-stage1-resource", "default"). + WithFieldChange("spec.forProvider.configData", "existing-value", "test-value"), + ExpectedError: false, + }, + // Paired composition-driven gating tests — the fixture conditional-gating-composition.yaml + // encodes gating in go-templating itself (no function-sequencer): stage1 is only rendered + // when observed stage0 has Ready=True. This isolates eventual-state simulation from + // function-sequencer and exercises the synthesize-Ready path directly. + "ConditionalGatingHidesLaterStageWithoutEventualState": { + Reason: "Without --eventual-state, template conditionally gates stage1 because observed stage0 is not Ready", + Timeout: simharness.LongTimeout, + OutputFormat: "json", + // eventualState deliberately false + InputFiles: []string{"testdata/diff/resources/conditional-gating-xr.yaml"}, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/conditional-gating-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + ExpectedExitCode: dp.ExitCodeDiffDetected, + // Expect: XR + stage0 added; stage1 NOT present (gated by the template's Ready check). + ExpectedStructured: simtest.Expect(). + WithSummary(2, 0, 0). + WithAddedResource("XNopResource", "conditional-gating-test", "default"). + WithField("spec.coolField", "test-value"). + And(). + WithAddedResource("XDownstreamResource", "0-stage0-resource", "default"). + WithField("spec.forProvider.configData", "test-value"), + ExpectedError: false, + }, + "ConditionalGatedStageAppearsWithEventualState": { + Reason: "With --eventual-state, synthesize-Ready flips stage0 Ready=True in observed so the template renders stage1", + Timeout: simharness.LongTimeout, // Multiple simulation iterations need more time + OutputFormat: "json", + EventualState: true, + InputFiles: []string{"testdata/diff/resources/conditional-gating-xr.yaml"}, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/conditional-gating-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + }, + ExpectedExitCode: dp.ExitCodeDiffDetected, + // Expect: XR + stage0 + stage1 all added. + ExpectedStructured: simtest.Expect(). + WithSummary(3, 0, 0). + WithAddedResource("XNopResource", "conditional-gating-test", "default"). + WithField("spec.coolField", "test-value"). + And(). + WithAddedResource("XDownstreamResource", "0-stage0-resource", "default"). + WithField("spec.forProvider.configData", "test-value"). + And(). + WithAddedResource("XDownstreamResource", "1-stage1-resource", "default"). + WithField("spec.forProvider.configData", "test-value"), + ExpectedError: false, + }, + "EventualStateWithSequencerAndEnvironmentConfigs": { + Reason: "Shows eventual state with function-sequencer AND function-environment-configs to verify requirements resolution during simulation", + Timeout: simharness.LongTimeout, // Multiple simulation iterations with requirements resolution need more time + // This test verifies that the eventual state simulation correctly resolves requirements + // (like environment configs) during its iterative rendering, not just in the final render. + // The composition uses: + // 1. function-environment-configs to fetch env config (requires requirements iteration) + // 2. function-go-templating to generate resources using env config data + // 3. function-sequencer to stage resources + // 4. function-auto-ready + OutputFormat: "json", + EventualState: true, + InputFiles: []string{"testdata/diff/resources/sequencer-with-envconfig-xr.yaml"}, + SetupFiles: []string{ + "../../../internal/testutils/integration/testdata/diff/resources/xrd.yaml", + "testdata/diff/resources/sequencer-with-envconfig-composition.yaml", + "../../../internal/testutils/integration/testdata/diff/resources/functions.yaml", + "testdata/diff/resources/environment-config-v1beta1.yaml", + }, + ExpectedExitCode: dp.ExitCodeDiffDetected, + ExpectedStructured: simtest.Expect(). + WithSummary(3, 0, 0). // 2 downstream resources + 1 XR + WithAddedResource("XDownstreamResource", "0-stage0-resource", "default"). + WithField("spec.forProvider.configData", "test-value"). + WithField("spec.forProvider.resourceTier", "staging"). // From environment config + And(). + WithAddedResource("XDownstreamResource", "1-stage1-resource", "default"). + WithField("spec.forProvider.configData", "test-value"). + WithField("spec.forProvider.resourceTier", "staging"). // From environment config + And(). + WithAddedResource("XNopResource", "sequencer-envconfig-test", "default"). + WithField("spec.coolField", "test-value"), + ExpectedError: false, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + simharness.RunIntegrationTest(t, simharness.XRTest, tt) + }) + } +} diff --git a/cmd/crossplane/xr/testdata/README.md b/cmd/crossplane/xr/testdata/README.md new file mode 100644 index 00000000..3de563b9 --- /dev/null +++ b/cmd/crossplane/xr/testdata/README.md @@ -0,0 +1,12 @@ +# xr simulate integration tests + +`integration_test.go` runs `TestDiffIntegration`: crossplane-diff's xr cases, run as +`crossplane xr simulate -f ` 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/diff`; those shared with another suite are in +`internal/testutils/integration/testdata/diff` (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 TestDiffIntegration ./cmd/crossplane/xr/`. diff --git a/cmd/crossplane/xr/testdata/diff/existing-xr-revision-ref-omitted.yaml b/cmd/crossplane/xr/testdata/diff/existing-xr-revision-ref-omitted.yaml new file mode 100644 index 00000000..fa56bf15 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/existing-xr-revision-ref-omitted.yaml @@ -0,0 +1,16 @@ +# testdata/comp/resources/existing-xr-revision-ref.yaml exactly as a user would write it: unchanged, +# but without the compositionRef and compositionRevisionRef that Crossplane's composite reconciler +# wrote into the cluster copy. Applying this leaves those refs where they are, so nothing would change — +# and the composition this composite uses (testdata/comp/resources/revision-templating-composition.yaml) +# reads the revision name into a composed resource. Only the inherited compositionRef resolves the +# revision whose name render must see, so a render that drops the cluster's refs shows a spurious +# change. See issue #499. +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: revision-ref-resource + namespace: default +spec: + coolField: revision-ref-existing-value + crossplane: + compositionUpdatePolicy: Automatic diff --git a/cmd/crossplane/xr/testdata/diff/first-xr-with-composition-ref.yaml b/cmd/crossplane/xr/testdata/diff/first-xr-with-composition-ref.yaml new file mode 100644 index 00000000..9c567aa2 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/first-xr-with-composition-ref.yaml @@ -0,0 +1,11 @@ +# first-xr.yaml naming its composition, so finding the composition needs no list of every Composition. +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: first-resource + namespace: default +spec: + coolField: "first-value" + crossplane: + compositionRef: + name: xnopresources.diff.example.org diff --git a/cmd/crossplane/xr/testdata/diff/first-xr.yaml b/cmd/crossplane/xr/testdata/diff/first-xr.yaml new file mode 100644 index 00000000..cc6c2fe4 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/first-xr.yaml @@ -0,0 +1,7 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: first-resource + namespace: default +spec: + coolField: "first-value" diff --git a/cmd/crossplane/xr/testdata/diff/generated-name-xr.yaml b/cmd/crossplane/xr/testdata/diff/generated-name-xr.yaml new file mode 100644 index 00000000..0be857b8 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/generated-name-xr.yaml @@ -0,0 +1,7 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + generateName: generated-xr- + namespace: default +spec: + coolField: "new-value" diff --git a/cmd/crossplane/xr/testdata/diff/invalid-schema-xr.yaml b/cmd/crossplane/xr/testdata/diff/invalid-schema-xr.yaml new file mode 100644 index 00000000..785a6207 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/invalid-schema-xr.yaml @@ -0,0 +1,8 @@ +# XR with invalid schema - coolField should be a string, not an integer +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: invalid-schema-xr + namespace: default +spec: + coolField: 12345 diff --git a/cmd/crossplane/xr/testdata/diff/modified-api-version-xr-rev2-changed-field.yaml b/cmd/crossplane/xr/testdata/diff/modified-api-version-xr-rev2-changed-field.yaml new file mode 100644 index 00000000..49be07d7 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/modified-api-version-xr-rev2-changed-field.yaml @@ -0,0 +1,13 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-api-version-xr + namespace: default +spec: + crossplane: + compositionRef: + name: xapimigrateresources.example.org + compositionRevisionRef: + name: xapimigrateresources.example.org-311ee4f + compositionUpdatePolicy: Manual + coolField: changed-value diff --git a/cmd/crossplane/xr/testdata/diff/modified-api-version-xr-rev2.yaml b/cmd/crossplane/xr/testdata/diff/modified-api-version-xr-rev2.yaml new file mode 100644 index 00000000..0baf92f0 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/modified-api-version-xr-rev2.yaml @@ -0,0 +1,13 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-api-version-xr + namespace: default +spec: + crossplane: + compositionRef: + name: xapimigrateresources.example.org + compositionRevisionRef: + name: xapimigrateresources.example.org-311ee4f + compositionUpdatePolicy: Manual + coolField: test-value diff --git a/cmd/crossplane/xr/testdata/diff/modified-claim-nested.yaml b/cmd/crossplane/xr/testdata/diff/modified-claim-nested.yaml new file mode 100644 index 00000000..67844ef1 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/modified-claim-nested.yaml @@ -0,0 +1,12 @@ +apiVersion: claimnested.diff.example.org/v1alpha1 +kind: ParentNopClaim +metadata: + name: existing-parent-claim + namespace: default + labels: + new-label: added-value +spec: + parentField: modified-parent-value + compositeDeletePolicy: Background + compositionRef: + name: parent-nop-claim-composition diff --git a/cmd/crossplane/xr/testdata/diff/modified-claim-omitting-defaulted-fields.yaml b/cmd/crossplane/xr/testdata/diff/modified-claim-omitting-defaulted-fields.yaml new file mode 100644 index 00000000..3c6645d8 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/modified-claim-omitting-defaulted-fields.yaml @@ -0,0 +1,11 @@ +# modified-claim.yaml without spec.compositeDeletePolicy, which the claim's CRD defaults (as it does +# spec.compositionUpdatePolicy, also omitted). +apiVersion: diff.example.org/v1alpha1 +kind: NopClaim +metadata: + name: test-claim + namespace: existing-namespace +spec: + coolField: modified-value + compositionRef: + name: claim-composition diff --git a/cmd/crossplane/xr/testdata/diff/modified-claim.yaml b/cmd/crossplane/xr/testdata/diff/modified-claim.yaml new file mode 100644 index 00000000..3116b173 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/modified-claim.yaml @@ -0,0 +1,10 @@ +apiVersion: diff.example.org/v1alpha1 +kind: NopClaim +metadata: + name: test-claim + namespace: existing-namespace +spec: + compositeDeletePolicy: Background + coolField: modified-value + compositionRef: + name: claim-composition diff --git a/cmd/crossplane/xr/testdata/diff/modified-cluster-xr.yaml b/cmd/crossplane/xr/testdata/diff/modified-cluster-xr.yaml new file mode 100644 index 00000000..dcbaf051 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/modified-cluster-xr.yaml @@ -0,0 +1,6 @@ +apiVersion: diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-resource +spec: + coolField: "modified-value" diff --git a/cmd/crossplane/xr/testdata/diff/modified-cross-ns-xr.yaml b/cmd/crossplane/xr/testdata/diff/modified-cross-ns-xr.yaml new file mode 100644 index 00000000..78068e4d --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/modified-cross-ns-xr.yaml @@ -0,0 +1,8 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-cross-ns-resource + namespace: default +spec: + coolField: modified-cross-ns-value + environment: production \ No newline at end of file diff --git a/cmd/crossplane/xr/testdata/diff/modified-env-xr.yaml b/cmd/crossplane/xr/testdata/diff/modified-env-xr.yaml new file mode 100644 index 00000000..3664be56 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/modified-env-xr.yaml @@ -0,0 +1,7 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XEnvResource +metadata: + name: test-env-resource +spec: + compositionUpdatePolicy: Automatic + configKey: "modified-config-value" diff --git a/cmd/crossplane/xr/testdata/diff/modified-legacy-xr-manual-upgrade-to-v2.yaml b/cmd/crossplane/xr/testdata/diff/modified-legacy-xr-manual-upgrade-to-v2.yaml new file mode 100644 index 00000000..92f64376 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/modified-legacy-xr-manual-upgrade-to-v2.yaml @@ -0,0 +1,11 @@ +apiVersion: legacycluster.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-legacy-manual-v1 +spec: + compositionRef: + name: xlegacynopresources.diff.example.org + compositionRevisionRef: + name: xlegacynopresources.diff.example.org-97a39c0 + compositionUpdatePolicy: Manual + coolField: modified-value diff --git a/cmd/crossplane/xr/testdata/diff/modified-legacy-xr-manual-v1-omits-revision-ref.yaml b/cmd/crossplane/xr/testdata/diff/modified-legacy-xr-manual-v1-omits-revision-ref.yaml new file mode 100644 index 00000000..12f18008 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/modified-legacy-xr-manual-v1-omits-revision-ref.yaml @@ -0,0 +1,12 @@ +# The v1-path (spec.compositionRevisionRef) twin of modified-xr-manual-v1-omits-revision-ref.yaml: +# resources/existing-legacy-xr-manual-v1.yaml with coolField modified and compositionRevisionRef left +# out, so it must stay pinned to the -2f488b6 revision (the v1 template). See issue #499. +apiVersion: legacycluster.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-legacy-manual-v1 +spec: + compositionRef: + name: xlegacynopresources.diff.example.org + compositionUpdatePolicy: Manual + coolField: modified-value diff --git a/cmd/crossplane/xr/testdata/diff/modified-legacy-xr.yaml b/cmd/crossplane/xr/testdata/diff/modified-legacy-xr.yaml new file mode 100644 index 00000000..47df1975 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/modified-legacy-xr.yaml @@ -0,0 +1,7 @@ +apiVersion: legacycluster.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-resource +spec: + compositionUpdatePolicy: Automatic + coolField: "modified-value" diff --git a/cmd/crossplane/xr/testdata/diff/modified-manual-pinned-claim.yaml b/cmd/crossplane/xr/testdata/diff/modified-manual-pinned-claim.yaml new file mode 100644 index 00000000..be846bd7 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/modified-manual-pinned-claim.yaml @@ -0,0 +1,12 @@ +# resources/pinned-claim/existing-manual.yaml's claim as its author keeps it, with coolField changed. +# Like the cluster copy it names no revision: the pin is the backing XR's. +apiVersion: pinned.diff.example.org/v1alpha1 +kind: Pinned +metadata: + name: manual-claim + namespace: existing-namespace +spec: + coolField: modified-value + compositionUpdatePolicy: Manual + compositionRef: + name: xpinneds.pinned.diff.example.org diff --git a/cmd/crossplane/xr/testdata/diff/modified-nested-xr.yaml b/cmd/crossplane/xr/testdata/diff/modified-nested-xr.yaml new file mode 100644 index 00000000..842e568b --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/modified-nested-xr.yaml @@ -0,0 +1,8 @@ +# A modified parent XR - value changed from "existing-value" to "modified-value" +apiVersion: ns.nested.example.org/v1alpha1 +kind: XParentResource +metadata: + name: test-parent + namespace: default +spec: + parentField: "modified-value" diff --git a/cmd/crossplane/xr/testdata/diff/modified-xr-automatic.yaml b/cmd/crossplane/xr/testdata/diff/modified-xr-automatic.yaml new file mode 100644 index 00000000..b0e59196 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/modified-xr-automatic.yaml @@ -0,0 +1,11 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-automatic + namespace: default +spec: + crossplane: + compositionRef: + name: xnopresources.diff.example.org + compositionUpdatePolicy: Automatic + coolField: modified-value diff --git a/cmd/crossplane/xr/testdata/diff/modified-xr-manual-upgrade-to-v2.yaml b/cmd/crossplane/xr/testdata/diff/modified-xr-manual-upgrade-to-v2.yaml new file mode 100644 index 00000000..bb285ab2 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/modified-xr-manual-upgrade-to-v2.yaml @@ -0,0 +1,13 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-manual-v1 + namespace: default +spec: + crossplane: + compositionRef: + name: xnopresources.diff.example.org + compositionRevisionRef: + name: xnopresources.diff.example.org-430f3f3 + compositionUpdatePolicy: Manual + coolField: modified-value diff --git a/cmd/crossplane/xr/testdata/diff/modified-xr-manual-v1-omits-revision-ref.yaml b/cmd/crossplane/xr/testdata/diff/modified-xr-manual-v1-omits-revision-ref.yaml new file mode 100644 index 00000000..7216d4f3 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/modified-xr-manual-v1-omits-revision-ref.yaml @@ -0,0 +1,16 @@ +# resources/existing-xr-manual-v1.yaml with coolField modified, and with compositionRevisionRef left +# out — as a hand-written manifest would, since Crossplane is what writes that field. An apply that +# omits a field does not remove it, so the composite stays pinned to the revision the cluster copy +# names (-911318d, the v1 template). Rendering against the latest revision instead would show the v2 +# template's output for a change that cannot cause it. See issue #499. +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-manual-v1 + namespace: default +spec: + crossplane: + compositionRef: + name: xnopresources.diff.example.org + compositionUpdatePolicy: Manual + coolField: modified-value diff --git a/cmd/crossplane/xr/testdata/diff/modified-xr-manual-v1.yaml b/cmd/crossplane/xr/testdata/diff/modified-xr-manual-v1.yaml new file mode 100644 index 00000000..9ed3f2cd --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/modified-xr-manual-v1.yaml @@ -0,0 +1,13 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-manual-v1 + namespace: default +spec: + crossplane: + compositionRef: + name: xnopresources.diff.example.org + compositionRevisionRef: + name: xnopresources.diff.example.org-911318d + compositionUpdatePolicy: Manual + coolField: modified-value diff --git a/cmd/crossplane/xr/testdata/diff/modified-xr-selector-bound.yaml b/cmd/crossplane/xr/testdata/diff/modified-xr-selector-bound.yaml new file mode 100644 index 00000000..e2e6c3a2 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/modified-xr-selector-bound.yaml @@ -0,0 +1,13 @@ +# resources/selector-bound/setup.yaml's composite as its author keeps it, with coolField changed. It +# selects its composition by label and leaves compositionRef to Crossplane, which already set it. +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: selector-bound + namespace: default +spec: + coolField: modified-value + crossplane: + compositionSelector: + matchLabels: + binding: shared diff --git a/cmd/crossplane/xr/testdata/diff/modified-xr-selector-nomatch.yaml b/cmd/crossplane/xr/testdata/diff/modified-xr-selector-nomatch.yaml new file mode 100644 index 00000000..0f45934e --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/modified-xr-selector-nomatch.yaml @@ -0,0 +1,14 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-selector-nomatch + namespace: default +spec: + crossplane: + compositionRef: + name: xnopresources.diff.example.org + compositionUpdatePolicy: Automatic + compositionRevisionSelector: + matchLabels: + channel: nonexistent + coolField: modified-value diff --git a/cmd/crossplane/xr/testdata/diff/modified-xr-selector-stable.yaml b/cmd/crossplane/xr/testdata/diff/modified-xr-selector-stable.yaml new file mode 100644 index 00000000..b4dfec12 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/modified-xr-selector-stable.yaml @@ -0,0 +1,14 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-selector + namespace: default +spec: + crossplane: + compositionRef: + name: xnopresources.diff.example.org + compositionUpdatePolicy: Automatic + compositionRevisionSelector: + matchLabels: + channel: stable + coolField: modified-value diff --git a/cmd/crossplane/xr/testdata/diff/modified-xr-switch-to-automatic.yaml b/cmd/crossplane/xr/testdata/diff/modified-xr-switch-to-automatic.yaml new file mode 100644 index 00000000..8d721546 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/modified-xr-switch-to-automatic.yaml @@ -0,0 +1,11 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-manual-v1 + namespace: default +spec: + crossplane: + compositionRef: + name: xnopresources.diff.example.org + compositionUpdatePolicy: Automatic + coolField: modified-value diff --git a/cmd/crossplane/xr/testdata/diff/modified-xr-with-external-dep.yaml b/cmd/crossplane/xr/testdata/diff/modified-xr-with-external-dep.yaml new file mode 100644 index 00000000..eba2a6e3 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/modified-xr-with-external-dep.yaml @@ -0,0 +1,8 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-resource + namespace: default +spec: + coolField: modified-with-external-dep + environment: testing diff --git a/cmd/crossplane/xr/testdata/diff/new-claim-with-claimref-composition.yaml b/cmd/crossplane/xr/testdata/diff/new-claim-with-claimref-composition.yaml new file mode 100644 index 00000000..bd63e462 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/new-claim-with-claimref-composition.yaml @@ -0,0 +1,10 @@ +apiVersion: diff.example.org/v1alpha1 +kind: NopClaim +metadata: + name: test-claim + namespace: existing-namespace +spec: + compositeDeletePolicy: Background + compositionRef: + name: claim-composition-with-claimref + coolField: new-value diff --git a/cmd/crossplane/xr/testdata/diff/new-claim.yaml b/cmd/crossplane/xr/testdata/diff/new-claim.yaml new file mode 100644 index 00000000..6d153fee --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/new-claim.yaml @@ -0,0 +1,11 @@ +apiVersion: diff.example.org/v1alpha1 +kind: NopClaim +metadata: + name: test-claim + namespace: existing-namespace +spec: + compositeDeletePolicy: Background + compositionRef: + name: claim-composition + coolField: new-value + diff --git a/cmd/crossplane/xr/testdata/diff/new-cycle-xr.yaml b/cmd/crossplane/xr/testdata/diff/new-cycle-xr.yaml new file mode 100644 index 00000000..45f9b375 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/new-cycle-xr.yaml @@ -0,0 +1,8 @@ +# A new XR whose composition starts the XCycleA -> XCycleB -> XCycleA cycle. +apiVersion: ns.cycle.example.org/v1alpha1 +kind: XCycleA +metadata: + name: test-cycle + namespace: default +spec: + cycleField: "cycle-value" diff --git a/cmd/crossplane/xr/testdata/diff/new-manual-default-xr.yaml b/cmd/crossplane/xr/testdata/diff/new-manual-default-xr.yaml new file mode 100644 index 00000000..89fbe026 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/new-manual-default-xr.yaml @@ -0,0 +1,15 @@ +# A new composite pinned to revision 1, the older of its composition's two revisions, that leaves its +# update policy to its XRD's default (Manual). Under Manual, Crossplane renders a composite against the +# revision it names, so this one renders revision 1; under Automatic it would render the latest. +apiVersion: manualdefault.diff.example.org/v1alpha1 +kind: XManualDefault +metadata: + name: new-manual-default + namespace: default +spec: + coolField: new-value + crossplane: + compositionRef: + name: xmanualdefaults.manualdefault.diff.example.org + compositionRevisionRef: + name: xmanualdefaults.manualdefault.diff.example.org-373e832 diff --git a/cmd/crossplane/xr/testdata/diff/new-nested-child-xr.yaml b/cmd/crossplane/xr/testdata/diff/new-nested-child-xr.yaml new file mode 100644 index 00000000..37a82217 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/new-nested-child-xr.yaml @@ -0,0 +1,9 @@ +# The child XR that new-nested-xr.yaml's composition renders (named -child). Passed alongside +# that parent it is an input error: the parent's composition writes it, so this would be a second writer. +apiVersion: ns.nested.example.org/v1alpha1 +kind: XChildResource +metadata: + name: test-parent-child + namespace: default +spec: + childField: "parent-value" diff --git a/cmd/crossplane/xr/testdata/diff/new-nested-xr.yaml b/cmd/crossplane/xr/testdata/diff/new-nested-xr.yaml new file mode 100644 index 00000000..07ea96d7 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/new-nested-xr.yaml @@ -0,0 +1,8 @@ +# A new parent XR that will create a child XR, which will then create managed resources +apiVersion: ns.nested.example.org/v1alpha1 +kind: XParentResource +metadata: + name: test-parent + namespace: default +spec: + parentField: "parent-value" diff --git a/cmd/crossplane/xr/testdata/diff/new-parent-claim-omitting-defaulted-fields.yaml b/cmd/crossplane/xr/testdata/diff/new-parent-claim-omitting-defaulted-fields.yaml new file mode 100644 index 00000000..143783ac --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/new-parent-claim-omitting-defaulted-fields.yaml @@ -0,0 +1,11 @@ +# A new claim that omits every field a CRD defaults: spec.tier (defaulted by both the claim's and the +# XR's CRD), spec.compositeDeletePolicy (claim CRD only) and spec.compositionUpdatePolicy (XR CRD only). +apiVersion: claimnested.diff.example.org/v1alpha1 +kind: ParentNopClaim +metadata: + name: new-parent-claim + namespace: default +spec: + parentField: new-value + compositionRef: + name: parent-nop-claim-defaults-composition diff --git a/cmd/crossplane/xr/testdata/diff/new-rendered-defaults-xrs.yaml b/cmd/crossplane/xr/testdata/diff/new-rendered-defaults-xrs.yaml new file mode 100644 index 00000000..b11d2e92 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/new-rendered-defaults-xrs.yaml @@ -0,0 +1,16 @@ +# New composites that set only size, leaving region and compositionUpdatePolicy to their CRDs' +# defaults; see resources/rendered-defaults/definitions.yaml. +apiVersion: rendered-defaults.diff.example.org/v1alpha1 +kind: XRegionalDatabase +metadata: + name: modern-database + namespace: default +spec: + size: large +--- +apiVersion: rendered-defaults.diff.example.org/v1alpha1 +kind: XLegacyRegionalDatabase +metadata: + name: legacy-database +spec: + size: small diff --git a/cmd/crossplane/xr/testdata/diff/new-xr-manual-no-ref.yaml b/cmd/crossplane/xr/testdata/diff/new-xr-manual-no-ref.yaml new file mode 100644 index 00000000..21557827 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/new-xr-manual-no-ref.yaml @@ -0,0 +1,11 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-manual-no-ref + namespace: default +spec: + crossplane: + compositionRef: + name: xnopresources.diff.example.org + compositionUpdatePolicy: Manual + coolField: new-value diff --git a/cmd/crossplane/xr/testdata/diff/new-xr-missing-namespace-second.yaml b/cmd/crossplane/xr/testdata/diff/new-xr-missing-namespace-second.yaml new file mode 100644 index 00000000..e5abd110 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/new-xr-missing-namespace-second.yaml @@ -0,0 +1,10 @@ +# A second XR targeting the same namespace as new-xr-missing-namespace.yaml, which does not exist in +# the cluster. Diffed together, the two give the unverified-addition warning more than one resource +# to count. +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: second-resource + namespace: nonexistent-namespace +spec: + coolField: "second-value" diff --git a/cmd/crossplane/xr/testdata/diff/new-xr-missing-namespace.yaml b/cmd/crossplane/xr/testdata/diff/new-xr-missing-namespace.yaml new file mode 100644 index 00000000..03399959 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/new-xr-missing-namespace.yaml @@ -0,0 +1,13 @@ +# An XR targeting a namespace that does not exist in the cluster. +# +# This is a supported workflow, not a mistake: a Namespace and the resources inside it are routinely +# applied together (one `kubectl apply -f ./manifests/`), so a user previewing that apply legitimately +# has resources whose namespace is not there yet. The dry-run create cannot be performed, so the +# resources degrade to rendered output and say so — they must NOT fail the run. +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-resource + namespace: nonexistent-namespace +spec: + coolField: "new-value" diff --git a/cmd/crossplane/xr/testdata/diff/new-xr-second.yaml b/cmd/crossplane/xr/testdata/diff/new-xr-second.yaml new file mode 100644 index 00000000..b31482df --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/new-xr-second.yaml @@ -0,0 +1,7 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: second-resource + namespace: default +spec: + coolField: "second-value" diff --git a/cmd/crossplane/xr/testdata/diff/new-xr-with-creds.yaml b/cmd/crossplane/xr/testdata/diff/new-xr-with-creds.yaml new file mode 100644 index 00000000..07c0fa53 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/new-xr-with-creds.yaml @@ -0,0 +1,10 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-resource-with-creds + namespace: default +spec: + coolField: test-value-creds + crossplane: + compositionRef: + name: xnopresources-with-creds.diff.example.org diff --git a/cmd/crossplane/xr/testdata/diff/new-xr-with-cross-ns-external-dep.yaml b/cmd/crossplane/xr/testdata/diff/new-xr-with-cross-ns-external-dep.yaml new file mode 100644 index 00000000..d1997d5c --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/new-xr-with-cross-ns-external-dep.yaml @@ -0,0 +1,11 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-resource + namespace: default +spec: + coolField: "new-value" + # Matches the ConfigMap in "other-namespace" (labelled environment: crossns). + # The go-templating matchLabels ExtraResources selector omits the namespace, + # so resolving it requires an all-namespaces lookup (issue #376). + environment: crossns diff --git a/cmd/crossplane/xr/testdata/diff/resources/admission-policy-canary.yaml b/cmd/crossplane/xr/testdata/diff/resources/admission-policy-canary.yaml new file mode 100644 index 00000000..2130c04e --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/admission-policy-canary.yaml @@ -0,0 +1,15 @@ +# A resource that rejecting-admission-policy.yaml must refuse. +# +# The harness dry-run creates this (never persisting it) until the apiserver rejects it, which is how +# a test learns that the policy is actually enforcing. Admission policies reach the apiserver's +# admission plugin through an informer, so for a short window after creation the cluster still +# accepts what the policy will later refuse; a diff issued inside that window would observe no +# rejection at all. +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: admission-policy-canary + namespace: default +spec: + forProvider: + configData: new-value diff --git a/cmd/crossplane/xr/testdata/diff/resources/api-version-composition-revision-v1.yaml b/cmd/crossplane/xr/testdata/diff/resources/api-version-composition-revision-v1.yaml new file mode 100644 index 00000000..887eb013 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/api-version-composition-revision-v1.yaml @@ -0,0 +1,34 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: CompositionRevision +metadata: + labels: + crossplane.io/composition-name: xapimigrateresources.example.org +spec: + revision: 1 + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-api-versioned-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: diff.example.org/v1beta1 + kind: XApiMigrateResource + metadata: + name: {{ .observed.composite.resource.metadata.name }}-api-resource + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: api-migrate-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/api-version-composition.yaml b/cmd/crossplane/xr/testdata/diff/resources/api-version-composition.yaml new file mode 100644 index 00000000..defbc52a --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/api-version-composition.yaml @@ -0,0 +1,34 @@ +# The in-cluster xapimigrateresources.example.org, which renders the resource at v1beta2. The harness +# creates its latest revision from it; api-version-composition-revision-v1.yaml is the earlier one. +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xapimigrateresources.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-api-versioned-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: diff.example.org/v1beta2 + kind: XApiMigrateResource + metadata: + name: {{ .observed.composite.resource.metadata.name }}-api-resource + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: api-migrate-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/claim-composition-with-claimref.yaml b/cmd/crossplane/xr/testdata/diff/resources/claim-composition-with-claimref.yaml new file mode 100644 index 00000000..c468c446 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/claim-composition-with-claimref.yaml @@ -0,0 +1,35 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: claim-composition-with-claimref +spec: + compositeTypeRef: + apiVersion: diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + name: {{ .observed.composite.resource.metadata.name }} + spec: + forProvider: + {{- if .observed.composite.resource.spec.claimRef }} + configData: {{ .observed.composite.resource.spec.claimRef.namespace }}/{{ .observed.composite.resource.spec.claimRef.name }} + {{- else }} + configData: {{ .observed.composite.resource.spec.coolField }} + {{- end }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/claim-composition.yaml b/cmd/crossplane/xr/testdata/diff/resources/claim-composition.yaml new file mode 100644 index 00000000..f07fb97e --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/claim-composition.yaml @@ -0,0 +1,31 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: claim-composition +spec: + compositeTypeRef: + apiVersion: diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + name: {{ .observed.composite.resource.metadata.name }} + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/claim-nested/child-composition.yaml b/cmd/crossplane/xr/testdata/diff/resources/claim-nested/child-composition.yaml new file mode 100644 index 00000000..824162c4 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/claim-nested/child-composition.yaml @@ -0,0 +1,34 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: child-nop-claim-composition +spec: + compositeTypeRef: + apiVersion: claimnested.diff.example.org/v1alpha1 + kind: XNopChild + mode: Pipeline + pipeline: + - step: create-nop-resource + functionRef: + name: function-go-templating + input: + apiVersion: gotemplating.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: nop.crossplane.io/v1alpha1 + kind: ClusterNopResource + metadata: + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + child-field: {{ .observed.composite.resource.spec.childField }} + spec: + forProvider: + conditionAfter: + - conditionType: Ready + conditionStatus: "True" + time: 0s + - step: auto-ready + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/claim-nested/child-definition.yaml b/cmd/crossplane/xr/testdata/diff/resources/claim-nested/child-definition.yaml new file mode 100644 index 00000000..cc63922d --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/claim-nested/child-definition.yaml @@ -0,0 +1,29 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: CompositeResourceDefinition +metadata: + name: xnopchildren.claimnested.diff.example.org +spec: + group: claimnested.diff.example.org + names: + kind: XNopChild + plural: xnopchildren + versions: + - name: v1alpha1 + served: true + referenceable: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + childField: + type: string + required: + - childField + status: + type: object + properties: + message: + type: string diff --git a/cmd/crossplane/xr/testdata/diff/resources/claim-nested/existing-child-xr.yaml b/cmd/crossplane/xr/testdata/diff/resources/claim-nested/existing-child-xr.yaml new file mode 100644 index 00000000..4b39acb4 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/claim-nested/existing-child-xr.yaml @@ -0,0 +1,11 @@ +apiVersion: claimnested.diff.example.org/v1alpha1 +kind: XNopChild +metadata: + name: existing-parent-claim-82crv-child + generateName: existing-parent-claim-82crv- + annotations: + crossplane.io/composition-resource-name: child-xr +spec: + childField: existing-parent-value + compositionRef: + name: child-nop-claim-composition diff --git a/cmd/crossplane/xr/testdata/diff/resources/claim-nested/existing-claim.yaml b/cmd/crossplane/xr/testdata/diff/resources/claim-nested/existing-claim.yaml new file mode 100644 index 00000000..ba4e28ca --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/claim-nested/existing-claim.yaml @@ -0,0 +1,18 @@ +apiVersion: claimnested.diff.example.org/v1alpha1 +kind: ParentNopClaim +metadata: + name: existing-parent-claim + namespace: default +spec: + parentField: existing-parent-value + compositeDeletePolicy: Background + compositionRef: + name: parent-nop-claim-composition + # Copied back from the backing XR, as the claim syncer does under the XR's Automatic update policy. + compositionRevisionRef: + name: parent-nop-claim-composition-cfac93e + # Bound to its backing XR, existing-parent-xr.yaml, as Crossplane's claim syncer binds it. + resourceRef: + apiVersion: claimnested.diff.example.org/v1alpha1 + kind: XNopParent + name: existing-parent-claim-82crv diff --git a/cmd/crossplane/xr/testdata/diff/resources/claim-nested/existing-managed-resource.yaml b/cmd/crossplane/xr/testdata/diff/resources/claim-nested/existing-managed-resource.yaml new file mode 100644 index 00000000..21a97b6d --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/claim-nested/existing-managed-resource.yaml @@ -0,0 +1,14 @@ +apiVersion: nop.crossplane.io/v1alpha1 +kind: ClusterNopResource +metadata: + name: existing-parent-claim-82crv-nop + generateName: existing-parent-claim-82crv- + annotations: + child-field: existing-parent-value + crossplane.io/composition-resource-name: nop-resource +spec: + forProvider: + conditionAfter: + - conditionType: Ready + conditionStatus: "True" + time: 0s diff --git a/cmd/crossplane/xr/testdata/diff/resources/claim-nested/existing-parent-xr.yaml b/cmd/crossplane/xr/testdata/diff/resources/claim-nested/existing-parent-xr.yaml new file mode 100644 index 00000000..20399443 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/claim-nested/existing-parent-xr.yaml @@ -0,0 +1,24 @@ +# The backing XR Crossplane's claim syncer created for existing-claim.yaml, tracking its composition's +# current revision. A case also diffs this file as an input, so it spells out what the harness would +# otherwise derive. +apiVersion: claimnested.diff.example.org/v1alpha1 +kind: XNopParent +metadata: + name: existing-parent-claim-82crv + generateName: existing-parent-claim- + labels: + crossplane.io/claim-name: existing-parent-claim + crossplane.io/claim-namespace: default + crossplane.io/composite: existing-parent-claim-82crv +spec: + claimRef: + apiVersion: claimnested.diff.example.org/v1alpha1 + kind: ParentNopClaim + name: existing-parent-claim + namespace: default + parentField: existing-parent-value + compositionUpdatePolicy: Automatic + compositionRef: + name: parent-nop-claim-composition + compositionRevisionRef: + name: parent-nop-claim-composition-cfac93e diff --git a/cmd/crossplane/xr/testdata/diff/resources/claim-nested/parent-composition.yaml b/cmd/crossplane/xr/testdata/diff/resources/claim-nested/parent-composition.yaml new file mode 100644 index 00000000..f2475f2d --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/claim-nested/parent-composition.yaml @@ -0,0 +1,31 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: parent-nop-claim-composition +spec: + compositeTypeRef: + apiVersion: claimnested.diff.example.org/v1alpha1 + kind: XNopParent + mode: Pipeline + pipeline: + - step: create-child-xr + functionRef: + name: function-go-templating + input: + apiVersion: gotemplating.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: claimnested.diff.example.org/v1alpha1 + kind: XNopChild + metadata: + generateName: {{ .observed.composite.resource.metadata.name }}- + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: child-xr + spec: + childField: {{ .observed.composite.resource.spec.parentField }} + - step: auto-ready + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/claim-nested/parent-defaults-composition.yaml b/cmd/crossplane/xr/testdata/diff/resources/claim-nested/parent-defaults-composition.yaml new file mode 100644 index 00000000..d3e331ca --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/claim-nested/parent-defaults-composition.yaml @@ -0,0 +1,35 @@ +# Renders, into a composed resource, the two fields of its XR that only the XR's CRD defaults: +# spec.tier (a user default, which the claim's CRD shares) and spec.compositionUpdatePolicy (machinery, +# which the claim's CRD does not default). A claim whose manifest omits both therefore shows, in this +# resource, whether the XR that render consumed was defaulted with the XR's CRD. +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: parent-nop-claim-defaults-composition +spec: + compositeTypeRef: + apiVersion: claimnested.diff.example.org/v1alpha1 + kind: XNopParent + mode: Pipeline + pipeline: + - step: render-defaults + functionRef: + name: function-go-templating + input: + apiVersion: gotemplating.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }}-defaults + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: defaults + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.tier }}/{{ .observed.composite.resource.spec.compositionUpdatePolicy }} + - step: auto-ready + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/claim-nested/parent-definition.yaml b/cmd/crossplane/xr/testdata/diff/resources/claim-nested/parent-definition.yaml new file mode 100644 index 00000000..d3ef4162 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/claim-nested/parent-definition.yaml @@ -0,0 +1,35 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: CompositeResourceDefinition +metadata: + name: xnopparents.claimnested.diff.example.org +spec: + group: claimnested.diff.example.org + names: + kind: XNopParent + plural: xnopparents + claimNames: + kind: ParentNopClaim + plural: parentnopclaims + versions: + - name: v1alpha1 + served: true + referenceable: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + parentField: + type: string + tier: + type: string + default: standard + required: + - parentField + status: + type: object + properties: + message: + type: string diff --git a/cmd/crossplane/xr/testdata/diff/resources/claim-xrd.yaml b/cmd/crossplane/xr/testdata/diff/resources/claim-xrd.yaml new file mode 100644 index 00000000..af86a8af --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/claim-xrd.yaml @@ -0,0 +1,30 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: CompositeResourceDefinition +metadata: + name: xnopresources.diff.example.org +spec: + group: diff.example.org + names: + kind: XNopResource + plural: xnopresources + claimNames: + kind: NopClaim + plural: nopclaims + versions: + - name: v1alpha1 + served: true + referenceable: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + coolField: + type: string + tier: + type: string + default: standard + writeConnectionSecretsToNamespace: + type: string diff --git a/cmd/crossplane/xr/testdata/diff/resources/composition-no-namespace-patch.yaml b/cmd/crossplane/xr/testdata/diff/resources/composition-no-namespace-patch.yaml new file mode 100644 index 00000000..a4cb8739 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/composition-no-namespace-patch.yaml @@ -0,0 +1,32 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnopresources.diff.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + # NOTE: No namespace template - testing automatic namespace propagation + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready \ No newline at end of file diff --git a/cmd/crossplane/xr/testdata/diff/resources/composition-revision-preview.yaml b/cmd/crossplane/xr/testdata/diff/resources/composition-revision-preview.yaml new file mode 100644 index 00000000..18d67a21 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/composition-revision-preview.yaml @@ -0,0 +1,38 @@ +# A later revision of xnopresources.diff.example.org, from when the composition carried the +# channel=preview label. The harness names and labels it from the hash of the composition it was made +# from, as Crossplane does. +apiVersion: apiextensions.crossplane.io/v1 +kind: CompositionRevision +metadata: + labels: + crossplane.io/composition-name: xnopresources.diff.example.org + channel: preview +spec: + revision: 2 + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: preview-{{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/composition-revision-stable.yaml b/cmd/crossplane/xr/testdata/diff/resources/composition-revision-stable.yaml new file mode 100644 index 00000000..5dff855b --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/composition-revision-stable.yaml @@ -0,0 +1,38 @@ +# An earlier revision of xnopresources.diff.example.org, from when the composition carried the +# channel=stable label. The harness names and labels it from the hash of the composition it was made +# from, as Crossplane does. The XR in existing-xr-selector-stable.yaml tracks it. +apiVersion: apiextensions.crossplane.io/v1 +kind: CompositionRevision +metadata: + labels: + crossplane.io/composition-name: xnopresources.diff.example.org + channel: stable +spec: + revision: 1 + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: stable-{{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/composition-revision-v1.yaml b/cmd/crossplane/xr/testdata/diff/resources/composition-revision-v1.yaml new file mode 100644 index 00000000..109940aa --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/composition-revision-v1.yaml @@ -0,0 +1,34 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: CompositionRevision +metadata: + labels: + crossplane.io/composition-name: xnopresources.diff.example.org +spec: + revision: 1 + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: v1-{{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/composition-v2.yaml b/cmd/crossplane/xr/testdata/diff/resources/composition-v2.yaml new file mode 100644 index 00000000..17c012c6 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/composition-v2.yaml @@ -0,0 +1,32 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnopresources.diff.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: v2-{{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/composition-with-builtin-defaults.yaml b/cmd/crossplane/xr/testdata/diff/resources/composition-with-builtin-defaults.yaml new file mode 100644 index 00000000..c032a4cd --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/composition-with-builtin-defaults.yaml @@ -0,0 +1,61 @@ +# Renders a built-in Kubernetes resource whose apiserver defaults are NOT applied locally. +# +# This fixture exists to exercise the apiserver round-trip for additions (issue #334), and the +# choice of a built-in type is load-bearing rather than incidental. +# +# The lenient Defaulter predicts CRD-derived `default:` values locally for an addition that gets no +# apiserver result, so a CRD-backed composed resource can show its defaults in a `+++` diff with no +# apiserver involvement. Deployment has no CRD, so IsCRDRequired is false and the Defaulter passes it +# through unchanged — which makes it the smallest resource whose defaulted fields can only come from +# the apiserver. +# +# The Deployment deliberately sets only the fields a caller must set (selector, template, one +# container). Everything asserted in the corresponding test — spec.strategy.type, +# spec.template.spec.restartPolicy, spec.template.spec.dnsPolicy — is left for the apiserver to +# populate. Deployment is preferred over Service here because Service creation allocates a +# clusterIP even under dry-run, which would put a non-deterministic value in the diff; Deployment's +# defaults are pure schema defaulting with no allocator involved. +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnopresources.diff.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: apps/v1 + kind: Deployment + metadata: + name: {{ .observed.composite.resource.metadata.name }}-deploy + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: deployment + spec: + selector: + matchLabels: + app: {{ .observed.composite.resource.metadata.name }} + template: + metadata: + labels: + app: {{ .observed.composite.resource.metadata.name }} + spec: + containers: + - name: main + image: nginx:1.27 + env: + - name: CONFIG_DATA + value: {{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/composition-with-configmap.yaml b/cmd/crossplane/xr/testdata/diff/resources/composition-with-configmap.yaml new file mode 100644 index 00000000..d0ca422f --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/composition-with-configmap.yaml @@ -0,0 +1,48 @@ +# Renders a built-in Kubernetes resource (ConfigMap) alongside a composed XR. +# +# ConfigMap has no CRD, so every code path that determines a resource's scope +# or validates it against a schema has to cope with its absence. Scope comes +# from the discovery API; schema validation comes from the embedded Kubernetes +# scheme in crossplane/cli's validate package. +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnopresources.diff.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + --- + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ .observed.composite.resource.metadata.name }}-config + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: config-map + data: + configData: {{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/composition-with-crd-defaulted-downstream.yaml b/cmd/crossplane/xr/testdata/diff/resources/composition-with-crd-defaulted-downstream.yaml new file mode 100644 index 00000000..48f1a0af --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/composition-with-crd-defaulted-downstream.yaml @@ -0,0 +1,33 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnopresources-defaulted-downstream.diff.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + # spec.forProvider.size is deliberately not rendered: its CRD defaults it. + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDefaultedDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }}-defaulted + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: defaulted-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/composition-with-defaults.yaml b/cmd/crossplane/xr/testdata/diff/resources/composition-with-defaults.yaml new file mode 100644 index 00000000..0eedc809 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/composition-with-defaults.yaml @@ -0,0 +1,16 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xdefaultresources.ns.diff.example.org + labels: + provider: nop + service: nop +spec: + mode: Pipeline + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XTestDefaultResource + pipeline: + - step: auto-ready + functionRef: + name: function-auto-ready \ No newline at end of file diff --git a/cmd/crossplane/xr/testdata/diff/resources/composition-with-missing-function.yaml b/cmd/crossplane/xr/testdata/diff/resources/composition-with-missing-function.yaml new file mode 100644 index 00000000..cb17b40f --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/composition-with-missing-function.yaml @@ -0,0 +1,17 @@ +# Names a function, function-not-installed, that no Function on the cluster provides. +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnopresources.diff.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-not-installed + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/composition-with-non-storage-version-downstream.yaml b/cmd/crossplane/xr/testdata/diff/resources/composition-with-non-storage-version-downstream.yaml new file mode 100644 index 00000000..c0edae36 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/composition-with-non-storage-version-downstream.yaml @@ -0,0 +1,33 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnopresources-non-storage-version-downstream.diff.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + # Rendered at v1alpha1, which its CRD serves but does not store. + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XVersionedDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }}-versioned + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: versioned-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/composition-with-status.yaml b/cmd/crossplane/xr/testdata/diff/resources/composition-with-status.yaml new file mode 100644 index 00000000..be9332b7 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/composition-with-status.yaml @@ -0,0 +1,41 @@ +# A composition whose pipeline authors a `status` on the composed resource it renders. +# +# Composition pipelines are explicitly allowed to write their own status, so a rendered status is +# user-authored output rather than a stale read of the cluster. The composed kind here +# (XStatusResource) declares the status subresource, so the apiserver strips status from the dry-run +# create and returns an object with none — which means the status below can only reach the `+++` +# diff if the calculator re-attaches it (crossplane-diff#334). +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnopresources.diff.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XStatusResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: status-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + status: + phase: authored-by-composition + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/composition-with-unserved-k8s-group.yaml b/cmd/crossplane/xr/testdata/diff/resources/composition-with-unserved-k8s-group.yaml new file mode 100644 index 00000000..17ecbf25 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/composition-with-unserved-k8s-group.yaml @@ -0,0 +1,42 @@ +# Renders a composed resource in a *.k8s.io group that the cluster does not serve. +# +# This shape slips past every earlier guard. IsCRDRequired (schema_client.go) assumes any group +# ending in .k8s.io other than apiextensions.k8s.io is built-in and returns false WITHOUT consulting +# discovery, so no CRD is demanded. FetchCurrentObject then propagates only non-NotFound errors, and +# a discovery 404 for an unserved group/version is NotFound-shaped, so it is swallowed as "this +# resource is new". The resource therefore reaches the addition path with a GVK that cannot be +# resolved at all. +# +# It stands in for a typo in a group name, an apiVersion skew, a stripped-down cluster, or an +# aggregated API that was never deployed. +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnopresources.diff.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: totallynotserved.k8s.io/v1 + kind: Widget + metadata: + name: {{ .observed.composite.resource.metadata.name }}-widget + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: widget + spec: + size: large + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/conditional-gating-xr.yaml b/cmd/crossplane/xr/testdata/diff/resources/conditional-gating-xr.yaml new file mode 100644 index 00000000..b871f6bd --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/conditional-gating-xr.yaml @@ -0,0 +1,7 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: conditional-gating-test + namespace: default +spec: + coolField: "test-value" diff --git a/cmd/crossplane/xr/testdata/diff/resources/credentials/cli-credentials.yaml b/cmd/crossplane/xr/testdata/diff/resources/credentials/cli-credentials.yaml new file mode 100644 index 00000000..73d57dbd --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/credentials/cli-credentials.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: function-credentials + namespace: crossplane-system +type: Opaque +data: + # base64 encoded "cli-override-key" + credentials: Y2xpLW92ZXJyaWRlLWtleQ== diff --git a/cmd/crossplane/xr/testdata/diff/resources/credentials/composition-with-creds.yaml b/cmd/crossplane/xr/testdata/diff/resources/credentials/composition-with-creds.yaml new file mode 100644 index 00000000..2820c988 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/credentials/composition-with-creds.yaml @@ -0,0 +1,39 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnopresources-with-creds.diff.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + # Credentials reference - this tests auto-fetch from cluster + credentials: + - name: test-creds + source: Secret + secretRef: + namespace: crossplane-system + name: function-credentials + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/credentials/function-credentials-secret.yaml b/cmd/crossplane/xr/testdata/diff/resources/credentials/function-credentials-secret.yaml new file mode 100644 index 00000000..7c070b9f --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/credentials/function-credentials-secret.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: crossplane-system +--- +apiVersion: v1 +kind: Secret +metadata: + name: function-credentials + namespace: crossplane-system +type: Opaque +data: + # base64 encoded "test-api-key-12345" + credentials: dGVzdC1hcGkta2V5LTEyMzQ1 diff --git a/cmd/crossplane/xr/testdata/diff/resources/cross-namespace-configmap.yaml b/cmd/crossplane/xr/testdata/diff/resources/cross-namespace-configmap.yaml new file mode 100644 index 00000000..4fa3771b --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/cross-namespace-configmap.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: cross-namespace-config + namespace: kube-system + labels: + app: cross-namespace-app + environment: production +data: + config-data: "cross-namespace-data" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: another-cross-namespace-config + namespace: kube-public + labels: + app: another-cross-namespace-app + environment: staging +data: + config-data: "another-cross-namespace-data" \ No newline at end of file diff --git a/cmd/crossplane/xr/testdata/diff/resources/cross-namespace-fn-composition.yaml b/cmd/crossplane/xr/testdata/diff/resources/cross-namespace-fn-composition.yaml new file mode 100644 index 00000000..a1bcb0e0 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/cross-namespace-fn-composition.yaml @@ -0,0 +1,72 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnopresources.cross-namespace.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: fetch-cross-namespace-resources + functionRef: + name: function-extra-resources + input: + apiVersion: extra-resources.fn.crossplane.io/v1beta1 + kind: Input + spec: + extraResources: + # Cross-namespace ConfigMap by label selector + - apiVersion: v1 + kind: ConfigMap + into: crossNamespaceConfig + type: Selector + namespace: kube-system + selector: + matchLabels: + - key: app + type: Value + value: cross-namespace-app + - key: environment + type: Value + value: production + # Another cross-namespace ConfigMap by name reference + - apiVersion: v1 + kind: ConfigMap + into: namedCrossNamespaceConfig + type: Reference + namespace: kube-public + ref: + name: another-cross-namespace-config + # Cluster-scoped resource for comparison + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + into: namedClusterRole + type: Reference + ref: + name: external-named-clusterrole + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + {{- $crossNsConfig := index .context "apiextensions.crossplane.io/extra-resources" "crossNamespaceConfig" 0 -}} + {{- $namedCrossNsConfig := index .context "apiextensions.crossplane.io/extra-resources" "namedCrossNamespaceConfig" 0 -}} + {{- $clusterRole := index .context "apiextensions.crossplane.io/extra-resources" "namedClusterRole" 0 -}} + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: cross-ns-resource + spec: + forProvider: + configData: {{ index $crossNsConfig "data" "config-data" }}-{{ index $namedCrossNsConfig "data" "config-data" }}-{{ $clusterRole.metadata.name }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready \ No newline at end of file diff --git a/cmd/crossplane/xr/testdata/diff/resources/default-composition.yaml b/cmd/crossplane/xr/testdata/diff/resources/default-composition.yaml new file mode 100644 index 00000000..b7ccde46 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/default-composition.yaml @@ -0,0 +1,36 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: default-composition + labels: + provider: default + environment: any +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-default-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: default-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + resourceTier: "default" + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/env-composition.yaml b/cmd/crossplane/xr/testdata/diff/resources/env-composition.yaml new file mode 100644 index 00000000..6583c4de --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/env-composition.yaml @@ -0,0 +1,46 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xenvresources.diff.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XEnvResource + mode: Pipeline + pipeline: + - step: environmentConfigs + functionRef: + name: function-environment-configs + input: + apiVersion: environmentconfigs.fn.crossplane.io/v1beta1 + kind: Input + spec: + environmentConfigs: + - type: Reference + ref: + name: test-env-config + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + {{ $envConfig := index .context "apiextensions.crossplane.io/environment" }} + apiVersion: nop.example.org/v1alpha1 + kind: XDownstreamEnvResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: env-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.configKey }} + region: {{ $envConfig.region }} + environment: {{ $envConfig.environment }} + serviceLevel: {{ $envConfig.serviceLevel }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready \ No newline at end of file diff --git a/cmd/crossplane/xr/testdata/diff/resources/env-xrd.yaml b/cmd/crossplane/xr/testdata/diff/resources/env-xrd.yaml new file mode 100644 index 00000000..9d137cac --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/env-xrd.yaml @@ -0,0 +1,22 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: CompositeResourceDefinition +metadata: + name: xenvresources.ns.diff.example.org +spec: + group: ns.diff.example.org + names: + kind: XEnvResource + plural: xenvresources + versions: + - name: v1alpha1 + served: true + referenceable: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + configKey: + type: string diff --git a/cmd/crossplane/xr/testdata/diff/resources/environment-config-v1beta1.yaml b/cmd/crossplane/xr/testdata/diff/resources/environment-config-v1beta1.yaml new file mode 100644 index 00000000..ddb284c0 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/environment-config-v1beta1.yaml @@ -0,0 +1,8 @@ +apiVersion: apiextensions.crossplane.io/v1beta1 +kind: EnvironmentConfig +metadata: + name: test-env-config +data: + region: us-west-2 + environment: staging + serviceLevel: premium diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-api-version-downstream-v1beta1.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-api-version-downstream-v1beta1.yaml new file mode 100644 index 00000000..7caf33a2 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-api-version-downstream-v1beta1.yaml @@ -0,0 +1,10 @@ +apiVersion: diff.example.org/v1beta1 +kind: XApiMigrateResource +metadata: + name: test-api-version-xr-api-resource + namespace: default + annotations: + crossplane.io/composition-resource-name: api-migrate-resource +spec: + forProvider: + configData: test-value diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-api-version-xr-rev1.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-api-version-xr-rev1.yaml new file mode 100644 index 00000000..558b7168 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-api-version-xr-rev1.yaml @@ -0,0 +1,13 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-api-version-xr + namespace: default +spec: + crossplane: + compositionRef: + name: xapimigrateresources.example.org + compositionRevisionRef: + name: xapimigrateresources.example.org-2067855 + compositionUpdatePolicy: Manual + coolField: test-value diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-claim-defaulted-fields-other-manager.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-claim-defaulted-fields-other-manager.yaml new file mode 100644 index 00000000..ab5d01ab --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-claim-defaulted-fields-other-manager.yaml @@ -0,0 +1,10 @@ +# A different field manager sets two fields the claim's CRD defaults to non-default values on the +# claim, whose manifest (modified-claim-omitting-defaulted-fields.yaml) omits both. +apiVersion: diff.example.org/v1alpha1 +kind: NopClaim +metadata: + name: test-claim + namespace: existing-namespace +spec: + compositeDeletePolicy: Foreground + compositionUpdatePolicy: Manual diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-claim-downstream-resource.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-claim-downstream-resource.yaml new file mode 100644 index 00000000..bdea322d --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-claim-downstream-resource.yaml @@ -0,0 +1,9 @@ +apiVersion: nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + annotations: + crossplane.io/composition-resource-name: nop-resource + name: test-claim-82crv +spec: + forProvider: + configData: existing-value diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-claim-xr.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-claim-xr.yaml new file mode 100644 index 00000000..e5ed847a --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-claim-xr.yaml @@ -0,0 +1,21 @@ +# The backing XR Crossplane's claim syncer created for existing-claim.yaml, and whose name it wrote to +# that claim's spec.resourceRef. Its composed resource is existing-claim-downstream-resource.yaml. +apiVersion: diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-claim-82crv + generateName: test-claim- +spec: + coolField: existing-value + compositionUpdatePolicy: Automatic + compositionRef: + name: claim-composition + claimRef: + apiVersion: diff.example.org/v1alpha1 + kind: NopClaim + name: test-claim + namespace: existing-namespace + resourceRefs: + - apiVersion: nop.example.org/v1alpha1 + kind: XDownstreamResource + name: test-claim-82crv diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-claim.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-claim.yaml new file mode 100644 index 00000000..0de70f11 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-claim.yaml @@ -0,0 +1,18 @@ +apiVersion: diff.example.org/v1alpha1 +kind: NopClaim +metadata: + name: test-claim + namespace: existing-namespace +spec: + coolField: existing-value + compositeDeletePolicy: Background + compositionRef: + name: claim-composition + # Copied back from the backing XR, as the claim syncer does under the XR's Automatic update policy. + compositionRevisionRef: + name: claim-composition-caff3a7 + # Bound to its backing XR, existing-claim-xr.yaml, as Crossplane's claim syncer binds it. + resourceRef: + apiVersion: diff.example.org/v1alpha1 + kind: XNopResource + name: test-claim-82crv diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-cluster-xr.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-cluster-xr.yaml new file mode 100644 index 00000000..03e34f05 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-cluster-xr.yaml @@ -0,0 +1,10 @@ +# TODO: use opposite pattern of prefixing namespaced api group instead of prefixing cluster api group +apiVersion: diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-resource +spec: + coolField: "existing-value" + crossplane: + compositionRef: + name: removal-test-composition diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-cross-ns-downstream.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-cross-ns-downstream.yaml new file mode 100644 index 00000000..45fdbc88 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-cross-ns-downstream.yaml @@ -0,0 +1,10 @@ +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: test-cross-ns-resource + namespace: default + annotations: + crossplane.io/composition-resource-name: cross-ns-resource +spec: + forProvider: + configData: existing-cross-ns-data-existing-named-data-old-cross-ns-role \ No newline at end of file diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-cross-ns-xr.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-cross-ns-xr.yaml new file mode 100644 index 00000000..5830cd0e --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-cross-ns-xr.yaml @@ -0,0 +1,11 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-cross-ns-resource + namespace: default +spec: + coolField: existing-cross-ns-value + environment: staging + crossplane: + compositionRef: + name: xnopresources.cross-namespace.example.org diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-defaulted-downstream-composed.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-defaulted-downstream-composed.yaml new file mode 100644 index 00000000..cb92c60d --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-defaulted-downstream-composed.yaml @@ -0,0 +1,14 @@ +# The fields Crossplane's composed-resource field manager applies. spec.forProvider.size is absent, +# exactly as the composition renders it. +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDefaultedDownstreamResource +metadata: + name: test-resource-defaulted + namespace: default + annotations: + crossplane.io/composition-resource-name: defaulted-resource + labels: + crossplane.io/composite: test-resource +spec: + forProvider: + configData: new-value diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-defaulted-downstream-other-manager.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-defaulted-downstream-other-manager.yaml new file mode 100644 index 00000000..04a6509a --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-defaulted-downstream-other-manager.yaml @@ -0,0 +1,10 @@ +# A different field manager (a user or another controller) sets the CRD-defaulted field to a +# non-default value on the composed resource Crossplane already manages. +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDefaultedDownstreamResource +metadata: + name: test-resource-defaulted + namespace: default +spec: + forProvider: + size: large diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-downstream-automatic.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-downstream-automatic.yaml new file mode 100644 index 00000000..f8abdd1d --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-downstream-automatic.yaml @@ -0,0 +1,10 @@ +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: test-automatic + namespace: default + annotations: + crossplane.io/composition-resource-name: nop-resource +spec: + forProvider: + configData: v1-existing-value diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-downstream-manual-v1.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-downstream-manual-v1.yaml new file mode 100644 index 00000000..8909fb23 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-downstream-manual-v1.yaml @@ -0,0 +1,10 @@ +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: test-manual-v1 + namespace: default + annotations: + crossplane.io/composition-resource-name: nop-resource +spec: + forProvider: + configData: v1-existing-value diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-downstream-resource-with-argocd.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-downstream-resource-with-argocd.yaml new file mode 100644 index 00000000..650c6e68 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-downstream-resource-with-argocd.yaml @@ -0,0 +1,15 @@ +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + annotations: + crossplane.io/composition-resource-name: nop-resource + argocd.argoproj.io/tracking-id: "test-app:/XDownstreamResource:default/test-resource" + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"ns.nop.example.org/v1alpha1","kind":"XDownstreamResource"} + labels: + argocd.argoproj.io/instance: test-app + name: test-resource + namespace: default +spec: + forProvider: + configData: new-value diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-downstream-selector-stable.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-downstream-selector-stable.yaml new file mode 100644 index 00000000..5f53076d --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-downstream-selector-stable.yaml @@ -0,0 +1,10 @@ +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: test-selector + namespace: default + annotations: + crossplane.io/composition-resource-name: nop-resource +spec: + forProvider: + configData: stable-existing-value diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-downstream-with-external-dep.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-downstream-with-external-dep.yaml new file mode 100644 index 00000000..f25c13dc --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-downstream-with-external-dep.yaml @@ -0,0 +1,11 @@ +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + annotations: + crossplane.io/composition-resource-name: nop-resource + name: test-resource + namespace: default +spec: + forProvider: + configData: existing-value + roleName: old-role-name diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-downstream-with-generated-name.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-downstream-with-generated-name.yaml new file mode 100644 index 00000000..e8cc8c56 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-downstream-with-generated-name.yaml @@ -0,0 +1,13 @@ +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + annotations: + crossplane.io/composition-resource-name: nop-resource + generateName: test-resource- + labels: + crossplane.io/composite: test-resource + name: test-resource-abc123 + namespace: default +spec: + forProvider: + configData: existing-value diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-env-downstream-resource.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-env-downstream-resource.yaml new file mode 100644 index 00000000..19da0e39 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-env-downstream-resource.yaml @@ -0,0 +1,16 @@ +# Deliberately no compositionRef or compositionRevisionRef. XDownstreamEnvResource is an XR kind (its +# XRD is xdownstreamenvresource-xrd.yaml), but no Composition targets it, so Crossplane can select +# none and never writes either ref. The diff likewise skips rendering it as a nested XR. +apiVersion: nop.example.org/v1alpha1 +kind: XDownstreamEnvResource +metadata: + annotations: + crossplane.io/composition-resource-name: env-resource + name: test-env-resource +spec: + compositionUpdatePolicy: Automatic + forProvider: + configData: existing-config-value + environment: staging + region: us-west-2 + serviceLevel: premium \ No newline at end of file diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-env-xr.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-env-xr.yaml new file mode 100644 index 00000000..1768c99a --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-env-xr.yaml @@ -0,0 +1,9 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XEnvResource +metadata: + name: test-env-resource +spec: + compositionRef: + name: xenvresources.diff.example.org + compositionUpdatePolicy: Automatic + configKey: "existing-config-value" diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-generate-name-xr.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-generate-name-xr.yaml new file mode 100644 index 00000000..72d7f866 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-generate-name-xr.yaml @@ -0,0 +1,13 @@ +# existing-xr.yaml as Crossplane leaves it once generated-name-composition.yaml has composed it, so it +# tracks that composition's revision rather than composition.yaml's. +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-resource + namespace: default +spec: + coolField: "existing-value" + crossplane: + compositionRef: + name: xnopresources.diff.example.org + compositionUpdatePolicy: Automatic diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-legacy-downstream-manual-v1.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-legacy-downstream-manual-v1.yaml new file mode 100644 index 00000000..0134d82c --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-legacy-downstream-manual-v1.yaml @@ -0,0 +1,9 @@ +apiVersion: legacycluster.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: test-legacy-manual-v1 + annotations: + crossplane.io/composition-resource-name: nop-resource +spec: + forProvider: + configData: v1-existing-value diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-legacy-xr-manual-v1.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-legacy-xr-manual-v1.yaml new file mode 100644 index 00000000..4999cd65 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-legacy-xr-manual-v1.yaml @@ -0,0 +1,11 @@ +apiVersion: legacycluster.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-legacy-manual-v1 +spec: + compositionRef: + name: xlegacynopresources.diff.example.org + compositionRevisionRef: + name: xlegacynopresources.diff.example.org-2f488b6 + compositionUpdatePolicy: Manual + coolField: existing-value diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-legacy-xr.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-legacy-xr.yaml new file mode 100644 index 00000000..5c375919 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-legacy-xr.yaml @@ -0,0 +1,9 @@ +apiVersion: legacycluster.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-resource +spec: + compositionRef: + name: removal-test-composition + compositionUpdatePolicy: Automatic + coolField: "existing-value" diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-namespace.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-namespace.yaml new file mode 100644 index 00000000..3fc6cd48 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: existing-namespace \ No newline at end of file diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-removal-test-xr.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-removal-test-xr.yaml new file mode 100644 index 00000000..bc841ee6 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-removal-test-xr.yaml @@ -0,0 +1,13 @@ +# existing-xr.yaml as Crossplane leaves it once removal-test-composition.yaml has composed it, so it +# tracks that composition and its revision. +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-resource + namespace: default +spec: + coolField: "existing-value" + crossplane: + compositionRef: + name: removal-test-composition + compositionUpdatePolicy: Automatic diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-sequencer-gating-stage0-ready.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-sequencer-gating-stage0-ready.yaml new file mode 100644 index 00000000..8ce45267 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-sequencer-gating-stage0-ready.yaml @@ -0,0 +1,19 @@ +# The stage0 resource sequencer-gating-composition.yaml composed for existing-sequencer-gating-xr.yaml, +# once it has become Ready. Crossplane then composed stage1 too +# (existing-sequencer-gating-stage1-ready.yaml). +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + annotations: + crossplane.io/composition-resource-name: stage0-resource + name: 0-stage0-resource + namespace: default +spec: + forProvider: + configData: existing-value +status: + conditions: + - type: Ready + status: "True" + reason: Available + lastTransitionTime: "2024-01-01T00:00:00Z" diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-sequencer-gating-stage0.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-sequencer-gating-stage0.yaml new file mode 100644 index 00000000..e84cc250 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-sequencer-gating-stage0.yaml @@ -0,0 +1,19 @@ +# The stage0 resource sequencer-gating-composition.yaml composed for existing-sequencer-gating-xr.yaml, +# still provisioning: it is not Ready, so function-sequencer keeps stage1 gated until +# --eventual-state synthesizes its Ready. +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + annotations: + crossplane.io/composition-resource-name: stage0-resource + name: 0-stage0-resource + namespace: default +spec: + forProvider: + configData: existing-value +status: + conditions: + - type: Ready + status: "False" + reason: Creating + lastTransitionTime: "2024-01-01T00:00:00Z" diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-sequencer-gating-stage1-ready.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-sequencer-gating-stage1-ready.yaml new file mode 100644 index 00000000..a209807c --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-sequencer-gating-stage1-ready.yaml @@ -0,0 +1,18 @@ +# The stage1 resource sequencer-gating-composition.yaml composed for existing-sequencer-gating-xr.yaml +# once its stage0 (existing-sequencer-gating-stage0-ready.yaml) became Ready. It is Ready too. +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + annotations: + crossplane.io/composition-resource-name: stage1-resource + name: 1-stage1-resource + namespace: default +spec: + forProvider: + configData: existing-value +status: + conditions: + - type: Ready + status: "True" + reason: Available + lastTransitionTime: "2024-01-01T00:00:00Z" diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-sequencer-gating-xr.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-sequencer-gating-xr.yaml new file mode 100644 index 00000000..b259f22a --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-sequencer-gating-xr.yaml @@ -0,0 +1,13 @@ +# sequencer-xr.yaml as it stood in the cluster before its coolField was changed, once Crossplane had +# reconciled it against sequencer-gating-composition.yaml, recording the composition it selected at +# spec.crossplane.* as the v2 XRD has them. +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: sequencer-test + namespace: default +spec: + coolField: existing-value + crossplane: + compositionRef: + name: sequencer-gating-composition diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-sequencer-xr.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-sequencer-xr.yaml new file mode 100644 index 00000000..6c50f601 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-sequencer-xr.yaml @@ -0,0 +1,12 @@ +# sequencer-xr.yaml as it stands in the cluster once Crossplane has reconciled it against +# sequencer-composition.yaml. +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: sequencer-test + namespace: default +spec: + coolField: "test-value" + crossplane: + compositionRef: + name: sequencer-composition diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-v2xrd-v1paths-downstream.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-v2xrd-v1paths-downstream.yaml new file mode 100644 index 00000000..e1c4dcb2 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-v2xrd-v1paths-downstream.yaml @@ -0,0 +1,10 @@ +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: test-v2xrd-v1paths + namespace: default + annotations: + crossplane.io/composition-resource-name: nop-resource +spec: + forProvider: + configData: existing-value diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-v2xrd-v1paths-xr.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-v2xrd-v1paths-xr.yaml new file mode 100644 index 00000000..2f1b1b25 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-v2xrd-v1paths-xr.yaml @@ -0,0 +1,14 @@ +# v2 XRD with v1-style composition paths test case (issue #206) +# This XR uses v1-style spec.compositionRef instead of spec.crossplane.compositionRef +apiVersion: v2withv1paths.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-v2xrd-v1paths + namespace: default +spec: + # v1-style composition reference (at spec.compositionRef instead of spec.crossplane.compositionRef), + # which is what this case is about. The harness derives the revision ref at the same v1-style path. + compositionRef: + name: xnopresources.v2withv1paths.diff.example.org + compositionUpdatePolicy: Automatic + coolField: existing-value diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-xr-automatic.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-xr-automatic.yaml new file mode 100644 index 00000000..52060d5d --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-xr-automatic.yaml @@ -0,0 +1,13 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-automatic + namespace: default +spec: + crossplane: + compositionRef: + name: xnopresources.diff.example.org + compositionRevisionRef: + name: xnopresources.diff.example.org-911318d # Currently on v1, but should auto-upgrade + compositionUpdatePolicy: Automatic + coolField: existing-value diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-xr-deleting.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-xr-deleting.yaml new file mode 100644 index 00000000..b84ed1af --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-xr-deleting.yaml @@ -0,0 +1,14 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-resource + namespace: default + # The finalizer keeps the object around after a delete is issued, so envtest (which runs no + # controllers) leaves it with metadata.deletionTimestamp set — a resource mid-deletion. + finalizers: + - composite.apiextensions.crossplane.io +spec: + coolField: "existing-value" + crossplane: + compositionRef: + name: xnopresources.diff.example.org diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-xr-listing-uncontrolled-resources.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-xr-listing-uncontrolled-resources.yaml new file mode 100644 index 00000000..a68ebbd7 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-xr-listing-uncontrolled-resources.yaml @@ -0,0 +1,23 @@ +# existing-removal-test-xr.yaml whose resourceRefs still name two composed resources it no longer +# controls: one another XR (existing-other-xr.yaml) now controls, and one whose controller reference +# was removed. Crossplane's garbage collector deletes only a resource the composite controls, so +# neither is removed although the composition no longer renders them. The harness appends the +# resources this XR does control. +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-resource + namespace: default +spec: + coolField: "existing-value" + crossplane: + compositionRef: + name: removal-test-composition + compositionUpdatePolicy: Automatic + resourceRefs: + - apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + name: other-resource + - apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + name: resource-without-controller diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-xr-manual-v1.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-xr-manual-v1.yaml new file mode 100644 index 00000000..61c2c65c --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-xr-manual-v1.yaml @@ -0,0 +1,13 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-manual-v1 + namespace: default +spec: + crossplane: + compositionRef: + name: xnopresources.diff.example.org + compositionRevisionRef: + name: xnopresources.diff.example.org-911318d + compositionUpdatePolicy: Manual + coolField: existing-value diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-xr-selector-stable.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-xr-selector-stable.yaml new file mode 100644 index 00000000..bb1edeb2 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-xr-selector-stable.yaml @@ -0,0 +1,16 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-selector + namespace: default +spec: + crossplane: + compositionRef: + name: xnopresources.diff.example.org + compositionRevisionRef: + name: xnopresources.diff.example.org-1623e01 + compositionUpdatePolicy: Automatic + compositionRevisionSelector: + matchLabels: + channel: stable + coolField: existing-value diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-xr-with-argocd.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-xr-with-argocd.yaml new file mode 100644 index 00000000..af4bc154 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-xr-with-argocd.yaml @@ -0,0 +1,19 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-resource + namespace: default + annotations: + argocd.argoproj.io/tracking-id: "test-app-OLD:/XNopResource:default/test-resource" + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"ns.diff.example.org/v1alpha1","kind":"XNopResource","metadata":{"name":"test-resource","namespace":"default"},"spec":{"coolField":"old-value"}} + labels: + argocd.argoproj.io/instance: test-app-OLD +spec: + coolField: "new-value" + # What any XR Crossplane has reconciled carries: the reconciler writes spec.crossplane, and the + # XR's generated CRD then defaults the policy. + crossplane: + compositionRef: + name: xnopresources.diff.example.org + compositionUpdatePolicy: Automatic diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-xr-with-defaults-other-manager.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-xr-with-defaults-other-manager.yaml new file mode 100644 index 00000000..678d6a0e --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-xr-with-defaults-other-manager.yaml @@ -0,0 +1,9 @@ +# A different field manager (a user or another controller) sets the XRD-defaulted spec.region to a +# non-default value on an XR whose own manifest (xr-with-missing-defaults.yaml) omits it. +apiVersion: ns.diff.example.org/v1alpha1 +kind: XTestDefaultResource +metadata: + name: test-resource-with-defaults + namespace: default +spec: + region: eu-west-1 diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-xr-with-defaults-reconciled.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-xr-with-defaults-reconciled.yaml new file mode 100644 index 00000000..6a855f8d --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-xr-with-defaults-reconciled.yaml @@ -0,0 +1,16 @@ +# What Crossplane's composite reconciler writes to xr-with-missing-defaults.yaml once it has reconciled +# it: its own composite label, and the composition and revision it selected (the revision the harness +# creates for composition-with-defaults.yaml, as the revision controller does). +apiVersion: ns.diff.example.org/v1alpha1 +kind: XTestDefaultResource +metadata: + name: test-resource-with-defaults + namespace: default + labels: + crossplane.io/composite: test-resource-with-defaults +spec: + crossplane: + compositionRef: + name: xdefaultresources.ns.diff.example.org + compositionRevisionRef: + name: xdefaultresources.ns.diff.example.org-d0e5887 diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-xr-with-external-dep-multistep-fatal.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-xr-with-external-dep-multistep-fatal.yaml new file mode 100644 index 00000000..8a000a9b --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-xr-with-external-dep-multistep-fatal.yaml @@ -0,0 +1,14 @@ +# existing-xr-with-external-dep.yaml as Crossplane leaves it once +# multistep-fatal-after-extra-res-composition.yaml has composed it, so it tracks that composition and +# its revision. +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-resource + namespace: default +spec: + coolField: existing-value + environment: staging + crossplane: + compositionRef: + name: multistep-fatal-after-extra-res.diff.example.org diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-xr-with-external-dep-templated.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-xr-with-external-dep-templated.yaml new file mode 100644 index 00000000..7c16d606 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-xr-with-external-dep-templated.yaml @@ -0,0 +1,13 @@ +# existing-xr-with-external-dep.yaml as Crossplane leaves it once external-res-gotpl-composition.yaml +# has composed it, so it tracks that composition and its revision. +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-resource + namespace: default +spec: + coolField: existing-value + environment: staging + crossplane: + compositionRef: + name: templated-extra-resources.diff.example.org diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-xr-with-external-dep-unguarded.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-xr-with-external-dep-unguarded.yaml new file mode 100644 index 00000000..2f09685b --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-xr-with-external-dep-unguarded.yaml @@ -0,0 +1,14 @@ +# existing-xr-with-external-dep.yaml as Crossplane leaves it once +# unguarded-external-res-gotpl-composition.yaml has composed it, so it tracks that composition and its +# revision. +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-resource + namespace: default +spec: + coolField: existing-value + environment: staging + crossplane: + compositionRef: + name: unguarded-templated-extra-resources.diff.example.org diff --git a/cmd/crossplane/xr/testdata/diff/resources/existing-xr-with-external-dep.yaml b/cmd/crossplane/xr/testdata/diff/resources/existing-xr-with-external-dep.yaml new file mode 100644 index 00000000..852cd656 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/existing-xr-with-external-dep.yaml @@ -0,0 +1,11 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-resource + namespace: default +spec: + coolField: existing-value + environment: staging + crossplane: + compositionRef: + name: xnopresources.diff.example.org diff --git a/cmd/crossplane/xr/testdata/diff/resources/external-named-clusterrole.yaml b/cmd/crossplane/xr/testdata/diff/resources/external-named-clusterrole.yaml new file mode 100644 index 00000000..3f6b02ef --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/external-named-clusterrole.yaml @@ -0,0 +1,8 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: external-named-clusterrole +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "watch"] diff --git a/cmd/crossplane/xr/testdata/diff/resources/external-res-fn-composition.yaml b/cmd/crossplane/xr/testdata/diff/resources/external-res-fn-composition.yaml new file mode 100644 index 00000000..dacd3b43 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/external-res-fn-composition.yaml @@ -0,0 +1,64 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnopresources.diff.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: fetch-external-resources + functionRef: + name: function-extra-resources + input: + apiVersion: extra-resources.fn.crossplane.io/v1beta1 + kind: Input + spec: + extraResources: + - apiVersion: v1 + kind: ConfigMap + into: cfgMap + type: Selector + selector: + matchLabels: + - key: app + type: Value + value: test-app + - key: environment + type: FromCompositeFieldPath + valueFromFieldPath: spec.environment + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + # function-extra-resources only supports Cluster-scoped resources as name refs + into: namedClusterRole + type: Reference + ref: + name: external-named-clusterrole + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + {{- /* we can't use the same call to 'index' on these because it goes map -> array -> map */ -}} + {{- $extras := index .context "apiextensions.crossplane.io/extra-resources" -}} + {{- $cfgMap := index $extras "cfgMap" 0 -}} + {{- $clusterRole := index $extras "namedClusterRole" 0 -}} + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: {{ index $cfgMap "data" "resource-data" }} + roleName: {{ $clusterRole.metadata.name }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/external-res-gotpl-composition.yaml b/cmd/crossplane/xr/testdata/diff/resources/external-res-gotpl-composition.yaml new file mode 100644 index 00000000..eb3c88aa --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/external-res-gotpl-composition.yaml @@ -0,0 +1,46 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: templated-extra-resources.diff.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: render-templates + functionRef: + name: function-go-templating + input: + apiVersion: gotemplating.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + --- + apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1 + kind: ExtraResources + requirements: + configmaps: + apiVersion: v1 + kind: ConfigMap + matchLabels: + app: test-app + environment: {{ .observed.composite.resource.spec.environment }} + {{- with .extraResources }} + {{ $configMaps := index . "configmaps" }} + {{- range $i, $configMap := $configMaps.items }} + --- + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ $.observed.composite.resource.metadata.name }} + namespace: {{ $.observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: {{ $.observed.composite.resource.spec.coolField }} + roleName: templated-{{ $configMap.resource.metadata.name }} + {{- end }} + {{- end }} diff --git a/cmd/crossplane/xr/testdata/diff/resources/external-res-matchname-gotpl-composition.yaml b/cmd/crossplane/xr/testdata/diff/resources/external-res-matchname-gotpl-composition.yaml new file mode 100644 index 00000000..a886976c --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/external-res-matchname-gotpl-composition.yaml @@ -0,0 +1,46 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: templated-extra-resources-matchname.diff.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: render-templates + functionRef: + name: function-go-templating + input: + apiVersion: gotemplating.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + # Regression scenario for crossplane-contrib/crossplane-diff#355: + # ExtraResources block uses matchName to reference a Secret that + # does not exist in the cluster. Pre-fix this aborted the diff with + # "cannot get referenced resource"; post-fix the unmet requirement + # is silently skipped (mirroring upstream xfn behavior) and the + # composition continues to render its downstream resource. + template: | + --- + apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1 + kind: ExtraResources + requirements: + optional-secret: + apiVersion: v1 + kind: Secret + matchName: nonexistent-secret + namespace: default + --- + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + roleName: static-role diff --git a/cmd/crossplane/xr/testdata/diff/resources/external-resource-configmap-other-namespace.yaml b/cmd/crossplane/xr/testdata/diff/resources/external-resource-configmap-other-namespace.yaml new file mode 100644 index 00000000..4a02772e --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/external-resource-configmap-other-namespace.yaml @@ -0,0 +1,22 @@ +# Reproduction fixture for crossplane-contrib/crossplane-diff#376. +# +# A ConfigMap matched by a matchLabels ExtraResources selector (no namespace) +# that lives in a DIFFERENT namespace than the XR being diffed. The XR is in +# "default"; this resource is in "other-namespace". A correct all-namespaces +# lookup (matching upstream Crossplane) finds it; a lookup wrongly scoped to the +# XR namespace does not. +apiVersion: v1 +kind: Namespace +metadata: + name: other-namespace +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: external-resource-crossns + namespace: other-namespace + labels: + app: test-app + environment: crossns +data: + resource-data: "cross-namespace-external-resource-data" diff --git a/cmd/crossplane/xr/testdata/diff/resources/external-resource-configmap.yaml b/cmd/crossplane/xr/testdata/diff/resources/external-resource-configmap.yaml new file mode 100644 index 00000000..53200b9e --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/external-resource-configmap.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: external-resource-staging + namespace: default + labels: + app: test-app + environment: staging +data: + resource-data: "staging-external-resource-data" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: external-resource-testing + namespace: default + labels: + app: test-app + environment: testing +data: + resource-data: "testing-external-resource-data" \ No newline at end of file diff --git a/cmd/crossplane/xr/testdata/diff/resources/generated-name-composition.yaml b/cmd/crossplane/xr/testdata/diff/resources/generated-name-composition.yaml new file mode 100644 index 00000000..f16e10c1 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/generated-name-composition.yaml @@ -0,0 +1,32 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnopresources.diff.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + generateName: test-resource- + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + namespace: {{ .observed.composite.resource.metadata.namespace }} + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/legacy-composition-revision-v1.yaml b/cmd/crossplane/xr/testdata/diff/resources/legacy-composition-revision-v1.yaml new file mode 100644 index 00000000..8f7eec5d --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/legacy-composition-revision-v1.yaml @@ -0,0 +1,33 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: CompositionRevision +metadata: + labels: + crossplane.io/composition-name: xlegacynopresources.diff.example.org +spec: + revision: 1 + compositeTypeRef: + apiVersion: legacycluster.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: legacycluster.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: v1-{{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/legacy-composition-v2.yaml b/cmd/crossplane/xr/testdata/diff/resources/legacy-composition-v2.yaml new file mode 100644 index 00000000..fc492bb9 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/legacy-composition-v2.yaml @@ -0,0 +1,31 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xlegacynopresources.diff.example.org +spec: + compositeTypeRef: + apiVersion: legacycluster.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: legacycluster.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: v2-{{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/legacy-xrd.yaml b/cmd/crossplane/xr/testdata/diff/resources/legacy-xrd.yaml new file mode 100644 index 00000000..c8648a5a --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/legacy-xrd.yaml @@ -0,0 +1,49 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: CompositeResourceDefinition +metadata: + name: xnopresources.legacycluster.diff.example.org +spec: + group: legacycluster.diff.example.org + names: + kind: XNopResource + plural: xnopresources + versions: + - name: v1alpha1 + served: true + referenceable: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + coolField: + type: string + environment: + type: string + status: + type: object + properties: + conditions: + description: Conditions of the resource. + type: array + items: + type: object + required: + - lastTransitionTime + - reason + - status + - type + properties: + lastTransitionTime: + type: string + format: date-time + message: + type: string + reason: + type: string + status: + type: string + type: + type: string diff --git a/cmd/crossplane/xr/testdata/diff/resources/manual-default/definitions.yaml b/cmd/crossplane/xr/testdata/diff/resources/manual-default/definitions.yaml new file mode 100644 index 00000000..11c29f88 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/manual-default/definitions.yaml @@ -0,0 +1,96 @@ +# An XRD whose composites default to the Manual update policy, and a composition with two revisions. +# Each revision's template writes its own version into the composed resource, so the value shows which +# revision rendered. The composition itself is the newest revision (v2), as it is in a cluster, and the +# harness creates that revision from it; revision 1 is declared here. +apiVersion: apiextensions.crossplane.io/v2 +kind: CompositeResourceDefinition +metadata: + name: xmanualdefaults.manualdefault.diff.example.org +spec: + group: manualdefault.diff.example.org + names: + kind: XManualDefault + plural: xmanualdefaults + scope: Namespaced + defaultCompositionUpdatePolicy: Manual + versions: + - name: v1alpha1 + served: true + referenceable: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + coolField: + type: string +--- +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xmanualdefaults.manualdefault.diff.example.org +spec: + compositeTypeRef: + apiVersion: manualdefault.diff.example.org/v1alpha1 + kind: XManualDefault + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: v2-{{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready +--- +apiVersion: apiextensions.crossplane.io/v1 +kind: CompositionRevision +metadata: + labels: + crossplane.io/composition-name: xmanualdefaults.manualdefault.diff.example.org +spec: + revision: 1 + compositeTypeRef: + apiVersion: manualdefault.diff.example.org/v1alpha1 + kind: XManualDefault + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: v1-{{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/modified-v2xrd-v1paths-xr.yaml b/cmd/crossplane/xr/testdata/diff/resources/modified-v2xrd-v1paths-xr.yaml new file mode 100644 index 00000000..a974f9f5 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/modified-v2xrd-v1paths-xr.yaml @@ -0,0 +1,13 @@ +# v2 XRD with v1-style composition paths test case (issue #206) +# This XR uses v1-style spec.compositionRef instead of spec.crossplane.compositionRef +apiVersion: v2withv1paths.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-v2xrd-v1paths + namespace: default +spec: + # v1-style composition reference (at spec.compositionRef instead of spec.crossplane.compositionRef) + compositionRef: + name: xnopresources.v2withv1paths.diff.example.org + compositionUpdatePolicy: Automatic + coolField: modified-value diff --git a/cmd/crossplane/xr/testdata/diff/resources/multistep-fatal-after-extra-res-composition.yaml b/cmd/crossplane/xr/testdata/diff/resources/multistep-fatal-after-extra-res-composition.yaml new file mode 100644 index 00000000..55419e07 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/multistep-fatal-after-extra-res-composition.yaml @@ -0,0 +1,65 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: multistep-fatal-after-extra-res.diff.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + # Step 1: guarded gotpl. Emits an ExtraResources requirement AND (once + # .extraResources is populated on the inner fetcher iteration) a composed + # resource. Stabilizes cleanly within the single render call. + - step: render-templates + functionRef: + name: function-go-templating + input: + apiVersion: gotemplating.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + --- + apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1 + kind: ExtraResources + requirements: + configmaps: + apiVersion: v1 + kind: ConfigMap + matchLabels: + app: test-app + environment: {{ .observed.composite.resource.spec.environment }} + {{- with .extraResources }} + {{ $configMaps := index . "configmaps" }} + {{- range $i, $configMap := $configMaps.items }} + --- + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ $.observed.composite.resource.metadata.name }} + namespace: {{ $.observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: {{ $.observed.composite.resource.spec.coolField }} + roleName: templated-{{ $configMap.resource.metadata.name }} + {{- end }} + {{- end }} + # Step 2: always fatals. `index .missing.path 0` — .missing is , + # indexing returns "index of untyped nil". This step does NOT emit + # requirements — so rsp.Requirements is nil for this step, but the render + # library has already accumulated step 1's requirements into its outputs. + # This produces the combination the PR's render-loop fix addresses: + # renderErr != nil AND len(output.Requirements) > 0. + - step: always-fatal + functionRef: + name: function-go-templating + input: + apiVersion: gotemplating.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + {{ index .missing.path 0 }} diff --git a/cmd/crossplane/xr/testdata/diff/resources/nested/child-composition.yaml b/cmd/crossplane/xr/testdata/diff/resources/nested/child-composition.yaml new file mode 100644 index 00000000..17ae8f7d --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/nested/child-composition.yaml @@ -0,0 +1,33 @@ +# Child composition - creates managed resources downstream +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xchildresources.nested.example.org +spec: + compositeTypeRef: + apiVersion: ns.nested.example.org/v1alpha1 + kind: XChildResource + mode: Pipeline + pipeline: + - step: generate-managed-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }}-managed + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: managed-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.childField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/nested/child-revision-templating-composition.yaml b/cmd/crossplane/xr/testdata/diff/resources/nested/child-revision-templating-composition.yaml new file mode 100644 index 00000000..54b1faa3 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/nested/child-revision-templating-composition.yaml @@ -0,0 +1,38 @@ +# A child composition whose template reads the CompositionRevision's own name off the nested composite +# and propagates it into the composed resource — the nested counterpart of +# testdata/comp/resources/revision-templating-composition.yaml. A nested XR is rendered from its +# parent's output, which never carries compositionRevisionRef (Crossplane's composite reconciler writes +# that), so unless the render inherits the cluster copy's ref this template has nothing to read. +# Used in place of child-composition.yaml, never alongside it: both target XChildResource. +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xchildresources.nested.example.org +spec: + compositeTypeRef: + apiVersion: ns.nested.example.org/v1alpha1 + kind: XChildResource + mode: Pipeline + pipeline: + - step: generate-managed-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }}-managed + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: managed-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.crossplane.compositionRevisionRef.name }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/nested/child-xrd.yaml b/cmd/crossplane/xr/testdata/diff/resources/nested/child-xrd.yaml new file mode 100644 index 00000000..c8da4876 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/nested/child-xrd.yaml @@ -0,0 +1,55 @@ +# Child XRD - this will be created as a composed resource of the parent +# and then will itself create downstream managed resources +apiVersion: apiextensions.crossplane.io/v2 +kind: CompositeResourceDefinition +metadata: + name: xchildresources.ns.nested.example.org +spec: + group: ns.nested.example.org + names: + kind: XChildResource + plural: xchildresources + scope: Namespaced + versions: + - name: v1alpha1 + served: true + referenceable: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + childField: + type: string + description: A field for the child resource + tier: + type: string + default: standard + description: Defaulted by the XRD; the parent composition never sets it + status: + type: object + properties: + conditions: + description: Conditions of the resource. + type: array + items: + type: object + required: + - lastTransitionTime + - reason + - status + - type + properties: + lastTransitionTime: + type: string + format: date-time + message: + type: string + reason: + type: string + status: + type: string + type: + type: string diff --git a/cmd/crossplane/xr/testdata/diff/resources/nested/existing-child-xr-revision-ref.yaml b/cmd/crossplane/xr/testdata/diff/resources/nested/existing-child-xr-revision-ref.yaml new file mode 100644 index 00000000..7004e4f1 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/nested/existing-child-xr-revision-ref.yaml @@ -0,0 +1,18 @@ +# existing-child-xr.yaml as Crossplane's composite reconciler leaves it once it has selected +# child-revision-templating-composition.yaml and its revision: carrying spec.crossplane.compositionRef +# and, from the harness, compositionRevisionRef. That composition reads the revision name into the +# managed resource, so a nested render that did not inherit the refs would report the managed resource +# as changing. +apiVersion: ns.nested.example.org/v1alpha1 +kind: XChildResource +metadata: + name: test-parent-child + annotations: + crossplane.io/composition-resource-name: child-xr + namespace: default +spec: + childField: "existing-value" + crossplane: + compositionRef: + name: xchildresources.nested.example.org + compositionUpdatePolicy: Automatic diff --git a/cmd/crossplane/xr/testdata/diff/resources/nested/existing-child-xr-tier-other-manager.yaml b/cmd/crossplane/xr/testdata/diff/resources/nested/existing-child-xr-tier-other-manager.yaml new file mode 100644 index 00000000..3cef855c --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/nested/existing-child-xr-tier-other-manager.yaml @@ -0,0 +1,9 @@ +# A different field manager sets the XRD-defaulted spec.tier to a non-default value on the child XR, +# which the parent composition renders without it. +apiVersion: ns.nested.example.org/v1alpha1 +kind: XChildResource +metadata: + name: test-parent-child + namespace: default +spec: + tier: premium diff --git a/cmd/crossplane/xr/testdata/diff/resources/nested/existing-child-xr.yaml b/cmd/crossplane/xr/testdata/diff/resources/nested/existing-child-xr.yaml new file mode 100644 index 00000000..fa857d35 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/nested/existing-child-xr.yaml @@ -0,0 +1,13 @@ +# Existing child XR in the cluster (created by parent) +apiVersion: ns.nested.example.org/v1alpha1 +kind: XChildResource +metadata: + name: test-parent-child + annotations: + crossplane.io/composition-resource-name: child-xr + namespace: default +spec: + childField: "existing-value" + crossplane: + compositionRef: + name: xchildresources.nested.example.org diff --git a/cmd/crossplane/xr/testdata/diff/resources/nested/existing-managed-resource-revision-ref.yaml b/cmd/crossplane/xr/testdata/diff/resources/nested/existing-managed-resource-revision-ref.yaml new file mode 100644 index 00000000..de74ec9e --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/nested/existing-managed-resource-revision-ref.yaml @@ -0,0 +1,15 @@ +# The managed resource composed by existing-child-xr-revision-ref.yaml through +# child-revision-templating-composition.yaml, so configData holds the revision the child tracks. +# Carries crossplane.io/composition-resource-name (and, from the harness, the composite label) because +# the render adds both: a fixture missing them is reported as modified on every run, which would hide +# whether the revision name propagated. +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: test-parent-child-managed + namespace: default + annotations: + crossplane.io/composition-resource-name: managed-resource +spec: + forProvider: + configData: xchildresources.nested.example.org-96afc06 diff --git a/cmd/crossplane/xr/testdata/diff/resources/nested/existing-managed-resource.yaml b/cmd/crossplane/xr/testdata/diff/resources/nested/existing-managed-resource.yaml new file mode 100644 index 00000000..1211a7e4 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/nested/existing-managed-resource.yaml @@ -0,0 +1,11 @@ +# Existing managed resource in the cluster (created by child XR) +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: test-parent-child-managed + namespace: default + annotations: + crossplane.io/composition-resource-name: managed-resource +spec: + forProvider: + configData: "existing-value" diff --git a/cmd/crossplane/xr/testdata/diff/resources/nested/existing-parent-xr.yaml b/cmd/crossplane/xr/testdata/diff/resources/nested/existing-parent-xr.yaml new file mode 100644 index 00000000..9ce1afe0 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/nested/existing-parent-xr.yaml @@ -0,0 +1,11 @@ +# Existing parent XR in the cluster +apiVersion: ns.nested.example.org/v1alpha1 +kind: XParentResource +metadata: + name: test-parent + namespace: default +spec: + parentField: "existing-value" + crossplane: + compositionRef: + name: xparentresources.nested.example.org diff --git a/cmd/crossplane/xr/testdata/diff/resources/nested/parent-composition.yaml b/cmd/crossplane/xr/testdata/diff/resources/nested/parent-composition.yaml new file mode 100644 index 00000000..5a4ce994 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/nested/parent-composition.yaml @@ -0,0 +1,32 @@ +# Parent composition - creates a child XR as its composed resource +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xparentresources.nested.example.org +spec: + compositeTypeRef: + apiVersion: ns.nested.example.org/v1alpha1 + kind: XParentResource + mode: Pipeline + pipeline: + - step: generate-child-xr + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nested.example.org/v1alpha1 + kind: XChildResource + metadata: + name: {{ .observed.composite.resource.metadata.name }}-child + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: child-xr + spec: + childField: {{ .observed.composite.resource.spec.parentField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/nested/parent-xrd.yaml b/cmd/crossplane/xr/testdata/diff/resources/nested/parent-xrd.yaml new file mode 100644 index 00000000..04de284e --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/nested/parent-xrd.yaml @@ -0,0 +1,50 @@ +# Parent XRD - this will create nested XRs as its composed resources +apiVersion: apiextensions.crossplane.io/v2 +kind: CompositeResourceDefinition +metadata: + name: xparentresources.ns.nested.example.org +spec: + group: ns.nested.example.org + names: + kind: XParentResource + plural: xparentresources + scope: Namespaced + versions: + - name: v1alpha1 + served: true + referenceable: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + parentField: + type: string + description: A field that will be passed to child XR + status: + type: object + properties: + conditions: + description: Conditions of the resource. + type: array + items: + type: object + required: + - lastTransitionTime + - reason + - status + - type + properties: + lastTransitionTime: + type: string + format: date-time + message: + type: string + reason: + type: string + status: + type: string + type: + type: string diff --git a/cmd/crossplane/xr/testdata/diff/resources/pinned-claim/definitions.yaml b/cmd/crossplane/xr/testdata/diff/resources/pinned-claim/definitions.yaml new file mode 100644 index 00000000..34bff81d --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/pinned-claim/definitions.yaml @@ -0,0 +1,96 @@ +# A claim-capable XRD and a composition with two revisions, for the revision tests. Each revision's +# template writes "::" +# into the composed resource, so one value shows both which revision rendered and which ref the render +# saw. The composition itself is the newest revision (v2), as it is in a cluster, and the harness creates +# that revision from it; revision 1 is declared here. +apiVersion: apiextensions.crossplane.io/v1 +kind: CompositeResourceDefinition +metadata: + name: xpinneds.pinned.diff.example.org +spec: + group: pinned.diff.example.org + names: + kind: XPinned + plural: xpinneds + claimNames: + kind: Pinned + plural: pinneds + versions: + - name: v1alpha1 + served: true + referenceable: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + coolField: + type: string +--- +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xpinneds.pinned.diff.example.org +spec: + compositeTypeRef: + apiVersion: pinned.diff.example.org/v1alpha1 + kind: XPinned + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: v2:{{ .observed.composite.resource.spec.compositionRevisionRef.name }}:{{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready +--- +apiVersion: apiextensions.crossplane.io/v1 +kind: CompositionRevision +metadata: + labels: + crossplane.io/composition-name: xpinneds.pinned.diff.example.org +spec: + revision: 1 + compositeTypeRef: + apiVersion: pinned.diff.example.org/v1alpha1 + kind: XPinned + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: v1:{{ .observed.composite.resource.spec.compositionRevisionRef.name }}:{{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-automatic-downstream.yaml b/cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-automatic-downstream.yaml new file mode 100644 index 00000000..d0672a40 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-automatic-downstream.yaml @@ -0,0 +1,10 @@ +# The composed resource of existing-automatic-xr.yaml, as revision 2 rendered it. +apiVersion: nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: automatic-claim-a1b2c + annotations: + crossplane.io/composition-resource-name: nop-resource +spec: + forProvider: + configData: v2:xpinneds.pinned.diff.example.org-806289b:existing-value diff --git a/cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-automatic-xr.yaml b/cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-automatic-xr.yaml new file mode 100644 index 00000000..ecd3a8a9 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-automatic-xr.yaml @@ -0,0 +1,15 @@ +# The backing XR of existing-automatic.yaml, tracking revision 2, the latest. +apiVersion: pinned.diff.example.org/v1alpha1 +kind: XPinned +metadata: + name: automatic-claim-a1b2c +spec: + coolField: existing-value + compositionUpdatePolicy: Automatic + compositionRef: + name: xpinneds.pinned.diff.example.org + claimRef: + apiVersion: pinned.diff.example.org/v1alpha1 + kind: Pinned + name: automatic-claim + namespace: existing-namespace diff --git a/cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-automatic.yaml b/cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-automatic.yaml new file mode 100644 index 00000000..7d3eda85 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-automatic.yaml @@ -0,0 +1,19 @@ +# An Automatic claim bound to a backing XR tracking revision 2, the latest: existing-automatic-xr.yaml, +# whose composed resource, as revision 2 rendered it, is existing-automatic-downstream.yaml. The claim +# carries the ref the claim syncer copies back from its XR under Automatic. +apiVersion: pinned.diff.example.org/v1alpha1 +kind: Pinned +metadata: + name: automatic-claim + namespace: existing-namespace +spec: + coolField: existing-value + compositionUpdatePolicy: Automatic + compositionRef: + name: xpinneds.pinned.diff.example.org + compositionRevisionRef: + name: xpinneds.pinned.diff.example.org-806289b + resourceRef: + apiVersion: pinned.diff.example.org/v1alpha1 + kind: XPinned + name: automatic-claim-a1b2c diff --git a/cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-lagging-automatic-downstream.yaml b/cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-lagging-automatic-downstream.yaml new file mode 100644 index 00000000..8fca08a8 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-lagging-automatic-downstream.yaml @@ -0,0 +1,10 @@ +# The composed resource of existing-lagging-automatic-xr.yaml, as revision 1 rendered it. +apiVersion: nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: lagging-xr + annotations: + crossplane.io/composition-resource-name: nop-resource +spec: + forProvider: + configData: v1:xpinneds.pinned.diff.example.org-3d33597:existing-value diff --git a/cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-lagging-automatic-xr.yaml b/cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-lagging-automatic-xr.yaml new file mode 100644 index 00000000..553bd228 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-lagging-automatic-xr.yaml @@ -0,0 +1,16 @@ +# An Automatic XR, bound to no claim, whose compositionRevisionRef still names revision 1 although revision +# 2 is the latest. Its composed resource, as revision 1 rendered it, is +# existing-lagging-automatic-downstream.yaml. Crossplane re-points an Automatic composite at the latest +# revision on its next reconcile, so the ref lags until then: say, when the composite is diffed straight +# after its composition changed. +apiVersion: pinned.diff.example.org/v1alpha1 +kind: XPinned +metadata: + name: lagging-xr +spec: + coolField: existing-value + compositionUpdatePolicy: Automatic + compositionRef: + name: xpinneds.pinned.diff.example.org + compositionRevisionRef: + name: xpinneds.pinned.diff.example.org-3d33597 diff --git a/cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-manual-downstream.yaml b/cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-manual-downstream.yaml new file mode 100644 index 00000000..f6d3a87d --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-manual-downstream.yaml @@ -0,0 +1,10 @@ +# The composed resource of existing-manual-xr.yaml, as revision 1 rendered it. +apiVersion: nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: manual-claim-x1y2z + annotations: + crossplane.io/composition-resource-name: nop-resource +spec: + forProvider: + configData: v1:xpinneds.pinned.diff.example.org-3d33597:existing-value diff --git a/cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-manual-xr.yaml b/cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-manual-xr.yaml new file mode 100644 index 00000000..a5d186fa --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-manual-xr.yaml @@ -0,0 +1,17 @@ +# The backing XR of existing-manual.yaml, pinned to revision 1. +apiVersion: pinned.diff.example.org/v1alpha1 +kind: XPinned +metadata: + name: manual-claim-x1y2z +spec: + coolField: existing-value + compositionUpdatePolicy: Manual + compositionRef: + name: xpinneds.pinned.diff.example.org + compositionRevisionRef: + name: xpinneds.pinned.diff.example.org-3d33597 + claimRef: + apiVersion: pinned.diff.example.org/v1alpha1 + kind: Pinned + name: manual-claim + namespace: existing-namespace diff --git a/cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-manual.yaml b/cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-manual.yaml new file mode 100644 index 00000000..8ece1692 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/pinned-claim/existing-manual.yaml @@ -0,0 +1,19 @@ +# A Manual claim bound to a backing XR that is pinned to revision 1, the older of the composition's two +# revisions: existing-manual-xr.yaml, whose composed resource, as revision 1 rendered it, is +# existing-manual-downstream.yaml. The claim carries no compositionRevisionRef: the claim syncer copies +# the ref back to a claim only under Automatic, so a Manual claim has one only if its author set it. The +# pin therefore lives on the backing XR alone. +apiVersion: pinned.diff.example.org/v1alpha1 +kind: Pinned +metadata: + name: manual-claim + namespace: existing-namespace +spec: + coolField: existing-value + compositionUpdatePolicy: Manual + compositionRef: + name: xpinneds.pinned.diff.example.org + resourceRef: + apiVersion: pinned.diff.example.org/v1alpha1 + kind: XPinned + name: manual-claim-x1y2z diff --git a/cmd/crossplane/xr/testdata/diff/resources/production-composition.yaml b/cmd/crossplane/xr/testdata/diff/resources/production-composition.yaml new file mode 100644 index 00000000..1e0b06ba --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/production-composition.yaml @@ -0,0 +1,36 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: production-composition + labels: + provider: aws + environment: production +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-production-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: production-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + resourceTier: "production" + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/rejecting-admission-policy.yaml b/cmd/crossplane/xr/testdata/diff/resources/rejecting-admission-policy.yaml new file mode 100644 index 00000000..ff10e3eb --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/rejecting-admission-policy.yaml @@ -0,0 +1,38 @@ +# A ValidatingAdmissionPolicy that refuses XDownstreamResources carrying the configData values the +# diff fixtures render, so a dry-run against the apiserver is rejected (crossplane-diff#334). +# +# Why a VAP and not a CEL x-kubernetes-validations rule on the CRD: crossplane-diff's own schema +# validator evaluates CEL rules locally, before the diff calculator runs, so a CEL rule would reject +# the resource without the apiserver ever being consulted. The test would pass on code that never +# dry-ran anything — the exact false positive this instrument has to avoid. A VAP is invisible to the +# local validator, needs no webhook server, and has been GA since Kubernetes 1.30. +# +# CREATE covers the addition path (dry-run create); UPDATE covers the existing-resource path, where +# a server-side-apply dry-run reaches admission as an UPDATE. The same policy therefore drives the +# rejection tests for both, including the deliberate reclassification of an existing resource's +# rejection from a tool error to a validation failure. +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: reject-marked-downstream-resources +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: ["ns.nop.example.org"] + apiVersions: ["v1alpha1"] + operations: ["CREATE", "UPDATE"] + resources: ["xdownstreamresources"] + validations: + - expression: >- + !has(object.spec) || !has(object.spec.forProvider) || !has(object.spec.forProvider.configData) + || !(object.spec.forProvider.configData in ['new-value', 'modified-value']) + message: "this cluster refuses that configData value" +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: reject-marked-downstream-resources +spec: + policyName: reject-marked-downstream-resources + validationActions: ["Deny"] diff --git a/cmd/crossplane/xr/testdata/diff/resources/removal-test-cluster-composition.yaml b/cmd/crossplane/xr/testdata/diff/resources/removal-test-cluster-composition.yaml new file mode 100644 index 00000000..766a06f8 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/removal-test-cluster-composition.yaml @@ -0,0 +1,31 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: removal-test-composition +spec: + compositeTypeRef: + apiVersion: diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-remaining-resource + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: resource-to-be-kept + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: resource1 + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/removal-test-cluster-downstream-resource1.yaml b/cmd/crossplane/xr/testdata/diff/resources/removal-test-cluster-downstream-resource1.yaml new file mode 100644 index 00000000..4d6bc8d1 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/removal-test-cluster-downstream-resource1.yaml @@ -0,0 +1,9 @@ +apiVersion: nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + annotations: + crossplane.io/composition-resource-name: resource1 + name: resource-to-be-kept +spec: + forProvider: + configData: existing-value diff --git a/cmd/crossplane/xr/testdata/diff/resources/removal-test-legacy-child-composition.yaml b/cmd/crossplane/xr/testdata/diff/resources/removal-test-legacy-child-composition.yaml new file mode 100644 index 00000000..67995f9f --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/removal-test-legacy-child-composition.yaml @@ -0,0 +1,31 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xlegacychildresources.legacycluster.diff.example.org +spec: + compositeTypeRef: + apiVersion: legacycluster.diff.example.org/v1alpha1 + kind: XLegacyChildResource + mode: Pipeline + pipeline: + - step: generate-managed-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: legacycluster.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }}-managed + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: managed-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.childField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/removal-test-legacy-child-xrd.yaml b/cmd/crossplane/xr/testdata/diff/resources/removal-test-legacy-child-xrd.yaml new file mode 100644 index 00000000..ce48cad4 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/removal-test-legacy-child-xrd.yaml @@ -0,0 +1,24 @@ +# A legacy (v1) XR that a legacy XR composes, so a removal hierarchy at the v1 spec.resourceRefs path is +# two real XRs deep, as in a cluster: removal-test-legacy-nested-xr.yaml is one. +apiVersion: apiextensions.crossplane.io/v1 +kind: CompositeResourceDefinition +metadata: + name: xlegacychildresources.legacycluster.diff.example.org +spec: + group: legacycluster.diff.example.org + names: + kind: XLegacyChildResource + plural: xlegacychildresources + versions: + - name: v1alpha1 + served: true + referenceable: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + childField: + type: string diff --git a/cmd/crossplane/xr/testdata/diff/resources/removal-test-legacy-composition.yaml b/cmd/crossplane/xr/testdata/diff/resources/removal-test-legacy-composition.yaml new file mode 100644 index 00000000..e8fa8909 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/removal-test-legacy-composition.yaml @@ -0,0 +1,31 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: removal-test-composition +spec: + compositeTypeRef: + apiVersion: legacycluster.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-remaining-resource + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: legacycluster.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: resource-to-be-kept + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: resource1 + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/removal-test-legacy-nested-xr-managed.yaml b/cmd/crossplane/xr/testdata/diff/resources/removal-test-legacy-nested-xr-managed.yaml new file mode 100644 index 00000000..10d846bf --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/removal-test-legacy-nested-xr-managed.yaml @@ -0,0 +1,11 @@ +# The managed resource removal-test-legacy-child-composition.yaml composed for +# removal-test-legacy-nested-xr.yaml. +apiVersion: legacycluster.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: resource-to-be-removed-managed + annotations: + crossplane.io/composition-resource-name: managed-resource +spec: + forProvider: + configData: child-value diff --git a/cmd/crossplane/xr/testdata/diff/resources/removal-test-legacy-nested-xr.yaml b/cmd/crossplane/xr/testdata/diff/resources/removal-test-legacy-nested-xr.yaml new file mode 100644 index 00000000..e421b6f2 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/removal-test-legacy-nested-xr.yaml @@ -0,0 +1,13 @@ +# removal-test-nested-xr.yaml at the legacy (v1) paths: the nested XR removal-test-legacy-composition +# once composed as resource2. It composes removal-test-legacy-nested-xr-managed.yaml in turn, through +# the revision the harness creates for removal-test-legacy-child-composition.yaml. +apiVersion: legacycluster.diff.example.org/v1alpha1 +kind: XLegacyChildResource +metadata: + name: resource-to-be-removed + annotations: + crossplane.io/composition-resource-name: resource2 +spec: + childField: child-value + compositionRef: + name: xlegacychildresources.legacycluster.diff.example.org diff --git a/cmd/crossplane/xr/testdata/diff/resources/removal-test-legacycluster-downstream-resource1.yaml b/cmd/crossplane/xr/testdata/diff/resources/removal-test-legacycluster-downstream-resource1.yaml new file mode 100644 index 00000000..e03c4398 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/removal-test-legacycluster-downstream-resource1.yaml @@ -0,0 +1,9 @@ +apiVersion: legacycluster.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + annotations: + crossplane.io/composition-resource-name: resource1 + name: resource-to-be-kept +spec: + forProvider: + configData: existing-value diff --git a/cmd/crossplane/xr/testdata/diff/resources/removal-test-nested-xr-managed.yaml b/cmd/crossplane/xr/testdata/diff/resources/removal-test-nested-xr-managed.yaml new file mode 100644 index 00000000..0bde272b --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/removal-test-nested-xr-managed.yaml @@ -0,0 +1,11 @@ +# The managed resource nested/child-composition.yaml composed for removal-test-nested-xr.yaml. +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: resource-to-be-removed-managed + namespace: default + annotations: + crossplane.io/composition-resource-name: managed-resource +spec: + forProvider: + configData: child-value diff --git a/cmd/crossplane/xr/testdata/diff/resources/removal-test-nested-xr.yaml b/cmd/crossplane/xr/testdata/diff/resources/removal-test-nested-xr.yaml new file mode 100644 index 00000000..6ff9ea7b --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/removal-test-nested-xr.yaml @@ -0,0 +1,15 @@ +# The nested XR the removal-test compositions once composed as resource2, as Crossplane left it, with +# the composition of its own kind it was composed with. It composes removal-test-nested-xr-managed.yaml in +# turn, so removing it removes that too. +apiVersion: ns.nested.example.org/v1alpha1 +kind: XChildResource +metadata: + name: resource-to-be-removed + namespace: default + annotations: + crossplane.io/composition-resource-name: resource2 +spec: + childField: child-value + crossplane: + compositionRef: + name: xchildresources.nested.example.org diff --git a/cmd/crossplane/xr/testdata/diff/resources/rendered-defaults/definitions.yaml b/cmd/crossplane/xr/testdata/diff/resources/rendered-defaults/definitions.yaml new file mode 100644 index 00000000..5976be20 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/rendered-defaults/definitions.yaml @@ -0,0 +1,129 @@ +# XRDs whose defaults a composition reads, one modern (v2, machinery under spec.crossplane) and one +# legacy (v1, machinery directly under spec), each with a composition that writes the composite's +# defaulted fields into a composed resource. The CRDs are generated from these XRDs (upstream xcrd), so +# the XR CRDs default region from the XRD's schema and, for the legacy XR, spec.compositionUpdatePolicy +# from defaultCompositionUpdatePolicy, as in a real cluster. A render input that was not defaulted would +# write "" instead. +# +# The modern template does not read spec.crossplane.compositionUpdatePolicy: its CRD defaults the +# policy only inside an existing spec.crossplane, which a new XR's manifest does not have, so the tool +# renders a new modern XR without one. (In a cluster Crossplane writes spec.crossplane before it first +# composes, so the apiserver defaults the policy by then.) +apiVersion: apiextensions.crossplane.io/v2 +kind: CompositeResourceDefinition +metadata: + name: xregionaldatabases.rendered-defaults.diff.example.org +spec: + group: rendered-defaults.diff.example.org + names: + kind: XRegionalDatabase + plural: xregionaldatabases + scope: Namespaced + defaultCompositionUpdatePolicy: Automatic + versions: + - name: v1alpha1 + served: true + referenceable: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + size: + type: string + region: + type: string + default: us-east-1 +--- +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xregionaldatabases.rendered-defaults.diff.example.org +spec: + compositeTypeRef: + apiVersion: rendered-defaults.diff.example.org/v1alpha1 + kind: XRegionalDatabase + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: database + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.region }}/{{ .observed.composite.resource.spec.size }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready +--- +apiVersion: apiextensions.crossplane.io/v1 +kind: CompositeResourceDefinition +metadata: + name: xlegacyregionaldatabases.rendered-defaults.diff.example.org +spec: + group: rendered-defaults.diff.example.org + names: + kind: XLegacyRegionalDatabase + plural: xlegacyregionaldatabases + versions: + - name: v1alpha1 + served: true + referenceable: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + size: + type: string + region: + type: string + default: eu-west-1 +--- +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xlegacyregionaldatabases.rendered-defaults.diff.example.org +spec: + compositeTypeRef: + apiVersion: rendered-defaults.diff.example.org/v1alpha1 + kind: XLegacyRegionalDatabase + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: database + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.region }}/{{ .observed.composite.resource.spec.size }}/{{ .observed.composite.resource.spec.compositionUpdatePolicy }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/selector-bound/existing-downstream.yaml b/cmd/crossplane/xr/testdata/diff/resources/selector-bound/existing-downstream.yaml new file mode 100644 index 00000000..8e54b1b0 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/selector-bound/existing-downstream.yaml @@ -0,0 +1,11 @@ +# The composed resource of existing-xr.yaml, as composition selector-bound-a rendered it. +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + name: selector-bound + namespace: default + annotations: + crossplane.io/composition-resource-name: nop-resource +spec: + forProvider: + configData: a-existing-value diff --git a/cmd/crossplane/xr/testdata/diff/resources/selector-bound/existing-xr.yaml b/cmd/crossplane/xr/testdata/diff/resources/selector-bound/existing-xr.yaml new file mode 100644 index 00000000..9711b44c --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/selector-bound/existing-xr.yaml @@ -0,0 +1,16 @@ +# A composite whose compositionSelector matches both compositions in setup.yaml, and which Crossplane +# bound to the first, selector-bound-a. +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: selector-bound + namespace: default +spec: + coolField: existing-value + crossplane: + compositionSelector: + matchLabels: + binding: shared + compositionRef: + name: selector-bound-a + compositionUpdatePolicy: Automatic diff --git a/cmd/crossplane/xr/testdata/diff/resources/selector-bound/setup.yaml b/cmd/crossplane/xr/testdata/diff/resources/selector-bound/setup.yaml new file mode 100644 index 00000000..e33e1e16 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/selector-bound/setup.yaml @@ -0,0 +1,74 @@ +# Two compositions that both match one compositionSelector. existing-xr.yaml is a composite Crossplane +# has already bound to the first, and existing-downstream.yaml its composed resource. Crossplane selects +# a composition once, when the composite has no compositionRef, and never re-selects +# (composite.APILabelSelectorResolver), so the composite stays on composition A however many +# compositions its selector matches. +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: selector-bound-a + labels: + binding: shared +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: a-{{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready +--- +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: selector-bound-b + labels: + binding: shared +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: b-{{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/sequencer-composition.yaml b/cmd/crossplane/xr/testdata/diff/resources/sequencer-composition.yaml new file mode 100644 index 00000000..3a56fdf9 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/sequencer-composition.yaml @@ -0,0 +1,53 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: sequencer-composition +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-all-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: 0-stage0-resource + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: stage0-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + --- + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: 1-stage1-resource + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: stage1-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready + - step: sequence-resources + functionRef: + name: function-sequencer + input: + apiVersion: sequencer.fn.crossplane.io/v1beta1 + kind: Input + rules: + - sequence: + - ^stage0- # Stage 0 (composition-resource-name starts with stage0-) + - ^stage1- # Stage 1 (hidden until stage 0 is ready, unless it already exists) diff --git a/cmd/crossplane/xr/testdata/diff/resources/sequencer-stage0-downstream.yaml b/cmd/crossplane/xr/testdata/diff/resources/sequencer-stage0-downstream.yaml new file mode 100644 index 00000000..fa1608b9 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/sequencer-stage0-downstream.yaml @@ -0,0 +1,10 @@ +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + annotations: + crossplane.io/composition-resource-name: stage0-resource + name: 0-stage0-resource + namespace: default +spec: + forProvider: + configData: test-value diff --git a/cmd/crossplane/xr/testdata/diff/resources/sequencer-stage1-downstream.yaml b/cmd/crossplane/xr/testdata/diff/resources/sequencer-stage1-downstream.yaml new file mode 100644 index 00000000..23e1180e --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/sequencer-stage1-downstream.yaml @@ -0,0 +1,10 @@ +apiVersion: ns.nop.example.org/v1alpha1 +kind: XDownstreamResource +metadata: + annotations: + crossplane.io/composition-resource-name: stage1-resource + name: 1-stage1-resource + namespace: default +spec: + forProvider: + configData: test-value diff --git a/cmd/crossplane/xr/testdata/diff/resources/sequencer-with-envconfig-composition.yaml b/cmd/crossplane/xr/testdata/diff/resources/sequencer-with-envconfig-composition.yaml new file mode 100644 index 00000000..706f8194 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/sequencer-with-envconfig-composition.yaml @@ -0,0 +1,67 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: sequencer-with-envconfig-composition +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: get-environment-configs + functionRef: + name: function-environment-configs + input: + apiVersion: environmentconfigs.fn.crossplane.io/v1beta1 + kind: Input + spec: + environmentConfigs: + - type: Reference + ref: + name: test-env-config + - step: generate-all-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + {{ $envConfig := index .context "apiextensions.crossplane.io/environment" }} + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: 0-stage0-resource + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: stage0-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + resourceTier: {{ $envConfig.environment }} + --- + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: 1-stage1-resource + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: stage1-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + resourceTier: {{ $envConfig.environment }} + - step: sequence-resources + functionRef: + name: function-sequencer + input: + apiVersion: sequencer.fn.crossplane.io/v1beta1 + kind: Input + rules: + - sequence: + - ^0- # Stage 0 resources (rendered first) + - ^1- # Stage 1 resources (hidden until stage 0 is ready) + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/sequencer-with-envconfig-xr.yaml b/cmd/crossplane/xr/testdata/diff/resources/sequencer-with-envconfig-xr.yaml new file mode 100644 index 00000000..52c724cd --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/sequencer-with-envconfig-xr.yaml @@ -0,0 +1,10 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: sequencer-envconfig-test + namespace: default +spec: + crossplane: + compositionRef: + name: sequencer-with-envconfig-composition + coolField: "test-value" diff --git a/cmd/crossplane/xr/testdata/diff/resources/sequencer-xr.yaml b/cmd/crossplane/xr/testdata/diff/resources/sequencer-xr.yaml new file mode 100644 index 00000000..de0f52ec --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/sequencer-xr.yaml @@ -0,0 +1,7 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: sequencer-test + namespace: default +spec: + coolField: "test-value" diff --git a/cmd/crossplane/xr/testdata/diff/resources/staging-composition.yaml b/cmd/crossplane/xr/testdata/diff/resources/staging-composition.yaml new file mode 100644 index 00000000..5cf736f9 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/staging-composition.yaml @@ -0,0 +1,36 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: staging-composition + labels: + provider: aws + environment: staging +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-staging-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: staging-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + resourceTier: "staging" + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/unestablished-xrd.yaml b/cmd/crossplane/xr/testdata/diff/resources/unestablished-xrd.yaml new file mode 100644 index 00000000..286e15ee --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/unestablished-xrd.yaml @@ -0,0 +1,25 @@ +# An XRD unrelated to any input, applied with no generated CRD: one Crossplane has not established yet, +# so the apiserver serves neither its XR type nor its claim type. +apiVersion: apiextensions.crossplane.io/v2 +kind: CompositeResourceDefinition +metadata: + name: xunestablishedresources.unrelated.example.org +spec: + group: unrelated.example.org + names: + kind: XUnestablishedResource + plural: xunestablishedresources + scope: Namespaced + versions: + - name: v1alpha1 + served: true + referenceable: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + coolField: + type: string diff --git a/cmd/crossplane/xr/testdata/diff/resources/unguarded-external-res-gotpl-composition.yaml b/cmd/crossplane/xr/testdata/diff/resources/unguarded-external-res-gotpl-composition.yaml new file mode 100644 index 00000000..26561167 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/unguarded-external-res-gotpl-composition.yaml @@ -0,0 +1,44 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: unguarded-templated-extra-resources.diff.example.org +spec: + compositeTypeRef: + apiVersion: ns.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: render-templates + functionRef: + name: function-go-templating + input: + apiVersion: gotemplating.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + --- + apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1 + kind: ExtraResources + requirements: + configmaps: + apiVersion: v1 + kind: ConfigMap + matchLabels: + app: test-app + environment: {{ .observed.composite.resource.spec.environment }} + {{- $configMaps := index .extraResources "configmaps" }} + {{- range $i, $configMap := $configMaps.items }} + --- + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ $.observed.composite.resource.metadata.name }} + namespace: {{ $.observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: {{ $.observed.composite.resource.spec.coolField }} + roleName: templated-{{ $configMap.resource.metadata.name }} + {{- end }} diff --git a/cmd/crossplane/xr/testdata/diff/resources/v2-xrd-with-v1-paths-composition.yaml b/cmd/crossplane/xr/testdata/diff/resources/v2-xrd-with-v1-paths-composition.yaml new file mode 100644 index 00000000..380d66c5 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/v2-xrd-with-v1-paths-composition.yaml @@ -0,0 +1,32 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xnopresources.v2withv1paths.diff.example.org +spec: + compositeTypeRef: + apiVersion: v2withv1paths.diff.example.org/v1alpha1 + kind: XNopResource + mode: Pipeline + pipeline: + - step: generate-resources + functionRef: + name: function-go-templating + input: + apiVersion: template.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + apiVersion: ns.nop.example.org/v1alpha1 + kind: XDownstreamResource + metadata: + name: {{ .observed.composite.resource.metadata.name }} + namespace: {{ .observed.composite.resource.metadata.namespace }} + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: nop-resource + spec: + forProvider: + configData: {{ .observed.composite.resource.spec.coolField }} + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready diff --git a/cmd/crossplane/xr/testdata/diff/resources/v2-xrd-with-v1-paths.yaml b/cmd/crossplane/xr/testdata/diff/resources/v2-xrd-with-v1-paths.yaml new file mode 100644 index 00000000..e150c8a9 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/v2-xrd-with-v1-paths.yaml @@ -0,0 +1,101 @@ +# This is a v2 XRD that explicitly supports both v1-style and v2-style composition paths. +# Used to test issue #206: v2 XRs using v1-style composition paths are not recognized. +apiVersion: apiextensions.crossplane.io/v2 +kind: CompositeResourceDefinition +metadata: + name: xnopresources.v2withv1paths.diff.example.org +spec: + group: v2withv1paths.diff.example.org + names: + kind: XNopResource + plural: xnopresources + scope: Namespaced + versions: + - name: v1alpha1 + served: true + referenceable: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + coolField: + type: string + environment: + type: string + # v1-style composition fields (for backwards compatibility) + compositionRef: + type: object + properties: + name: + type: string + compositionRevisionRef: + type: object + properties: + name: + type: string + compositionUpdatePolicy: + type: string + enum: + - Automatic + - Manual + compositionSelector: + type: object + properties: + matchLabels: + type: object + additionalProperties: + type: string + # v2-style composition fields (nested under crossplane) + crossplane: + type: object + properties: + compositionRef: + type: object + properties: + name: + type: string + compositionRevisionRef: + type: object + properties: + name: + type: string + compositionUpdatePolicy: + type: string + enum: + - Automatic + - Manual + compositionSelector: + type: object + properties: + matchLabels: + type: object + additionalProperties: + type: string + status: + type: object + properties: + conditions: + description: Conditions of the resource. + type: array + items: + type: object + required: + - lastTransitionTime + - reason + - status + - type + properties: + lastTransitionTime: + type: string + format: date-time + message: + type: string + reason: + type: string + status: + type: string + type: + type: string diff --git a/cmd/crossplane/xr/testdata/diff/resources/xdownstreamenvresource-xrd.yaml b/cmd/crossplane/xr/testdata/diff/resources/xdownstreamenvresource-xrd.yaml new file mode 100644 index 00000000..5e2954ba --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/xdownstreamenvresource-xrd.yaml @@ -0,0 +1,31 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: CompositeResourceDefinition +metadata: + name: xdownstreamenvresources.nop.example.org +spec: + group: nop.example.org + names: + kind: XDownstreamEnvResource + plural: xdownstreamenvresources + versions: + - name: v1alpha1 + served: true + referenceable: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + forProvider: + type: object + properties: + configData: + type: string + region: + type: string + environment: + type: string + serviceLevel: + type: string diff --git a/cmd/crossplane/xr/testdata/diff/resources/xrd-with-defaults.yaml b/cmd/crossplane/xr/testdata/diff/resources/xrd-with-defaults.yaml new file mode 100644 index 00000000..bbd6eb8e --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/resources/xrd-with-defaults.yaml @@ -0,0 +1,79 @@ +apiVersion: apiextensions.crossplane.io/v2 +kind: CompositeResourceDefinition +metadata: + name: xtestdefaultresources.ns.diff.example.org +spec: + group: ns.diff.example.org + names: + kind: XTestDefaultResource + plural: xtestdefaultresources + scope: Namespaced + versions: + - name: v1alpha1 + served: true + referenceable: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + region: + type: string + default: "us-east-1" + description: "AWS region with default value" + size: + type: string + default: "small" + description: "Instance size with default" + settings: + type: object + default: + enabled: true + timeout: 30 + properties: + enabled: + type: boolean + default: true + timeout: + type: integer + default: 30 + retries: + type: integer + default: 3 + tags: + type: object + default: + environment: "development" + properties: + environment: + type: string + default: "development" + team: + type: string + status: + type: object + properties: + conditions: + description: Conditions of the resource. + type: array + items: + type: object + required: + - lastTransitionTime + - reason + - status + - type + properties: + lastTransitionTime: + type: string + format: date-time + message: + type: string + reason: + type: string + status: + type: string + type: + type: string \ No newline at end of file diff --git a/cmd/crossplane/xr/testdata/diff/unchanged-automatic-pinned-claim.yaml b/cmd/crossplane/xr/testdata/diff/unchanged-automatic-pinned-claim.yaml new file mode 100644 index 00000000..c22ad2e2 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/unchanged-automatic-pinned-claim.yaml @@ -0,0 +1,12 @@ +# resources/pinned-claim/existing-automatic.yaml's claim as its author keeps it: unchanged, but without +# the compositionRevisionRef the claim syncer copied onto the cluster copy (issue #498). +apiVersion: pinned.diff.example.org/v1alpha1 +kind: Pinned +metadata: + name: automatic-claim + namespace: existing-namespace +spec: + coolField: existing-value + compositionUpdatePolicy: Automatic + compositionRef: + name: xpinneds.pinned.diff.example.org diff --git a/cmd/crossplane/xr/testdata/diff/unchanged-lagging-automatic-xr.yaml b/cmd/crossplane/xr/testdata/diff/unchanged-lagging-automatic-xr.yaml new file mode 100644 index 00000000..10905951 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/unchanged-lagging-automatic-xr.yaml @@ -0,0 +1,11 @@ +# resources/pinned-claim/existing-lagging-automatic-xr.yaml's XR as its author keeps it: unchanged, and +# without the compositionRevisionRef Crossplane's composite reconciler writes. +apiVersion: pinned.diff.example.org/v1alpha1 +kind: XPinned +metadata: + name: lagging-xr +spec: + coolField: existing-value + compositionUpdatePolicy: Automatic + compositionRef: + name: xpinneds.pinned.diff.example.org diff --git a/cmd/crossplane/xr/testdata/diff/xr-with-ambiguous-selector.yaml b/cmd/crossplane/xr/testdata/diff/xr-with-ambiguous-selector.yaml new file mode 100644 index 00000000..86c1d6c4 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/xr-with-ambiguous-selector.yaml @@ -0,0 +1,11 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-resource + namespace: default +spec: + coolField: "test-value" + crossplane: + compositionSelector: + matchLabels: + provider: aws diff --git a/cmd/crossplane/xr/testdata/diff/xr-with-argocd-annotations.yaml b/cmd/crossplane/xr/testdata/diff/xr-with-argocd-annotations.yaml new file mode 100644 index 00000000..f9dcd869 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/xr-with-argocd-annotations.yaml @@ -0,0 +1,13 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-resource + namespace: default + annotations: + argocd.argoproj.io/tracking-id: "test-app:/XNopResource:default/test-resource" + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"ns.diff.example.org/v1alpha1","kind":"XNopResource","metadata":{"name":"test-resource","namespace":"default"}} + labels: + argocd.argoproj.io/instance: test-app +spec: + coolField: "new-value" diff --git a/cmd/crossplane/xr/testdata/diff/xr-with-argocd-mixed-changes.yaml b/cmd/crossplane/xr/testdata/diff/xr-with-argocd-mixed-changes.yaml new file mode 100644 index 00000000..3a399e9f --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/xr-with-argocd-mixed-changes.yaml @@ -0,0 +1,13 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-resource + namespace: default + annotations: + argocd.argoproj.io/tracking-id: "test-app:/XNopResource:default/test-resource" + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"ns.diff.example.org/v1alpha1","kind":"XNopResource","metadata":{"name":"test-resource","namespace":"default"}} + labels: + argocd.argoproj.io/instance: test-app +spec: + coolField: "modified-value" diff --git a/cmd/crossplane/xr/testdata/diff/xr-with-composition-ref.yaml b/cmd/crossplane/xr/testdata/diff/xr-with-composition-ref.yaml new file mode 100644 index 00000000..e417e325 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/xr-with-composition-ref.yaml @@ -0,0 +1,10 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-resource + namespace: default +spec: + coolField: "test-value" + crossplane: + compositionRef: + name: production-composition diff --git a/cmd/crossplane/xr/testdata/diff/xr-with-composition-selector.yaml b/cmd/crossplane/xr/testdata/diff/xr-with-composition-selector.yaml new file mode 100644 index 00000000..bf5bdc13 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/xr-with-composition-selector.yaml @@ -0,0 +1,12 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XNopResource +metadata: + name: test-resource + namespace: default +spec: + coolField: "test-value" + crossplane: + compositionSelector: + matchLabels: + environment: staging + provider: aws diff --git a/cmd/crossplane/xr/testdata/diff/xr-with-missing-defaults.yaml b/cmd/crossplane/xr/testdata/diff/xr-with-missing-defaults.yaml new file mode 100644 index 00000000..d1d9007d --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/xr-with-missing-defaults.yaml @@ -0,0 +1,9 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XTestDefaultResource +metadata: + name: test-resource-with-defaults + namespace: default +spec: + # Only specify one field, leaving others to use defaults from XRD + size: "large" + # Note: region, settings, and tags are not specified and should get defaults \ No newline at end of file diff --git a/cmd/crossplane/xr/testdata/diff/xr-with-overridden-defaults.yaml b/cmd/crossplane/xr/testdata/diff/xr-with-overridden-defaults.yaml new file mode 100644 index 00000000..53f5dfc2 --- /dev/null +++ b/cmd/crossplane/xr/testdata/diff/xr-with-overridden-defaults.yaml @@ -0,0 +1,19 @@ +apiVersion: ns.diff.example.org/v1alpha1 +kind: XTestDefaultResource +metadata: + name: test-resource-with-overrides + namespace: default +spec: + # Override the default region (default: us-east-1) + region: "us-west-2" + # Override the default size (default: small) + size: "xlarge" + # Partially override settings (default: {enabled: true, timeout: 30}) + settings: + enabled: false # Override default true + timeout: 60 # Override default 30 + # retries should get default value of 3 + # Override default tags (default: {environment: "development"}) + tags: + environment: "production" # Override default "development" + team: "platform" # New field, no default to override \ No newline at end of file From 4f4c4ff360dc3a217189f1be9851fa0d3636048f Mon Sep 17 00:00:00 2001 From: Jonathan Ogilvie Date: Fri, 6 Nov 2026 18:29:23 -0500 Subject: [PATCH 4/6] build(nix): run integration packages one at a time Each suite already runs its cases in parallel; three suites rendering through Docker at once starve each other into timeouts. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Jonathan Ogilvie --- nix/apps.nix | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nix/apps.nix b/nix/apps.nix index 43998fa5..89304216 100644 --- a/nix/apps.nix +++ b/nix/apps.nix @@ -77,9 +77,11 @@ in echo "integration-test: found no packages with //go:build integration test files" >&2 exit 1 fi + # -p 1: each package's cases already run in parallel, and several packages' + # rendering through Docker at once starve each other into timeouts. # -coverpkg credits the tests' coverage to the packages they exercise; # without it the profile only covers the test packages themselves. - go test -tags integration -covermode=count \ + go test -tags integration -p 1 -covermode=count \ -coverpkg=./... -coverprofile=coverage-integration.txt "''${pkgs[@]}" "$@" ''; } From 53b2465c584c5aa594456a55879521ff5ec094f5 Mon Sep 17 00:00:00 2001 From: Jonathan Ogilvie Date: Fri, 6 Nov 2026 18:32:04 -0500 Subject: [PATCH 5/6] refactor(simulate): rename ProjectResult to Result Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Jonathan Ogilvie --- .../project/integration_plan_output_test.go | 18 ++++----- .../project/integration_project_test.go | 4 +- .../project/integration_provider_plan_test.go | 14 +++---- internal/simulate/report.go | 14 +++---- internal/simulate/report_test.go | 20 +++++----- internal/simulate/result.go | 39 +++++++++---------- internal/simulate/result_test.go | 22 +++++------ internal/simulate/run.go | 4 +- internal/simulate/run_test.go | 14 +++---- internal/simulate/simtest/plan_assertions.go | 18 ++++----- .../simulate/simtest/structured_assertions.go | 6 +-- .../simtest/structured_assertions_test.go | 14 +++---- 12 files changed, 93 insertions(+), 94 deletions(-) diff --git a/cmd/crossplane/project/integration_plan_output_test.go b/cmd/crossplane/project/integration_plan_output_test.go index 08153a8a..dbed8e77 100644 --- a/cmd/crossplane/project/integration_plan_output_test.go +++ b/cmd/crossplane/project/integration_plan_output_test.go @@ -38,7 +38,7 @@ import ( ) // TestSimulatePlanOutput runs simulate's own output formats, -o plan and -// -o json's simulate.ProjectResult, for xr, composition and the plain project +// -o json's simulate.Result, 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) { @@ -134,7 +134,7 @@ 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.", + reason: "-o json emits simulate.Result: the XR as a changed composite, with both its plans grouped under it.", tt: &xrCase, args: append([]string{"-o", "json"}, xrArgs...), wantResult: &planShape{ @@ -178,7 +178,7 @@ Note: Provider plan not run: no provider diff server planned these resources. `, }, "CompPlanJSON": { - reason: "-o json emits simulate.ProjectResult: the Composition's revision impact, both affected XRs as changed composites, and their plans.", + reason: "-o json emits simulate.Result: the Composition's revision impact, both affected XRs as changed composites, and their plans.", compArgv: true, args: append([]string{"-o", "json"}, compArgs...), wantResult: &planShape{ @@ -272,9 +272,9 @@ Note: Provider plan not run: no provider diff server planned these resources. return } - 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 res.FormatVersion != plan.ResultFormatVersion || res.Errored { t.Errorf("\n%s\nsimulate -o json: want formatVersion %q and not errored, got %q, errored %t", tc.reason, plan.ResultFormatVersion, res.FormatVersion, res.Errored) @@ -286,7 +286,7 @@ Note: Provider plan not run: no provider diff server planned these resources. } } -// planShape is the part of a simulate.ProjectResult these cases pin: everything but +// planShape is the part of a simulate.Result these cases pin: everything but // the field changes, dry-run details and notes, which the plan text cases and // the mapper's unit tests cover. type planShape struct { @@ -303,7 +303,7 @@ type planEntry struct { Composite string } -func summarize(r simulate.ProjectResult) planShape { +func summarize(r simulate.Result) planShape { s := planShape{Summary: r.Summary, Composites: r.Composites, Compositions: r.Compositions} for _, p := range r.Plans { e := planEntry{Kind: p.Kind, Name: p.Name, Action: p.Action} @@ -407,9 +407,9 @@ func TestSimulatePinnedChangedFunction(t *testing.T) { if err != nil { t.Fatalf("\n%s\nsimulate: unexpected error: %v\nstderr:\n%s", tc.reason, err, stderr.String()) } - 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 len(res.Composites) != 1 { t.Fatalf("\n%s\nsimulate -o json: want 1 composite, got %+v", tc.reason, res.Composites) diff --git a/cmd/crossplane/project/integration_project_test.go b/cmd/crossplane/project/integration_project_test.go index 8148685b..e1a2d3e9 100644 --- a/cmd/crossplane/project/integration_project_test.go +++ b/cmd/crossplane/project/integration_project_test.go @@ -218,9 +218,9 @@ func TestProjectSimulateIntegration(t *testing.T) { 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) diff --git a/cmd/crossplane/project/integration_provider_plan_test.go b/cmd/crossplane/project/integration_provider_plan_test.go index 2d4918cf..561eb48f 100644 --- a/cmd/crossplane/project/integration_provider_plan_test.go +++ b/cmd/crossplane/project/integration_provider_plan_test.go @@ -102,7 +102,7 @@ func TestSimulateProviderPlan(t *testing.T) { wantOut, wantNotOut []string wantErr []string // check inspects the -o json result, when args ask for it. - check func(t *testing.T, res simulate.ProjectResult) + check func(t *testing.T, res simulate.Result) }{ "ProviderUpdate": { reason: "A provider's extra field change joins the composed resource's plan, and the cloud-layer note is gone.", @@ -110,7 +110,7 @@ func TestSimulateProviderPlan(t *testing.T) { fn: update(region), wantCode: dp.ExitCodeDiffDetected, wantNotOut: []string{"Provider plan not run"}, - check: func(t *testing.T, res simulate.ProjectResult) { + check: func(t *testing.T, res simulate.Result) { t.Helper() for _, p := range res.Plans { if p.Kind != "XDownstreamResource" { @@ -172,7 +172,7 @@ func TestSimulateProviderPlan(t *testing.T) { return nil, status.Error(codes.Internal, "boom") }, wantCode: dp.ExitCodeToolError, - check: func(t *testing.T, res simulate.ProjectResult) { + check: func(t *testing.T, res simulate.Result) { t.Helper() if !res.Errored { t.Errorf("want errored result, got %+v", res) @@ -200,7 +200,7 @@ func TestSimulateProviderPlan(t *testing.T) { return nil, st.Err() }, wantCode: dp.ExitCodeDiffDetected, - check: func(t *testing.T, res simulate.ProjectResult) { + check: func(t *testing.T, res simulate.Result) { t.Helper() if res.Errored || res.Summary.Approximate == 0 { t.Errorf("want non-errored result with approximate plans, got errored=%t summary=%+v", res.Errored, res.Summary) @@ -266,9 +266,9 @@ func TestSimulateProviderPlan(t *testing.T) { } } if tc.check != nil { - var res simulate.ProjectResult + var res simulate.Result if err := json.Unmarshal(stdout.Bytes(), &res); err != nil { - t.Fatalf("\n%s\nstdout is not a simulate.ProjectResult: %v\n%s\nstderr:\n%s", tc.reason, err, stdout.String(), stderr.String()) + t.Fatalf("\n%s\nstdout is not a simulate.Result: %v\n%s\nstderr:\n%s", tc.reason, err, stdout.String(), stderr.String()) } tc.check(t, res) } @@ -276,7 +276,7 @@ func TestSimulateProviderPlan(t *testing.T) { } } -func planJSON(t *testing.T, res simulate.ProjectResult) string { +func planJSON(t *testing.T, res simulate.Result) string { t.Helper() b, err := json.Marshal(res) if err != nil { diff --git a/internal/simulate/report.go b/internal/simulate/report.go index 5ea4cba8..6a6bed0e 100644 --- a/internal/simulate/report.go +++ b/internal/simulate/report.go @@ -36,7 +36,7 @@ import ( // plans, so its plans say nothing about the cloud resources. const noteProviderPlanNotRun = "Provider plan not run." -// Notes a caller sets on ProjectResult.Notes when no provider plans refined the +// Notes a caller sets on Result.Notes when no provider plans refined the // cluster diff, saying why. A result built from provider plans // carries no note: each approximate plan already says why it is approximate. const ( @@ -44,7 +44,7 @@ const ( NoteNoDiffServers = "Provider plan not run: no provider diff server planned these resources." ) -// FromXRReport maps an xr or project diff run onto a ProjectResult: one plan per +// FromXRReport maps an xr or project diff run onto a Result: one plan per // resource diff (the XR's own included), each grouped under its input XR or // claim, and one Composites entry per input. plans are the provider plans // the run collected, each merged into the engine's plan of a diff that @@ -52,7 +52,7 @@ const ( // means none ran. keep is the filter PlanDiffs used: a kept equal diff no // provider planned is dropped, as one whose provider plan is a no-op is. nil // keeps none. -func FromXRReport(r *dt.XRReport, plans *ProviderPlans, keep func(*unstructured.Unstructured) bool) ProjectResult { +func FromXRReport(r *dt.XRReport, plans *ProviderPlans, keep func(*unstructured.Unstructured) bool) Result { if r == nil { r = &dt.XRReport{} } @@ -86,11 +86,11 @@ func FromXRReport(r *dt.XRReport, plans *ProviderPlans, keep func(*unstructured. return a.result(slices.Concat(r.Warnings, dt.XRDryRunWarnings(r.Groups))) } -// FromCompReport maps a composition diff run onto a ProjectResult: a plan and a +// FromCompReport maps a composition diff run onto a Result: a plan and a // Compositions entry per Composition, and a Composites entry per affected // composite, with that composite's downstream plans grouped under it. plans // and keep are used as FromXRReport uses them. -func FromCompReport(o *dt.CompDiffOutput, plans *ProviderPlans, keep func(*unstructured.Unstructured) bool) ProjectResult { +func FromCompReport(o *dt.CompDiffOutput, plans *ProviderPlans, keep func(*unstructured.Unstructured) bool) Result { if o == nil { o = &dt.CompDiffOutput{} } @@ -362,8 +362,8 @@ func (a *assembler) addRunErrors(all, owned []dt.OutputError) { } } -func (a *assembler) result(warnings []dt.OutputWarning) ProjectResult { - res := NewProjectResult(a.plans, a.composites, a.errors) +func (a *assembler) result(warnings []dt.OutputWarning) Result { + res := NewResult(a.plans, a.composites, a.errors) if a.provider == nil { res.Notes = []string{noteProviderPlanNotRun} } diff --git a/internal/simulate/report_test.go b/internal/simulate/report_test.go index 7d5e69f0..241ee7b3 100644 --- a/internal/simulate/report_test.go +++ b/internal/simulate/report_test.go @@ -117,7 +117,7 @@ func TestFromCompReport(t *testing.T) { cases := map[string]struct { reason string in *dt.CompDiffOutput - want ProjectResult + want Result }{ "CompositionAndRevisionImpact": { reason: "A changed Composition is an ungrouped plan with a Compositions entry carrying its revision impact; affected XRs group their plans.", @@ -131,7 +131,7 @@ func TestFromCompReport(t *testing.T) { Diffs: diffs(rdiff(cmGVK, "cm", dt.DiffTypeRemoved, map[string]any{}, nil)), }}, }), - want: ProjectResult{ + want: Result{ Result: plan.Result{ FormatVersion: plan.ResultFormatVersion, Summary: plan.Summary{Update: 1, Destroy: 1}, @@ -161,7 +161,7 @@ func TestFromCompReport(t *testing.T) { filtered("deleting", dt.FilterReasonDeleting, ""), }, }), - want: ProjectResult{ + want: Result{ Result: plan.Result{ FormatVersion: plan.ResultFormatVersion, Summary: plan.Summary{NoOp: 1}, @@ -181,7 +181,7 @@ func TestFromCompReport(t *testing.T) { in: composition(dt.CompositionDiff{ ImpactAnalysis: []dt.XRImpact{{ObjectReference: xrRef, Status: dt.XRStatusUnchanged, Pinned: true}}, }), - want: ProjectResult{ + want: Result{ Result: plan.Result{ FormatVersion: plan.ResultFormatVersion, Summary: plan.Summary{NoOp: 1}, @@ -197,7 +197,7 @@ func TestFromCompReport(t *testing.T) { in: composition(dt.CompositionDiff{ ImpactAnalysis: []dt.XRImpact{{ObjectReference: xrRef, Status: dt.XRStatusChanged, Pinned: true, RenderedRevision: "net-6"}}, }), - want: ProjectResult{ + want: Result{ Result: plan.Result{ FormatVersion: plan.ResultFormatVersion, Summary: plan.Summary{NoOp: 1}, @@ -215,7 +215,7 @@ func TestFromCompReport(t *testing.T) { ImpactAnalysisSkipped: true, RevisionImpact: dt.RevisionImpact{ChangeScope: "metadata", CreatesRevision: true, RepointedComposites: 3}, }), - want: ProjectResult{ + want: Result{ Result: plan.Result{ FormatVersion: plan.ResultFormatVersion, Summary: plan.Summary{NoOp: 1}, @@ -234,7 +234,7 @@ func TestFromCompReport(t *testing.T) { in: composition(dt.CompositionDiff{ Error: &dt.OutputError{ResourceID: "Composition/net", Message: "cannot load"}, }), - want: ProjectResult{ + want: Result{ Result: plan.Result{ FormatVersion: plan.ResultFormatVersion, Errored: true, @@ -254,7 +254,7 @@ func TestFromCompReport(t *testing.T) { {Name: "net", ImpactAnalysis: []dt.XRImpact{{ObjectReference: xrRef, Status: dt.XRStatusUnchanged}}}, {Name: "db", ImpactAnalysis: []dt.XRImpact{filtered("qa", dt.FilterReasonDeleting, "")}}, }}, - want: ProjectResult{ + want: Result{ Result: plan.Result{ FormatVersion: plan.ResultFormatVersion, Summary: plan.Summary{NoOp: 2}, @@ -276,7 +276,7 @@ func TestFromCompReport(t *testing.T) { in: composition(dt.CompositionDiff{ ImpactAnalysis: []dt.XRImpact{{ObjectReference: xrRef, Status: dt.XRStatus("bogus")}}, }), - want: ProjectResult{ + want: Result{ Result: plan.Result{ FormatVersion: plan.ResultFormatVersion, Summary: plan.Summary{NoOp: 1}, @@ -296,7 +296,7 @@ func TestFromCompReport(t *testing.T) { }}, Errors: []dt.OutputError{xrErr, {Message: "interrupted"}}, }, - want: ProjectResult{ + want: Result{ Result: plan.Result{ FormatVersion: plan.ResultFormatVersion, Errored: true, diff --git a/internal/simulate/result.go b/internal/simulate/result.go index da2182fc..1e25976d 100644 --- a/internal/simulate/result.go +++ b/internal/simulate/result.go @@ -66,7 +66,7 @@ const ( CompositeReasonUnknown CompositeReason = "unknown" ) -// CompositeResult is the per-composite outcome of a project simulate run. +// CompositeResult is the per-composite outcome of a simulate run. type CompositeResult struct { Ref plan.ResourceRef `json:"ref"` Status CompositeStatus `json:"status"` @@ -125,7 +125,7 @@ type CompositionResult struct { // ImpactAnalysisSkipped means the composites that would adopt the // Composition's revision were deliberately not simulated, so no // changed, unchanged or error verdict exists for them. Composites the - // revision would not reach are still listed in ProjectResult.Composites. + // revision would not reach are still listed in Result.Composites. ImpactAnalysisSkipped bool `json:"impactAnalysisSkipped,omitempty"` } @@ -141,12 +141,11 @@ type RevisionImpact struct { PredictedRevision string `json:"predictedRevision,omitempty"` } -// ProjectResult is the serialized form of a project simulate run, for -o -// json|yaml: resource simulate's plan.Result plus the composites, -// compositions, errors, warnings and notes only a project run has. Its -// summary serializes as Result.Summary plus a deferred count taken from +// Result is the serialized form of a simulate run, for -o json|yaml: +// plan.Result plus the composites, compositions, errors, warnings and notes a +// diff run has. Its summary serializes as plan.Result's Summary plus a deferred count taken from // Composites. -type ProjectResult struct { +type Result struct { plan.Result Composites []CompositeResult `json:"composites,omitempty"` @@ -158,17 +157,17 @@ type ProjectResult struct { Notes []string `json:"notes,omitempty"` } -// NewProjectResult assembles the ProjectResult for the given plans and +// NewResult assembles the Result for the given plans and // composites; errs marks the result errored. -func NewProjectResult(plans []*plan.ResourcePlan, composites []CompositeResult, errs []Error) ProjectResult { - res := ProjectResult{Result: plan.NewResult(plans), Composites: composites, Errors: errs} +func NewResult(plans []*plan.ResourcePlan, composites []CompositeResult, errs []Error) Result { + res := Result{Result: plan.NewResult(plans), Composites: composites, Errors: errs} res.Errored = res.Errored || len(errs) > 0 return res } // Deferred counts the composites pinned to a composition revision, which // applying changes nothing for. It does not count as a change. -func (r ProjectResult) Deferred() int { +func (r Result) Deferred() int { n := 0 for _, c := range r.Composites { if c.Status == CompositeDeferred { @@ -182,7 +181,7 @@ func (r ProjectResult) Deferred() int { // ahead of the plans, which can be long, and with the deferred count inside // summary. YAML goes through it too. Decoding needs no counterpart: summary // decodes into Result.Summary, and Deferred is counted from Composites. -func (r ProjectResult) MarshalJSON() ([]byte, error) { +func (r Result) MarshalJSON() ([]byte, error) { type summary struct { plan.Summary @@ -211,9 +210,9 @@ func (r ProjectResult) MarshalJSON() ([]byte, error) { }) } -// A Renderer writes a project simulate result to w in one output format. +// A Renderer writes a simulate result to w in one output format. type Renderer interface { - Render(w io.Writer, r ProjectResult) error + Render(w io.Writer, r Result) error } // NewRenderer returns the Renderer for the given output format: one of @@ -230,11 +229,11 @@ func NewRenderer(format string) (Renderer, error) { return nil, errors.Errorf("unknown output format %q", format) } -// jsonRenderer renders the ProjectResult as indented JSON. +// jsonRenderer renders the Result as indented JSON. type jsonRenderer struct{} // Render writes the result to w as JSON. -func (jsonRenderer) Render(w io.Writer, r ProjectResult) error { +func (jsonRenderer) Render(w io.Writer, r Result) error { out, err := json.MarshalIndent(r, "", " ") if err != nil { return errors.Wrap(err, "cannot marshal plans to JSON") @@ -243,11 +242,11 @@ func (jsonRenderer) Render(w io.Writer, r ProjectResult) error { return err } -// yamlRenderer renders the ProjectResult as YAML. +// yamlRenderer renders the Result as YAML. type yamlRenderer struct{} // Render writes the result to w as YAML. -func (yamlRenderer) Render(w io.Writer, r ProjectResult) error { +func (yamlRenderer) Render(w io.Writer, r Result) error { out, err := yaml.Marshal(r) if err != nil { return errors.Wrap(err, "cannot marshal plans to YAML") @@ -265,7 +264,7 @@ func (yamlRenderer) Render(w io.Writer, r ProjectResult) error { type planRenderer struct{} // Render writes the result to w in the human-readable plan format. -func (planRenderer) Render(w io.Writer, r ProjectResult) error { +func (planRenderer) Render(w io.Writer, r Result) error { impacts := revisionImpactLines(r.Compositions) if len(r.Composites) == 0 && !slices.ContainsFunc(r.Plans, func(p *plan.ResourcePlan) bool { return p.Composite != nil }) { for _, p := range r.Plans { @@ -316,7 +315,7 @@ func revisionImpactLines(comps []CompositionResult) map[plan.ResourceRef]string // renderGrouped prints ungrouped plans first, then each composite (in // Composites order, then any only plans name) with its plans indented // beneath. A composite's own plan is omitted when it is a no-op. -func renderGrouped(w io.Writer, r ProjectResult, impacts map[plan.ResourceRef]string) { +func renderGrouped(w io.Writer, r Result, impacts map[plan.ResourceRef]string) { var refs []plan.ResourceRef byRef := map[plan.ResourceRef][]*plan.ResourcePlan{} status := map[plan.ResourceRef]CompositeResult{} diff --git a/internal/simulate/result_test.go b/internal/simulate/result_test.go index c65078f4..8da19d12 100644 --- a/internal/simulate/result_test.go +++ b/internal/simulate/result_test.go @@ -30,7 +30,7 @@ import ( func TestRenderPlanGrouped(t *testing.T) { xr := plan.ResourceRef{APIVersion: "example.org/v1", Kind: "XNetwork", Name: "prod-network"} pinned := plan.ResourceRef{APIVersion: "example.org/v1", Kind: "XNetwork", Name: "staging-network"} - r := ProjectResult{ + r := Result{ Result: plan.Result{ Summary: plan.Summary{Update: 1, Destroy: 1, NoOp: 1}, Plans: []*plan.ResourcePlan{ @@ -80,7 +80,7 @@ Plan: 0 to add, 1 to change, 1 to destroy. func TestRenderPlanCompositeDetail(t *testing.T) { xr := plan.ResourceRef{APIVersion: "example.org/v1", Kind: "XNetwork", Name: "prod-network"} - r := ProjectResult{ + r := Result{ Result: plan.Result{}, Composites: []CompositeResult{{Ref: xr, Status: CompositeUnchanged, Pinned: true, Detail: "rendered against pinned revision net-6 with the changed functions"}}, } @@ -103,7 +103,7 @@ func TestRenderPlanPinnedHeader(t *testing.T) { xr := plan.ResourceRef{APIVersion: "example.org/v1", Kind: "XNetwork", Name: "prod-network"} var buf bytes.Buffer r, _ := NewRenderer(plan.FormatPlan) - if err := r.Render(&buf, ProjectResult{Composites: []CompositeResult{{Ref: xr, Status: CompositeUnchanged, Pinned: true}}}); err != nil { + if err := r.Render(&buf, Result{Composites: []CompositeResult{{Ref: xr, Status: CompositeUnchanged, Pinned: true}}}); err != nil { t.Fatalf("Render(plan): unexpected error: %v", err) } if want := "XR: XNetwork.example.org prod-network (pinned)\n"; !strings.HasPrefix(buf.String(), want) { @@ -116,7 +116,7 @@ func TestRenderPlanCompositions(t *testing.T) { same := plan.ResourceRef{APIVersion: comp.APIVersion, Kind: comp.Kind, Name: "same"} unknown := plan.ResourceRef{APIVersion: comp.APIVersion, Kind: comp.Kind, Name: "unknown"} xr := plan.ResourceRef{APIVersion: "example.org/v1", Kind: "XNetwork", Name: "prod-network"} - r := ProjectResult{ + r := Result{ Result: plan.Result{ Summary: plan.Summary{Update: 2, NoOp: 2}, Plans: []*plan.ResourcePlan{ @@ -174,7 +174,7 @@ Note: Provider plan not run. func TestRenderJSONProjectFields(t *testing.T) { xr := plan.ResourceRef{APIVersion: "example.org/v1", Kind: "XNetwork", Name: "prod-network"} - in := ProjectResult{ + in := Result{ Composites: []CompositeResult{{Ref: xr, Status: CompositeDeferred, Reason: "manualPolicy"}}, Compositions: []CompositionResult{{ Ref: plan.ResourceRef{APIVersion: "apiextensions.crossplane.io/v1", Kind: "Composition", Name: "net"}, @@ -205,7 +205,7 @@ func TestRenderJSONProjectFields(t *testing.T) { if err := r.Render(&buf, in); err != nil { t.Fatalf("Render(json): unexpected error: %v", err) } - got := ProjectResult{} + got := Result{} if err := json.Unmarshal(buf.Bytes(), &got); err != nil { t.Fatalf("Render(json): output is not valid JSON: %v", err) } @@ -248,7 +248,7 @@ func TestRenderJSONProjectFields(t *testing.T) { func TestRenderYAMLSummaryDeferred(t *testing.T) { xr := plan.ResourceRef{APIVersion: "example.org/v1", Kind: "XNetwork", Name: "prod-network"} - in := NewProjectResult(nil, []CompositeResult{{Ref: xr, Status: CompositeDeferred}}, nil) + in := NewResult(nil, []CompositeResult{{Ref: xr, Status: CompositeDeferred}}, nil) var buf bytes.Buffer r, _ := NewRenderer(plan.FormatYAML) if err := r.Render(&buf, in); err != nil { @@ -260,7 +260,7 @@ func TestRenderYAMLSummaryDeferred(t *testing.T) { } } -func TestNewProjectResult(t *testing.T) { +func TestNewResult(t *testing.T) { xr := plan.ResourceRef{APIVersion: "example.org/v1", Kind: "XNetwork", Name: "prod-network"} pinned := plan.ResourceRef{APIVersion: "example.org/v1", Kind: "XNetwork", Name: "staging-network"} plans := []*plan.ResourcePlan{{APIVersion: "v1", Kind: "ConfigMap", Name: "cm", Action: plan.ActionCreate, Composite: &xr}} @@ -293,12 +293,12 @@ func TestNewProjectResult(t *testing.T) { } for name, tc := range cases { t.Run(name, func(t *testing.T) { - got := NewProjectResult(plans, composites, tc.errs) + got := NewResult(plans, composites, tc.errs) if diff := cmp.Diff(tc.want, want{summary: got.Summary, deferred: got.Deferred(), errored: got.Errored}, cmp.AllowUnexported(want{})); diff != "" { - t.Errorf("\n%s\nNewProjectResult(...): -want, +got:\n%s", tc.reason, diff) + t.Errorf("\n%s\nNewResult(...): -want, +got:\n%s", tc.reason, diff) } if got.FormatVersion != plan.ResultFormatVersion { - t.Errorf("\n%s\nNewProjectResult(...).FormatVersion: want %q, got %q", tc.reason, plan.ResultFormatVersion, got.FormatVersion) + t.Errorf("\n%s\nNewResult(...).FormatVersion: want %q, got %q", tc.reason, plan.ResultFormatVersion, got.FormatVersion) } }) } diff --git a/internal/simulate/run.go b/internal/simulate/run.go index df489dad..f442d656 100644 --- a/internal/simulate/run.go +++ b/internal/simulate/run.go @@ -482,7 +482,7 @@ func (c *Flags) renderCompReport(k *kong.Context, logger logging.Logger, o *dt.C // whether the run changed anything, provider plans included, and runErr // with any render failure joined to it, so neither is lost. changed is // whether the report itself has changes. -func (c *Flags) renderReport(k *kong.Context, logger logging.Logger, runErr error, res ProjectResult, changed bool, warnings []dt.OutputWarning, renderDiff func(renderer.DiffOptions) error) (bool, error) { +func (c *Flags) renderReport(k *kong.Context, logger logging.Logger, runErr error, res Result, changed bool, warnings []dt.OutputWarning, renderDiff func(renderer.DiffOptions) error) (bool, error) { opts := c.diffOptions(k) plans := c.providerPlans() if plans != nil { @@ -512,7 +512,7 @@ func (c *Flags) renderReport(k *kong.Context, logger logging.Logger, runErr erro // writes derived, the warnings derived from the report, to stderr, as the // engine's renderers do. The run's other warnings went to stderr when they // were raised. -func (c *Flags) renderPlan(k *kong.Context, res ProjectResult, derived []dt.OutputWarning) error { +func (c *Flags) renderPlan(k *kong.Context, res Result, derived []dt.OutputWarning) error { r, err := NewRenderer(c.Output) if err != nil { return err diff --git a/internal/simulate/run_test.go b/internal/simulate/run_test.go index a8c4de47..f65061e0 100644 --- a/internal/simulate/run_test.go +++ b/internal/simulate/run_test.go @@ -344,7 +344,7 @@ func TestSimulateDiffOptions(t *testing.T) { } // TestSimulateRenderReports pins which renderer each -o value selects, for -// both report types: ProjectResult for plan, json and yaml, and the engine's +// both report types: Result for plan, json and yaml, and the engine's // human diff for diff. func TestSimulateRenderReports(t *testing.T) { xr := corev1.ObjectReference{APIVersion: "example.org/v1", Kind: "XNetwork", Name: "prod"} @@ -361,16 +361,16 @@ func TestSimulateRenderReports(t *testing.T) { ImpactAnalysis: []dt.XRImpact{{ObjectReference: xr, Status: dt.XRStatusUnchanged}}, }}} - // isPlanResult checks out decodes as a ProjectResult naming the composite. + // isPlanResult checks out decodes as a Result naming the composite. isPlanResult := func(unmarshal func([]byte, any) error) func(*testing.T, string) { return func(t *testing.T, out string) { t.Helper() - var r ProjectResult + var r Result if err := unmarshal([]byte(out), &r); err != nil { - t.Fatalf("output is not a ProjectResult: %v\n%s", err, out) + t.Fatalf("output is not a Result: %v\n%s", err, out) } if r.FormatVersion != plan.ResultFormatVersion || len(r.Composites) != 1 || r.Composites[0].Ref.Name != "prod" { - t.Errorf("want a ProjectResult for composite prod, got:\n%s", out) + t.Errorf("want a Result for composite prod, got:\n%s", out) } } } @@ -398,13 +398,13 @@ func TestSimulateRenderReports(t *testing.T) { wantComp: contains("[=] Composition.apiextensions.crossplane.io net (no-op)\n Applying this creates a new CompositionRevision, which 1 composite would adopt.", "XR: XNetwork.example.org prod", "Note: "), }, "JSON": { - reason: "-o json should print ProjectResult as JSON.", + reason: "-o json should print Result as JSON.", cmd: Flags{Output: "json"}, wantXR: isPlanResult(json.Unmarshal), wantComp: isPlanResult(json.Unmarshal), }, "YAML": { - reason: "-o yaml should print ProjectResult as YAML.", + reason: "-o yaml should print Result as YAML.", cmd: Flags{Output: "yaml"}, wantXR: isPlanResult(func(b []byte, v any) error { return yaml.Unmarshal(b, v) }), wantComp: isPlanResult(func(b []byte, v any) error { return yaml.Unmarshal(b, v) }), diff --git a/internal/simulate/simtest/plan_assertions.go b/internal/simulate/simtest/plan_assertions.go index a3c2615e..e3582028 100644 --- a/internal/simulate/simtest/plan_assertions.go +++ b/internal/simulate/simtest/plan_assertions.go @@ -18,9 +18,9 @@ import ( "github.com/crossplane/cli/v2/internal/simulate" ) -// The assertions below read project simulate's structured output, simulate.ProjectResult, behind the builder API +// The assertions below read project simulate's structured output, simulate.Result, behind the builder API // in structured_assertions.go. The builders speak crossplane-diff's vocabulary (added/modified/removed -// changes, xrs[] entries, impactAnalysis[] entries); this file maps each onto its simulate.ProjectResult home: +// changes, xrs[] entries, impactAnalysis[] entries); this file maps each onto its simulate.Result home: // // - a change is a ResourcePlan, its type the plan's Action (create/update/delete); // - a field value or old/new pair is read from the plan's leaf FieldChanges, a non-leaf path being @@ -45,17 +45,17 @@ func changeAction(changeType string) plan.Action { } // statusFiltered is crossplane-diff's status for a composite left out of impact analysis, which -// simulate.ProjectResult splits into deferred (Manual policy) and skipped (any other reason). +// simulate.Result splits into deferred (Manual policy) and skipped (any other reason). const statusFiltered = "filtered" -// AssertStructured checks a simulate.ProjectResult against expected, running each section only if it was +// AssertStructured checks a simulate.Result against expected, running each section only if it was // expected. Accepts any builder level, so callers don't need And()/AndXR()/AndComp(). func AssertStructured(t *testing.T, jsonOutput string, e Expectation) { t.Helper() expected := e.expectation() - var res simulate.ProjectResult + var res simulate.Result if err := json.Unmarshal([]byte(jsonOutput), &res); err != nil { t.Fatalf("failed to parse plan result JSON output: %v\nOutput was:\n%s", err, jsonOutput) } @@ -105,7 +105,7 @@ func AssertStructured(t *testing.T, jsonOutput string, e Expectation) { } } -func assertComposition(t *testing.T, res simulate.ProjectResult, want *CompositionExpectation) { +func assertComposition(t *testing.T, res simulate.Result, want *CompositionExpectation) { t.Helper() i := slices.IndexFunc(res.Compositions, func(c simulate.CompositionResult) bool { return c.Ref.Name == want.name }) @@ -234,7 +234,7 @@ func assertRevisionImpact(t *testing.T, label string, got *simulate.RevisionImpa } } -func assertXRImpact(t *testing.T, res simulate.ProjectResult, label string, composites []simulate.CompositeResult, want *XRImpactExpectation) { +func assertXRImpact(t *testing.T, res simulate.Result, label string, composites []simulate.CompositeResult, want *XRImpactExpectation) { t.Helper() i := slices.IndexFunc(composites, func(c simulate.CompositeResult) bool { @@ -303,7 +303,7 @@ func assertXRImpact(t *testing.T, res simulate.ProjectResult, label string, comp } } -// statusMatches compares a composite's status with crossplane-diff's, whose "filtered" simulate.ProjectResult +// statusMatches compares a composite's status with crossplane-diff's, whose "filtered" simulate.Result // splits into deferred and skipped. func statusMatches(got simulate.CompositeStatus, want string) bool { if want == statusFiltered { @@ -319,7 +319,7 @@ func statusMatches(got simulate.CompositeStatus, want string) bool { // Each expectation consumes the entry it matches, so two expectations can never both resolve to the // same entry, and the entry count is pinned once the test has an opinion on xrs[] at all. Without those // guards a duplicated identity satisfied every expectation that matched it (issues #477, #488). -func assertXRExpectations(t *testing.T, res simulate.ProjectResult, expected []*XRExpectation) { +func assertXRExpectations(t *testing.T, res simulate.Result, expected []*XRExpectation) { t.Helper() if len(expected) == 0 { diff --git a/internal/simulate/simtest/structured_assertions.go b/internal/simulate/simtest/structured_assertions.go index c4999091..f59f9cbf 100644 --- a/internal/simulate/simtest/structured_assertions.go +++ b/internal/simulate/simtest/structured_assertions.go @@ -1,4 +1,4 @@ -// Package simtest asserts on project simulate's structured output, simulate.ProjectResult, through +// Package simtest asserts on project simulate's structured output, simulate.Result, through // crossplane-diff's expectation builders. package simtest @@ -14,7 +14,7 @@ type Expectation interface { expectation() *Expected } -// Expected is a fluent builder for test expectations on a simulate.ProjectResult, covering both xr-style sections +// Expected is a fluent builder for test expectations on a simulate.Result, covering both xr-style sections // (summary, flat resources, errors, XR groups) and composition-style sections. Each section is asserted // only if it was expected. type Expected struct { @@ -567,7 +567,7 @@ type XRImpactExpectation struct { status string // "changed", "unchanged", "error", "filtered" filterReason string // "manualPolicy", "revisionSelectorMismatch", "deleting"; only checked when set errorContains string // substring of an "error" impact's Error; only checked when set - errorIDs []string // resourceIDs the simulate.ProjectResult errors[] attributed to the XR must contain + errorIDs []string // resourceIDs the simulate.Result errors[] attributed to the XR must contain downstreamSummary *expectedSummary downstreamResources []*ResourceExpectation } diff --git a/internal/simulate/simtest/structured_assertions_test.go b/internal/simulate/simtest/structured_assertions_test.go index e1720cf4..9462375b 100644 --- a/internal/simulate/simtest/structured_assertions_test.go +++ b/internal/simulate/simtest/structured_assertions_test.go @@ -12,8 +12,8 @@ import ( "github.com/crossplane/cli/v2/internal/simulate" ) -// resultJSON serializes r the way project simulate's -o json does. -func resultJSON(t *testing.T, r simulate.ProjectResult) string { +// resultJSON serializes r the way simulate's -o json does. +func resultJSON(t *testing.T, r simulate.Result) string { t.Helper() out, err := json.Marshal(r) @@ -55,7 +55,7 @@ func bucket(region string) *unstructured.Unstructured { // xrResult is an xr run: one changed XR (a modified Bucket, a created ConfigMap and a deleted Secret), // one unchanged XR, and one errored XR. -func xrResult() simulate.ProjectResult { +func xrResult() simulate.Result { plans := []*plan.ResourcePlan{ { APIVersion: "example.org/v1", Kind: "Bucket", Name: "bucket-a", Namespace: "default", Action: plan.ActionUpdate, Composite: &xrA, @@ -81,7 +81,7 @@ func xrResult() simulate.ProjectResult { {APIVersion: xrC.APIVersion, Kind: xrC.Kind, Name: xrC.Name, Namespace: xrC.Namespace, Error: "cannot get composition", Composite: &xrC}, } - return simulate.ProjectResult{ + return simulate.Result{ Result: plan.Result{ Summary: plan.Summarize(plans), Plans: plans, @@ -216,8 +216,8 @@ var ( // compResult is a comp run over two compositions: comp-a reaches a changed, an unchanged and a deleting // composite; comp-b reaches only a composite whose analysis failed. -func compResult() simulate.ProjectResult { - return simulate.ProjectResult{ +func compResult() simulate.Result { + return simulate.Result{ Result: plan.Result{ Plans: []*plan.ResourcePlan{ { @@ -425,7 +425,7 @@ func TestAssertFieldsAndWarnings(t *testing.T) { } // TestAssertStructuredCombined checks that xr-style and composition-style sections of one Expect() -// are asserted against a single simulate.ProjectResult, and that each still fails where it did on its own. +// are asserted against a single simulate.Result, and that each still fails where it did on its own. func TestAssertStructuredCombined(t *testing.T) { res := xrResult() res.Compositions = []simulate.CompositionResult{{Ref: compA, RevisionImpact: &simulate.RevisionImpact{ChangeScope: "spec", CreatesRevision: true, RepointedComposites: 1}}} From f7354532b10ee252766ea21520abddf1d7c571a0 Mon Sep 17 00:00:00 2001 From: Jonathan Ogilvie Date: Sun, 8 Nov 2026 17:46:34 -0500 Subject: [PATCH 6/6] test(simulate): fail cases whose structured expectation would not run Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Jonathan Ogilvie --- internal/testutils/integration/simharness/cases.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/testutils/integration/simharness/cases.go b/internal/testutils/integration/simharness/cases.go index 1718c570..4fe44816 100644 --- a/internal/testutils/integration/simharness/cases.go +++ b/internal/testutils/integration/simharness/cases.go @@ -181,6 +181,12 @@ func RunIntegrationTest(t *testing.T, testType TestType, tt IntegrationTestCase) args = append(args, "--no-color") } + // A structured expectation is only asserted for JSON output; without + // it the case would silently pass on the empty text-output check. + if tt.ExpectedStructured != nil && tt.OutputFormat != "json" { + t.Fatalf("ExpectedStructured is set but OutputFormat is %q; set OutputFormat to \"json\" so the structured assertion runs", tt.OutputFormat) + } + // Add JSON output format if specified if tt.OutputFormat == "json" { args = append(args, "--output=json")