Skip to content

feat(xrd): add an alpha xrd lint command (stacked on #449) - #465

Closed
GenerikUser wants to merge 7 commits into
crossplane:mainfrom
GenerikUser:erik.miller--xrd-lint-impl
Closed

GenerikUser wants to merge 7 commits into
crossplane:mainfrom
GenerikUser:erik.miller--xrd-lint-impl

Conversation

@GenerikUser

Copy link
Copy Markdown
Contributor

Description of your changes

Stacked on #449. This branch is built on the design one-pager in #449. GitHub cannot target a fork branch from upstream, so this PR also shows #449's six design: commits. Only the last commit, be6278c ("feat(xrd): add an alpha xrd lint command with built-in rules"), is new here and needs review. Review the design in #449. I will rebase this branch once #449 merges.

This is the first slice of crossplane xrd lint, as described in design/one-pager-cli-xrd-lint.md. It lays down the foundation that later PRs build on:

  • Command. crossplane xrd lint [flags] <files-or-dirs|->... is added under xrd as alpha. It has --config, -o text|json|yaml and --max-warnings.
  • internal/xrdlint. This package holds the rule interface (SchemaRule, Meta, Context), the registry, the config, a yaml.v3 loader that keeps line numbers and comments, a schema walker, the runner and the renderers. It is internal while the command is alpha, as the design says.
  • Config. .crossplane-lint.yaml is found by searching upward from the working directory. It is decoded with yaml.v3 and KnownFields(true), so unknown and duplicate keys are errors.
    • A rule value is rule: <severity> or rule: {severity, options}. The list form is a config error.
    • Options are validated against the schema the rule declares.
    • An entry naming an unknown rule is an error.
    • customRules is rejected with a "not supported yet" error.
  • Validity first. Each XRD first gets the structural check from Report an XRD schema Kubernetes would reject as non-structural #301, via internal/crd.FromXRD. An XRD that fails it is reported as lint/invalid-xrd, and no other rule runs on it.
  • Rule failures. A rule that returns an error or panics is reported as lint/rule-error. That finding is always an error and cannot be suppressed, so it fails the run whatever the rule's severity. The other rules still run and report.
  • Suppression. A # crossplane-lint-disable-next-line rule-a, rule-b -- reason comment covers the next node and everything beneath it. A named rule that suppresses nothing is reported as lint/unused-disable (warn).
  • Exit codes. The command exits non-zero when any of these is true:
    • there is an error finding
    • warnings exceed --max-warnings
    • the config is invalid
    • an input cannot be read
    • the inputs hold no XRDs
  • Built-in rules. These are the rules on by default that need neither crdify nor CEL:
    • xrd/status-fields-optional (error)
    • xrd/status-no-value-validation (error)
    • xrd/lists-need-list-type (error)
    • xrd/no-bools (warn)
  • xrdlinttest.Run. A table-driven rule tester, which needs at least one passing and one failing case per rule.
$ crossplane xrd lint xrd.yaml
xrd.yaml
  xnetworks.example.org v1alpha1
    21:15  error  spec.subnets: array has no x-kubernetes-list-type  xrd/lists-need-list-type
    28:15  warn   spec.public: prefer an enum to a boolean           xrd/no-bools
    33:15  error  status.cidr: status field must not set pattern     xrd/status-no-value-validation

2 errors, 1 warning
crossplane: error: lint found 2 errors

Deferred to follow-up PRs, in this order:

  1. The off-by-default xrd/ rules: array-of-objects-needs-key, no-floats, no-maps, no-preserve-unknown-fields, no-phase, require-description (ignoreStatus) and string-max-length (max), plus version-naming. They need nothing beyond this slice.
  2. xrd-diff/* on crdify's CompareProperties, together with xrd/served-versions-compatible (error by default, every incompatible pair of served versions reported). xrd-diff/unhandled defaults to error, and xrd-diff/no-version-removal and xrd-diff/scope are included. This adds the DiffRule interface and the crdify dependency.
  3. --old sources: a plain path or file://, git://<ref>?path=, kube://[<name>][?context=] and xpkg://<ref>.
  4. CEL custom rules (customRules, with match: property | xrd | diff): an environment with the Kubernetes CEL libraries, and a runtime cost limit at the API server's per-call limit.

For reviewers:

  • SchemaRule.Check returns an error. The one-pager's sketch has no return value. An error return is how a rule that cannot run surfaces as lint/rule-error.
  • Built-in rules link their docs to the one-pager's "Built-in Rules" section until docs.crossplane.io has rule pages.
  • Map values under additionalProperties get the path segment .*, and array items get [*].

Refs #69

I have:

  • Read and followed Crossplane's contribution process.
  • Run ./nix.sh flake check to ensure this PR is ready for review. Not run locally. I ran go test ./... and golangci-lint run ./... with the repo's .golangci.yml instead; both pass (0 issues).
  • Added or updated unit tests.
  • Linked a PR or a docs tracking issue to document this change. The command is alpha and its help text documents it. Docs will follow once the rule set settles.
  • Added backport release-x.y labels to auto-backport this PR. This is a new feature.

- Build the xrd-diff/ rules on sigs.k8s.io/crdify's validations package,
  with one rule per crdify comparator plus xrd-diff/unhandled, and list
  crdify as prior art.
- Evaluate xrd-diff/no-version-removal over the union of old and new
  version names, so removed or unserved versions are caught.
- Add xrd/served-versions-compatible to compare the served versions of a
  single XRD.
- Let --old read from files, git refs, the current cluster or an xpkg.
- Use the {severity, options} object form for rule options.
- Decode the config with yaml.v3 and KnownFields only.
- Build the CEL environment with the Kubernetes CEL libraries and a cost
  limit, give lint/rule-error the failing rule's severity, add an optional
  docsURL to custom rules, and state that diff rules see the schema root.
- Keep the Go rule API internal until the command reaches beta.
- Replace the diff examples that are now built-in rules, and wrap long
  table rows to 100 columns.

Signed-off-by: GenerikUser <[email protected]>
- A rule that fails to evaluate, including on exceeding the CEL cost
  limit, now fails the whole run with a non-zero exit, whatever the
  rule's configured severity. lint/rule-error is always an error and
  cannot be disabled or suppressed.
- Make xrd/served-versions-compatible an error by default, alongside the
  other rules that guard against real misbehaviour.
- State the crdify dependency plainly: it is pre-1.0 and lint pins it.

Signed-off-by: GenerikUser <[email protected]>
Name a kubeconfig context and a single XRD in kube:// sources, and allow
a directory in git:// paths. Describe the served-version check as crdify
runs it: every pair of served versions in Kubernetes version order, only
new incompatibilities when --old is given, and its conversion option.
Add xrd-diff/scope from crdify's scope check. Enforce the CEL cost limit
at runtime with the API server's per-call limit, since an estimate cannot
bound dynamic maps, and build the environment at the vendored
compatibility version. Keep the rule test helper internal.

Signed-off-by: GenerikUser <[email protected]>
A rule value is a bare severity or a {severity, options} object. Say
explicitly that ESLint's array form is not accepted and that a YAML
sequence as a rule value is a config error.

Signed-off-by: GenerikUser <[email protected]>
xrd/served-versions-compatible now reports every incompatible pair of
served versions, with or without --old, and checks every XRD whether or
not it configures a conversion webhook. Drop the conversion option and
the subtraction of incompatibilities the old XRD already had.

Signed-off-by: GenerikUser <[email protected]>
Add `crossplane xrd lint`, the first slice of the design in
design/one-pager-cli-xrd-lint.md. It checks XRDs against a registry of
rules and reports each finding with its rule, severity, location and
docs link.

- internal/xrdlint holds the rule interface, registry, config, loader,
  schema walker, runner and renderers. It is internal while the command
  is alpha.
- .crossplane-lint.yaml is found by searching upward from the working
  directory, or set with --config. It is parsed with yaml.v3 and
  KnownFields, so unknown and duplicate keys are errors. A rule value is
  a severity or {severity, options}; the list form is rejected. Options
  are validated against the schema a rule declares, and entries naming
  no known rule are errors. customRules is rejected until CEL rules land.
- Each XRD first gets the structural check `xrd convert` applies. An XRD
  that fails it is reported as lint/invalid-xrd and is not linted further.
- A rule that returns an error or panics is reported as lint/rule-error,
  which always fails the run, whatever the rule's severity. The other
  rules still run.
- Inline `# crossplane-lint-disable-next-line rule -- reason` comments
  suppress findings on the next node and its children. A comment that
  suppresses nothing is reported as lint/unused-disable.
- Output is text, json or yaml. The command fails on any error, on
  warnings beyond --max-warnings, on an invalid config, on unreadable
  input, or when the inputs hold no XRDs.
- Built-in rules: xrd/status-fields-optional,
  xrd/status-no-value-validation and xrd/lists-need-list-type (error),
  and xrd/no-bools (warn).
- xrdlinttest runs table-driven rule tests.

Signed-off-by: GenerikUser <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant