Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
7 changes: 7 additions & 0 deletions .custom-gcl.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
version: v2.7.2
name: golangci-lint-kube-api
destination: ./bin

plugins:
- module: sigs.k8s.io/kube-api-linter
version: latest
16 changes: 16 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ linters:
- unconvert
- unparam
- whitespace
- kubeapilinter
settings:
errorlint:
errorf: false
Expand All @@ -42,6 +43,17 @@ linters:
alias: bsemver
- pkg: ^github.com/operator-framework/operator-controller/internal/shared/util/([^/]+)$
alias: ${1}util
custom:
kubeapilinter:
type: module
description: "Kube API Linter plugin"
original-url: "sigs.k8s.io/kube-api-linter"
settings:
linters: {}
lintersConfig:
optionalfields:
pointers:
preference: WhenRequired
exclusions:
generated: lax
presets:
Expand All @@ -53,6 +65,10 @@ linters:
- third_party$
- builtin$
- examples$
rules:
- path-except: "^api/"
linters:
- kubeapilinter
Comment on lines +68 to +71
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

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

The exclusion rule appears to be inverted. The configuration "path-except: ^api/" with "linters: - kubeapilinter" means the kubeapilinter will be excluded from running on paths matching ^api/, which is the opposite of what's intended. The kubeapilinter should specifically check the api/ directory for Kubernetes API conventions. Consider using a positive path filter instead, or use the issues exclusion syntax to only run kubeapilinter on api/ paths.

Copilot uses AI. Check for mistakes.
formatters:
enable:
- gci
Expand Down
14 changes: 12 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,19 @@ help-extended: #HELP Display extended help.

#SECTION Development

GOLANGCI_LINT_KUBE_API := bin/golangci-lint-kube-api

$(GOLANGCI_LINT_KUBE_API): $(GOLANGCI_LINT) .custom-gcl.yml
@echo "Building custom golangci-lint with kubeapilinter plugin..."
@$(GOLANGCI_LINT) custom || { \
echo "Failed to build custom golangci-lint."; \
rm -f $(GOLANGCI_LINT_KUBE_API); \
exit 1; \
}

.PHONY: lint
lint: lint-custom $(GOLANGCI_LINT) #HELP Run golangci linter.
$(GOLANGCI_LINT) run --build-tags $(GO_BUILD_TAGS) $(GOLANGCI_LINT_ARGS)
lint: lint-custom $(GOLANGCI_LINT_KUBE_API) #HELP Run golangci linter.
$(GOLANGCI_LINT_KUBE_API) run --build-tags $(GO_BUILD_TAGS) $(GOLANGCI_LINT_ARGS)

