Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,46 @@ jobs:
file: result/coverage.txt
token: ${{ secrets.CODECOV_TOKEN }}

# The integration tests run CLI commands against an envtest API server. They
# run outside the Nix sandbox (and can reach the runner's Docker daemon), so
# they use the integration-test app rather than a check, and cache Go modules
# and build output themselves.
integration-tests:
runs-on: ubuntu-24.04

steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

- name: Install Nix
uses: cachix/install-nix-action@75c75c31ebece974e48ebc36b4ad125bd97bd97b # v31

- name: Setup Cachix
uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
with:
name: crossplane
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}

- name: Cache Go modules and build cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-integration-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-integration-

- name: Run Integration Tests
run: nix run .#integration-test --option warn-dirty false

- name: Publish Integration Test Coverage
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7
with:
flags: integration
file: coverage-integration.txt
token: ${{ secrets.CODECOV_TOKEN }}

# Build all artifacts
build-artifacts:
permissions:
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,6 @@ AGENTS.md

# Ignore local dev environment variables
.env

# Integration test coverage (nix run .#integration-test)
/coverage-integration.txt
4 changes: 4 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ run:
# runs our unit tests. Lint them anyway, so they don't rot.
build-tags:
- compilegate
# The integration tests need envtest's binaries, so they sit behind the
# integration tag and run in their own target, not the unit tests. Lint
# them anyway.
- integration

output:
formats:
Expand Down
16 changes: 16 additions & 0 deletions cmd/crossplane/trace/testdata/integration/composed-failing.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: nop.example.org/v1alpha1
kind: NopResource
metadata:
annotations:
crossplane.io/composition-resource-name: failing
name: test-claim-xr-failing
spec:
forProvider:
configData: claim-value
status:
conditions:
- type: Synced
status: "False"
reason: ReconcileError
message: cannot observe external resource
lastTransitionTime: "2026-01-01T00:00:00Z"
19 changes: 19 additions & 0 deletions cmd/crossplane/trace/testdata/integration/composed-ready.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
apiVersion: nop.example.org/v1alpha1
kind: NopResource
metadata:
annotations:
crossplane.io/composition-resource-name: ready
name: test-claim-xr-ready
spec:
forProvider:
configData: claim-value
status:
conditions:
- type: Synced
status: "True"
reason: ReconcileSuccess
lastTransitionTime: "2026-01-01T00:00:00Z"
- type: Ready
status: "True"
reason: Available
lastTransitionTime: "2026-01-01T00:00:00Z"
49 changes: 49 additions & 0 deletions cmd/crossplane/trace/testdata/integration/crds/nopresource.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# A stand-in for a provider's managed resource, which the XR composes. It has
# no status subresource, so a fixture's status conditions are stored on create.
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: nopresources.nop.example.org
spec:
group: nop.example.org
names:
kind: NopResource
listKind: NopResourceList
plural: nopresources
singular: nopresource
scope: Cluster
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
forProvider:
type: object
properties:
configData:
type: string
status:
type: object
properties:
conditions:
type: array
items:
type: object
properties:
type:
type: string
status:
type: string
reason:
type: string
message:
type: string
lastTransitionTime:
type: string
format: date-time
60 changes: 60 additions & 0 deletions cmd/crossplane/trace/testdata/integration/setup.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# A v1 XRD with a claim, its Composition, and a claim bound to the XR in
# xr.yaml, as Crossplane's claim syncer binds it.
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xnops.trace.example.org
spec:
group: trace.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
---
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: xnops.trace.example.org
spec:
compositeTypeRef:
apiVersion: trace.example.org/v1alpha1
kind: XNop
mode: Pipeline
pipeline:
- step: generate-resources
functionRef:
name: function-dummy
---
apiVersion: v1
kind: Namespace
metadata:
name: test-namespace
---
apiVersion: trace.example.org/v1alpha1
kind: NopClaim
metadata:
name: test-claim
namespace: test-namespace
spec:
coolField: claim-value
compositionRef:
name: xnops.trace.example.org
resourceRef:
apiVersion: trace.example.org/v1alpha1
kind: XNop
name: test-claim-xr
15 changes: 15 additions & 0 deletions cmd/crossplane/trace/testdata/integration/xr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# The XR backing the claim in setup.yaml. Start fills in its resourceRefs from
# the composed resources it is set up with.
apiVersion: trace.example.org/v1alpha1
kind: XNop
metadata:
name: test-claim-xr
spec:
claimRef:
apiVersion: trace.example.org/v1alpha1
kind: NopClaim
name: test-claim
namespace: test-namespace
coolField: claim-value
compositionRef:
name: xnops.trace.example.org
107 changes: 107 additions & 0 deletions cmd/crossplane/trace/trace_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//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 trace

