diff --git a/tools/go.mod b/tools/go.mod index fb131555fc1..99a7420c8b1 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -30,7 +30,7 @@ require ( k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 sigs.k8s.io/controller-tools v0.15.0 - sigs.k8s.io/kube-api-linter v0.0.0-20250723124831-1b29e82a0f55 + sigs.k8s.io/kube-api-linter v0.0.0-20250729132427-47bfeef6cd38 sigs.k8s.io/yaml v1.4.0 ) diff --git a/tools/go.sum b/tools/go.sum index 288848598b6..11a2784c634 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -992,6 +992,8 @@ sigs.k8s.io/kube-api-linter v0.0.0-20250723124227-8eacb1639327 h1:hKuD0+fceBSqCy sigs.k8s.io/kube-api-linter v0.0.0-20250723124227-8eacb1639327/go.mod h1:Jxl3NU9lRf9WJ8dgwgF4U6tLF229jR/KEvtxSwRAKnE= sigs.k8s.io/kube-api-linter v0.0.0-20250723124831-1b29e82a0f55 h1:kD9x5uu1/A7wvhwPcuSBk1UiG5wq/nstFxgYALOeZ/Q= sigs.k8s.io/kube-api-linter v0.0.0-20250723124831-1b29e82a0f55/go.mod h1:Jxl3NU9lRf9WJ8dgwgF4U6tLF229jR/KEvtxSwRAKnE= +sigs.k8s.io/kube-api-linter v0.0.0-20250729132427-47bfeef6cd38 h1:5WuFSvNbquqwM82aBQ36AfsFGsf2Jc0OJM4SCC2rw4w= +sigs.k8s.io/kube-api-linter v0.0.0-20250729132427-47bfeef6cd38/go.mod h1:Jxl3NU9lRf9WJ8dgwgF4U6tLF229jR/KEvtxSwRAKnE= sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= diff --git a/tools/vendor/k8s.io/utils/set/OWNERS b/tools/vendor/k8s.io/utils/set/OWNERS new file mode 100644 index 00000000000..9d2d33e7b0d --- /dev/null +++ b/tools/vendor/k8s.io/utils/set/OWNERS @@ -0,0 +1,8 @@ +# See the OWNERS docs at https://go.k8s.io/owners + +reviewers: + - logicalhan + - thockin +approvers: + - logicalhan + - thockin diff --git a/tools/vendor/k8s.io/utils/set/ordered.go b/tools/vendor/k8s.io/utils/set/ordered.go new file mode 100644 index 00000000000..2b2c11fc7b4 --- /dev/null +++ b/tools/vendor/k8s.io/utils/set/ordered.go @@ -0,0 +1,53 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package set + +// ordered is a constraint that permits any ordered type: any type +// that supports the operators < <= >= >. +// If future releases of Go add new ordered types, +// this constraint will be modified to include them. +type ordered interface { + integer | float | ~string +} + +// integer is a constraint that permits any integer type. +// If future releases of Go add new predeclared integer types, +// this constraint will be modified to include them. +type integer interface { + signed | unsigned +} + +// float is a constraint that permits any floating-point type. +// If future releases of Go add new predeclared floating-point types, +// this constraint will be modified to include them. +type float interface { + ~float32 | ~float64 +} + +// signed is a constraint that permits any signed integer type. +// If future releases of Go add new predeclared signed integer types, +// this constraint will be modified to include them. +type signed interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 +} + +// unsigned is a constraint that permits any unsigned integer type. +// If future releases of Go add new predeclared unsigned integer types, +// this constraint will be modified to include them. +type unsigned interface { + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr +} diff --git a/tools/vendor/k8s.io/utils/set/set.go b/tools/vendor/k8s.io/utils/set/set.go new file mode 100644 index 00000000000..fc25b2feaa4 --- /dev/null +++ b/tools/vendor/k8s.io/utils/set/set.go @@ -0,0 +1,215 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package set + +import ( + "sort" +) + +// Empty is public since it is used by some internal API objects for conversions between external +// string arrays and internal sets, and conversion logic requires public types today. +type Empty struct{} + +// Set is a set of the same type elements, implemented via map[ordered]struct{} for minimal memory consumption. +type Set[E ordered] map[E]Empty + +// New creates a new set. +func New[E ordered](items ...E) Set[E] { + ss := Set[E]{} + ss.Insert(items...) + return ss +} + +// KeySet creates a Set[E] from a keys of a map[E](? extends interface{}). +func KeySet[E ordered, A any](theMap map[E]A) Set[E] { + ret := Set[E]{} + for key := range theMap { + ret.Insert(key) + } + return ret +} + +// Insert adds items to the set. +func (s Set[E]) Insert(items ...E) Set[E] { + for _, item := range items { + s[item] = Empty{} + } + return s +} + +// Delete removes all items from the set. +func (s Set[E]) Delete(items ...E) Set[E] { + for _, item := range items { + delete(s, item) + } + return s +} + +// Has returns true if and only if item is contained in the set. +func (s Set[E]) Has(item E) bool { + _, contained := s[item] + return contained +} + +// HasAll returns true if and only if all items are contained in the set. +func (s Set[E]) HasAll(items ...E) bool { + for _, item := range items { + if !s.Has(item) { + return false + } + } + return true +} + +// HasAny returns true if any items are contained in the set. +func (s Set[E]) HasAny(items ...E) bool { + for _, item := range items { + if s.Has(item) { + return true + } + } + return false +} + +// Union returns a new set which includes items in either s1 or s2. +// For example: +// s1 = {a1, a2} +// s2 = {a3, a4} +// s1.Union(s2) = {a1, a2, a3, a4} +// s2.Union(s1) = {a1, a2, a3, a4} +func (s Set[E]) Union(s2 Set[E]) Set[E] { + result := Set[E]{} + result.Insert(s.UnsortedList()...) + result.Insert(s2.UnsortedList()...) + return result +} + +// Len returns the number of elements in the set. +func (s Set[E]) Len() int { + return len(s) +} + +// Intersection returns a new set which includes the item in BOTH s1 and s2 +// For example: +// s1 = {a1, a2} +// s2 = {a2, a3} +// s1.Intersection(s2) = {a2} +func (s Set[E]) Intersection(s2 Set[E]) Set[E] { + var walk, other Set[E] + result := Set[E]{} + if s.Len() < s2.Len() { + walk = s + other = s2 + } else { + walk = s2 + other = s + } + for key := range walk { + if other.Has(key) { + result.Insert(key) + } + } + return result +} + +// IsSuperset returns true if and only if s1 is a superset of s2. +func (s Set[E]) IsSuperset(s2 Set[E]) bool { + for item := range s2 { + if !s.Has(item) { + return false + } + } + return true +} + +// Difference returns a set of objects that are not in s2 +// For example: +// s1 = {a1, a2, a3} +// s2 = {a1, a2, a4, a5} +// s1.Difference(s2) = {a3} +// s2.Difference(s1) = {a4, a5} +func (s Set[E]) Difference(s2 Set[E]) Set[E] { + result := Set[E]{} + for key := range s { + if !s2.Has(key) { + result.Insert(key) + } + } + return result +} + +// Equal returns true if and only if s1 is equal (as a set) to s2. +// Two sets are equal if their membership is identical. +func (s Set[E]) Equal(s2 Set[E]) bool { + return s.Len() == s2.Len() && s.IsSuperset(s2) +} + +type sortableSlice[E ordered] []E + +func (s sortableSlice[E]) Len() int { + return len(s) +} +func (s sortableSlice[E]) Less(i, j int) bool { return s[i] < s[j] } +func (s sortableSlice[E]) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// SortedList returns the contents as a sorted slice. +func (s Set[E]) SortedList() []E { + res := make(sortableSlice[E], 0, s.Len()) + for key := range s { + res = append(res, key) + } + sort.Sort(res) + return res +} + +// UnsortedList returns the slice with contents in random order. +func (s Set[E]) UnsortedList() []E { + res := make([]E, 0, len(s)) + for key := range s { + res = append(res, key) + } + return res +} + +// PopAny returns a single element from the set. +func (s Set[E]) PopAny() (E, bool) { + for key := range s { + s.Delete(key) + return key, true + } + var zeroValue E + return zeroValue, false +} + +// Clone returns a new set which is a copy of the current set. +func (s Set[T]) Clone() Set[T] { + result := make(Set[T], len(s)) + for key := range s { + result.Insert(key) + } + return result +} + +// SymmetricDifference returns a set of elements which are in either of the sets, but not in their intersection. +// For example: +// s1 = {a1, a2, a3} +// s2 = {a1, a2, a4, a5} +// s1.SymmetricDifference(s2) = {a3, a4, a5} +// s2.SymmetricDifference(s1) = {a3, a4, a5} +func (s Set[T]) SymmetricDifference(s2 Set[T]) Set[T] { + return s.Difference(s2).Union(s2.Difference(s)) +} diff --git a/tools/vendor/k8s.io/utils/set/set_go_1.20.go b/tools/vendor/k8s.io/utils/set/set_go_1.20.go new file mode 100644 index 00000000000..6ab79682b8e --- /dev/null +++ b/tools/vendor/k8s.io/utils/set/set_go_1.20.go @@ -0,0 +1,33 @@ +//go:build !go1.21 + +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package set + +// Clear empties the set. +// It is preferable to replace the set with a newly constructed set, +// but not all callers can do that (when there are other references to the map). +// In some cases the set *won't* be fully cleared, e.g. a Set[float32] containing NaN +// can't be cleared because NaN can't be removed. +// For sets containing items of a type that is reflexive for ==, +// this is optimized to a single call to runtime.mapclear(). +func (s Set[T]) Clear() Set[T] { + for key := range s { + delete(s, key) + } + return s +} diff --git a/tools/vendor/k8s.io/utils/set/set_go_1.21.go b/tools/vendor/k8s.io/utils/set/set_go_1.21.go new file mode 100644 index 00000000000..e95a368794b --- /dev/null +++ b/tools/vendor/k8s.io/utils/set/set_go_1.21.go @@ -0,0 +1,27 @@ +//go:build go1.21 + +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package set + +// Clear empties the set. +// It is preferable to replace the set with a newly constructed set, +// but not all callers can do that (when there are other references to the map). +func (s Set[T]) Clear() Set[T] { + clear(s) + return s +} diff --git a/tools/vendor/modules.txt b/tools/vendor/modules.txt index 5edbdcd1ed3..0b49fa4ca16 100644 --- a/tools/vendor/modules.txt +++ b/tools/vendor/modules.txt @@ -2340,6 +2340,7 @@ k8s.io/utils/net k8s.io/utils/path k8s.io/utils/pointer k8s.io/utils/ptr +k8s.io/utils/set k8s.io/utils/trace # mvdan.cc/gofumpt v0.8.0 ## explicit; go 1.23.0 @@ -2377,10 +2378,11 @@ sigs.k8s.io/controller-tools/pkg/webhook ## explicit; go 1.21 sigs.k8s.io/json sigs.k8s.io/json/internal/golang/encoding/json -# sigs.k8s.io/kube-api-linter v0.0.0-20250723124831-1b29e82a0f55 +# sigs.k8s.io/kube-api-linter v0.0.0-20250729132427-47bfeef6cd38 ## explicit; go 1.24.0 sigs.k8s.io/kube-api-linter/pkg/analysis/commentstart sigs.k8s.io/kube-api-linter/pkg/analysis/conditions +sigs.k8s.io/kube-api-linter/pkg/analysis/conflictingmarkers sigs.k8s.io/kube-api-linter/pkg/analysis/duplicatemarkers sigs.k8s.io/kube-api-linter/pkg/analysis/errors sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/conflictingmarkers/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/conflictingmarkers/analyzer.go new file mode 100644 index 00000000000..65a3b55ec84 --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/conflictingmarkers/analyzer.go @@ -0,0 +1,132 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package conflictingmarkers + +import ( + "fmt" + "go/ast" + "strings" + + "golang.org/x/tools/go/analysis" + "k8s.io/apimachinery/pkg/util/sets" + kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors" + "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags" + "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/inspector" + "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers" + "sigs.k8s.io/kube-api-linter/pkg/analysis/utils" +) + +const name = "conflictingmarkers" + +type analyzer struct { + conflictSets []ConflictSet +} + +func newAnalyzer(cfg *ConflictingMarkersConfig) *analysis.Analyzer { + if cfg == nil { + cfg = &ConflictingMarkersConfig{} + } + + // Register markers from configuration + for _, conflictSet := range cfg.Conflicts { + for _, set := range conflictSet.Sets { + for _, markerID := range set { + markers.DefaultRegistry().Register(markerID) + } + } + } + + a := &analyzer{ + conflictSets: cfg.Conflicts, + } + + return &analysis.Analyzer{ + Name: name, + Doc: "Check that fields do not have conflicting markers from mutually exclusive sets", + Run: a.run, + Requires: []*analysis.Analyzer{inspector.Analyzer}, + } +} + +func (a *analyzer) run(pass *analysis.Pass) (any, error) { + inspect, ok := pass.ResultOf[inspector.Analyzer].(inspector.Inspector) + if !ok { + return nil, kalerrors.ErrCouldNotGetInspector + } + + inspect.InspectFields(func(field *ast.Field, stack []ast.Node, _ extractjsontags.FieldTagInfo, markersAccess markers.Markers) { + checkField(pass, field, markersAccess, a.conflictSets) + }) + + return nil, nil //nolint:nilnil +} + +func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markers.Markers, conflictSets []ConflictSet) { + if field == nil || len(field.Names) == 0 { + return + } + + markers := utils.TypeAwareMarkerCollectionForField(pass, markersAccess, field) + + for _, conflictSet := range conflictSets { + checkConflict(pass, field, markers, conflictSet) + } +} + +func checkConflict(pass *analysis.Pass, field *ast.Field, markers markers.MarkerSet, conflictSet ConflictSet) { + // Track which sets have markers present + conflictingMarkers := make([]sets.Set[string], 0) + + for _, set := range conflictSet.Sets { + foundMarkers := sets.New[string]() + + for _, markerID := range set { + if markers.Has(markerID) { + foundMarkers.Insert(markerID) + } + } + // Only add the set if it has at least one marker + if foundMarkers.Len() > 0 { + conflictingMarkers = append(conflictingMarkers, foundMarkers) + } + } + + // If two or more sets have markers, report the conflict + if len(conflictingMarkers) >= 2 { + reportConflict(pass, field, conflictSet, conflictingMarkers) + } +} + +func reportConflict(pass *analysis.Pass, field *ast.Field, conflictSet ConflictSet, conflictingMarkers []sets.Set[string]) { + // Build a descriptive message showing which sets conflict + setDescriptions := make([]string, 0, len(conflictingMarkers)) + + for _, set := range conflictingMarkers { + markersList := sets.List(set) + setDescriptions = append(setDescriptions, fmt.Sprintf("%v", markersList)) + } + + message := fmt.Sprintf("field %s has conflicting markers: %s: {%s}. %s", + field.Names[0].Name, + conflictSet.Name, + strings.Join(setDescriptions, ", "), + conflictSet.Description) + + pass.Report(analysis.Diagnostic{ + Pos: field.Pos(), + Message: message, + }) +} diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/conflictingmarkers/config.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/conflictingmarkers/config.go new file mode 100644 index 00000000000..5317cc6431b --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/conflictingmarkers/config.go @@ -0,0 +1,39 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package conflictingmarkers + +// ConflictingMarkersConfig contains the configuration for the conflictingmarkers linter. +type ConflictingMarkersConfig struct { + // Conflicts allows users to define sets of conflicting markers. + // Each entry defines a conflict between multiple sets of markers. + Conflicts []ConflictSet `json:"conflicts"` +} + +// ConflictSet represents a conflict between multiple sets of markers. +// Markers within each set are mutually exclusive with markers in all other sets. +// The linter will emit a diagnostic when a field has markers from two or more sets. +type ConflictSet struct { + // Name is a human-readable name for this conflict set. + // This name will appear in diagnostic messages to identify the type of conflict. + Name string `json:"name"` + // Sets contains the sets of markers that are mutually exclusive with each other. + // Each set is a slice of marker identifiers. + // The linter will emit a diagnostic when a field has markers from two or more sets. + Sets [][]string `json:"sets"` + // Description provides a description of why these markers conflict. + // The linter will include this description in the diagnostic message when a conflict is detected. + Description string `json:"description"` +} diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/conflictingmarkers/doc.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/conflictingmarkers/doc.go new file mode 100644 index 00000000000..765f60cf6c5 --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/conflictingmarkers/doc.go @@ -0,0 +1,60 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +conflictingmarkers is a linter that detects and reports when mutually exclusive markers are used on the same field. +This prevents common configuration errors and unexpected behavior in Kubernetes API types. + +The linter reports issues when markers from two or more sets of a conflict definition are present on the same field. +It does NOT report issues when multiple markers from the same set are present - only when markers from +different sets within the same conflict definition are found together. + +The linter is fully configurable and requires users to define all conflict sets they want to check. +There are no built-in conflict sets - all conflicts must be explicitly configured. + +Each conflict set must specify: +- A unique name for the conflict +- Multiple sets of markers that are mutually exclusive with each other (at least 2 sets) +- A description explaining why the markers conflict + +Example configuration: +```yaml +lintersConfig: + + conflictingmarkers: + conflicts: + - name: "optional_vs_required" + sets: + - ["optional", "+kubebuilder:validation:Optional", "+k8s:validation:optional"] + - ["required", "+kubebuilder:validation:Required", "+k8s:validation:required"] + description: "A field cannot be both optional and required" + - name: "my_custom_conflict" + sets: + - ["custom:marker1", "custom:marker2"] + - ["custom:marker3", "custom:marker4"] + - ["custom:marker5", "custom:marker6"] + description: "These markers define different storage backends that cannot be used simultaneously" + +``` + +Configuration options: +- `conflicts`: Required list of conflict set definitions. + +Note: This linter is not enabled by default and must be explicitly enabled in the configuration. + +The linter does not provide automatic fixes as it cannot determine which conflicting marker should be removed. +*/ +package conflictingmarkers diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/conflictingmarkers/initializer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/conflictingmarkers/initializer.go new file mode 100644 index 00000000000..7f05424dfe1 --- /dev/null +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/conflictingmarkers/initializer.go @@ -0,0 +1,117 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package conflictingmarkers + +import ( + "fmt" + + "golang.org/x/tools/go/analysis" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation/field" + "sigs.k8s.io/kube-api-linter/pkg/analysis/initializer" + "sigs.k8s.io/kube-api-linter/pkg/analysis/registry" +) + +func init() { + registry.DefaultRegistry().RegisterLinter(Initializer()) +} + +// Initializer returns the AnalyzerInitializer for this +// Analyzer so that it can be added to the registry. +func Initializer() initializer.AnalyzerInitializer { + return initializer.NewConfigurableInitializer( + name, + initAnalyzer, + false, + validateConfig, + ) +} + +// initAnalyzer returns the initialized Analyzer. +func initAnalyzer(cfg *ConflictingMarkersConfig) (*analysis.Analyzer, error) { + return newAnalyzer(cfg), nil +} + +// validateConfig validates the configuration in the config.ConflictingMarkersConfig struct. +func validateConfig(cfg *ConflictingMarkersConfig, fldPath *field.Path) field.ErrorList { + if cfg == nil { + return field.ErrorList{} + } + + fieldErrors := field.ErrorList{} + + // Validate that at least one conflict set is defined + if len(cfg.Conflicts) == 0 { + fieldErrors = append(fieldErrors, field.Required(fldPath.Child("conflicts"), "at least one conflict set is required")) + return fieldErrors + } + + nameSet := sets.New[string]() + + for i, conflictSet := range cfg.Conflicts { + if nameSet.Has(conflictSet.Name) { + fieldErrors = append(fieldErrors, field.Duplicate(fldPath.Child("conflicts").Index(i).Child("name"), conflictSet.Name)) + continue + } + + fieldErrors = append(fieldErrors, validateConflictSet(conflictSet, fldPath.Child("conflicts").Index(i))...) + + nameSet.Insert(conflictSet.Name) + } + + return fieldErrors +} + +func validateConflictSet(conflictSet ConflictSet, fldPath *field.Path) field.ErrorList { + fieldErrors := field.ErrorList{} + + if conflictSet.Name == "" { + fieldErrors = append(fieldErrors, field.Required(fldPath.Child("name"), "name is required")) + } + + if conflictSet.Description == "" { + fieldErrors = append(fieldErrors, field.Required(fldPath.Child("description"), "description is required")) + } + + if len(conflictSet.Sets) < 2 { + fieldErrors = append(fieldErrors, field.Required(fldPath.Child("sets"), "at least 2 sets are required")) + return fieldErrors + } + + // Validate each set is non-empty + for i, set := range conflictSet.Sets { + if len(set) == 0 { + fieldErrors = append(fieldErrors, field.Required(fldPath.Child("sets").Index(i), "set cannot be empty")) + } + } + + // Check for overlapping markers between any sets + for i := range conflictSet.Sets { + for j := i + 1; j < len(conflictSet.Sets); j++ { + setI := sets.New(conflictSet.Sets[i]...) + setJ := sets.New(conflictSet.Sets[j]...) + + if intersection := setI.Intersection(setJ); intersection.Len() > 0 { + fieldErrors = append(fieldErrors, field.Invalid( + fldPath.Child("sets"), + conflictSet, + fmt.Sprintf("sets %d and %d cannot contain overlapping markers: %v", i+1, j+1, sets.List(intersection)))) + } + } + } + + return fieldErrors +} diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags/analyzer.go index 14e0031143c..221a0ee7fa5 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags/analyzer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags/analyzer.go @@ -25,10 +25,15 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" - + "k8s.io/utils/set" kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors" ) +const ( + omitEmpty = "omitempty" + omitZero = "omitzero" +) + // StructFieldTags is used to find information about // json tags on fields within struct. type StructFieldTags interface { @@ -137,10 +142,12 @@ func extractTagInfo(tag *ast.BasicLit) FieldTagInfo { } tagName := tagValues[0] + tagSet := set.New[string](tagValues...) return FieldTagInfo{ Name: tagName, - OmitEmpty: len(tagValues) == 2 && tagValues[1] == "omitempty", + OmitEmpty: tagSet.Has(omitEmpty), + OmitZero: tagSet.Has(omitZero), RawValue: tagValue, Pos: pos, End: end, @@ -159,6 +166,9 @@ type FieldTagInfo struct { // OmitEmpty is true if the field has the omitempty option in the json tag. OmitEmpty bool + // OmitZero is true if the field has the omitzero option in the json tag. + OmitZero bool + // Inline is true if the field has the inline option in the json tag. Inline bool diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields/analyzer.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields/analyzer.go index 20e58b7a38c..eb241ebf59e 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields/analyzer.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields/analyzer.go @@ -49,6 +49,7 @@ type analyzer struct { pointerPolicy OptionalFieldsPointerPolicy pointerPreference OptionalFieldsPointerPreference omitEmptyPolicy OptionalFieldsOmitEmptyPolicy + omitZeroPolicy OptionalFieldsOmitZeroPolicy } // newAnalyzer creates a new analyzer. @@ -63,6 +64,7 @@ func newAnalyzer(cfg *OptionalFieldsConfig) *analysis.Analyzer { pointerPolicy: cfg.Pointers.Policy, pointerPreference: cfg.Pointers.Preference, omitEmptyPolicy: cfg.OmitEmpty.Policy, + omitZeroPolicy: cfg.OmitZero.Policy, } return &analysis.Analyzer{ @@ -125,11 +127,16 @@ func defaultConfig(cfg *OptionalFieldsConfig) { if cfg.OmitEmpty.Policy == "" { cfg.OmitEmpty.Policy = OptionalFieldsOmitEmptyPolicySuggestFix } + + if cfg.OmitZero.Policy == "" { + cfg.OmitZero.Policy = OptionalFieldsOmitZeroPolicySuggestFix + } } func (a *analyzer) checkFieldProperties(pass *analysis.Pass, field *ast.Field, fieldName string, markersAccess markershelper.Markers, jsonTags extractjsontags.FieldTagInfo) { - hasValidZeroValue, completeValidation := utils.IsZeroValueValid(pass, field, field.Type, markersAccess) + hasValidZeroValue, completeValidation := utils.IsZeroValueValid(pass, field, field.Type, markersAccess, a.omitZeroPolicy != OptionalFieldsOmitZeroPolicyForbid) hasOmitEmpty := jsonTags.OmitEmpty + hasOmitZero := jsonTags.OmitZero isPointer, underlying := isStarExpr(field.Type) isStruct := utils.IsStructType(pass, field.Type) @@ -142,7 +149,17 @@ func (a *analyzer) checkFieldProperties(pass *analysis.Pass, field *ast.Field, f } // The pointer preference is now when required. + // Validate for omitzero policy. + if a.omitZeroPolicy != OptionalFieldsOmitZeroPolicyForbid { + // If we require omitzero, we can check the field properties based on it being an omitzero field. + a.checkFieldPropertiesWithOmitZeroRequired(pass, field, fieldName, jsonTags, hasOmitZero, isPointer, isStruct, hasValidZeroValue) + } else { + // when the omitzero policy is set to forbid, we need to report removing omitzero if set on the struct fields. + a.checkFieldPropertiesWithOmitZeroForbidPolicy(pass, field, fieldName, isStruct, hasOmitZero, jsonTags) + } + // The pointer preference is now when required. + // Validate for omitempty policy. if a.omitEmptyPolicy != OptionalFieldsOmitEmptyPolicyIgnore || hasOmitEmpty { // If we require omitempty, or the field has omitempty, we can check the field properties based on it being an omitempty field. a.checkFieldPropertiesWithOmitEmptyRequired(pass, field, fieldName, jsonTags, underlying, hasOmitEmpty, hasValidZeroValue, completeValidation, isPointer, isStruct) @@ -157,6 +174,9 @@ func (a *analyzer) checkFieldPropertiesWithOmitEmptyRequired(pass *analysis.Pass a.handleFieldShouldHaveOmitEmpty(pass, field, fieldName, hasOmitEmpty, jsonTags) switch { + case isStruct && !hasValidZeroValue && a.omitZeroPolicy != OptionalFieldsOmitZeroPolicyForbid: + // The struct field need not be pointer if it does not have a valid zero value. + return case hasValidZeroValue && !completeValidation: a.handleIncompleteFieldValidation(pass, field, fieldName, isPointer, underlying) fallthrough // Since it's a valid zero value, we should still enforce the pointer. @@ -183,8 +203,26 @@ func (a *analyzer) checkFieldPropertiesWithoutOmitEmpty(pass *analysis.Pass, fie // Once it has the omitempty tag, it will also need to be a pointer in some cases. // Now handle it as if it had the omitempty already. // We already handle the omitempty tag above, so force the `hasOmitEmpty` to true. - a.checkFieldPropertiesWithOmitEmptyRequired(pass, field, fieldName, jsonTags, underlying, true, hasValidZeroValue, completeValidation, isPointer, isStruct) + a.checkFieldPropertiesWithOmitEmptyRequired(pass, field, fieldName, jsonTags, underlying, false, hasValidZeroValue, completeValidation, isPointer, isStruct) + } +} + +func (a *analyzer) checkFieldPropertiesWithOmitZeroRequired(pass *analysis.Pass, field *ast.Field, fieldName string, jsonTags extractjsontags.FieldTagInfo, hasOmitZero, isPointer, isStruct, hasValidZeroValue bool) { + if !isStruct || hasValidZeroValue { + return } + + a.handleFieldShouldHaveOmitZero(pass, field, fieldName, hasOmitZero, jsonTags) + a.handleFieldShouldNotBePointer(pass, field, fieldName, isPointer, "field %s is optional and does not have a valid zero value. The field does not need to be a pointer.") +} + +func (a *analyzer) checkFieldPropertiesWithOmitZeroForbidPolicy(pass *analysis.Pass, field *ast.Field, fieldName string, isStruct, hasOmitZero bool, jsonTags extractjsontags.FieldTagInfo) { + if !isStruct || !hasOmitZero { + // Handle omitzero only for struct field having omitZero tag. + return + } + + reportShouldRemoveOmitZero(pass, field, fieldName, jsonTags) } func (a *analyzer) handleFieldShouldBePointer(pass *analysis.Pass, field *ast.Field, fieldName string, isPointer bool, underlying ast.Expr) { @@ -229,6 +267,14 @@ func (a *analyzer) handleFieldShouldHaveOmitEmpty(pass *analysis.Pass, field *as reportShouldAddOmitEmpty(pass, field, a.omitEmptyPolicy, fieldName, "field %s is optional and should have the omitempty tag", jsonTags) } +func (a *analyzer) handleFieldShouldHaveOmitZero(pass *analysis.Pass, field *ast.Field, fieldName string, hasOmitZero bool, jsonTags extractjsontags.FieldTagInfo) { + if hasOmitZero { + return + } + // Currently, add omitzero tags to only struct fields. + reportShouldAddOmitZero(pass, field, a.omitZeroPolicy, fieldName, "field %s is optional and does not allow the zero value. It must have the omitzero tag.", jsonTags) +} + func (a *analyzer) handleIncompleteFieldValidation(pass *analysis.Pass, field *ast.Field, fieldName string, isPointer bool, underlying ast.Expr) { if isPointer || isPointerType(pass, underlying) { // Don't warn them if the field is already a pointer. diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields/config.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields/config.go index a37186c6cca..f627addc43b 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields/config.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields/config.go @@ -26,6 +26,11 @@ type OptionalFieldsConfig struct { // This defines how the linter should handle optional fields, and whether they should have the omitempty tag or not. // By default, all fields will be expected to have the `omitempty` tag. OmitEmpty OptionalFieldsOmitEmpty `json:"omitempty"` + + // omitzero is the policy for the `omitzero` tag within the json tag for fields. + // This defines how the linter should handle optional fields, and whether they should have the omitzero tag or not. + // By default, all the struct fields will be expected to have the `omitzero` tag when their zero value is not an acceptable user choice. + OmitZero OptionalFieldsOmitZero `json:"omitzero"` } // OptionalFieldsPointers is the configuration for pointers in optional fields. @@ -49,7 +54,7 @@ type OptionalFieldsPointers struct { // OptionalFieldsOmitEmpty is the configuration for the `omitempty` tag on optional fields. type OptionalFieldsOmitEmpty struct { // policy determines whether the linter should require omitempty for all optional fields. - // Valid values are "SuggestFix" and "Ignore". + // Valid values are "SuggestFix", "Warn" and "Ignore". // When set to "SuggestFix", the linter will suggest adding the `omitempty` tag when an optional field does not have it. // When set to "Warn", the linter will emit a warning if the field does not have the `omitempty` tag. // When set to "Ignore", and optional field missing the `omitempty` tag will be ignored. @@ -57,6 +62,19 @@ type OptionalFieldsOmitEmpty struct { Policy OptionalFieldsOmitEmptyPolicy `json:"policy"` } +// OptionalFieldsOmitZero is the configuration for the `omitzero` tag on optional fields. +type OptionalFieldsOmitZero struct { + // policy determines whether the linter should require omitzero for all optional `struct` fields. + // Valid values are "SuggestFix", "Warn" and "Forbid". + // When set to "SuggestFix", the linter will suggest adding the `omitzero` tag when an optional field does not have it. + // When set to "Warn", the linter will emit a warning if the field does not have the `omitzero` tag. + // When set to "Forbid", 'omitzero' tags wont be considered. + // Note, when set to "Forbid", and a field have the `omitzero` tag, the linter will suggest to remove the `omitzero` tag. + // Note, `omitzero` tag is supported in go version starting from go 1.24. + // Note, Configure omitzero policy to 'Forbid', if using with go version less than go 1.24. + Policy OptionalFieldsOmitZeroPolicy `json:"policy"` +} + // OptionalFieldsPointerPreference is the preference for pointers in optional fields. type OptionalFieldsPointerPreference string @@ -92,3 +110,17 @@ const ( // OptionalFieldsOmitEmptyPolicyIgnore indicates that the linter will ignore any field missing the omitempty tag. OptionalFieldsOmitEmptyPolicyIgnore OptionalFieldsOmitEmptyPolicy = "Ignore" ) + +// OptionalFieldsOmitZeroPolicy is the policy for the omitzero tag on optional fields. +type OptionalFieldsOmitZeroPolicy string + +const ( + // OptionalFieldsOmitZeroPolicySuggestFix indicates that the linter will emit a warning if the field does not have omitzero, and suggest a fix. + OptionalFieldsOmitZeroPolicySuggestFix OptionalFieldsOmitZeroPolicy = "SuggestFix" + + // OptionalFieldsOmitZeroPolicyWarn indicates that the linter will emit a warning if the field does not have omitzero. + OptionalFieldsOmitZeroPolicyWarn OptionalFieldsOmitZeroPolicy = "Warn" + + // OptionalFieldsOmitZeroPolicyForbid indicates that the linter will forbid using omitzero tag. + OptionalFieldsOmitZeroPolicyForbid OptionalFieldsOmitZeroPolicy = "Forbid" +) diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields/util.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields/util.go index 2aaf06f6f9b..2ccc9f372e4 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields/util.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields/util.go @@ -19,6 +19,7 @@ import ( "fmt" "go/ast" "go/token" + "strings" "golang.org/x/tools/go/analysis" "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags" @@ -143,3 +144,52 @@ func reportShouldAddOmitEmpty(pass *analysis.Pass, field *ast.Field, omitEmptyPo panic(fmt.Sprintf("unknown omit empty policy: %s", omitEmptyPolicy)) } } + +// reportShouldAddOmitZero adds an analysis diagnostic that explains that an omitzero tag should be added. +func reportShouldAddOmitZero(pass *analysis.Pass, field *ast.Field, omitZeroPolicy OptionalFieldsOmitZeroPolicy, fieldName, messageFmt string, fieldTagInfo extractjsontags.FieldTagInfo) { + switch omitZeroPolicy { + case OptionalFieldsOmitZeroPolicySuggestFix: + pass.Report(analysis.Diagnostic{ + Pos: field.Pos(), + Message: fmt.Sprintf(messageFmt, fieldName), + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: fmt.Sprintf("should add 'omitzero' to the field tag for field %s", fieldName), + TextEdits: []analysis.TextEdit{ + { + Pos: fieldTagInfo.Pos + token.Pos(len(fieldTagInfo.Name)), + NewText: []byte(",omitzero"), + }, + }, + }, + }, + }) + case OptionalFieldsOmitZeroPolicyWarn: + pass.Reportf(field.Pos(), messageFmt, fieldName) + case OptionalFieldsOmitZeroPolicyForbid: + // Do nothing, as the policy is to ignore the missing omitzero tag. + default: + panic(fmt.Sprintf("unknown omit zero policy: %s", omitZeroPolicy)) + } +} + +// reportShouldRemoveOmitZero adds an analysis diagnostic that explains that an omitzero tag should be removed. +func reportShouldRemoveOmitZero(pass *analysis.Pass, field *ast.Field, fieldName string, jsonTags extractjsontags.FieldTagInfo) { + omitZeroPos := jsonTags.Pos + token.Pos(strings.Index(jsonTags.RawValue, ",omitzero")) + pass.Report(analysis.Diagnostic{ + Pos: field.Pos(), + Message: fmt.Sprintf("field %s has the omitzero tag, but by policy is not allowed. The omitzero tag should be removed.", fieldName), + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: "should remove the omitzero tag", + TextEdits: []analysis.TextEdit{ + { + Pos: omitZeroPos, + End: omitZeroPos + token.Pos(len(",omitzero")), + NewText: nil, // Clear the omitzero tag. + }, + }, + }, + }, + }) +} diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/utils/zero_value.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/utils/zero_value.go index 90505117a55..39281ce480b 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/utils/zero_value.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/analysis/utils/zero_value.go @@ -41,22 +41,22 @@ var ( // For structs, for the zero value to be valid, all fields within the struct that would not be omitted must accept their zero values. // The second return value indicates whether the field validation is complete. Complete validation means that we are certain whether or not the zero value is valid. // Incomplete validation means that if additional validation were added (e.g. to add a min length to a string), the zero value might become invalid. -func IsZeroValueValid(pass *analysis.Pass, field *ast.Field, typeExpr ast.Expr, markersAccess markershelper.Markers) (bool, bool) { +func IsZeroValueValid(pass *analysis.Pass, field *ast.Field, typeExpr ast.Expr, markersAccess markershelper.Markers, considerOmitzero bool) (bool, bool) { underlyingType := getUnderlyingType(typeExpr) switch t := underlyingType.(type) { case *ast.StructType: // For structs, we have to check if there are any non-omitted fields, that do not accept a zero value. - return isStructZeroValueValid(pass, field, t, markersAccess) + return isStructZeroValueValid(pass, field, t, markersAccess, considerOmitzero) case *ast.Ident: - return isIdentZeroValueValid(pass, field, t, markersAccess) + return isIdentZeroValueValid(pass, field, t, markersAccess, considerOmitzero) case *ast.MapType: return isMapZeroValueValid(pass, field, markersAccess) case *ast.ArrayType: // For arrays, we can use a zero value if the array is not required to have a minimum number of items. return isArrayZeroValueValid(pass, field, t, markersAccess) case *ast.StarExpr: - return IsZeroValueValid(pass, field, t.X, markersAccess) + return IsZeroValueValid(pass, field, t.X, markersAccess, considerOmitzero) } // We don't know what the type is so can't assert the zero value is valid. @@ -76,7 +76,7 @@ func getUnderlyingType(expr ast.Expr) ast.Expr { // isStructZeroValueValid checks if the zero value of a struct is valid. // It checks if all non-omitted fields within the struct accept their zero values. // It also checks if the struct has a minProperties marker, and if so, whether the number of non-omitted fields is greater than or equal to the minProperties value. -func isStructZeroValueValid(pass *analysis.Pass, field *ast.Field, structType *ast.StructType, markersAccess markershelper.Markers) (bool, bool) { +func isStructZeroValueValid(pass *analysis.Pass, field *ast.Field, structType *ast.StructType, markersAccess markershelper.Markers, considerOmitzero bool) (bool, bool) { if structType == nil { return false, false } @@ -86,7 +86,7 @@ func isStructZeroValueValid(pass *analysis.Pass, field *ast.Field, structType *a panic("could not get struct field tags from pass result") } - zeroValueValid, nonOmittedFields := areStructFieldZeroValuesValid(pass, structType, markersAccess, jsonTagInfo) + zeroValueValid, nonOmittedFields := areStructFieldZeroValuesValid(pass, structType, markersAccess, jsonTagInfo, considerOmitzero) markerSet := TypeAwareMarkerCollectionForField(pass, markersAccess, field) @@ -112,22 +112,38 @@ func isStructZeroValueValid(pass *analysis.Pass, field *ast.Field, structType *a } // areStructFieldZeroValuesValid checks if all non-omitted fields within a struct accept their zero values. -func areStructFieldZeroValuesValid(pass *analysis.Pass, structType *ast.StructType, markersAccess markershelper.Markers, jsonTagInfo extractjsontags.StructFieldTags) (bool, int) { +// +//nolint:cyclop +func areStructFieldZeroValuesValid(pass *analysis.Pass, structType *ast.StructType, markersAccess markershelper.Markers, jsonTagInfo extractjsontags.StructFieldTags, considerOmitzero bool) (bool, int) { zeroValueValid := true nonOmittedFields := 0 for _, field := range structType.Fields.List { + fieldRequired := isFieldRequired(field, markersAccess) fieldTagInfo := jsonTagInfo.FieldTags(field) - - if fieldTagInfo.OmitEmpty { - // If the field is omitted, we can use a zero value. - // For structs, if they aren't a pointer another error will be raised. - continue + isStruct := IsStructType(pass, field.Type) + + // Assume the field has omitempty. + // Then the zero value (omitted) for a required field is not valid, and for an optional field it is valid. + validValue := !fieldRequired + + // non-omitted fields are required fields or fields without an omitempty tag or struct fields without omitzero tag (if valid omitzero policy is set) + // This allows us to count them towards the min-properties count in the parent function. + switch { + case fieldRequired: + nonOmittedFields++ + case isStruct && considerOmitzero && fieldTagInfo.OmitZero: + // struct with omitzero field should be omitted. + case !fieldTagInfo.OmitEmpty: + nonOmittedFields++ } - nonOmittedFields++ - - validValue, _ := IsZeroValueValid(pass, field, field.Type, markersAccess) + // When the field is not omitted, we need to check if the zero value is valid (required or not). + switch { + case isStruct && considerOmitzero && fieldTagInfo.OmitZero: + case !fieldTagInfo.OmitEmpty: + validValue, _ = IsZeroValueValid(pass, field, field.Type, markersAccess, considerOmitzero) + } // If either value is false then the collected values will be false. zeroValueValid = zeroValueValid && validValue @@ -137,7 +153,7 @@ func areStructFieldZeroValuesValid(pass *analysis.Pass, structType *ast.StructTy } // isIdentZeroValueValid checks if the zero value of an identifier is valid. -func isIdentZeroValueValid(pass *analysis.Pass, field *ast.Field, ident *ast.Ident, markersAccess markershelper.Markers) (bool, bool) { +func isIdentZeroValueValid(pass *analysis.Pass, field *ast.Field, ident *ast.Ident, markersAccess markershelper.Markers, considerOmitzero bool) (bool, bool) { if ident == nil { return false, false } @@ -161,7 +177,7 @@ func isIdentZeroValueValid(pass *analysis.Pass, field *ast.Field, ident *ast.Ide return false, false } - return IsZeroValueValid(pass, field, typeSpec.Type, markersAccess) + return IsZeroValueValid(pass, field, typeSpec.Type, markersAccess, considerOmitzero) } // isStringZeroValueValid checks if a string field can have a zero value. @@ -417,3 +433,13 @@ func isBoolIdent(ident *ast.Ident) bool { func isFloatIdent(ident *ast.Ident) bool { return ident.Name == "float32" || ident.Name == "float64" } + +// isFieldRequired checks if the field is required. +// It checks for the presence of the required marker, the kubebuilder required marker, or the k8s required marker. +func isFieldRequired(field *ast.Field, markersAccess markershelper.Markers) bool { + fieldMarkers := markersAccess.FieldMarkers(field) + + return fieldMarkers.Has(markers.RequiredMarker) || + fieldMarkers.Has(markers.KubebuilderRequiredMarker) || + fieldMarkers.Has(markers.K8sRequiredMarker) +} diff --git a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/registration/doc.go b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/registration/doc.go index 19bcddbbf97..5ca855e8fb7 100644 --- a/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/registration/doc.go +++ b/tools/vendor/sigs.k8s.io/kube-api-linter/pkg/registration/doc.go @@ -25,6 +25,7 @@ package registration import ( _ "sigs.k8s.io/kube-api-linter/pkg/analysis/commentstart" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/conditions" + _ "sigs.k8s.io/kube-api-linter/pkg/analysis/conflictingmarkers" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/duplicatemarkers" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/integers" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/jsontags"