Adding FQDN address type for EndpointSlice

This commit is contained in:
Rob Scott 2019-10-18 11:18:58 -07:00
parent cd274ff270
commit d410bd28c3
No known key found for this signature in database
GPG Key ID: 05B37CFC2CDE8B85
9 changed files with 229 additions and 45 deletions

View File

@ -12025,7 +12025,7 @@
"description": "Endpoint represents a single logical \"backend\" implementing a service.",
"properties": {
"addresses": {
"description": "addresses of this endpoint. The contents of this field are interpreted according to the corresponding EndpointSlice addressType field. This allows for cases like dual-stack (IPv4 and IPv6) networking. Consumers (e.g. kube-proxy) must handle different types of addresses in the context of their own capabilities. This must contain at least one address but no more than 100.",
"description": "addresses of this endpoint. The contents of this field are interpreted according to the corresponding EndpointSlice addressType field. This allows for cases like dual-stack networking where both IPv4 and IPv6 addresses would be included with the IP addressType. Consumers (e.g. kube-proxy) must handle different types of addresses in the context of their own capabilities. This must contain at least one address but no more than 100.",
"items": {
"type": "string"
},
@ -12090,7 +12090,7 @@
"description": "EndpointSlice represents a subset of the endpoints that implement a service. For a given service there may be multiple EndpointSlice objects, selected by labels, which must be joined to produce the full set of endpoints.",
"properties": {
"addressType": {
"description": "addressType specifies the type of address carried by this EndpointSlice. All addresses in this slice must be the same type. Default is IP",
"description": "addressType specifies the type of address carried by this EndpointSlice. All addresses in this slice must be the same type. The following address types are currently supported: * IP: Represents an IP Address. This can include both IPv4 and IPv6\n addresses.\n* FQDN: Represents a Fully Qualified Domain Name. Default is IP",
"type": "string"
},
"apiVersion": {

View File

@ -32,7 +32,11 @@ type EndpointSlice struct {
// +optional
metav1.ObjectMeta
// addressType specifies the type of address carried by this EndpointSlice.
// All addresses in this slice must be the same type.
// All addresses in this slice must be the same type. The following address
// types are currently supported:
// * IP: Represents an IP Address. This can include both IPv4 and IPv6
// addresses.
// * FQDN: Represents a Fully Qualified Domain Name.
// +optional
AddressType *AddressType
// endpoints is a list of unique endpoints in this slice. Each slice may
@ -53,17 +57,21 @@ type EndpointSlice struct {
type AddressType string
const (
// AddressTypeIP represents an IP Address.
// AddressTypeIP represents an IP Address. Inclusive of IPv4 and IPv6
// addresses.
AddressTypeIP = AddressType("IP")
// AddressTypeFQDN represents a Fully Qualified Domain Name.
AddressTypeFQDN = AddressType("FQDN")
)
// Endpoint represents a single logical "backend" implementing a service.
type Endpoint struct {
// addresses of this endpoint. The contents of this field are interpreted
// according to the corresponding EndpointSlice addressType field. This
// allows for cases like dual-stack (IPv4 and IPv6) networking. Consumers
// (e.g. kube-proxy) must handle different types of addresses in the context
// of their own capabilities. This must contain at least one address but no
// allows for cases like dual-stack networking where both IPv4 and IPv6
// addresses would be included with the IP addressType. Consumers (e.g.
// kube-proxy) must handle different types of addresses in the context of
// their own capabilities. This must contain at least one address but no
// more than 100.
// +listType=set
Addresses []string

View File

@ -28,7 +28,7 @@ import (
)
var (
supportedAddressTypes = sets.NewString(string(discovery.AddressTypeIP))
supportedAddressTypes = sets.NewString(string(discovery.AddressTypeIP), string(discovery.AddressTypeFQDN))
supportedPortProtocols = sets.NewString(string(api.ProtocolTCP), string(api.ProtocolUDP), string(api.ProtocolSCTP))
maxTopologyLabels = 16
maxAddresses = 100
@ -45,6 +45,8 @@ var ValidateEndpointSliceName = apimachineryvalidation.NameIsDNSSubdomain
func ValidateEndpointSlice(endpointSlice *discovery.EndpointSlice) field.ErrorList {
allErrs := apivalidation.ValidateObjectMeta(&endpointSlice.ObjectMeta, true, ValidateEndpointSliceName, field.NewPath("metadata"))
// AddressType should have had a default value set at this point, this is
// just a safety check if for some reason that changes or doesn't work.
addrType := discovery.AddressType("")
if endpointSlice.AddressType == nil {
allErrs = append(allErrs, field.Required(field.NewPath("addressType"), ""))
@ -52,8 +54,8 @@ func ValidateEndpointSlice(endpointSlice *discovery.EndpointSlice) field.ErrorLi
addrType = *endpointSlice.AddressType
}
if endpointSlice.AddressType != nil && !supportedAddressTypes.Has(string(*endpointSlice.AddressType)) {
allErrs = append(allErrs, field.NotSupported(field.NewPath("addressType"), *endpointSlice.AddressType, supportedAddressTypes.List()))
if !supportedAddressTypes.Has(string(addrType)) {
allErrs = append(allErrs, field.NotSupported(field.NewPath("addressType"), addrType, supportedAddressTypes.List()))
}
allErrs = append(allErrs, validateEndpoints(endpointSlice.Endpoints, addrType, field.NewPath("endpoints"))...)
@ -83,17 +85,20 @@ func validateEndpoints(endpoints []discovery.Endpoint, addrType discovery.Addres
idxPath := fldPath.Index(i)
addressPath := idxPath.Child("addresses")
if addrType == discovery.AddressTypeIP {
if len(endpoint.Addresses) == 0 {
allErrs = append(allErrs, field.Required(addressPath, "must contain at least 1 address"))
} else if len(endpoint.Addresses) > maxAddresses {
allErrs = append(allErrs, field.TooMany(addressPath, len(endpoint.Addresses), maxAddresses))
}
if len(endpoint.Addresses) == 0 {
allErrs = append(allErrs, field.Required(addressPath, "must contain at least 1 address"))
} else if len(endpoint.Addresses) > maxAddresses {
allErrs = append(allErrs, field.TooMany(addressPath, len(endpoint.Addresses), maxAddresses))
}
for i, address := range endpoint.Addresses {
for i, address := range endpoint.Addresses {
switch addrType {
case discovery.AddressTypeIP:
for _, msg := range validation.IsValidIP(address) {
allErrs = append(allErrs, field.Invalid(addressPath.Index(i), address, msg))
}
case discovery.AddressTypeFQDN:
allErrs = append(allErrs, validation.IsFullyQualifiedDomainName(addressPath.Index(i), address)...)
}
}

View File

@ -46,7 +46,22 @@ func TestValidateEndpointSlice(t *testing.T) {
Protocol: protocolPtr(api.ProtocolTCP),
}},
Endpoints: []discovery.Endpoint{{
Addresses: generateAddresses(1),
Addresses: generateIPAddresses(1),
Hostname: utilpointer.StringPtr("valid-123"),
}},
},
},
"good-fqdns": {
expectedErrors: 0,
endpointSlice: &discovery.EndpointSlice{
ObjectMeta: standardMeta,
AddressType: addressTypePtr(discovery.AddressTypeFQDN),
Ports: []discovery.EndpointPort{{
Name: utilpointer.StringPtr("http"),
Protocol: protocolPtr(api.ProtocolTCP),
}},
Endpoints: []discovery.Endpoint{{
Addresses: []string{"foo.example.com", "example.com", "example.com.", "hyphens-are-good.example.com"},
Hostname: utilpointer.StringPtr("valid-123"),
}},
},
@ -67,7 +82,7 @@ func TestValidateEndpointSlice(t *testing.T) {
Protocol: protocolPtr(api.ProtocolSCTP),
}},
Endpoints: []discovery.Endpoint{{
Addresses: generateAddresses(1),
Addresses: generateIPAddresses(1),
Hostname: utilpointer.StringPtr("valid-123"),
}},
},
@ -85,7 +100,7 @@ func TestValidateEndpointSlice(t *testing.T) {
Protocol: protocolPtr(api.ProtocolTCP),
}},
Endpoints: []discovery.Endpoint{{
Addresses: generateAddresses(1),
Addresses: generateIPAddresses(1),
}},
},
},
@ -125,7 +140,7 @@ func TestValidateEndpointSlice(t *testing.T) {
Protocol: protocolPtr(api.ProtocolTCP),
}},
Endpoints: []discovery.Endpoint{{
Addresses: generateAddresses(maxAddresses),
Addresses: generateIPAddresses(maxAddresses),
}},
},
},
@ -139,7 +154,7 @@ func TestValidateEndpointSlice(t *testing.T) {
Protocol: protocolPtr(api.ProtocolTCP),
}},
Endpoints: []discovery.Endpoint{{
Addresses: generateAddresses(1),
Addresses: generateIPAddresses(1),
Topology: generateTopology(maxTopologyLabels),
}},
},
@ -223,7 +238,7 @@ func TestValidateEndpointSlice(t *testing.T) {
Protocol: protocolPtr(api.ProtocolTCP),
}},
Endpoints: []discovery.Endpoint{{
Addresses: generateAddresses(0),
Addresses: generateIPAddresses(0),
}},
},
},
@ -237,7 +252,7 @@ func TestValidateEndpointSlice(t *testing.T) {
Protocol: protocolPtr(api.ProtocolTCP),
}},
Endpoints: []discovery.Endpoint{{
Addresses: generateAddresses(maxAddresses + 1),
Addresses: generateIPAddresses(maxAddresses + 1),
}},
},
},
@ -251,7 +266,7 @@ func TestValidateEndpointSlice(t *testing.T) {
Protocol: protocolPtr(api.ProtocolTCP),
}},
Endpoints: []discovery.Endpoint{{
Addresses: generateAddresses(1),
Addresses: generateIPAddresses(1),
}},
},
},
@ -265,7 +280,7 @@ func TestValidateEndpointSlice(t *testing.T) {
Protocol: protocolPtr(api.ProtocolTCP),
}},
Endpoints: []discovery.Endpoint{{
Addresses: generateAddresses(1),
Addresses: generateIPAddresses(1),
Topology: map[string]string{"--INVALID": "example"},
}},
},
@ -280,7 +295,7 @@ func TestValidateEndpointSlice(t *testing.T) {
Protocol: protocolPtr(api.ProtocolTCP),
}},
Endpoints: []discovery.Endpoint{{
Addresses: generateAddresses(1),
Addresses: generateIPAddresses(1),
Topology: generateTopology(maxTopologyLabels + 1),
}},
},
@ -295,7 +310,7 @@ func TestValidateEndpointSlice(t *testing.T) {
Protocol: protocolPtr(api.ProtocolTCP),
}},
Endpoints: []discovery.Endpoint{{
Addresses: generateAddresses(1),
Addresses: generateIPAddresses(1),
Hostname: utilpointer.StringPtr("--INVALID"),
}},
},
@ -313,14 +328,46 @@ func TestValidateEndpointSlice(t *testing.T) {
Protocol: protocolPtr(api.ProtocolTCP),
}},
Endpoints: []discovery.Endpoint{{
Addresses: generateAddresses(1),
Addresses: generateIPAddresses(1),
Hostname: utilpointer.StringPtr("valid-123"),
}},
},
},
"bad-ip": {
expectedErrors: 1,
endpointSlice: &discovery.EndpointSlice{
ObjectMeta: standardMeta,
AddressType: addressTypePtr(discovery.AddressTypeIP),
Ports: []discovery.EndpointPort{{
Name: utilpointer.StringPtr("http"),
Protocol: protocolPtr(api.ProtocolTCP),
}},
Endpoints: []discovery.Endpoint{{
Addresses: []string{"123.456.789.012"},
Hostname: utilpointer.StringPtr("valid-123"),
}},
},
},
"bad-fqdns": {
expectedErrors: 4,
endpointSlice: &discovery.EndpointSlice{
ObjectMeta: standardMeta,
AddressType: addressTypePtr(discovery.AddressTypeFQDN),
Ports: []discovery.EndpointPort{{
Name: utilpointer.StringPtr("http"),
Protocol: protocolPtr(api.ProtocolTCP),
}},
Endpoints: []discovery.Endpoint{{
Addresses: []string{"foo.*", "FOO.example.com", "underscores_are_bad.example.com", "*.example.com"},
Hostname: utilpointer.StringPtr("valid-123"),
}},
},
},
"empty-everything": {
expectedErrors: 3,
endpointSlice: &discovery.EndpointSlice{},
endpointSlice: &discovery.EndpointSlice{
AddressType: addressTypePtr(""),
},
},
}
@ -422,7 +469,7 @@ func generateEndpoints(n int) []discovery.Endpoint {
return endpoints
}
func generateAddresses(n int) []string {
func generateIPAddresses(n int) []string {
addresses := []string{}
for i := 0; i < n; i++ {
addresses = append(addresses, fmt.Sprintf("10.1.2.%d", i%255))

View File

@ -33,9 +33,10 @@ option go_package = "v1alpha1";
message Endpoint {
// addresses of this endpoint. The contents of this field are interpreted
// according to the corresponding EndpointSlice addressType field. This
// allows for cases like dual-stack (IPv4 and IPv6) networking. Consumers
// (e.g. kube-proxy) must handle different types of addresses in the context
// of their own capabilities. This must contain at least one address but no
// allows for cases like dual-stack networking where both IPv4 and IPv6
// addresses would be included with the IP addressType. Consumers (e.g.
// kube-proxy) must handle different types of addresses in the context of
// their own capabilities. This must contain at least one address but no
// more than 100.
// +listType=set
repeated string addresses = 1;
@ -115,7 +116,11 @@ message EndpointSlice {
optional k8s.io.apimachinery.pkg.apis.meta.v1.ObjectMeta metadata = 1;
// addressType specifies the type of address carried by this EndpointSlice.
// All addresses in this slice must be the same type.
// All addresses in this slice must be the same type. The following address
// types are currently supported:
// * IP: Represents an IP Address. This can include both IPv4 and IPv6
// addresses.
// * FQDN: Represents a Fully Qualified Domain Name.
// Default is IP
// +optional
optional string addressType = 4;

View File

@ -33,7 +33,11 @@ type EndpointSlice struct {
// +optional
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
// addressType specifies the type of address carried by this EndpointSlice.
// All addresses in this slice must be the same type.
// All addresses in this slice must be the same type. The following address
// types are currently supported:
// * IP: Represents an IP Address. This can include both IPv4 and IPv6
// addresses.
// * FQDN: Represents a Fully Qualified Domain Name.
// Default is IP
// +optional
AddressType *AddressType `json:"addressType" protobuf:"bytes,4,rep,name=addressType"`
@ -55,17 +59,21 @@ type EndpointSlice struct {
type AddressType string
const (
// AddressTypeIP represents an IP Address.
// AddressTypeIP represents an IP Address. Inclusive of IPv4 and IPv6
// addresses.
AddressTypeIP = AddressType("IP")
// AddressTypeFQDN represents a Fully Qualified Domain Name.
AddressTypeFQDN = AddressType("FQDN")
)
// Endpoint represents a single logical "backend" implementing a service.
type Endpoint struct {
// addresses of this endpoint. The contents of this field are interpreted
// according to the corresponding EndpointSlice addressType field. This
// allows for cases like dual-stack (IPv4 and IPv6) networking. Consumers
// (e.g. kube-proxy) must handle different types of addresses in the context
// of their own capabilities. This must contain at least one address but no
// allows for cases like dual-stack networking where both IPv4 and IPv6
// addresses would be included with the IP addressType. Consumers (e.g.
// kube-proxy) must handle different types of addresses in the context of
// their own capabilities. This must contain at least one address but no
// more than 100.
// +listType=set
Addresses []string `json:"addresses" protobuf:"bytes,1,rep,name=addresses"`

View File

@ -29,7 +29,7 @@ package v1alpha1
// AUTO-GENERATED FUNCTIONS START HERE. DO NOT EDIT.
var map_Endpoint = map[string]string{
"": "Endpoint represents a single logical \"backend\" implementing a service.",
"addresses": "addresses of this endpoint. The contents of this field are interpreted according to the corresponding EndpointSlice addressType field. This allows for cases like dual-stack (IPv4 and IPv6) networking. Consumers (e.g. kube-proxy) must handle different types of addresses in the context of their own capabilities. This must contain at least one address but no more than 100.",
"addresses": "addresses of this endpoint. The contents of this field are interpreted according to the corresponding EndpointSlice addressType field. This allows for cases like dual-stack networking where both IPv4 and IPv6 addresses would be included with the IP addressType. Consumers (e.g. kube-proxy) must handle different types of addresses in the context of their own capabilities. This must contain at least one address but no more than 100.",
"conditions": "conditions contains information about the current status of the endpoint.",
"hostname": "hostname of this endpoint. This field may be used by consumers of endpoints to distinguish endpoints from each other (e.g. in DNS names). Multiple endpoints which use the same hostname should be considered fungible (e.g. multiple A values in DNS). Must pass DNS Label (RFC 1123) validation.",
"targetRef": "targetRef is a reference to a Kubernetes object that represents this endpoint.",
@ -63,7 +63,7 @@ func (EndpointPort) SwaggerDoc() map[string]string {
var map_EndpointSlice = map[string]string{
"": "EndpointSlice represents a subset of the endpoints that implement a service. For a given service there may be multiple EndpointSlice objects, selected by labels, which must be joined to produce the full set of endpoints.",
"metadata": "Standard object's metadata.",
"addressType": "addressType specifies the type of address carried by this EndpointSlice. All addresses in this slice must be the same type. Default is IP",
"addressType": "addressType specifies the type of address carried by this EndpointSlice. All addresses in this slice must be the same type. The following address types are currently supported: * IP: Represents an IP Address. This can include both IPv4 and IPv6\n addresses.\n* FQDN: Represents a Fully Qualified Domain Name. Default is IP",
"endpoints": "endpoints is a list of unique endpoints in this slice. Each slice may include a maximum of 1000 endpoints.",
"ports": "ports specifies the list of network ports exposed by each endpoint in this slice. Each port must have a unique name. When ports is empty, it indicates that there are no defined ports. When a port is defined with a nil port value, it indicates \"all ports\". Each slice may include a maximum of 100 ports.",
}

View File

@ -70,7 +70,11 @@ func IsQualifiedName(value string) []string {
return errs
}
// IsFullyQualifiedName checks if the name is fully qualified.
// IsFullyQualifiedName checks if the name is fully qualified. This is similar
// to IsFullyQualifiedDomainName but requires a minimum of 3 segments instead of
// 2 and does not accept a trailing . as valid.
// TODO: This function is deprecated and preserved until all callers migrate to
// IsFullyQualifiedDomainName; please don't add new callers.
func IsFullyQualifiedName(fldPath *field.Path, name string) field.ErrorList {
var allErrors field.ErrorList
if len(name) == 0 {
@ -85,6 +89,26 @@ func IsFullyQualifiedName(fldPath *field.Path, name string) field.ErrorList {
return allErrors
}
// IsFullyQualifiedDomainName checks if the domain name is fully qualified. This
// is similar to IsFullyQualifiedName but only requires a minimum of 2 segments
// instead of 3 and accepts a trailing . as valid.
func IsFullyQualifiedDomainName(fldPath *field.Path, name string) field.ErrorList {
var allErrors field.ErrorList
if len(name) == 0 {
return append(allErrors, field.Required(fldPath, ""))
}
if strings.HasSuffix(name, ".") {
name = name[:len(name)-1]
}
if errs := IsDNS1123Subdomain(name); len(errs) > 0 {
return append(allErrors, field.Invalid(fldPath, name, strings.Join(errs, ",")))
}
if len(strings.Split(name, ".")) < 2 {
return append(allErrors, field.Invalid(fldPath, name, "should be a domain with at least two segments separated by dots"))
}
return allErrors
}
const labelValueFmt string = "(" + qualifiedNameFmt + ")?"
const labelValueErrMsg string = "a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character"

View File

@ -477,8 +477,85 @@ func TestIsWildcardDNS1123Subdomain(t *testing.T) {
}
}
func TestIsFullyQualifiedDomainName(t *testing.T) {
goodValues := []string{
"a.com",
"k8s.io",
"dev.k8s.io",
"dev.k8s.io.",
"foo.example.com",
"this.is.a.really.long.fqdn",
"bbc.co.uk",
"10.0.0.1", // DNS labels can start with numbers and there is no requirement for letters.
"hyphens-are-good.k8s.io",
strings.Repeat("a", 246) + ".k8s.io",
}
for _, val := range goodValues {
if err := IsFullyQualifiedDomainName(field.NewPath(""), val).ToAggregate(); err != nil {
t.Errorf("expected no errors for %q: %v", val, err)
}
}
badValues := []string{
".",
"...",
".io",
"com",
".com",
"Dev.k8s.io",
".foo.example.com",
"*.example.com",
"*.bar.com",
"*.foo.bar.com",
"underscores_are_bad.k8s.io",
"foo@bar.example.com",
"http://foo.example.com",
strings.Repeat("a", 247) + ".k8s.io",
}
for _, val := range badValues {
if err := IsFullyQualifiedDomainName(field.NewPath(""), val).ToAggregate(); err == nil {
t.Errorf("expected errors for %q", val)
}
}
}
func TestIsFullyQualifiedName(t *testing.T) {
tests := []struct {
goodValues := []string{
"dev.k8s.io",
"foo.example.com",
"this.is.a.really.long.fqdn",
"bbc.co.uk",
"10.0.0.1", // DNS labels can start with numbers and there is no requirement for letters.
"hyphens-are-good.k8s.io",
strings.Repeat("a", 246) + ".k8s.io",
}
for _, val := range goodValues {
if err := IsFullyQualifiedName(field.NewPath(""), val).ToAggregate(); err != nil {
t.Errorf("expected no errors for %q: %v", val, err)
}
}
badValues := []string{
"...",
"dev.k8s.io.",
".io",
"Dev.k8s.io",
"k8s.io",
"*.example.com",
"*.bar.com",
"*.foo.bar.com",
"underscores_are_bad.k8s.io",
"foo@bar.example.com",
"http://foo.example.com",
strings.Repeat("a", 247) + ".k8s.io",
}
for _, val := range badValues {
if err := IsFullyQualifiedName(field.NewPath(""), val).ToAggregate(); err == nil {
t.Errorf("expected errors for %q", val)
}
}
messageTests := []struct {
name string
targetName string
err string
@ -488,6 +565,16 @@ func TestIsFullyQualifiedName(t *testing.T) {
targetName: "k8s.io",
err: "should be a domain with at least three segments separated by dots",
},
{
name: "name should not include scheme",
targetName: "http://foo.k8s.io",
err: "a DNS-1123 subdomain must consist of lower case alphanumeric characters",
},
{
name: "email should be invalid",
targetName: "example@foo.k8s.io",
err: "a DNS-1123 subdomain must consist of lower case alphanumeric characters",
},
{
name: "name cannot be empty",
targetName: "",
@ -499,7 +586,7 @@ func TestIsFullyQualifiedName(t *testing.T) {
err: "a DNS-1123 subdomain must consist of lower case alphanumeric characters",
},
}
for _, tc := range tests {
for _, tc := range messageTests {
err := IsFullyQualifiedName(field.NewPath(""), tc.targetName).ToAggregate()
switch {
case tc.err == "" && err != nil: