Skip to content
Merged
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
59 changes: 52 additions & 7 deletions cmd/crossplane/validate/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,26 +32,63 @@ import (
"github.com/crossplane/crossplane-runtime/v2/pkg/version"

"github.com/crossplane/cli/v2/cmd/crossplane/common/load"
pkgvalidate "github.com/crossplane/cli/v2/pkg/validate"
"github.com/crossplane/cli/v2/pkg/validate/output"

_ "embed"
)

//go:embed help/validate.md
var helpDetail string

// errWriteOutput is the error message wrapped around I/O failures when the
// validate command writes to its output writer.
const errWriteOutput = "cannot write output"
Comment on lines +44 to +46

@coderabbitai coderabbitai Bot Jun 3, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Unused constant: errWriteOutput is defined but never referenced.

Thanks for the thoughtful organization here! I notice this constant is declared but the actual error wrapping on line 166 uses a different string ("cannot render validation result"). Could you clarify whether this constant was intended to be used there, or should it be removed as dead code?

🧹 Suggested fix: either use the constant or remove it

Option A — use the constant:

-	if err := c.Output.Render(result, k.Stdout, render.Options{SkipSuccessResults: c.SkipSuccessResults}); err != nil {
-		return errors.Wrap(err, "cannot render validation result")
-	}
+	if err := c.Output.Render(result, k.Stdout, render.Options{SkipSuccessResults: c.SkipSuccessResults}); err != nil {
+		return errors.Wrap(err, errWriteOutput)
+	}

Option B — remove the unused constant:

