Skip to content

Conversation

@krishagarwal278
Copy link
Contributor

@krishagarwal278 krishagarwal278 commented Nov 5, 2025

Description

This PR introduces a new enums linter to enforce best practices for enumerated fields in Kubernetes APIs.

The new linter, enums, achieves the following:

  • Documents Enum Fields: Enforces use of type aliases with +enum markers instead of plain strings. Ensures enum values are explicitly documented in OpenAPI schemas with clear justifications for their purpose.
  • Prevents Regressions: Validates that enum fields match documented patterns. Prevents undocumented or accidental changes to enum naming conventions and type definitions.
  • Improves API Quality: Enforces PascalCase naming for enum values (e.g., "PhasePending"). Ensures thoughtful consideration for enum field design, improving schema validation and generated client type safety.
  • The linter is disabled by default and supports an allowlist for special cases like command-line tool names.

Fixes #17

@k8s-ci-robot k8s-ci-robot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Nov 5, 2025
@k8s-ci-robot k8s-ci-robot added cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. labels Nov 5, 2025
@krishagarwal278 krishagarwal278 force-pushed the enums branch 2 times, most recently from fa477bf to a76ac80 Compare November 6, 2025 21:17
@krishagarwal278 krishagarwal278 changed the title [WIP]: Added enums linter Added enums linter Nov 7, 2025
@k8s-ci-robot k8s-ci-robot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Nov 7, 2025
@krishagarwal278
Copy link
Contributor Author

/label tide/merge-method-squash

@k8s-ci-robot k8s-ci-robot added tide/merge-method-squash Denotes a PR that should be squashed by tide when it merges. needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. and removed needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. labels Nov 7, 2025
Copy link
Contributor

@JoelSpeed JoelSpeed left a comment

Choose a reason for hiding this comment

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

Please review the kubebuilder validations documentation for how the kubebuilder version of the enum marker works, this PR is not handling it correctly at the moment

docs/linters.md Outdated

This provides better API evolution, self-documentation, and validation compared to plain strings.

By default, `enums` is not enabled.
Copy link
Contributor

Choose a reason for hiding this comment

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

It probably should be

The `duplicatemarkers` linter can automatically fix all markers that are exact match to another markers.
If there are duplicates across fields and their underlying type, the marker on the type will be preferred and the marker on the field will be removed.

## Enums
Copy link
Contributor

Choose a reason for hiding this comment

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

We are likely to need configuration on this linter for accepted exceptions to the rules here

package a

// Valid enum type with proper marker
// +kubebuilder:validation:Enum
Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't actually equivalent to +enum. +enum will pick up constants for the type, this marker requires them to be specified explicitly

+kubebuilder:validation:Enum=Pending;Running;Succeeded;Failed

)

// Alternative marker format
// +k8s:enum
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this exist already? I wasn't sure if DV was at this point

Copy link
Contributor Author

Choose a reason for hiding this comment

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

)

// Invalid: Missing +enum marker
// This type doesn't have +enum marker, so it will be flagged when used in fields
Copy link
Contributor

Choose a reason for hiding this comment

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

What triggers this to be flag? A string type alias with some constants assigned?

}