lint-helm: $(HELM) #HELP Run helm linter
helm lint helm/olmv1
Expand Down
25 changes: 13 additions & 12 deletions api/v1/clustercatalog_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,12 @@ type ClusterCatalog struct {

// metadata is the standard object's metadata.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
metav1.ObjectMeta `json:"metadata"`
// +optional
metav1.ObjectMeta `json:"metadata,omitempty"`

// spec is a required field that defines the desired state of the ClusterCatalog.
// The controller ensures that the catalog is unpacked and served over the catalog content HTTP server.
// +kubebuilder:validation:Required
// +required
Spec ClusterCatalogSpec `json:"spec"`

// status contains the following information about the state of the ClusterCatalog:
Expand All @@ -81,11 +82,11 @@ type ClusterCatalogList struct {

// metadata is the standard object's metadata.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
metav1.ListMeta `json:"metadata"`
metav1.ListMeta `json:"metadata,omitempty"`
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

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

According to Kubernetes API conventions, the metadata field in List types should not have the omitempty tag. The ListMeta contains important pagination and versioning information (like resourceVersion and continue tokens) that should always be present in list responses, even when empty. The metadata field should be json:"metadata" without omitempty.

Suggested change
metav1.ListMeta `json:"metadata,omitempty"`
metav1.ListMeta `json:"metadata"`

Copilot uses AI. Check for mistakes.

// items is a list of ClusterCatalogs.
// items is required.
// +kubebuilder:validation:Required
// +required
Items []ClusterCatalog `json:"items"`
}

Expand All @@ -105,7 +106,7 @@ type ClusterCatalogSpec struct {
// image:
// ref: quay.io/operatorhubio/catalog:latest
//
// +kubebuilder:validation:Required
// +required
Source CatalogSource `json:"source"`

// priority is an optional field that defines a priority for this ClusterCatalog.
Expand All @@ -125,8 +126,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"`

Expand Down Expand Up @@ -199,7 +200,7 @@ type ClusterCatalogURLs struct {
//
// New endpoints may be added as needs evolve.
//
// +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"
Expand All @@ -220,7 +221,7 @@ type CatalogSource struct {
//
// +unionDiscriminator
// +kubebuilder:validation:Enum:="Image"
// +kubebuilder:validation:Required
// +required
Type SourceType `json:"type"`
// image configures how catalog contents are sourced from an OCI image.
// It is required when type is Image, and forbidden otherwise.
Expand All @@ -241,7 +242,7 @@ type ResolvedCatalogSource struct {
//
// +unionDiscriminator
// +kubebuilder:validation:Enum:="Image"
// +kubebuilder:validation:Required
// +required
Type SourceType `json:"type"`
// image contains resolution information for a catalog sourced from an image.
// It must be set when type is Image, and forbidden otherwise.
Expand All @@ -253,7 +254,7 @@ type ResolvedImageSource struct {
// ref contains the resolved image digest-based reference.
// The digest format allows you to 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."
Expand Down Expand Up @@ -307,7 +308,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."
Expand Down
26 changes: 13 additions & 13 deletions api/v1/clusterextension_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,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 specifies a ServiceAccount used to perform all interactions with the cluster
Expand All @@ -72,7 +72,7 @@ type ClusterExtensionSpec struct {
// The ServiceAccount must exist in the namespace referenced in the spec.
// The serviceAccount field is required.
//
// +kubebuilder:validation:Required
// +required
ServiceAccount ServiceAccountReference `json:"serviceAccount"`

// source is required and selects the installation source of content for this ClusterExtension.
Expand All @@ -88,7 +88,7 @@ type ClusterExtensionSpec struct {
// catalog:
// packageName: example-package
//
// +kubebuilder:validation:Required
// +required
Source SourceConfig `json:"source"`

// install is optional and configures installation options for the ClusterExtension,
Expand Down Expand Up @@ -127,7 +127,7 @@ type SourceConfig struct {
//
// +unionDiscriminator
// +kubebuilder:validation:Enum:="Catalog"
// +kubebuilder:validation:Required
// +required
SourceType string `json:"sourceType"`

// catalog configures how information is sourced from a catalog.
Expand Down Expand Up @@ -167,7 +167,7 @@ type ClusterExtensionConfig struct {
//
// +unionDiscriminator
// +kubebuilder:validation:Enum:="Inline"
// +kubebuilder:validation:Required
// +required
ConfigType ClusterExtensionConfigType `json:"configType"`

// inline contains JSON or YAML values specified directly in the ClusterExtension.
Expand Down Expand Up @@ -206,11 +206,10 @@ type CatalogFilter struct {
//
// [RFC 1123]: https://tools.ietf.org/html/rfc1123
//
// +kubebuilder:validation.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
// +required
PackageName string `json:"packageName"`

// version is an optional semver constraint (a specific version or range of versions).
Expand Down Expand Up @@ -393,7 +392,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"`
}

Expand Down Expand Up @@ -421,7 +420,7 @@ type CRDUpgradeSafetyPreflightConfig struct {
// When set to "Strict", the CRD Upgrade Safety pre-flight check runs during an upgrade operation.
//
Comment on lines 420 to 421
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

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

The field CRDUpgradeSafety is a pointer type but marked with +required. According to Kubernetes API conventions and the configured linter preference (WhenRequired), required fields should not be pointers. Consider either removing the pointer type if the field must always be present, or removing the +required marker if the field should remain optional. The XValidation rule on the parent PreflightConfig struct already validates presence with has(self.crdUpgradeSafety), which suggests the field should be treated as required when PreflightConfig is specified.

Copilot uses AI. Check for mistakes.
// +kubebuilder:validation:Enum:="None";"Strict"
// +kubebuilder:validation:Required
// +required
Enforcement CRDUpgradeSafetyEnforcement `json:"enforcement"`
}

Expand All @@ -445,21 +444,22 @@ type BundleMetadata struct {
// 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
// +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 required and references the version that this bundle represents.
// It 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"`
}

// RevisionStatus defines the observed state of a ClusterExtensionRevision.
type RevisionStatus struct {
// name of the ClusterExtensionRevision resource
// +required
Name string `json:"name"`
// conditions optionally expose Progressing and Available condition of the revision,
// in case when it is not yet marked as successfully installed (condition Succeeded is not set to True).
Expand Down Expand Up @@ -521,7 +521,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"`
}

Expand Down Expand Up @@ -559,7 +559,7 @@ type ClusterExtensionList struct {

// items is a required list of ClusterExtension objects.
//
// +kubebuilder:validation:Required
// +required
Items []ClusterExtension `json:"items"`
}

Expand Down
7 changes: 5 additions & 2 deletions api/v1/clusterextensionrevision_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type ClusterExtensionRevisionSpec struct {
// +kubebuilder:default="Active"
// +kubebuilder:validation:Enum=Active;Archived
// +kubebuilder:validation:XValidation:rule="oldSelf == 'Active' || oldSelf == 'Archived' && oldSelf == self", message="cannot un-archive"
// +optional
LifecycleState ClusterExtensionRevisionLifecycleState `json:"lifecycleState,omitempty"`

// revision is a required, immutable sequence number representing a specific revision
Expand All @@ -61,7 +62,7 @@ type ClusterExtensionRevisionSpec struct {
// Each ClusterExtensionRevision belonging to the same parent ClusterExtension must have a unique revision number.
// The revision number must always be the previous revision number plus one, or 1 for the first revision.
//
// +kubebuilder:validation:Required
// +required
// +kubebuilder:validation:Minimum:=1
// +kubebuilder:validation:XValidation:rule="self == oldSelf", message="revision is immutable"
Revision int64 `json:"revision"`
Expand Down Expand Up @@ -116,6 +117,7 @@ type ClusterExtensionRevisionPhase struct {
//
// +kubebuilder:validation:MaxLength=63
// +kubebuilder:validation:Pattern=`^[a-z]([-a-z0-9]*[a-z0-9])?$`
// +required
Name string `json:"name"`

// objects is a required list of all Kubernetes objects that belong to this phase.
Expand All @@ -133,6 +135,7 @@ type ClusterExtensionRevisionObject struct {
//
// +kubebuilder:validation:EmbeddedResource
// +kubebuilder:pruning:PreserveUnknownFields
// +required
Object unstructured.Unstructured `json:"object"`

// collisionProtection controls whether the operator can adopt and modify objects
Expand Down Expand Up @@ -238,7 +241,7 @@ type ClusterExtensionRevisionList struct {

// items is a required list of ClusterExtensionRevision objects.
//
// +kubebuilder:validation:Required
// +required
Items []ClusterExtensionRevision `json:"items"`
}

Expand Down
Loading
Loading