-// errWriteOutput is the error message wrapped around I/O failures when the
-// validate command writes to its output writer.
-const errWriteOutput = "cannot write output"
-
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// errWriteOutput is the error message wrapped around I/O failures when the
// validate command writes to its output writer.
const errWriteOutput = "cannot write output"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/crossplane/validate/cmd.go` around lines 44 - 46, The constant
errWriteOutput is declared but not used; either replace the literal error string
used when wrapping I/O failures ("cannot render validation result") with the
errWriteOutput constant (so update the error wrap site in the validate command
to use errWriteOutput) or remove the unused errWriteOutput declaration entirely;
locate the validate output-writing code (the error wrap around
rendering/printing the validation result) and apply one of these two changes so
there is no dead/duplicated error message.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skipping this one — errWriteOutput is used in manager.go:315 (the "schemas does not exist, downloading: ..." Fprintln wrap), so removing it would break the build. The constant lives in cmd.go because the prior monolithic validate.go was where it was originally declared; cmd.go is now the only top-level non-test file in the package, hence the placement. Happy to move it to a dedicated errors.go if you'd prefer it not appear adjacent to a Cmd that doesn't use it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "errWriteOutput" --type=go -C3

Length of output: 1221



@jcogilvie, you're absolutely right — confirmed in manager.go:315:

return errors.Wrapf(err, errWriteOutput)

My apologies for the false positive! I missed the cross-file usage within the same package. The placement in cmd.go as the package's sole non-test top-level file makes complete sense, and no change is needed here. Thanks for the clear explanation!


🧠 Learnings used
Learnt from: adamwg
Repo: crossplane/cli PR: 13
File: cmd/crossplane/xr/generate.go:100-100
Timestamp: 2026-05-20T21:52:31.054Z
Learning: In this repository’s Go code (including usages of `errors.New` and `errors.Wrap`), ensure the string argument (the human-readable error message) starts with a lowercase letter, following standard Go error-string conventions. For example, prefer `errors.Wrap(err, "cannot unmarshal claim")` over `errors.Wrap(err, "Unmarshalling Error")`. This helps keep error messages consistent and idiomatic across the codebase.

Learnt from: adamwg
Repo: crossplane/cli PR: 10
File: cmd/crossplane/function/generate.go:242-244
Timestamp: 2026-05-19T19:10:34.938Z
Learning: In `cmd/crossplane/function/generate.go`, errors from filesystem discovery calls such as `filesystem.FindNestedFoldersWithPattern`, `afero.DirExists`, and `afero.Exists` on the schemas filesystem (c.schemasFS) are intentionally ignored (blank identifier `_`). This is by design to gracefully handle the common case where no schemas exist yet (e.g., the user has not added any project dependencies). In this scenario, the calls return empty results/errors but the function scaffold should still be generated without failing.

Learnt from: CR
Repo: crossplane/cli PR: 0
File: CONTRIBUTING.md:0-0
Timestamp: 2026-05-29T21:44:10.201Z
Learning: Applies to **/*.go : Add short descriptions for commands and flags using kong struct tags. Return longer help text from each command's `Help()` method formatted as Markdown.

Learnt from: CR
Repo: crossplane/cli PR: 0
File: CONTRIBUTING.md:0-0
Timestamp: 2026-05-29T21:44:10.201Z
Learning: Applies to **/*.go : Use kong as the CLI framework. Each command must be defined as a struct in which fields become subcommands, positional arguments, or flags. Use kong struct tags to control validation, auto-completion, and documentation.

Learnt from: adamwg
Repo: crossplane/cli PR: 10
File: internal/project/build.go:361-364
Timestamp: 2026-05-12T22:54:33.936Z
Learning: When reviewing Go code targeting Go 1.26+ (per go.mod and/or CI build settings), do not flag `new(expr)` as a compile error. In Go 1.26, the built-in `new` accepts an expression (not only a type): `new(expr)` allocates a variable of the type of `expr`, initializes it to the value of `expr`, and returns a pointer to it (`*T`). This means usages like `new(someFunc())` or `new(someStringConstant)` are valid syntax and should be allowed under Go 1.26 or later.

Learnt from: adamwg
Repo: crossplane/cli PR: 10
File: internal/project/build.go:361-364
Timestamp: 2026-05-12T22:54:33.936Z
Learning: When reviewing Go code, allow the Go 1.26+ syntax `new(expr)` where `expr` is any expression (not just a type). In Go 1.26, `new(expr)` allocates a value of the type of `expr`, initializes it to the value of `expr`, and returns a `*T` pointer to it (e.g., `new(someFunc())`, `new(someStringConstant)`, `new(pkg.Const)`). If the repository targets Go 1.26 or later (e.g., module `go` directive >= 1.26 and/or build/CI uses Go >= 1.26), do not flag these usages as compilation errors.

Learnt from: adamwg
Repo: crossplane/cli PR: 10
File: cmd/crossplane/dependency/dependency.go:21-25
Timestamp: 2026-05-13T18:59:33.289Z
Learning: In crossplane/cli, experimental/beta CLI command gating is done via the kong command registration struct tag `maturity:"beta"` (or similar) placed on the *parent/top-level* command fields (e.g., top-level `Cmd` fields in `cmd/crossplane/main.go`). Subcommands registered under a parent are already considered gated by that parent maturity. During code review, do not request/introduce programmatic feature flags or add a wrapper function to conditionally register commands; treat the `maturity` tag as the correct and sufficient mechanism. Instead, focus review attention on whether the appropriate maturity tag is set on the relevant parent command.

Learnt from: adamwg
Repo: crossplane/cli PR: 13
File: cmd/crossplane/xr/generate.go:136-143
Timestamp: 2026-05-20T21:52:28.707Z
Learning: In the crossplane/cli Go CLI code, prefer concise user-facing error messages for invalid resource/claim input (e.g., missing required fields like “Claim has no apiVersion” or “Claim has no kind section”). Avoid verbose, instructional messages that primarily list suggested next steps. In code review, do not flag short-form error strings as insufficiently user-friendly if they clearly state the specific problem; reserve longer remediation guidance for cases where it is necessary to understand or correct the failure.


// rendererFlag adapts output.RendererFor to Kong's MapperValue interface
// so the --output flag is decoded straight into a typed output.Renderer
// at parse time. Cmd then carries the resolved renderer as a dependency
// instead of a format identifier.
//
// The wrapper lives in this package (the CLI consumer) rather than in
// render so the render package stays free of any kong dependency and
// can be imported by non-CLI consumers like crossplane-diff.
type rendererFlag struct {
output.Renderer
}

// Decode implements kong.MapperValue.
func (f *rendererFlag) Decode(ctx *kong.DecodeContext) error {
var s string
if err := ctx.Scan.PopValueInto("output", &s); err != nil {
return err
}
r, err := output.RendererFor(output.Format(s))
if err != nil {
return err
}
f.Renderer = r
return nil
}

// Cmd arguments and flags for render subcommand.
type Cmd struct {
// Arguments.
Extensions string `arg:"" help:"Extension sources as a comma-separated list of files, directories, or '-' for standard input."`
Resources string `arg:"" help:"Resource sources as a comma-separated list of files, directories, or '-' for standard input."`

// Flags. Keep them in alphabetical order.
CacheDir string `default:"~/.crossplane/cache" help:"Absolute path to the cache directory for downloaded schemas." predictor:"directory"`
CacheDir string `default:"~/.crossplane/cache" help:"Absolute path to the cache directory for downloaded schemas." predictor:"directory"`
CleanCache bool `help:"Clean the cache directory before downloading package schemas."`
CrossplaneImage string `help:"Specify the Crossplane image for validating built-in schemas."`
ErrorOnMissingSchemas bool `default:"false" help:"Return non zero exit code if missing schemas."`
SkipSuccessResults bool `help:"Skip printing success results."`
UpdateCache bool `default:"false" help:"Update cached schemas by downloading the latest version that satisfies a constraint. May be useful if you are using semantic version constraints and want to get the latest version, but this slows down the cache lookup due to the required network calls."`
// rendererFlag.Decode rejects unknown formats, which is what Kong's
// "enum" tag would normally enforce — but enum doesn't apply to
// MapperValue-backed fields. The help text is the user-facing list
// of valid values.
Output rendererFlag `default:"text" help:"Output format for validation results (text, json, or yaml)." short:"o"`
SkipSuccessResults bool `help:"Skip printing success results."`
UpdateCache bool `default:"false" help:"Update cached schemas by downloading the latest version that satisfies a constraint. May be useful if you are using semantic version constraints and want to get the latest version, but this slows down the cache lookup due to the required network calls."`

fs afero.Fs
}
Expand All @@ -61,7 +98,9 @@ func (c *Cmd) Help() string {
return helpDetail
}

// AfterApply implements kong.AfterApply.
// AfterApply implements kong.AfterApply. The renderer is already resolved
// by Kong's MapperValue plumbing on Cmd.Output by the time this runs, so
// AfterApply only sets the filesystem.
func (c *Cmd) AfterApply() error {
c.fs = afero.NewOsFs()
return nil
Expand Down Expand Up @@ -116,10 +155,16 @@ func (c *Cmd) Run(k *kong.Context, _ logging.Logger) error {
return errors.Wrapf(err, "cannot download and load cache")
}

// Validate resources against schemas
if err := SchemaValidation(context.Background(), resources, m.crds, c.ErrorOnMissingSchemas, c.SkipSuccessResults, k.Stdout); err != nil {
// Validate resources against schemas, render in the requested format,
// and return a CLI-shaped error when validation didn't pass.
result, err := pkgvalidate.SchemaValidate(context.Background(), resources, m.crds)
if err != nil {
return errors.Wrapf(err, "cannot validate resources")
}

return nil
if err := c.Output.Render(result, k.Stdout, output.Options{SkipSuccessResults: c.SkipSuccessResults}); err != nil {
return errors.Wrap(err, "cannot render validation result")
}

return pkgvalidate.ResultError(result, c.ErrorOnMissingSchemas)
}
8 changes: 8 additions & 0 deletions cmd/crossplane/validate/help/validate.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,14 @@ Skip success log lines (only print problems):
crossplane resource validate extensionsDir/ resourceDir/ --skip-success-results
```

