Skip to content

Commit fc1d183

Browse files
Merge branch 'main' into KAL-25
2 parents 360add3 + 33e0e43 commit fc1d183

File tree

77 files changed

+2417
-1100
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+2417
-1100
lines changed

.custom-gcl.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
version: v2.0.2
2-
name: golangci-kube-api-linter
1+
version: v2.5.0
2+
name: golangci-lint-kube-api-linter
33
destination: ./bin
44
plugins:
55
- module: 'sigs.k8s.io/kube-api-linter'

.github/workflows/release.yaml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ jobs:
2222
uses: actions/setup-go@v5
2323
with:
2424
go-version: stable
25-
- name: Run GoReleaser
25+
- name: Run GoReleaser (on tags)
2626
uses: goreleaser/goreleaser-action@v6
27+
if: github.event_name == 'push'
2728
with:
2829
# either 'goreleaser' (default) or 'goreleaser-pro'
2930
distribution: goreleaser
@@ -32,3 +33,14 @@ jobs:
3233
args: release --clean
3334
env:
3435
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36+
- name: Run GoReleaser (on pull requests)
37+
uses: goreleaser/goreleaser-action@v6
38+
if: github.event_name == 'pull_request'
39+
with:
40+
# either 'goreleaser' (default) or 'goreleaser-pro'
41+
distribution: goreleaser
42+
# 'latest', 'nightly', or a semver
43+
version: "~> v2"
44+
args: release --snapshot --clean
45+
env:
46+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

README.md

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,42 +25,49 @@ under `linter-settings`.
2525

2626
### Golangci-lint Module
2727

28-
To install the `golangci-lint` module, first you must have `golangci-lint` installed.
28+
To install the `golangci-lint` module, first you must have `golangci-lint` v2 installed.
2929
If you do not have `golangci-lint` installed, review the `golangci-lint` [install guide][golangci-lint-install].
3030

3131
[golangci-lint-install]: https://golangci-lint.run/welcome/install/
3232

3333
You will need to create a `.custom-gcl.yml` file to describe the custom linters you want to run. The following is an example of a `.custom-gcl.yml` file:
3434

