Skip to content

Commit 2d09826

Browse files
authored
Merge pull request #17187 from justinsb/wip_cleanup_options_checking
Refactor validation logic around checking for multiple options
2 parents 0486349 + 5fa9690 commit 2d09826

File tree

6 files changed

+130
-72
lines changed

6 files changed

+130
-72
lines changed

pkg/apis/kops/networking.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@ limitations under the License.
1616

1717
package kops
1818

19-
import "k8s.io/apimachinery/pkg/api/resource"
19+
import (
20+
"k8s.io/apimachinery/pkg/api/resource"
21+
"k8s.io/apimachinery/pkg/util/sets"
22+
"k8s.io/klog/v2"
23+
"k8s.io/kops/util/pkg/reflectutils"
24+
)
2025

2126
// NetworkingSpec configures networking.
2227
type NetworkingSpec struct {
@@ -81,6 +86,16 @@ type NetworkingSpec struct {
8186
Kindnet *KindnetNetworkingSpec `json:"kindnet,omitempty"`
8287
}
8388

89+
// ConfiguredOptions returns the set of networking options that are configured (non-nil)
90+
// in the struct. We only expect a single option to be configured.
91+
func (n *NetworkingSpec) ConfiguredOptions() sets.Set[string] {
92+
options, err := reflectutils.FindSetFields(n, "classic", "kubenet", "external", "cni", "kopeio", "weave", "flannel", "calico", "canal", "kubeRouter", "romana", "amazonVPC", "cilium", "lyftvpc", "gcp", "kindnet")
93+
if err != nil {
94+
klog.Fatalf("error getting set fields: %v", err)
95+
}
96+
return options
97+
}
98+
8499
// UsesKubenet returns true if our networking is derived from kubenet
85100
func (n *NetworkingSpec) UsesKubenet() bool {
86101
if n == nil {

pkg/apis/kops/v1alpha2/networking.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ package v1alpha2
1818

1919
import (
2020
"k8s.io/apimachinery/pkg/api/resource"
21+
"k8s.io/apimachinery/pkg/util/sets"
22+
"k8s.io/klog/v2"
23+
"k8s.io/kops/util/pkg/reflectutils"
2124
)
2225

2326
// NetworkingSpec allows selection and configuration of a networking plugin
@@ -53,9 +56,17 @@ type NetworkingSpec struct {
5356
}
5457

5558
func (s *NetworkingSpec) IsEmpty() bool {
56-
return s.Classic == nil && s.Kubenet == nil && s.External == nil && s.CNI == nil && s.Kopeio == nil &&
57-
s.Weave == nil && s.Flannel == nil && s.Calico == nil && s.Canal == nil && s.KubeRouter == nil &&
58-
s.Romana == nil && s.AmazonVPC == nil && s.Cilium == nil && s.LyftVPC == nil && s.GCP == nil && s.Kindnet == nil
59+
return s.ConfiguredOptions().Len() == 0
60+
}
61+
62+
// ConfiguredOptions returns the set of networking options that are configured (non-nil)
63+
// in the struct. We only expect a single option to be configured.
64+
func (s *NetworkingSpec) ConfiguredOptions() sets.Set[string] {
65+
options, err := reflectutils.FindSetFields(s, "classic", "kubenet", "external", "cni", "kopeio", "weave", "flannel", "calico", "canal", "kuberouter", "romana", "amazonvpc", "cilium", "lyftvpc", "gce", "kindnet")
66+
if err != nil {
67+
klog.Fatalf("error getting configured options: %v", err)
68+
}
69+
return options
5970
}
6071

6172
// ClassicNetworkingSpec is the specification of classic networking mode, integrated into kubernetes.

pkg/apis/kops/validation/validation.go

Lines changed: 12 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,54 +1089,35 @@ func validateNetworking(cluster *kops.Cluster, v *kops.NetworkingSpec, fldPath *
10891089
allErrs = append(allErrs, validateTopology(cluster, v.Topology, fldPath.Child("topology"))...)
10901090
}
10911091

1092-
optionTaken := false
1093-
10941092
if v.Classic != nil {
10951093
allErrs = append(allErrs, field.Invalid(fldPath, "classic", "classic networking is not supported"))
10961094
}
10971095

10981096
if v.Kubenet != nil {
1099-
optionTaken = true
1100-
11011097
if cluster.Spec.IsIPv6Only() {
11021098
allErrs = append(allErrs, field.Forbidden(fldPath.Child("kubenet"), "Kubenet does not support IPv6"))
11031099
}
11041100
}
11051101

11061102
if v.External != nil {
1107-
if optionTaken {
1108-
allErrs = append(allErrs, field.Forbidden(fldPath.Child("external"), "only one networking option permitted"))
1109-
}
1110-
11111103
allErrs = append(allErrs, field.Forbidden(fldPath.Child("external"), "external is not supported for Kubernetes >= 1.26"))
1112-
optionTaken = true
11131104
}
11141105

11151106
if v.Kopeio != nil {
1116-
if optionTaken {
1117-
allErrs = append(allErrs, field.Forbidden(fldPath.Child("kopeio"), "only one networking option permitted"))
1118-
}
1119-
optionTaken = true
1120-
11211107
if cluster.Spec.IsIPv6Only() {
11221108
allErrs = append(allErrs, field.Forbidden(fldPath.Child("kopeio"), "Kopeio does not support IPv6"))
11231109
}
11241110
}
11251111

1126-
if v.CNI != nil && optionTaken {
1127-
allErrs = append(allErrs, field.Forbidden(fldPath.Child("cni"), "only one networking option permitted"))
1128-
}
1112+
// Nothing to validate for CNI
1113+
// if v.CNI != nil {
1114+
// }
11291115

11301116
if v.Weave != nil {
11311117
allErrs = append(allErrs, field.Forbidden(fldPath.Child("weave"), "Weave is no longer supported"))
11321118
}
11331119

11341120
if v.Flannel != nil {
1135-
if optionTaken {
1136-
allErrs = append(allErrs, field.Forbidden(fldPath.Child("flannel"), "only one networking option permitted"))
1137-
}
1138-
optionTaken = true
1139-
11401121
if cluster.IsKubernetesGTE("1.28") {
11411122
allErrs = append(allErrs, field.Forbidden(fldPath.Child("flannel"), "Flannel is not supported for Kubernetes >= 1.28"))
11421123
} else {
@@ -1145,20 +1126,10 @@ func validateNetworking(cluster *kops.Cluster, v *kops.NetworkingSpec, fldPath *
11451126
}
11461127

11471128
if v.Calico != nil {
1148-
if optionTaken {
1149-
allErrs = append(allErrs, field.Forbidden(fldPath.Child("calico"), "only one networking option permitted"))
1150-
}
1151-
optionTaken = true
1152-
11531129
allErrs = append(allErrs, validateNetworkingCalico(&cluster.Spec, v.Calico, fldPath.Child("calico"))...)
11541130
}
11551131

11561132
if v.Canal != nil {
1157-
if optionTaken {
1158-
allErrs = append(allErrs, field.Forbidden(fldPath.Child("canal"), "only one networking option permitted"))
1159-
}
1160-
optionTaken = true
1161-
11621133
if cluster.IsKubernetesGTE("1.28") {
11631134
allErrs = append(allErrs, field.Forbidden(fldPath.Child("canal"), "Canal is not supported for Kubernetes >= 1.28"))
11641135
} else {
@@ -1167,13 +1138,9 @@ func validateNetworking(cluster *kops.Cluster, v *kops.NetworkingSpec, fldPath *
11671138
}
11681139

11691140
if v.KubeRouter != nil {
1170-
if optionTaken {
1171-
allErrs = append(allErrs, field.Forbidden(fldPath.Child("kubeRouter"), "only one networking option permitted"))
1172-
}
11731141
if c.KubeProxy != nil && (c.KubeProxy.Enabled == nil || *c.KubeProxy.Enabled) {
11741142
allErrs = append(allErrs, field.Forbidden(fldPath.Root().Child("spec", "kubeProxy", "enabled"), "kube-router requires kubeProxy to be disabled"))
11751143
}
1176-
optionTaken = true
11771144

11781145
if cluster.Spec.IsIPv6Only() {
11791146
allErrs = append(allErrs, field.Forbidden(fldPath.Child("kubeRouter"), "kube-router does not support IPv6"))
@@ -1185,50 +1152,39 @@ func validateNetworking(cluster *kops.Cluster, v *kops.NetworkingSpec, fldPath *
11851152
}
11861153

11871154
if v.AmazonVPC != nil {
1188-
if optionTaken {
1189-
allErrs = append(allErrs, field.Forbidden(fldPath.Child("amazonVPC"), "only one networking option permitted"))
1190-
}
1191-
optionTaken = true
1192-
11931155
if cluster.GetCloudProvider() != kops.CloudProviderAWS {
11941156
allErrs = append(allErrs, field.Forbidden(fldPath.Child("amazonVPC"), "amazon-vpc-routed-eni networking is supported only in AWS"))
11951157
}
11961158

11971159
if cluster.Spec.IsIPv6Only() {
11981160
allErrs = append(allErrs, field.Forbidden(fldPath.Child("amazonVPC"), "amazon-vpc-routed-eni networking does not support IPv6"))
11991161
}
1200-
12011162
}
12021163

12031164
if v.Cilium != nil {
1204-
if optionTaken {
1205-
allErrs = append(allErrs, field.Forbidden(fldPath.Child("cilium"), "only one networking option permitted"))
1206-
}
1207-
optionTaken = true
1208-
12091165
allErrs = append(allErrs, validateNetworkingCilium(cluster, v.Cilium, fldPath.Child("cilium"))...)
12101166
}
12111167

12121168
if v.LyftVPC != nil {
1213-
allErrs = append(allErrs, field.Forbidden(fldPath.Child("lyftvp"), "support for LyftVPC has been removed"))
1169+
allErrs = append(allErrs, field.Forbidden(fldPath.Child("lyftvpc"), "support for LyftVPC has been removed"))
12141170
}
12151171

12161172
if v.GCP != nil {
1217-
if optionTaken {
1218-
allErrs = append(allErrs, field.Forbidden(fldPath.Child("gcp"), "only one networking option permitted"))
1219-
}
1220-
12211173
allErrs = append(allErrs, validateNetworkingGCP(cluster, v.GCP, fldPath.Child("gcp"))...)
12221174
}
12231175

12241176
if v.Kindnet != nil {
1225-
if optionTaken {
1226-
allErrs = append(allErrs, field.Forbidden(fldPath.Child("kindnet"), "only one networking option permitted"))
1227-
}
1228-
12291177
allErrs = append(allErrs, validateNetworkingKindnet(cluster, v.Kindnet, fldPath.Child("kindnet"))...)
12301178
}
12311179

1180+
options := v.ConfiguredOptions()
1181+
if options.Len() > 1 {
1182+
optionsList := sets.List(options)
1183+
for _, option := range optionsList {
1184+
allErrs = append(allErrs, field.Forbidden(fldPath.Child(option), fmt.Sprintf("only one networking option permitted, found %s", strings.Join(optionsList, ", "))))
1185+
}
1186+
}
1187+
12321188
return allErrs
12331189
}
12341190

util/pkg/reflectutils/access.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import (
2626
"k8s.io/apimachinery/pkg/api/resource"
2727
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2828
"k8s.io/apimachinery/pkg/util/intstr"
29-
"k8s.io/kops/pkg/apis/kops"
3029
)
3130

3231
func SetString(target interface{}, targetPath string, newValue string) error {
@@ -216,14 +215,24 @@ func setType(v reflect.Value, newValue string) error {
216215
newV = reflect.ValueOf(intstr.Parse(newValue))
217216

218217
case "kops.EnvVar":
219-
name, value, found := strings.Cut(newValue, "=")
220-
envVar := kops.EnvVar{
221-
Name: name,
218+
newV = reflect.New(v.Type()).Elem()
219+
220+
envVarType := newV.Type()
221+
222+
fdName, found := envVarType.FieldByName("Name")
223+
if !found {
224+
return fmt.Errorf("field Name not found in %T", newV.Interface())
222225
}
223-
if found {
224-
envVar.Value = value
226+
fdValue, found := envVarType.FieldByName("Value")
227+
if !found {
228+
return fmt.Errorf("field Value not found in %T", newV.Interface())
229+
}
230+
231+
name, value, hasValue := strings.Cut(newValue, "=")
232+
newV.FieldByIndex(fdName.Index).SetString(name)
233+
if hasValue {
234+
newV.FieldByIndex(fdValue.Index).SetString(value)
225235
}
226-
newV = reflect.ValueOf(envVar)
227236

228237
case "v1.Duration":
229238
duration, err := time.ParseDuration(newValue)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package reflectutils
18+
19+
import (
20+
"fmt"
21+
"reflect"
22+
"strings"
23+
24+
"k8s.io/apimachinery/pkg/util/sets"
25+
)
26+
27+
// FindSetFields returns the set of fields that are set in the struct,
28+
// using the json tags as the field names.
29+
// It only considers the fields that are listed in the fields argument.
30+
func FindSetFields[T any](v *T, fields ...string) (sets.Set[string], error) {
31+
val := reflect.ValueOf(v).Elem()
32+
valType := val.Type()
33+
34+
fieldsByJsonName := make(map[string]reflect.StructField)
35+
36+
for i := 0; i < val.NumField(); i++ {
37+
fd := valType.Field(i)
38+
jsonName := fd.Tag.Get("json")
39+
if jsonName == "" || jsonName == "-" {
40+
continue
41+
}
42+
jsonName = strings.TrimSuffix(jsonName, ",omitempty")
43+
fieldsByJsonName[jsonName] = fd
44+
}
45+
46+
setFields := sets.New[string]()
47+
for _, field := range fields {
48+
fd, ok := fieldsByJsonName[field]
49+
if !ok {
50+
return nil, fmt.Errorf("field %s is not known", field)
51+
}
52+
53+
fieldVal := val.FieldByIndex(fd.Index)
54+
switch fieldVal.Kind() {
55+
case reflect.Ptr:
56+
if !fieldVal.IsNil() {
57+
setFields.Insert(field)
58+
}
59+
default:
60+
return nil, fmt.Errorf("field %s is not a pointer", fd.Name)
61+
}
62+
63+
}
64+
65+
return setFields, nil
66+
}

util/pkg/reflectutils/access_test.go renamed to util/pkg/reflectutils/tests/access_test.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
package reflectutils
17+
package tests
1818

1919
import (
2020
"encoding/json"
@@ -25,6 +25,7 @@ import (
2525

2626
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2727
"k8s.io/kops/pkg/apis/kops"
28+
"k8s.io/kops/util/pkg/reflectutils"
2829
)
2930

3031
type fakeEnum string
@@ -239,7 +240,7 @@ func TestSet(t *testing.T) {
239240
t.Fatalf("failed to unmarshal input: %v", err)
240241
}
241242

242-
if err := SetString(c, g.Path, g.Value); err != nil {
243+
if err := reflectutils.SetString(c, g.Path, g.Value); err != nil {
243244
t.Fatalf("error from SetString: %v", err)
244245
}
245246

@@ -312,7 +313,7 @@ func TestSetInvalidPath(t *testing.T) {
312313
t.Fatalf("failed to unmarshal input: %v", err)
313314
}
314315

315-
err := SetString(c, g.Path, g.Value)
316+
err := reflectutils.SetString(c, g.Path, g.Value)
316317
if err == nil {
317318
t.Fatalf("Expected error for invalid path %s", g.Path)
318319
}
@@ -402,7 +403,7 @@ func TestUnset(t *testing.T) {
402403
t.Fatalf("failed to unmarshal input: %v", err)
403404
}
404405

405-
if err := Unset(c, g.Path); err != nil {
406+
if err := reflectutils.Unset(c, g.Path); err != nil {
406407
t.Fatalf("error from Unset: %v", err)
407408
}
408409

@@ -452,7 +453,7 @@ func TestUnsetInvalidPath(t *testing.T) {
452453
t.Fatalf("failed to unmarshal input: %v", err)
453454
}
454455

455-
err := Unset(c, g.Path)
456+
err := reflectutils.Unset(c, g.Path)
456457
if err == nil {
457458
t.Fatalf("Expected error for invalid path %s", g.Path)
458459
}

0 commit comments

Comments
 (0)