diff --git a/api/api-rules/violation_exceptions.list b/api/api-rules/violation_exceptions.list index c666da22f85..dd2245fd18c 100644 --- a/api/api-rules/violation_exceptions.list +++ b/api/api-rules/violation_exceptions.list @@ -355,6 +355,8 @@ API rule violation: names_match,k8s.io/api/core/v1,RBDVolumeSource,RBDPool API rule violation: names_match,k8s.io/api/core/v1,RBDVolumeSource,RadosUser API rule violation: names_match,k8s.io/api/core/v1,VolumeSource,CephFS API rule violation: names_match,k8s.io/api/core/v1,VolumeSource,StorageOS +API rule violation: names_match,k8s.io/api/networking/v1alpha1,ServiceCIDRSpec,IPv4 +API rule violation: names_match,k8s.io/api/networking/v1alpha1,ServiceCIDRSpec,IPv6 API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,Ref API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,Schema API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,XEmbeddedResource diff --git a/pkg/apis/networking/fuzzer/fuzzer.go b/pkg/apis/networking/fuzzer/fuzzer.go index 015739c1942..108b258432d 100644 --- a/pkg/apis/networking/fuzzer/fuzzer.go +++ b/pkg/apis/networking/fuzzer/fuzzer.go @@ -17,6 +17,7 @@ limitations under the License. package fuzzer import ( + "fmt" "net/netip" fuzz "github.com/google/gofuzz" @@ -84,6 +85,13 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} { ip := generateRandomIP(is6, c) obj.Name = ip }, + func(obj *networking.ServiceCIDR, c fuzz.Continue) { + c.FuzzNoCustom(obj) // fuzz self without calling this function again + cidrv4 := generateRandomCIDR(false, c) + obj.Spec.IPv4 = cidrv4 + cidrv6 := generateRandomCIDR(true, c) + obj.Spec.IPv6 = cidrv6 + }, } } @@ -101,10 +109,23 @@ func generateRandomIP(is6 bool, c fuzz.Continue) string { if ok { return ip.String() } - // this should not happen but is better to - // return a good IP address than nothing - if is6 { - return "2001:db8::1" - } - return "192.168.1.1" + // this should not happen + panic(fmt.Sprintf("invalid IP %v", bytes)) +} + +func generateRandomCIDR(is6 bool, c fuzz.Continue) string { + ip, err := netip.ParseAddr(generateRandomIP(is6, c)) + if err != nil { + // generateRandomIP already panics if returns a not valid ip + panic(err) + } + + n := 32 + if is6 { + n = 128 + } + + bits := c.Rand.Intn(n) + prefix := netip.PrefixFrom(ip, bits) + return prefix.Masked().String() } diff --git a/pkg/apis/networking/register.go b/pkg/apis/networking/register.go index 0e1a01af46f..bc2c7b68911 100644 --- a/pkg/apis/networking/register.go +++ b/pkg/apis/networking/register.go @@ -54,6 +54,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &IngressClassList{}, &IPAddress{}, &IPAddressList{}, + &ServiceCIDR{}, + &ServiceCIDRList{}, ) return nil } diff --git a/pkg/apis/networking/types.go b/pkg/apis/networking/types.go index 83a7a55f38e..0f67a6540b9 100644 --- a/pkg/apis/networking/types.go +++ b/pkg/apis/networking/types.go @@ -642,3 +642,57 @@ type IPAddressList struct { // Items is the list of IPAddress Items []IPAddress } + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ServiceCIDR defines a range of IPs using CIDR format (192.168.0.0/24 or 2001:db2::/64). +// This range is used by the cluster to allocate the ClusterIPs associated to the Services object. +type ServiceCIDR struct { + metav1.TypeMeta + // Standard object's metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + // +optional + metav1.ObjectMeta + // spec is the desired state of the ServiceCIDR. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + // +optional + Spec ServiceCIDRSpec + // status represents the current state of the ServiceCIDR. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + // +optional + Status ServiceCIDRStatus +} + +// ServiceCIDRSpec describe how the ServiceCIDR's specification looks like. +type ServiceCIDRSpec struct { + // IPv4 defines an IPv4 IP block in CIDR notation (e.g. "192.168.0.0/24"). + // This field is immutable. + // +optional + IPv4 string + // IPv6 defines an IPv6 IP block in CIDR notation (e.g. "2001:db8::/64"). + // This field is immutable. + // +optional + IPv6 string +} + +// ServiceCIDRStatus describes the current state of the ServiceCIDR. +type ServiceCIDRStatus struct { + // conditions holds an array of metav1.Condition that describe the state of the ServiceCIDR. + Conditions []metav1.Condition +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:prerelease-lifecycle-gen:introduced=1.27 + +// ServiceCIDRList contains a list of ServiceCIDR objects. +type ServiceCIDRList struct { + metav1.TypeMeta + // Standard object's metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + // +optional + metav1.ListMeta + // items is the list of ServiceCIDRs. + Items []ServiceCIDR +} diff --git a/pkg/apis/networking/validation/validation.go b/pkg/apis/networking/validation/validation.go index 3d2e589c000..6eba5e1ff3e 100644 --- a/pkg/apis/networking/validation/validation.go +++ b/pkg/apis/networking/validation/validation.go @@ -721,3 +721,66 @@ func ValidateIPAddressUpdate(update, old *networking.IPAddress) field.ErrorList allErrs = append(allErrs, apivalidation.ValidateImmutableField(update.Spec.ParentRef, old.Spec.ParentRef, field.NewPath("spec").Child("parentRef"))...) return allErrs } + +var ValidateServiceCIDRName = apimachineryvalidation.NameIsDNSSubdomain + +func ValidateServiceCIDR(cidrConfig *networking.ServiceCIDR) field.ErrorList { + allErrs := apivalidation.ValidateObjectMeta(&cidrConfig.ObjectMeta, false, ValidateServiceCIDRName, field.NewPath("metadata")) + + if cidrConfig.Spec.IPv4 == "" && cidrConfig.Spec.IPv6 == "" { + allErrs = append(allErrs, field.Invalid(field.NewPath("Spec"), cidrConfig.Spec, "at least one CIDR required")) + return allErrs + } + + if cidrConfig.Spec.IPv4 != "" { + prefix, err := netip.ParsePrefix(cidrConfig.Spec.IPv4) + if err != nil { + allErrs = append(allErrs, field.Invalid(field.NewPath("IPv4"), cidrConfig.Spec.IPv4, err.Error())) + } else { + if prefix.Addr() != prefix.Masked().Addr() { + allErrs = append(allErrs, field.Invalid(field.NewPath("IPv4"), cidrConfig.Spec.IPv4, "wrong CIDR format, IP doesn't match network IP address")) + } + if prefix.String() != cidrConfig.Spec.IPv4 { + allErrs = append(allErrs, field.Invalid(field.NewPath("IPv4"), cidrConfig.Spec.IPv4, "CIDR not in canonical format")) + } + if !prefix.Addr().Is4() { + allErrs = append(allErrs, field.Invalid(field.NewPath("IPv4"), cidrConfig.Spec.IPv4, "not IPv4 family CIDR")) + } + } + } + + if cidrConfig.Spec.IPv6 != "" { + prefix, err := netip.ParsePrefix(cidrConfig.Spec.IPv6) + if err != nil { + allErrs = append(allErrs, field.Invalid(field.NewPath("IPv6"), cidrConfig.Spec.IPv6, err.Error())) + } else { + if prefix.Addr() != prefix.Masked().Addr() { + allErrs = append(allErrs, field.Invalid(field.NewPath("IPv6"), cidrConfig.Spec.IPv6, "wrong CIDR format, IP doesn't match network IP address")) + } + if prefix.String() != cidrConfig.Spec.IPv6 { + allErrs = append(allErrs, field.Invalid(field.NewPath("IPv6"), cidrConfig.Spec.IPv6, "CIDR not in RFC 5952 canonical format")) + } + if !prefix.Addr().Is6() { + allErrs = append(allErrs, field.Invalid(field.NewPath("IPv6"), cidrConfig.Spec.IPv6, "not IPv6 family CIDR")) + } + } + } + + return allErrs +} + +// ValidateServiceCIDRUpdate tests if an update to a ServiceCIDR is valid. +func ValidateServiceCIDRUpdate(update, old *networking.ServiceCIDR) field.ErrorList { + var allErrs field.ErrorList + allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&update.ObjectMeta, &old.ObjectMeta, field.NewPath("metadata"))...) + allErrs = append(allErrs, apivalidation.ValidateImmutableField(update.Spec.IPv4, old.Spec.IPv4, field.NewPath("spec").Child("ipv4"))...) + allErrs = append(allErrs, apivalidation.ValidateImmutableField(update.Spec.IPv6, old.Spec.IPv6, field.NewPath("spec").Child("ipv6"))...) + + return allErrs +} + +// ValidateServiceCIDRStatusUpdate tests if if an update to a ServiceCIDR Status is valid. +func ValidateServiceCIDRStatusUpdate(update, old *networking.ServiceCIDR) field.ErrorList { + allErrs := apivalidation.ValidateObjectMetaUpdate(&update.ObjectMeta, &old.ObjectMeta, field.NewPath("metadata")) + return allErrs +} diff --git a/pkg/apis/networking/validation/validation_test.go b/pkg/apis/networking/validation/validation_test.go index b73935e44c9..30fb42aa728 100644 --- a/pkg/apis/networking/validation/validation_test.go +++ b/pkg/apis/networking/validation/validation_test.go @@ -2046,3 +2046,207 @@ func TestValidateIPAddressUpdate(t *testing.T) { }) } } + +func TestValidateServiceCIDR(t *testing.T) { + + testCases := map[string]struct { + expectedErrors int + ipRange *networking.ServiceCIDR + }{ + "empty-iprange": { + expectedErrors: 1, + ipRange: &networking.ServiceCIDR{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + }, + }, + "good-iprange-ipv4": { + expectedErrors: 0, + ipRange: &networking.ServiceCIDR{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: networking.ServiceCIDRSpec{ + IPv4: "192.168.0.0/24", + }, + }, + }, + "good-iprange-ipv6": { + expectedErrors: 0, + ipRange: &networking.ServiceCIDR{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: networking.ServiceCIDRSpec{ + IPv6: "fd00:1234::/64", + }, + }, + }, + "good-iprange-ipv4-ipv6": { + expectedErrors: 0, + ipRange: &networking.ServiceCIDR{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: networking.ServiceCIDRSpec{ + IPv4: "192.168.0.0/24", + IPv6: "fd00:1234::/64", + }, + }, + }, + "not-iprange-ipv4": { + expectedErrors: 1, + ipRange: &networking.ServiceCIDR{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: networking.ServiceCIDRSpec{ + IPv4: "sadasdsad", + }, + }, + }, + "iponly-iprange-ipv4": { + expectedErrors: 1, + ipRange: &networking.ServiceCIDR{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: networking.ServiceCIDRSpec{ + IPv4: "192.168.0.1", + }, + }, + }, + "badip-iprange-ipv4": { + expectedErrors: 1, + ipRange: &networking.ServiceCIDR{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: networking.ServiceCIDRSpec{ + IPv4: "192.168.0.1/24", + }, + }, + }, + "badip-iprange-ipv6": { + expectedErrors: 1, + ipRange: &networking.ServiceCIDR{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: networking.ServiceCIDRSpec{ + IPv6: "fd00:1234::2/64", + }, + }, + }, + "badip-iprange-caps-ipv6": { + expectedErrors: 2, + ipRange: &networking.ServiceCIDR{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: networking.ServiceCIDRSpec{ + IPv6: "FD00:1234::2/64", + }, + }, + }, + "good-iprange-ipv4-bad-ipv6": { + expectedErrors: 1, + ipRange: &networking.ServiceCIDR{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: networking.ServiceCIDRSpec{ + IPv4: "192.168.0.0/24", + IPv6: "FD00:1234::/64", + }, + }, + }, + "good-iprange-ipv6-bad-ipv4": { + expectedErrors: 1, + ipRange: &networking.ServiceCIDR{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: networking.ServiceCIDRSpec{ + IPv4: "192.168.007.0/24", + IPv6: "fd00:1234::/64", + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + errs := ValidateServiceCIDR(testCase.ipRange) + if len(errs) != testCase.expectedErrors { + t.Errorf("Expected %d errors, got %d errors: %v", testCase.expectedErrors, len(errs), errs) + } + }) + } +} + +func TestValidateServiceCIDRUpdate(t *testing.T) { + oldServiceCIDR := &networking.ServiceCIDR{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mysvc", + ResourceVersion: "1", + }, + Spec: networking.ServiceCIDRSpec{ + IPv4: "192.168.0.0/24", + IPv6: "2001:db2::/64", + }, + } + + testCases := []struct { + name string + svc func(svc *networking.ServiceCIDR) *networking.ServiceCIDR + expectErr bool + }{ + { + name: "Successful update, no changes", + svc: func(svc *networking.ServiceCIDR) *networking.ServiceCIDR { + out := svc.DeepCopy() + return out + }, + expectErr: false, + }, + + { + name: "Failed update, update spec.IPv4", + svc: func(svc *networking.ServiceCIDR) *networking.ServiceCIDR { + out := svc.DeepCopy() + out.Spec.IPv4 = "10.0.0.0/16" + return out + }, expectErr: true, + }, + { + name: "Failed update, update spec.IPv6", + svc: func(svc *networking.ServiceCIDR) *networking.ServiceCIDR { + out := svc.DeepCopy() + out.Spec.IPv6 = "fd00:1:2:3::/64" + return out + }, expectErr: true, + }, + { + name: "Failed update, update spec.IPv4 and spec.IPv6", + svc: func(svc *networking.ServiceCIDR) *networking.ServiceCIDR { + out := svc.DeepCopy() + out.Spec.IPv4 = "10.0.0.0/16" + out.Spec.IPv6 = "fd00:1:2:3::/64" + return out + }, expectErr: true, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + err := ValidateServiceCIDRUpdate(testCase.svc(oldServiceCIDR), oldServiceCIDR) + if !testCase.expectErr && err != nil { + t.Errorf("ValidateServiceCIDRUpdate must be successful for test '%s', got %v", testCase.name, err) + } + if testCase.expectErr && err == nil { + t.Errorf("ValidateServiceCIDRUpdate must return error for test: %s, but got nil", testCase.name) + } + }) + } +} diff --git a/staging/src/k8s.io/api/networking/v1alpha1/register.go b/staging/src/k8s.io/api/networking/v1alpha1/register.go index f45f8ed1ecc..c8f5856b5dc 100644 --- a/staging/src/k8s.io/api/networking/v1alpha1/register.go +++ b/staging/src/k8s.io/api/networking/v1alpha1/register.go @@ -54,6 +54,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &IPAddress{}, &IPAddressList{}, + &ServiceCIDR{}, + &ServiceCIDRList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/staging/src/k8s.io/api/networking/v1alpha1/types.go b/staging/src/k8s.io/api/networking/v1alpha1/types.go index 8c431e5b5c6..ab803a37113 100644 --- a/staging/src/k8s.io/api/networking/v1alpha1/types.go +++ b/staging/src/k8s.io/api/networking/v1alpha1/types.go @@ -81,3 +81,74 @@ type IPAddressList struct { // items is the list of IPAddresses. Items []IPAddress `json:"items" protobuf:"bytes,2,rep,name=items"` } + + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:prerelease-lifecycle-gen:introduced=1.27 + +// ServiceCIDR defines a range of IPs using CIDR format (192.168.0.0/24 or 2001:db2::/64). +// This range is used by the cluster to allocate the ClusterIPs associated to the Services object. +type ServiceCIDR struct { + metav1.TypeMeta `json:",inline"` + // Standard object's metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // spec is the desired state of the ServiceCIDR. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + // +optional + Spec ServiceCIDRSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` + // status represents the current state of the ServiceCIDR. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + // +optional + Status ServiceCIDRStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` +} + +// ServiceCIDRSpec define the CIDRs the user wants to use for allocating ClusterIPs for Services. +type ServiceCIDRSpec struct { + // IPv4 defines an IPv4 IP block in CIDR notation (e.g. "192.168.0.0/24"). + // This field is immutable. + // +optional + IPv4 string `json:"ipv4,omitempty" protobuf:"bytes,1,opt,name=ipv4"` + // IPv6 defines an IPv6 IP block in CIDR notation (e.g. "2001:db8::/64"). + // This field is immutable. + // +optional + IPv6 string `json:"ipv6,omitempty" protobuf:"bytes,2,opt,name=ipv6"` +} + +const ( + // ServiceCIDRConditionReady represents status of a ServiceCIDR that is ready to be used by the + // apiserver to allocate ClusterIPs for Services. + ServiceCIDRConditionReady = "Ready" + // ServiceCIDRReasonTerminating represents a reason where a ServiceCIDR is not ready because it is + // being deleted. + ServiceCIDRReasonTerminating = "Terminating" +) + +// ServiceCIDRStatus describes the current state of the ServiceCIDR. +type ServiceCIDRStatus struct { + // conditions holds an array of metav1.Condition that describe the state of the ServiceCIDR. + // Current service state + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:prerelease-lifecycle-gen:introduced=1.27 + +// ServiceCIDRList contains a list of ServiceCIDR objects. +type ServiceCIDRList struct { + metav1.TypeMeta `json:",inline"` + // Standard object's metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // items is the list of ServiceCIDRs. + Items []ServiceCIDR `json:"items" protobuf:"bytes,2,rep,name=items"` +}