// isPascalCase checks if a string follows PascalCase naming convention.
func isPascalCase(s string) bool {
Copy link
Contributor

Choose a reason for hiding this comment

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

I suspect checking pascal case is quite hard. What is the logic here trying to check? First character is capital and that not all characters are upper? All characters upper may actually be valid (e.g. HTTP or HTTPS would be valid)

Comment on lines 27 to 28
1. Fields must use type aliases, not plain strings: String fields that represent enums should
use a type alias with an +enum marker rather than a raw string type.
Copy link
Contributor

Choose a reason for hiding this comment

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

This is and isn't correct. If you see +kubebuilder:validation:Enum on a field, and it's using a plain string, recommending to use a type alias is valid.

For a generic string, this is not true

Comment on lines 29 to 30
2. Type aliases must have +enum marker: Type aliases used for enumerated values must be
annotated with either // +kubebuilder:validation:Enum or // +k8s:enum.
Copy link
Contributor

Choose a reason for hiding this comment

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

Only if we think they should be enums, which means they have a number of constants associated. There may be use cases for free-form strings

2. Type aliases must have +enum marker: Type aliases used for enumerated values must be
annotated with either // +kubebuilder:validation:Enum or // +k8s:enum.
3. Enum values must be PascalCase: Constant values for enums should follow PascalCase naming
(e.g., "PhasePending", "StateRunning") rather than lowercase, snake_case, or SCREAMING_SNAKE_CASE.
Copy link
Contributor

Choose a reason for hiding this comment

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

TIL that SCREAMING_SNAKE_CASE is a thing 😂

Copy link
Contributor Author

Choose a reason for hiding this comment

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

😂😂😂

Comment on lines 87 to 88
Note: This linter is disabled by default as enum usage is recommended but not strictly
required for all Kubernetes APIs. Enable it in your configuration to enforce these conventions.
Copy link
Contributor

Choose a reason for hiding this comment

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

If we get this linter right, it should be on by default IMO

Copy link
Contributor Author

Choose a reason for hiding this comment

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

linter now enabled by default so updated/fixed failing integration tests 👍🏻

	modified:   docs/linters.md
	new file:   pkg/analysis/enums/analyzer.go
	new file:   pkg/analysis/enums/analyzer_test.go
	new file:   pkg/analysis/enums/config.go
	new file:   pkg/analysis/enums/doc.go
	new file:   pkg/analysis/enums/initializer.go
	new file:   pkg/analysis/enums/testdata/src/a/a.go
        new file:   pkg/analysis/enums/testdata/src/b/b.go
	modified:   pkg/registration/doc.go
@k8s-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: krishagarwal278
Once this PR has been reviewed and has the lgtm label, please assign everettraven for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

- **Declarative Validation Marker** (`+k8s:enum`): Used in Kubernetes core API types for declarative validation.

**Enum Marker Modes:**
- **Auto-discovery** (`+k8s:enum` or `+kubebuilder:validation:Enum`): Validates that constants follow PascalCase
Copy link
Contributor

Choose a reason for hiding this comment

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

The kubebuilder version doesn't work like this, it must have explicit values


**Enum Marker Types:**
- **CRD Validation Marker** (`+kubebuilder:validation:Enum`): Used for CRD validation in projects with CustomResourceDefinitions. Processed by controller-gen to generate OpenAPI schema validation.
- **Declarative Validation Marker** (`+k8s:enum`): Used in Kubernetes core API types for declarative validation.
Copy link
Contributor

Choose a reason for hiding this comment

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

What about plan +enum?

allowlist:
- kubectl
- docker
# When true, flags plain string fields (default: false)
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably should default to true, I think this is preferred in Kube API conventions over strings with other validation

@@ -0,0 +1,141 @@
package a

// Valid: +kubebuilder:validation:Enum without explicit values (auto-discovers constants)
Copy link
Contributor

Choose a reason for hiding this comment

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

This comment is not accurate, as you have constants?

)

// Valid: +kubebuilder:validation:Enum with explicit values (doesn't check constants)
// +kubebuilder:validation:Enum=Pending;helper
Copy link
Contributor

Choose a reason for hiding this comment

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

helper should be flagged here as it isn't pascal case

Good:
// +kubebuilder:validation:Enum
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// +kubebuilder:validation:Enum
// +enum


// validateConfig implements validation of the enums linter config.
func validateConfig(cfg *Config, fldPath *field.Path) field.ErrorList {
// Config is optional, allowlist can be empty
Copy link
Contributor

Choose a reason for hiding this comment

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

We probably want to validate that the allowlist does not contain duplicates

// RequireTypeAliasForEnums when true, enforces that string fields representing enums
// must use type aliases instead of plain string types.
// Default: false (plain strings are allowed)
RequireTypeAliasForEnums bool `yaml:"requireTypeAliasForEnums" json:"requireTypeAliasForEnums"`
Copy link
Contributor

Choose a reason for hiding this comment

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

We try to avoid bools in favour of descriptive enum values, so that we can evolve in the future if we need to.

Maybe this needs to be KubebuilderEnumPolicy: RequireTypeAlias | AllowPlainString?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this is an accurate set of changes. I think these should be reverted.

Copy link
Contributor

Choose a reason for hiding this comment

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

The purpose of the integration tests is not to change the content of the files, but add extra want statements when there are new issues. Please revert the changes you've made here and instead add the newly wanted issues

//
// Returns false for:
// - +kubebuilder:validation:Enum=value1;value2 (CRD validation marker with explicit values)
func usesAutoDiscovery(pass *analysis.Pass, typeSpec *ast.TypeSpec) bool {
Copy link
Contributor

Choose a reason for hiding this comment

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

We provide markerAccess to get markers directly, you don't need to re-parse them here

// isPascalCase checks if a string follows PascalCase naming convention.
// Allows: PascalCase (Running), all-uppercase acronyms (HTTP, API), single letters (A).
// Rejects: lowercase start (running), snake_case (phase_pending), kebab-case (phase-pending).
func isPascalCase(s string) bool {
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably best to use a regex here rather than manually parsing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. tide/merge-method-squash Denotes a PR that should be squashed by tide when it merges.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

New Linter: enums

3 participants