import (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
"testing"

"github.com/alecthomas/kong"
"github.com/google/go-cmp/cmp"

"github.com/crossplane/crossplane-runtime/v2/pkg/logging"

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

func TestMain(m *testing.M) {
integration.ReapOrphanedEnvtest(func(format string, args ...any) { fmt.Fprintf(os.Stderr, format+"\n", args...) })
os.Exit(m.Run())
}

// TestTraceIntegration traces a claim through its XR to the resources the XR composes, read from a real API
// server, and checks the tree the default printer draws.
func TestTraceIntegration(t *testing.T) {
cluster := integration.Start(t, integration.Setup{
CRDDirs: []string{"testdata/integration/crds"},
SetupFiles: []string{"testdata/integration/setup.yaml"},
CrossplaneManagedResources: []integration.HierarchicalOwnershipRelation{
integration.Composes("testdata/integration/xr.yaml",
"testdata/integration/composed-ready.yaml",
"testdata/integration/composed-failing.yaml"),
},
XRDAPIVersion: integration.XRDV1,
})

kubeconfig := filepath.Join(t.TempDir(), "kubeconfig")
if err := os.WriteFile(kubeconfig, cluster.Kubeconfig, 0o600); err != nil {
t.Fatalf("cannot write kubeconfig: %v", err)
}

// trace loads its kubeconfig the way kubectl does.
t.Setenv("KUBECONFIG", kubeconfig)

var c Cmd

parser, err := kong.New(&c)
if err != nil {
t.Fatalf("kong.New(): %v", err)
}

kongCtx, err := parser.Parse([]string{"nopclaim", "test-claim", "--namespace=test-namespace"})
if err != nil {
t.Fatalf("Parse(): %v", err)
}

var stdout bytes.Buffer
kongCtx.Stdout = &stdout

if err := c.Run(kongCtx, logging.NewNopLogger()); err != nil {
t.Fatalf("Run(): %v", err)
}

// Children sort by apiVersion, kind and name, so failing comes before ready. No controller runs under envtest,
// so the claim and XR have no conditions.
//nolint:dupword // Columns of the table repeat values.
want := `
NAME SYNCED READY STATUS
NopClaim/test-claim (test-namespace) - -
└─ XNop/test-claim-xr - -
├─ NopResource/test-claim-xr-failing False - ReconcileError: cannot observe external resource
└─ NopResource/test-claim-xr-ready True True Available
`

if diff := cmp.Diff(trimLines(want), trimLines(stdout.String())); diff != "" {
t.Errorf("trace output: -want, +got:\n%s", diff)
}
}

// trimLines drops leading and trailing blank lines, and the padding tabwriter leaves at the end of a line.
func trimLines(s string) string {
lines := strings.Split(strings.TrimSpace(s), "\n")
for i, l := range lines {
lines[i] = strings.TrimRight(l, " ")
}

return strings.Join(lines, "\n")
}
8 changes: 8 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@
# Vendor-dependency derivation, used by `nix run .#tidy` to refresh
# the hash in nix/vendor-hashes.nix.
crossplane-vendor = build.vendor.root;
# Crossplane's CRDs, used by `nix run .#tidy` to refresh the hash in
# nix/crossplane-crds.nix. Wrapped in a derivation because that file
# evaluates to a path inside the fetched source, and packages must be
# derivations; building it fetches (and hash-checks) that source.
crossplane-crds = pkgs.runCommand "crossplane-crds" { } ''
ln -s ${import ./nix/crossplane-crds.nix { inherit pkgs; }} $out
'';
}
);

Expand Down Expand Up @@ -137,6 +144,7 @@
in
{
test = apps.test { };
integration-test = apps.integration-test { };
lint = apps.lint { };
generate = apps.generate { };
tidy = apps.tidy { };
Expand Down
Loading
Loading