diff --git a/pkg/analysis/arrayofstruct/analyzer.go b/pkg/analysis/arrayofstruct/analyzer.go index a604780e..ceb8b2e5 100644 --- a/pkg/analysis/arrayofstruct/analyzer.go +++ b/pkg/analysis/arrayofstruct/analyzer.go @@ -20,11 +20,13 @@ import ( "go/ast" "golang.org/x/tools/go/analysis" + 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" markershelper "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers" "sigs.k8s.io/kube-api-linter/pkg/analysis/utils" + "sigs.k8s.io/kube-api-linter/pkg/markers" ) const name = "arrayofstruct" @@ -38,6 +40,10 @@ var Analyzer = &analysis.Analyzer{ Requires: []*analysis.Analyzer{inspector.Analyzer}, } +func init() { + markershelper.DefaultRegistry().Register(markers.KubebuilderExactlyOneOf) +} + func run(pass *analysis.Pass) (any, error) { inspect, ok := pass.ResultOf[inspector.Analyzer].(inspector.Inspector) if !ok { @@ -74,6 +80,13 @@ func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markershelp return } + // Check if the struct has union markers that satisfy the required constraint + if hasExactlyOneOfMarker(structType, markersAccess) { + // ExactlyOneOf marker enforces that exactly one field is set, + // so we don't need to report an error + return + } + // Check if at least one field in the struct has a required marker if hasRequiredField(structType, markersAccess) { return @@ -208,3 +221,16 @@ func hasRequiredField(structType *ast.StructType, markersAccess markershelper.Ma return false } + +// hasExactlyOneOfMarker checks if the struct has an ExactlyOneOf marker, +// which satisfies the required field constraint by ensuring exactly one field is set. +func hasExactlyOneOfMarker(structType *ast.StructType, markersAccess markershelper.Markers) bool { + if structType == nil { + return false + } + + // Use StructMarkers to get the set of markers on the struct + markerSet := markersAccess.StructMarkers(structType) + + return markerSet.Has(markers.KubebuilderExactlyOneOf) +} diff --git a/pkg/analysis/arrayofstruct/testdata/src/a/a.go b/pkg/analysis/arrayofstruct/testdata/src/a/a.go index 969f4486..875ddf16 100644 --- a/pkg/analysis/arrayofstruct/testdata/src/a/a.go +++ b/pkg/analysis/arrayofstruct/testdata/src/a/a.go @@ -150,3 +150,14 @@ type ValidStructWithCustomBasicType struct { // This should not trigger the linter because CustomString is based on string, a basic type Items []CustomString } + +// Valid case - struct with ExactlyOneOf marker +type ValidWithExactlyOneOf struct { + Items []ValidExactlyOneOfItem +} + +// +kubebuilder:validation:ExactlyOneOf=FieldA;FieldB +type ValidExactlyOneOfItem struct { + FieldA *string `json:"fieldA,omitempty"` + FieldB *string `json:"fieldB,omitempty"` +} diff --git a/pkg/markers/markers.go b/pkg/markers/markers.go index d1a5c0ac..300303c6 100644 --- a/pkg/markers/markers.go +++ b/pkg/markers/markers.go @@ -93,6 +93,9 @@ const ( // KubebuilderRequiredMarker is the marker that indicates that a field is required in kubebuilder. KubebuilderRequiredMarker = "kubebuilder:validation:Required" + // KubebuilderExactlyOneOf is the marker that indicates that a field has an exactly one of in kubebuilder. + KubebuilderExactlyOneOf = "kubebuilder:validation:ExactlyOneOf" + // KubebuilderItemsMaxLengthMarker is the marker that indicates that a field has a maximum length in kubebuilder. KubebuilderItemsMaxLengthMarker = "kubebuilder:validation:items:MaxLength"