3535
```yaml
36-
version: v1.64.8
37-
name: golangci-kube-api-linter
36+
version: v2.5.0
37+
name: golangci-lint-kube-api-linter
3838
destination: ./bin
3939
plugins:
4040
- module: 'sigs.k8s.io/kube-api-linter'
41-
version: 'v0.0.0' # Replace with the latest version
41+
version: 'v0.0.0-20251029102002-9992248f8813'
4242
```
4343
44-
Once you have created the custom configuration file, you can run the following command to build the custom `golangci-kal` binary:
44+
**Important - Version Format**: Since this repository does not have releases yet, you must use a [pseudo-version](https://go.dev/ref/mod#pseudo-versions) in the format `v0.0.0-YYYYMMDDHHMMSS-commithash`.
45+
46+
To find the latest version listed, check [pkg.go.dev/sigs.k8s.io/kube-api-linter?tab=versions](https://pkg.go.dev/sigs.k8s.io/kube-api-linter?tab=versions)
47+
48+
Once you have created the custom configuration file, you can run the following command to build the custom binary:
4549

4650
```shell
4751
golangci-lint custom
4852
```
4953

50-
The output binary will be a combination of the initial `golangci-lint` binary and the Kube API linter plugin.
51-
This means that you can use any of the standard `golangci-lint` configuration or flags to run the binary, but may also include the Kube API Linter rules.
54+
The output binary will be created at the location specified by the `destination` field in `.custom-gcl.yml` and will be a combination of the `golangci-lint` binary with the Kube API Linter included as a module.
55+
56+
This means you can use any of the standard `golangci-lint` configuration or flags to run the binary, with the addition of the Kube API Linter rules.
5257

5358
If you wish to only use the Kube API Linter rules, you can configure your `.golangci.yml` file to only run the Kube API Linter:
5459

5560
```yaml
61+
version: "2"
5662
linters-settings:
5763
custom:
5864
kubeapilinter:
5965
type: "module"
60-
description: Kube API LInter lints Kube like APIs based on API conventions and best practices.
66+
description: Kube API Linter lints Kube like APIs based on API conventions and best practices.
6167
settings:
6268
linters: {}
6369
lintersConfig: {}
70+
6471
linters:
6572
disable-all: true
6673
enable:
@@ -77,6 +84,7 @@ issues:
7784
If you wish to only run selected linters you can do so by specifying the linters you want to enable in the `linters` section:
7885

7986
```yaml
87+
version: "2"
8088
linters-settings:
8189
custom:
8290
kubeapilinter:
@@ -89,6 +97,9 @@ linters-settings:
8997
- requiredfields
9098
- statusoptional
9199
- statussubresource
100+
linters:
101+
enable:
102+
- kubeapilinter
92103
```
93104

94105
The settings for Kube API Linter are based on the [GolangCIConfig][golangci-config-struct] struct and allow for finer control over the linter rules.
@@ -98,10 +109,10 @@ To provide further configuration, add the `custom.kubeapilinter` section to your
98109

99110
[golangci-config-struct]: https://pkg.go.dev/sigs.k8s.io/kube-api-linter/pkg/config#GolangCIConfig
100111

101-
Where fixes are available within a rule, these can be applied automatically with the `--fix` flag.
112+
Where fixes are available within a rule, these can be applied automatically with the `--fix` flag:
102113

103114
```shell
104-
golangci-kube-api-linter run path/to/api/types --fix
115+
golangci-lint-kube-api-linter run path/to/api/types --fix
105116
```
106117

107118
### Golangci-lint Plugin
@@ -113,48 +124,60 @@ More information about golangci-lint plugins can be found in the [golangci-lint
113124

114125
[golangci-lint-plugin-docs]: https://golangci-lint.run/plugins/go-plugins/
115126

127+
To build the plugin, use the `-buildmode=plugin` flag:
128+
116129
```shell
117130
go build -buildmode=plugin -o bin/kube-api-linter.so sigs.k8s.io/kube-api-linter/pkg/plugin
118131
```
119132

120-
This will create a `kube-api-linter.so` file in the `bin` directory.
133+
**Note**: If you're building the plugin from within another project that vendors kube-api-linter, use the vendor path:
121134

122-
The `golangci-lint` configuration is similar to the module configuration, however, you will need to specify the plugin path instead.
135+
```shell
136+
go build -mod=vendor -buildmode=plugin -o bin/kube-api-linter.so ./vendor/sigs.k8s.io/kube-api-linter/pkg/plugin
137+
```
138+
139+
This will create a `kube-api-linter.so` plugin file in the specified directory.
140+
141+
The `golangci-lint` configuration is similar to the module configuration, however, you will need to specify the plugin path instead in your `.golangci.yml`:
123142

124143
```yaml
144+
version: "2"
125145
linters-settings:
126146
custom:
127147
kubeapilinter:
128148
path: "bin/kube-api-linter.so"
129-
description: Kube API LInter lints Kube like APIs based on API conventions and best practices.
149+
description: Kube API Linter lints Kube like APIs based on API conventions and best practices.
130150
original-url: sigs.k8s.io/kube-api-linter
131151
settings:
132152
linters: {}
133153
lintersConfig: {}
154+
linters:
155+
enable:
156+
- kubeapilinter
134157
```
135158

136159
The rest of the configuration is the same as the module configuration, except the standard `golangci-lint` binary is invoked, rather than a custom binary.
137160

138161
#### VSCode integration
139162

140-
Since VSCode already integrates with `golangci-lint` via the [Go][vscode-go] extension, you can use the `golangci-kal` binary as a linter in VSCode.
163+
Since VSCode already integrates with `golangci-lint` via the [Go][vscode-go] extension, you can use the custom `golangci-lint-kube-api-linter` binary as a linter in VSCode.
164+
141165
If your project authors are already using VSCode and have the configuration to lint their code when saving, this can be a seamless integration.
142166

143-
Ensure that your project setup includes building the `golangci-kube-api-linter` binary, and then configure the `go.lintTool` and `go.alternateTools` settings in your project `.vscode/settings.json` file.
167+
Ensure that your project setup includes building the `golangci-lint-kube-api-linter` binary, then configure the `go.lintTool` and `go.alternateTools` settings in your project `.vscode/settings.json` file:
144168

145169
[vscode-go]: https://code.visualstudio.com/docs/languages/go
146170

147171
```json
148172
{
149173
"go.lintTool": "golangci-lint",
150174
"go.alternateTools": {
151-
"golangci-lint": "${workspaceFolder}/bin/golangci-kube-api-linter",
175+
"golangci-lint": "${workspaceFolder}/bin/golangci-lint-kube-api-linter"
152176
}
153177
}
154178
```
155179

156-
Alternatively, you can also replace the binary with a script that runs the `golangci-kube-api-linter` binary,
157-
allowing for customisation or automatic copmilation of the project should it not already exist.
180+
Alternatively, you can also replace the binary with a script that runs the `golangci-lint-kube-api-linter` binary, allowing for customization or automatic compilation of the project should it not already exist:
158181

159182
```json
160183
{

docs/linters.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
- [OptionalFields](#optionalfields) - Validates optional field conventions
2222
- [OptionalOrRequired](#optionalorrequired) - Ensures fields are explicitly marked as optional or required
2323
- [NoReferences](#noreferences) - Ensures field names use Ref/Refs instead of Reference/References
24+
- [PreferredMarkers](#preferredmarkers) - Ensures preferred markers are used instead of equivalent markers
2425
- [RequiredFields](#requiredfields) - Validates required field conventions
2526
- [SSATags](#ssatags) - Ensures proper Server-Side Apply (SSA) tags on array fields
2627
- [StatusOptional](#statusoptional) - Ensures status fields are marked as optional
@@ -570,6 +571,105 @@ The `optionalorrequired` linter can automatically fix fields that are using the
570571

571572
It will also remove the secondary marker where both the preferred and secondary marker are present on a field.
572573

574+
## PreferredMarkers
575+
576+
The `preferredmarkers` linter ensures that types and fields use preferred markers instead of equivalent but different marker identifiers.
577+
578+
By default, `preferredmarkers` is not enabled.
579+
580+
This linter is useful for projects that want to enforce consistent marker usage across their codebase, especially when multiple equivalent markers exist. For example, Kubernetes has multiple ways to mark fields as optional:
581+
- `+optional`
582+
- `+k8s:optional`
583+
- `+kubebuilder:validation:Optional`
584+
585+
The linter can be configured to enforce using one preferred marker identifier and report any equivalent markers that should be replaced.
586+
587+
### Configuration
588+
589+
The linter requires a configuration that specifies preferred markers and their equivalent identifiers.
590+
591+
**Scenario:** Enforce using `+k8s:optional` instead of `+kubebuilder:validation:Optional`
592+
593+
```yaml
594+
linterConfig:
595+
preferredmarkers:
596+
markers:
597+
- preferredIdentifier: "k8s:optional"
598+
equivalentIdentifiers:
599+
- identifier: "kubebuilder:validation:Optional"
600+
```
601+
602+
**Scenario:** Enforce using a custom marker instead of multiple equivalent markers
603+
604+
```yaml
605+
linterConfig:
606+
preferredmarkers:
607+
markers:
608+
- preferredIdentifier: "custom:preferred"
609+
equivalentIdentifiers:
610+
- identifier: "custom:old:marker"
611+
- identifier: "custom:deprecated:marker"
612+
- identifier: "custom:legacy:marker"
613+
```
614+
615+
**Scenario:** Multiple preferred markers with different equivalents
616+
617+
```yaml
618+
linterConfig:
619+
preferredmarkers:
620+
markers:
621+
- preferredIdentifier: "k8s:optional"
622+
equivalentIdentifiers:
623+
- identifier: "kubebuilder:validation:Optional"
624+
- preferredIdentifier: "k8s:required"
625+
equivalentIdentifiers:
626+
- identifier: "kubebuilder:validation:Required"
627+
```
628+
629+
The linter checks both type-level and field-level markers, including markers inherited from type aliases.
630+
631+
### Fixes
632+
633+
When one or more equivalent markers are found, the linter will:
634+
635+
1. Report a diagnostic message indicating which marker(s) should be preferred
636+
2. Suggest a fix that:
637+
- If the preferred marker does not already exist: replaces the first equivalent marker with the preferred identifier and preserves any marker expressions (e.g., `=value` or `:key=value`)
638+
- If the preferred marker already exists: removes all equivalent markers to avoid duplicates
639+
- Removes any additional equivalent markers
640+
641+
**Example 1:** If both `+kubebuilder:validation:Optional` and `+custom:optional` are configured as equivalents to `+k8s:optional`, the following code:
642+
643+
```go
644+
// +kubebuilder:validation:Optional
645+
// +custom:optional
646+
type MyType string
647+
```
648+
649+
will be automatically fixed to:
650+
651+
```go
652+
// +k8s:optional
653+
type MyType string
654+
```
655+
656+
**Example 2:** If the preferred marker already exists alongside equivalent markers:
657+
658+
```go
659+
// +k8s:optional
660+
// +kubebuilder:validation:Optional
661+
type MyType string
662+
```
663+
664+
will be automatically fixed to:
665+
666+
```go
667+
// +k8s:optional
668+
type MyType string
669+
```
670+
671+
Marker expressions are preserved during replacement. For example, `+kubebuilder:validation:Optional:=someValue` becomes `+k8s:optional=someValue`. Note that unnamed expressions (`:=value`) are normalized to use `=value` syntax for universal compatibility across different marker systems.
672+
573673
## RequiredFields
574674

575675
The `requiredfields` linter checks that all fields marked as required adhere to having `omitempty` or `omitzero` values in their `json` tags.

pkg/analysis/arrayofstruct/analyzer.go

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,14 @@ func run(pass *analysis.Pass) (any, error) {
4444
return nil, kalerrors.ErrCouldNotGetInspector
4545
}
4646

47-
inspect.InspectFields(func(field *ast.Field, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markershelper.Markers) {
48-
checkField(pass, field, markersAccess)
47+
inspect.InspectFields(func(field *ast.Field, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markershelper.Markers, qualifiedFieldName string) {
48+
checkField(pass, field, markersAccess, qualifiedFieldName)
4949
})
5050

5151
return nil, nil //nolint:nilnil
5252
}
5353

54-
func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers) {
54+
func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers, qualifiedFieldName string) {
5555
// Get the element type of the array
5656
elementType := getArrayElementType(pass, field)
5757
if elementType == nil {
@@ -79,7 +79,7 @@ func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markershelp
7979
return
8080
}
8181

82-
reportArrayOfStructIssue(pass, field)
82+
reportArrayOfStructIssue(pass, field, qualifiedFieldName)
8383
}
8484

8585
// getArrayElementType extracts the element type from an array field.
@@ -107,19 +107,8 @@ func getArrayElementType(pass *analysis.Pass, field *ast.Field) ast.Expr {
107107
}
108108

109109
// reportArrayOfStructIssue reports a diagnostic for an array of structs without required fields.
110-
func reportArrayOfStructIssue(pass *analysis.Pass, field *ast.Field) {
111-
fieldName := utils.FieldName(field)
112-
structName := utils.GetStructNameForField(pass, field)
113-
114-
var prefix string
115-
if structName != "" {
116-
prefix = fmt.Sprintf("%s.%s", structName, fieldName)
117-
} else {
118-
prefix = fieldName
119-
}
120-
121-
message := fmt.Sprintf("%s is an array of structs, but the struct has no required fields. At least one field should be marked as required to prevent ambiguous YAML configurations", prefix)
122-
110+
func reportArrayOfStructIssue(pass *analysis.Pass, field *ast.Field, qualifiedFieldName string) {
111+
message := fmt.Sprintf("%s is an array of structs, but the struct has no required fields. At least one field should be marked as required to prevent ambiguous YAML configurations", qualifiedFieldName)
123112
pass.Report(analysis.Diagnostic{
124113
Pos: field.Pos(),
125114
Message: message,

0 commit comments

Comments
 (0)