Emit machine-readable results (JSON or YAML) for piping to `jq`, scripts, or
CI systems. The structured payload includes per-resource status and
field-level error details:

```shell
crossplane resource validate extensionsDir/ resourceDir/ --output json | jq .
```

Validate the output of render against extensions in a directory:

```shell
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: testbuiltins.test.crossplane.io
spec:
group: test.crossplane.io
names:
kind: TestBuiltin
listKind: TestBuiltinList
plural: testbuiltins
singular: testbuiltin
scope: Cluster
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
foo:
type: string
27 changes: 27 additions & 0 deletions cmd/crossplane/validate/testdata/cmd/crd.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: tests.cmd.example.org
spec:
group: cmd.example.org
names:
kind: Test
listKind: TestList
plural: tests
singular: test
scope: Cluster
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
required:
- replicas
properties:
replicas:
type: integer
6 changes: 6 additions & 0 deletions cmd/crossplane/validate/testdata/cmd/resources_invalid.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: cmd.example.org/v1alpha1
kind: Test
metadata:
name: bad-instance
spec:
replicas: "not-an-int"
4 changes: 4 additions & 0 deletions cmd/crossplane/validate/testdata/cmd/resources_missing.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
apiVersion: nosuch.example.org/v1
kind: Unknown
metadata:
name: missing-schema
6 changes: 6 additions & 0 deletions cmd/crossplane/validate/testdata/cmd/resources_valid.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: cmd.example.org/v1alpha1
kind: Test
metadata:
name: ok-instance
spec:
replicas: 3
Loading
Loading