From c5710f648722afb1cca9c42e5a2568042d2518fc Mon Sep 17 00:00:00 2001 From: Bryce Palmer Date: Thu, 26 Jun 2025 15:17:52 -0400 Subject: [PATCH] wip: add kube-api-linter, fix linter issues Signed-off-by: Bryce Palmer --- .custom-gcl.yml | 6 +++++ .golangci.yaml | 21 ++++++++++++++++ Makefile | 3 ++- api/v1/clustercatalog_types.go | 31 +++++++++++++---------- api/v1/clusterextension_types.go | 42 ++++++++++++++++++-------------- go.mod | 2 ++ go.sum | 4 +++ 7 files changed, 77 insertions(+), 32 deletions(-) create mode 100644 .custom-gcl.yml diff --git a/.custom-gcl.yml b/.custom-gcl.yml new file mode 100644 index 000000000..71d1e4b8a --- /dev/null +++ b/.custom-gcl.yml @@ -0,0 +1,6 @@ +version: v1.64.8 +name: golangci-kube-api-linter +destination: ./bin +plugins: +- module: 'sigs.k8s.io/kube-api-linter' + version: 'v0.0.0-20250626111229-e719da12d840' # Replace with the latest version diff --git a/.golangci.yaml b/.golangci.yaml index 7f64bc040..91bd9fbf9 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -37,6 +37,7 @@ linters: - unparam - unused - whitespace + - kubeapilinter linters-settings: gci: @@ -69,7 +70,27 @@ linters-settings: alias: bsemver - pkg: "^github.com/operator-framework/operator-controller/internal/util/([^/]+)$" alias: "${1}util" + + custom: + kubeapilinter: + type: "module" + description: Kube API Linter lints Kube like APIs based on API conventions and best practices. + settings: + linters: {} + lintersConfig: + optionalFields: + pointers: + preference: WhenRequired + policy: Warn + omitempty: + policy: Warn output: formats: - format: tab + +issues: + exclude-rules: + - path-except: "api/*" + linters: + - kubeapilinter diff --git a/Makefile b/Makefile index 72443a498..aa66cde22 100644 --- a/Makefile +++ b/Makefile @@ -113,7 +113,8 @@ help-extended: #HELP Display extended help. .PHONY: lint lint: lint-custom $(GOLANGCI_LINT) #HELP Run golangci linter. - $(GOLANGCI_LINT) run --build-tags $(GO_BUILD_TAGS) $(GOLANGCI_LINT_ARGS) + $(GOLANGCI_LINT) custom + ./bin/golangci-kube-api-linter run --build-tags $(GO_BUILD_TAGS) $(GOLANGCI_LINT_ARGS) .PHONY: custom-linter-build custom-linter-build: #EXHELP Build custom linter diff --git a/api/v1/clustercatalog_types.go b/api/v1/clustercatalog_types.go index ee1391b79..5cb9d1f44 100644 --- a/api/v1/clustercatalog_types.go +++ b/api/v1/clustercatalog_types.go @@ -54,17 +54,19 @@ const ( // ClusterCatalog enables users to make File-Based Catalog (FBC) catalog data available to the cluster. // For more information on FBC, see https://olm.operatorframework.io/docs/reference/file-based-catalogs/#docs type ClusterCatalog struct { + // +optional metav1.TypeMeta `json:",inline"` // metadata is the standard object's metadata. // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + // +optional metav1.ObjectMeta `json:"metadata"` // spec is the desired state of the ClusterCatalog. // spec is required. // The controller will work to ensure that the desired // catalog is unpacked and served over the catalog content HTTP server. - // +kubebuilder:validation:Required + // +required Spec ClusterCatalogSpec `json:"spec"` // status contains information about the state of the ClusterCatalog such as: @@ -72,7 +74,7 @@ type ClusterCatalog struct { // - Whether or not the ClusterCatalog is progressing to a new state // - A reference to the source from which the catalog contents were retrieved // +optional - Status ClusterCatalogStatus `json:"status,omitempty"` + Status ClusterCatalogStatus `json:"status,omitempty"` //nolint: kubeapilinter } //+kubebuilder:object:root=true @@ -87,7 +89,7 @@ type ClusterCatalogList struct { // items is a list of ClusterCatalogs. // items is required. - // +kubebuilder:validation:Required + // +required Items []ClusterCatalog `json:"items"` } @@ -110,7 +112,7 @@ type ClusterCatalogSpec struct { // image: // ref: quay.io/operatorhubio/catalog:latest // - // +kubebuilder:validation:Required + // +required Source CatalogSource `json:"source"` // priority allows the user to define a priority for a ClusterCatalog. @@ -131,8 +133,8 @@ type ClusterCatalogSpec struct { // The highest possible value is 2147483647. // // +kubebuilder:default:=0 - // +kubebuilder:validation:minimum:=-2147483648 - // +kubebuilder:validation:maximum:=2147483647 + // +kubebuilder:validation:Minimum:=-2147483648 + // +kubebuilder:validation:Maximum:=2147483647 // +optional Priority int32 `json:"priority"` @@ -179,6 +181,8 @@ type ClusterCatalogStatus struct { // contents. This could occur when we've initially fetched the latest contents from the source for this catalog and when polling for changes // to the contents we identify that there are updates to the contents. // + // +patchMergeKey=type + // +patchStrategy=merge // +listType=map // +listMapKey=type // +optional @@ -214,7 +218,7 @@ type ClusterCatalogURLs struct { // // As the needs of users and clients of the evolve, new endpoints may be added. // - // +kubebuilder:validation:Required + // +required // +kubebuilder:validation:MaxLength:=525 // +kubebuilder:validation:XValidation:rule="isURL(self)",message="must be a valid URL" // +kubebuilder:validation:XValidation:rule="isURL(self) ? (url(self).getScheme() == \"http\" || url(self).getScheme() == \"https\") : true",message="scheme must be either http or https" @@ -236,7 +240,7 @@ type CatalogSource struct { // // +unionDiscriminator // +kubebuilder:validation:Enum:="Image" - // +kubebuilder:validation:Required + // +required Type SourceType `json:"type"` // image is used to configure how catalog contents are sourced from an OCI image. // This field is required when type is Image, and forbidden otherwise. @@ -258,11 +262,12 @@ type ResolvedCatalogSource struct { // // +unionDiscriminator // +kubebuilder:validation:Enum:="Image" - // +kubebuilder:validation:Required + // +required Type SourceType `json:"type"` // image is a field containing resolution information for a catalog sourced from an image. // This field must be set when type is Image, and forbidden otherwise. - Image *ResolvedImageSource `json:"image"` + // +optional + Image *ResolvedImageSource `json:"image,omitempty"` } // ResolvedImageSource provides information about the resolved source of a Catalog sourced from an image. @@ -270,7 +275,7 @@ type ResolvedImageSource struct { // ref contains the resolved image digest-based reference. // The digest format is used so users can use other tooling to fetch the exact // OCI manifests that were used to extract the catalog contents. - // +kubebuilder:validation:Required + // +required // +kubebuilder:validation:MaxLength:=1000 // +kubebuilder:validation:XValidation:rule="self.matches('^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])((\\\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(:[0-9]+)?\\\\b')",message="must start with a valid domain. valid domains must be alphanumeric characters (lowercase and uppercase) separated by the \".\" character." // +kubebuilder:validation:XValidation:rule="self.find('(\\\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?((\\\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?)+)?)') != \"\"",message="a valid name is required. valid names must contain lowercase alphanumeric characters separated only by the \".\", \"_\", \"__\", \"-\" characters." @@ -325,7 +330,7 @@ type ImageSource struct { // An example of a valid digest-based image reference is "quay.io/operatorhubio/catalog@sha256:200d4ddb2a73594b91358fe6397424e975205bfbe44614f5846033cad64b3f05" // An example of a valid tag-based image reference is "quay.io/operatorhubio/catalog:latest" // - // +kubebuilder:validation:Required + // +required // +kubebuilder:validation:MaxLength:=1000 // +kubebuilder:validation:XValidation:rule="self.matches('^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])((\\\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(:[0-9]+)?\\\\b')",message="must start with a valid domain. valid domains must be alphanumeric characters (lowercase and uppercase) separated by the \".\" character." // +kubebuilder:validation:XValidation:rule="self.find('(\\\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?((\\\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?)+)?)') != \"\"",message="a valid name is required. valid names must contain lowercase alphanumeric characters separated only by the \".\", \"_\", \"__\", \"-\" characters." @@ -344,7 +349,7 @@ type ImageSource struct { // When omitted, the image will not be polled for new content. // +kubebuilder:validation:Minimum:=1 // +optional - PollIntervalMinutes *int `json:"pollIntervalMinutes,omitempty"` + PollIntervalMinutes *int `json:"pollIntervalMinutes,omitempty"` //nolint: kubeapilinter } func init() { diff --git a/api/v1/clusterextension_types.go b/api/v1/clusterextension_types.go index 0141f1a7a..153996535 100644 --- a/api/v1/clusterextension_types.go +++ b/api/v1/clusterextension_types.go @@ -59,7 +59,7 @@ type ClusterExtensionSpec struct { // +kubebuilder:validation:MaxLength:=63 // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="namespace is immutable" // +kubebuilder:validation:XValidation:rule="self.matches(\"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$\")",message="namespace must be a valid DNS1123 label" - // +kubebuilder:validation:Required + // +required Namespace string `json:"namespace"` // serviceAccount is a reference to a ServiceAccount used to perform all interactions @@ -68,7 +68,7 @@ type ClusterExtensionSpec struct { // The ServiceAccount must exist in the namespace referenced in the spec. // serviceAccount is required. // - // +kubebuilder:validation:Required + // +required ServiceAccount ServiceAccountReference `json:"serviceAccount"` // source is a required field which selects the installation source of content @@ -84,7 +84,7 @@ type ClusterExtensionSpec struct { // catalog: // packageName: example-package // - // +kubebuilder:validation:Required + // +required Source SourceConfig `json:"source"` // install is an optional field used to configure the installation options @@ -112,7 +112,7 @@ type SourceConfig struct { // // +unionDiscriminator // +kubebuilder:validation:Enum:="Catalog" - // +kubebuilder:validation:Required + // +required SourceType string `json:"sourceType"` // catalog is used to configure how information is sourced from a catalog. @@ -162,11 +162,10 @@ type CatalogFilter struct { // // [RFC 1123]: https://tools.ietf.org/html/rfc1123 // - // +kubebuilder:validation.Required + // +required // +kubebuilder:validation:MaxLength:=253 // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="packageName is immutable" // +kubebuilder:validation:XValidation:rule="self.matches(\"^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$\")",message="packageName must be a valid DNS1123 subdomain. It must contain only lowercase alphanumeric characters, hyphens (-) or periods (.), start and end with an alphanumeric character, and be no longer than 253 characters" - // +kubebuilder:validation:Required PackageName string `json:"packageName"` // version is an optional semver constraint (a specific version or range of versions). When unspecified, the latest version available will be installed. @@ -244,6 +243,7 @@ type CatalogFilter struct { // For more information on semver, please see https://semver.org/ // // +kubebuilder:validation:MaxLength:=64 + // +kubebuilder:validation:MinLength:=1 // +kubebuilder:validation:XValidation:rule="self.matches(\"^(\\\\s*(=||!=|>|<|>=|=>|<=|=<|~|~>|\\\\^)\\\\s*(v?(0|[1-9]\\\\d*|[x|X|\\\\*])(\\\\.(0|[1-9]\\\\d*|x|X|\\\\*]))?(\\\\.(0|[1-9]\\\\d*|x|X|\\\\*))?(-([0-9A-Za-z\\\\-]+(\\\\.[0-9A-Za-z\\\\-]+)*))?(\\\\+([0-9A-Za-z\\\\-]+(\\\\.[0-9A-Za-z\\\\-]+)*))?)\\\\s*)((?:\\\\s+|,\\\\s*|\\\\s*\\\\|\\\\|\\\\s*)(=||!=|>|<|>=|=>|<=|=<|~|~>|\\\\^)\\\\s*(v?(0|[1-9]\\\\d*|x|X|\\\\*])(\\\\.(0|[1-9]\\\\d*|x|X|\\\\*))?(\\\\.(0|[1-9]\\\\d*|x|X|\\\\*]))?(-([0-9A-Za-z\\\\-]+(\\\\.[0-9A-Za-z\\\\-]+)*))?(\\\\+([0-9A-Za-z\\\\-]+(\\\\.[0-9A-Za-z\\\\-]+)*))?)\\\\s*)*$\")",message="invalid version expression" // +optional Version string `json:"version,omitempty"` @@ -356,7 +356,7 @@ type ServiceAccountReference struct { // +kubebuilder:validation:MaxLength:=253 // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="name is immutable" // +kubebuilder:validation:XValidation:rule="self.matches(\"^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$\")",message="name must be a valid DNS1123 subdomain. It must contain only lowercase alphanumeric characters, hyphens (-) or periods (.), start and end with an alphanumeric character, and be no longer than 253 characters" - // +kubebuilder:validation:Required + // +required Name string `json:"name"` } @@ -369,7 +369,8 @@ type PreflightConfig struct { // // The CRD Upgrade Safety pre-flight check safeguards from unintended // consequences of upgrading a CRD, such as data loss. - CRDUpgradeSafety *CRDUpgradeSafetyPreflightConfig `json:"crdUpgradeSafety"` + // +optional + CRDUpgradeSafety *CRDUpgradeSafetyPreflightConfig `json:"crdUpgradeSafety,omitempty"` } // CRDUpgradeSafetyPreflightConfig is the configuration for CRD upgrade safety preflight check. @@ -386,7 +387,7 @@ type CRDUpgradeSafetyPreflightConfig struct { // performing an upgrade operation. // // +kubebuilder:validation:Enum:="None";"Strict" - // +kubebuilder:validation:Required + // +required Enforcement CRDUpgradeSafetyEnforcement `json:"enforcement"` } @@ -411,20 +412,21 @@ type BundleMetadata struct { // hyphens (-) or periods (.), start and end with an alphanumeric character, // and be no longer than 253 characters. // - // +kubebuilder:validation:Required + // +required // +kubebuilder:validation:XValidation:rule="self.matches(\"^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$\")",message="packageName must be a valid DNS1123 subdomain. It must contain only lowercase alphanumeric characters, hyphens (-) or periods (.), start and end with an alphanumeric character, and be no longer than 253 characters" Name string `json:"name"` // version is a required field and is a reference to the version that this bundle represents // version follows the semantic versioning standard as defined in https://semver.org/. // - // +kubebuilder:validation:Required + // +required // +kubebuilder:validation:XValidation:rule="self.matches(\"^([0-9]+)(\\\\.[0-9]+)?(\\\\.[0-9]+)?(-([-0-9A-Za-z]+(\\\\.[-0-9A-Za-z]+)*))?(\\\\+([-0-9A-Za-z]+(-\\\\.[-0-9A-Za-z]+)*))?\")",message="version must be well-formed semver" Version string `json:"version"` } // ClusterExtensionStatus defines the observed state of a ClusterExtension. type ClusterExtensionStatus struct { + // conditions represents the observed state of the cluster extension. // The set of condition types which apply to all spec.source variations are Installed and Progressing. // // The Installed condition represents whether or not the bundle has been installed for this ClusterExtension. @@ -463,7 +465,7 @@ type ClusterExtensionInstallStatus struct { // A "bundle" is a versioned set of content that represents the resources that // need to be applied to a cluster to install a package. // - // +kubebuilder:validation:Required + // +required Bundle BundleMetadata `json:"bundle"` } @@ -478,16 +480,20 @@ type ClusterExtensionInstallStatus struct { // ClusterExtension is the Schema for the clusterextensions API type ClusterExtension struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` + // +optional + metav1.TypeMeta `json:",inline"` - // spec is an optional field that defines the desired state of the ClusterExtension. + // metadata is the object metadata // +optional - Spec ClusterExtensionSpec `json:"spec,omitempty"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // spec is a required field that defines the desired state of the ClusterExtension. + // +required + Spec ClusterExtensionSpec `json:"spec"` // status is an optional field that defines the observed state of the ClusterExtension. // +optional - Status ClusterExtensionStatus `json:"status,omitempty"` + Status ClusterExtensionStatus `json:"status,omitempty"` //nolint: kubeapilinter } // +kubebuilder:object:root=true @@ -501,7 +507,7 @@ type ClusterExtensionList struct { // items is a required list of ClusterExtension objects. // - // +kubebuilder:validation:Required + // +required Items []ClusterExtension `json:"items"` } diff --git a/go.mod b/go.mod index eafa8594f..b49264c95 100644 --- a/go.mod +++ b/go.mod @@ -124,6 +124,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect + github.com/golangci/plugin-module-register v0.1.1 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/cel-go v0.25.0 // indirect github.com/google/gnostic-models v0.6.9 // indirect @@ -251,6 +252,7 @@ require ( sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0 // indirect sigs.k8s.io/gateway-api v1.1.0 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/kube-api-linter v0.0.0-20250626111229-e719da12d840 // indirect sigs.k8s.io/kustomize/api v0.19.0 // indirect sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect sigs.k8s.io/randfill v1.0.0 // indirect diff --git a/go.sum b/go.sum index f0cf82998..eda714f19 100644 --- a/go.sum +++ b/go.sum @@ -224,6 +224,8 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golangci/plugin-module-register v0.1.1 h1:TCmesur25LnyJkpsVrupv1Cdzo+2f7zX0H6Jkw1Ol6c= +github.com/golangci/plugin-module-register v0.1.1/go.mod h1:TTpqoB6KkwOJMV8u7+NyXMrkwwESJLOkfl9TxR1DGFc= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY= @@ -792,6 +794,8 @@ sigs.k8s.io/gateway-api v1.1.0 h1:DsLDXCi6jR+Xz8/xd0Z1PYl2Pn0TyaFMOPPZIj4inDM= sigs.k8s.io/gateway-api v1.1.0/go.mod h1:ZH4lHrL2sDi0FHZ9jjneb8kKnGzFWyrTya35sWUTrRs= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/kube-api-linter v0.0.0-20250626111229-e719da12d840 h1:zYPk3+59kzZB2HurKhyNnqYQ2ZhskB5blDJEb/TNA9E= +sigs.k8s.io/kube-api-linter v0.0.0-20250626111229-e719da12d840/go.mod h1:eLCPJVcvhVcNkLOGu2IFzkF5ZpdNjrm+azKaxS+x4IQ= sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ= sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o= sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA=