Adding AppProtocol to Service and Endpoints Ports

This commit is contained in:
Rob Scott 2020-02-18 17:30:57 -08:00
parent 4e79344501
commit 6a33727632
No known key found for this signature in database
GPG Key ID: 05B37CFC2CDE8B85
29 changed files with 1598 additions and 986 deletions

View File

@ -5951,6 +5951,10 @@
"io.k8s.api.core.v1.EndpointPort": { "io.k8s.api.core.v1.EndpointPort": {
"description": "EndpointPort is a tuple that describes a single port.", "description": "EndpointPort is a tuple that describes a single port.",
"properties": { "properties": {
"appProtocol": {
"description": "The application protocol for this port. This field follows standard Kubernetes label syntax. Un-prefixed names are reserved for IANA standard service names (as per RFC-6335 and http://www.iana.org/assignments/service-names). Non-standard protocols should use prefixed names such as mycompany.com/my-custom-protocol. Field can be enabled with ServiceAppProtocol feature gate.",
"type": "string"
},
"name": { "name": {
"description": "The name of this port. This must match the 'name' field in the corresponding ServicePort. Must be a DNS_LABEL. Optional only if one port is defined.", "description": "The name of this port. This must match the 'name' field in the corresponding ServicePort. Must be a DNS_LABEL. Optional only if one port is defined.",
"type": "string" "type": "string"
@ -9847,6 +9851,10 @@
"io.k8s.api.core.v1.ServicePort": { "io.k8s.api.core.v1.ServicePort": {
"description": "ServicePort contains information on service's port.", "description": "ServicePort contains information on service's port.",
"properties": { "properties": {
"appProtocol": {
"description": "The application protocol for this port. This field follows standard Kubernetes label syntax. Un-prefixed names are reserved for IANA standard service names (as per RFC-6335 and http://www.iana.org/assignments/service-names). Non-standard protocols should use prefixed names such as mycompany.com/my-custom-protocol. Field can be enabled with ServiceAppProtocol feature gate.",
"type": "string"
},
"name": { "name": {
"description": "The name of this port within the service. This must be a DNS_LABEL. All ports within a ServiceSpec must have unique names. When considering the endpoints for a Service, this must match the 'name' field in the EndpointPort. Optional if only one ServicePort is defined on this service.", "description": "The name of this port within the service. This must be a DNS_LABEL. All ports within a ServiceSpec must have unique names. When considering the endpoints for a Service, this must match the 'name' field in the EndpointPort. Optional if only one ServicePort is defined on this service.",
"type": "string" "type": "string"
@ -10529,7 +10537,7 @@
"description": "EndpointPort represents a Port used by an EndpointSlice", "description": "EndpointPort represents a Port used by an EndpointSlice",
"properties": { "properties": {
"appProtocol": { "appProtocol": {
"description": "The application protocol for this port. This field follows standard Kubernetes label syntax. Un-prefixed names are reserved for IANA standard service names (as per RFC-6335 and http://www.iana.org/assignments/service-names). Non-standard protocols should use prefixed names. Default is empty string.", "description": "The application protocol for this port. This field follows standard Kubernetes label syntax. Un-prefixed names are reserved for IANA standard service names (as per RFC-6335 and http://www.iana.org/assignments/service-names). Non-standard protocols should use prefixed names such as mycompany.com/my-custom-protocol.",
"type": "string" "type": "string"
}, },
"name": { "name": {

View File

@ -3532,6 +3532,16 @@ type ServicePort struct {
// The IP protocol for this port. Supports "TCP", "UDP", and "SCTP". // The IP protocol for this port. Supports "TCP", "UDP", and "SCTP".
Protocol Protocol Protocol Protocol
// The application protocol for this port.
// This field follows standard Kubernetes label syntax.
// Un-prefixed names are reserved for IANA standard service names (as per
// RFC-6335 and http://www.iana.org/assignments/service-names).
// Non-standard protocols should use prefixed names such as
// mycompany.com/my-custom-protocol.
// Field can be enabled with ServiceAppProtocol feature gate.
// +optional
AppProtocol *string
// The port that will be exposed on the service. // The port that will be exposed on the service.
Port int32 Port int32
@ -3672,6 +3682,16 @@ type EndpointPort struct {
// The IP protocol for this port. // The IP protocol for this port.
Protocol Protocol Protocol Protocol
// The application protocol for this port.
// This field follows standard Kubernetes label syntax.
// Un-prefixed names are reserved for IANA standard service names (as per
// RFC-6335 and http://www.iana.org/assignments/service-names).
// Non-standard protocols should use prefixed names such as
// mycompany.com/my-custom-protocol.
// Field can be enabled with ServiceAppProtocol feature gate.
// +optional
AppProtocol *string
} }
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

View File

@ -3226,6 +3226,7 @@ func autoConvert_v1_EndpointPort_To_core_EndpointPort(in *v1.EndpointPort, out *
out.Name = in.Name out.Name = in.Name
out.Port = in.Port out.Port = in.Port
out.Protocol = core.Protocol(in.Protocol) out.Protocol = core.Protocol(in.Protocol)
out.AppProtocol = (*string)(unsafe.Pointer(in.AppProtocol))
return nil return nil
} }
@ -3238,6 +3239,7 @@ func autoConvert_core_EndpointPort_To_v1_EndpointPort(in *core.EndpointPort, out
out.Name = in.Name out.Name = in.Name
out.Port = in.Port out.Port = in.Port
out.Protocol = v1.Protocol(in.Protocol) out.Protocol = v1.Protocol(in.Protocol)
out.AppProtocol = (*string)(unsafe.Pointer(in.AppProtocol))
return nil return nil
} }
@ -7405,6 +7407,7 @@ func Convert_core_ServiceList_To_v1_ServiceList(in *core.ServiceList, out *v1.Se
func autoConvert_v1_ServicePort_To_core_ServicePort(in *v1.ServicePort, out *core.ServicePort, s conversion.Scope) error { func autoConvert_v1_ServicePort_To_core_ServicePort(in *v1.ServicePort, out *core.ServicePort, s conversion.Scope) error {
out.Name = in.Name out.Name = in.Name
out.Protocol = core.Protocol(in.Protocol) out.Protocol = core.Protocol(in.Protocol)
out.AppProtocol = (*string)(unsafe.Pointer(in.AppProtocol))
out.Port = in.Port out.Port = in.Port
out.TargetPort = in.TargetPort out.TargetPort = in.TargetPort
out.NodePort = in.NodePort out.NodePort = in.NodePort
@ -7419,6 +7422,7 @@ func Convert_v1_ServicePort_To_core_ServicePort(in *v1.ServicePort, out *core.Se
func autoConvert_core_ServicePort_To_v1_ServicePort(in *core.ServicePort, out *v1.ServicePort, s conversion.Scope) error { func autoConvert_core_ServicePort_To_v1_ServicePort(in *core.ServicePort, out *v1.ServicePort, s conversion.Scope) error {
out.Name = in.Name out.Name = in.Name
out.Protocol = v1.Protocol(in.Protocol) out.Protocol = v1.Protocol(in.Protocol)
out.AppProtocol = (*string)(unsafe.Pointer(in.AppProtocol))
out.Port = in.Port out.Port = in.Port
out.TargetPort = in.TargetPort out.TargetPort = in.TargetPort
out.NodePort = in.NodePort out.NodePort = in.NodePort

View File

@ -3908,7 +3908,7 @@ var supportedServiceType = sets.NewString(string(core.ServiceTypeClusterIP), str
var supportedServiceIPFamily = sets.NewString(string(core.IPv4Protocol), string(core.IPv6Protocol)) var supportedServiceIPFamily = sets.NewString(string(core.IPv4Protocol), string(core.IPv6Protocol))
// ValidateService tests if required fields/annotations of a Service are valid. // ValidateService tests if required fields/annotations of a Service are valid.
func ValidateService(service *core.Service) field.ErrorList { func ValidateService(service *core.Service, allowAppProtocol bool) field.ErrorList {
allErrs := ValidateObjectMeta(&service.ObjectMeta, true, ValidateServiceName, field.NewPath("metadata")) allErrs := ValidateObjectMeta(&service.ObjectMeta, true, ValidateServiceName, field.NewPath("metadata"))
specPath := field.NewPath("spec") specPath := field.NewPath("spec")
@ -3953,7 +3953,7 @@ func ValidateService(service *core.Service) field.ErrorList {
portsPath := specPath.Child("ports") portsPath := specPath.Child("ports")
for i := range service.Spec.Ports { for i := range service.Spec.Ports {
portPath := portsPath.Index(i) portPath := portsPath.Index(i)
allErrs = append(allErrs, validateServicePort(&service.Spec.Ports[i], len(service.Spec.Ports) > 1, isHeadlessService, &allPortNames, portPath)...) allErrs = append(allErrs, validateServicePort(&service.Spec.Ports[i], len(service.Spec.Ports) > 1, isHeadlessService, allowAppProtocol, &allPortNames, portPath)...)
} }
if service.Spec.Selector != nil { if service.Spec.Selector != nil {
@ -4125,7 +4125,7 @@ func ValidateService(service *core.Service) field.ErrorList {
return allErrs return allErrs
} }
func validateServicePort(sp *core.ServicePort, requireName, isHeadlessService bool, allNames *sets.String, fldPath *field.Path) field.ErrorList { func validateServicePort(sp *core.ServicePort, requireName, isHeadlessService, allowAppProtocol bool, allNames *sets.String, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{} allErrs := field.ErrorList{}
if requireName && len(sp.Name) == 0 { if requireName && len(sp.Name) == 0 {
@ -4151,6 +4151,16 @@ func validateServicePort(sp *core.ServicePort, requireName, isHeadlessService bo
allErrs = append(allErrs, ValidatePortNumOrName(sp.TargetPort, fldPath.Child("targetPort"))...) allErrs = append(allErrs, ValidatePortNumOrName(sp.TargetPort, fldPath.Child("targetPort"))...)
if sp.AppProtocol != nil {
if allowAppProtocol {
for _, msg := range validation.IsQualifiedName(*sp.AppProtocol) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("appProtocol"), sp.AppProtocol, msg))
}
} else {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("appProtocol"), "This field can be enabled with the ServiceAppProtocol feature gate"))
}
}
// in the v1 API, targetPorts on headless services were tolerated. // in the v1 API, targetPorts on headless services were tolerated.
// once we have version-specific validation, we can reject this on newer API versions, but until then, we have to tolerate it for compatibility. // once we have version-specific validation, we can reject this on newer API versions, but until then, we have to tolerate it for compatibility.
// //
@ -4207,6 +4217,14 @@ func ValidateServiceExternalTrafficFieldsCombination(service *core.Service) fiel
return allErrs return allErrs
} }
// ValidateServiceCreate validates Services as they are created.
func ValidateServiceCreate(service *core.Service) field.ErrorList {
// allow AppProtocol value if the feature gate is set.
allowAppProtocol := utilfeature.DefaultFeatureGate.Enabled(features.ServiceAppProtocol)
return ValidateService(service, allowAppProtocol)
}
// ValidateServiceUpdate tests if required fields in the service are set during an update // ValidateServiceUpdate tests if required fields in the service are set during an update
func ValidateServiceUpdate(service, oldService *core.Service) field.ErrorList { func ValidateServiceUpdate(service, oldService *core.Service) field.ErrorList {
allErrs := ValidateObjectMetaUpdate(&service.ObjectMeta, &oldService.ObjectMeta, field.NewPath("metadata")) allErrs := ValidateObjectMetaUpdate(&service.ObjectMeta, &oldService.ObjectMeta, field.NewPath("metadata"))
@ -4226,8 +4244,19 @@ func ValidateServiceUpdate(service, oldService *core.Service) field.ErrorList {
} }
} }
allErrs = append(allErrs, ValidateService(service)...) // allow AppProtocol value if the feature gate is set or the field is
return allErrs // already set on the resource.
allowAppProtocol := utilfeature.DefaultFeatureGate.Enabled(features.ServiceAppProtocol)
if !allowAppProtocol {
for _, port := range oldService.Spec.Ports {
if port.AppProtocol != nil {
allowAppProtocol = true
break
}
}
}
return append(allErrs, ValidateService(service, allowAppProtocol)...)
} }
// ValidateServiceStatusUpdate tests if required fields in the Service are set when updating status. // ValidateServiceStatusUpdate tests if required fields in the Service are set when updating status.
@ -5443,15 +5472,42 @@ func ValidateNamespaceFinalizeUpdate(newNamespace, oldNamespace *core.Namespace)
return allErrs return allErrs
} }
// ValidateEndpoints tests if required fields are set. // ValidateEndpoints validates Endpoints on create and update.
func ValidateEndpoints(endpoints *core.Endpoints) field.ErrorList { func ValidateEndpoints(endpoints *core.Endpoints, allowAppProtocol bool) field.ErrorList {
allErrs := ValidateObjectMeta(&endpoints.ObjectMeta, true, ValidateEndpointsName, field.NewPath("metadata")) allErrs := ValidateObjectMeta(&endpoints.ObjectMeta, true, ValidateEndpointsName, field.NewPath("metadata"))
allErrs = append(allErrs, ValidateEndpointsSpecificAnnotations(endpoints.Annotations, field.NewPath("annotations"))...) allErrs = append(allErrs, ValidateEndpointsSpecificAnnotations(endpoints.Annotations, field.NewPath("annotations"))...)
allErrs = append(allErrs, validateEndpointSubsets(endpoints.Subsets, field.NewPath("subsets"))...) allErrs = append(allErrs, validateEndpointSubsets(endpoints.Subsets, allowAppProtocol, field.NewPath("subsets"))...)
return allErrs return allErrs
} }
func validateEndpointSubsets(subsets []core.EndpointSubset, fldPath *field.Path) field.ErrorList { // ValidateEndpointsCreate validates Endpoints on create.
func ValidateEndpointsCreate(endpoints *core.Endpoints) field.ErrorList {
allowAppProtocol := utilfeature.DefaultFeatureGate.Enabled(features.ServiceAppProtocol)
return ValidateEndpoints(endpoints, allowAppProtocol)
}
// ValidateEndpointsUpdate validates Endpoints on update. NodeName changes are
// allowed during update to accommodate the case where nodeIP or PodCIDR is
// reused. An existing endpoint ip will have a different nodeName if this
// happens.
func ValidateEndpointsUpdate(newEndpoints, oldEndpoints *core.Endpoints) field.ErrorList {
allErrs := ValidateObjectMetaUpdate(&newEndpoints.ObjectMeta, &oldEndpoints.ObjectMeta, field.NewPath("metadata"))
allowAppProtocol := utilfeature.DefaultFeatureGate.Enabled(features.ServiceAppProtocol)
if !allowAppProtocol {
for _, oldSubset := range oldEndpoints.Subsets {
for _, port := range oldSubset.Ports {
if port.AppProtocol != nil {
allowAppProtocol = true
break
}
}
}
}
allErrs = append(allErrs, ValidateEndpoints(newEndpoints, allowAppProtocol)...)
return allErrs
}
func validateEndpointSubsets(subsets []core.EndpointSubset, allowAppProtocol bool, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{} allErrs := field.ErrorList{}
for i := range subsets { for i := range subsets {
ss := &subsets[i] ss := &subsets[i]
@ -5469,7 +5525,7 @@ func validateEndpointSubsets(subsets []core.EndpointSubset, fldPath *field.Path)
allErrs = append(allErrs, validateEndpointAddress(&ss.NotReadyAddresses[addr], idxPath.Child("notReadyAddresses").Index(addr))...) allErrs = append(allErrs, validateEndpointAddress(&ss.NotReadyAddresses[addr], idxPath.Child("notReadyAddresses").Index(addr))...)
} }
for port := range ss.Ports { for port := range ss.Ports {
allErrs = append(allErrs, validateEndpointPort(&ss.Ports[port], len(ss.Ports) > 1, idxPath.Child("ports").Index(port))...) allErrs = append(allErrs, validateEndpointPort(&ss.Ports[port], len(ss.Ports) > 1, allowAppProtocol, idxPath.Child("ports").Index(port))...)
} }
} }
@ -5520,7 +5576,7 @@ func validateNonSpecialIP(ipAddress string, fldPath *field.Path) field.ErrorList
return allErrs return allErrs
} }
func validateEndpointPort(port *core.EndpointPort, requireName bool, fldPath *field.Path) field.ErrorList { func validateEndpointPort(port *core.EndpointPort, requireName, allowAppProtocol bool, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{} allErrs := field.ErrorList{}
if requireName && len(port.Name) == 0 { if requireName && len(port.Name) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("name"), "")) allErrs = append(allErrs, field.Required(fldPath.Child("name"), ""))
@ -5535,16 +5591,15 @@ func validateEndpointPort(port *core.EndpointPort, requireName bool, fldPath *fi
} else if !supportedPortProtocols.Has(string(port.Protocol)) { } else if !supportedPortProtocols.Has(string(port.Protocol)) {
allErrs = append(allErrs, field.NotSupported(fldPath.Child("protocol"), port.Protocol, supportedPortProtocols.List())) allErrs = append(allErrs, field.NotSupported(fldPath.Child("protocol"), port.Protocol, supportedPortProtocols.List()))
} }
return allErrs if port.AppProtocol != nil {
} if allowAppProtocol {
for _, msg := range validation.IsQualifiedName(*port.AppProtocol) {
// ValidateEndpointsUpdate tests to make sure an endpoints update can be applied. allErrs = append(allErrs, field.Invalid(fldPath.Child("appProtocol"), port.AppProtocol, msg))
// NodeName changes are allowed during update to accommodate the case where nodeIP or PodCIDR is reused. }
// An existing endpoint ip will have a different nodeName if this happens. } else {
func ValidateEndpointsUpdate(newEndpoints, oldEndpoints *core.Endpoints) field.ErrorList { allErrs = append(allErrs, field.Forbidden(fldPath.Child("appProtocol"), "This field can be enabled with the ServiceAppProtocol feature gate"))
allErrs := ValidateObjectMetaUpdate(&newEndpoints.ObjectMeta, &oldEndpoints.ObjectMeta, field.NewPath("metadata")) }
allErrs = append(allErrs, validateEndpointSubsets(newEndpoints.Subsets, field.NewPath("subsets"))...) }
allErrs = append(allErrs, ValidateEndpointsSpecificAnnotations(newEndpoints.Annotations, field.NewPath("annotations"))...)
return allErrs return allErrs
} }

View File

@ -9387,14 +9387,15 @@ func TestValidatePodEphemeralContainersUpdate(t *testing.T) {
} }
} }
func TestValidateService(t *testing.T) { func TestValidateServiceCreate(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.SCTPSupport, true)() defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.SCTPSupport, true)()
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceTopology, true)() defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceTopology, true)()
testCases := []struct { testCases := []struct {
name string name string
tweakSvc func(svc *core.Service) // given a basic valid service, each test case can customize it tweakSvc func(svc *core.Service) // given a basic valid service, each test case can customize it
numErrs int numErrs int
appProtocolEnabled bool
}{ }{
{ {
name: "missing namespace", name: "missing namespace",
@ -10128,15 +10129,57 @@ func TestValidateService(t *testing.T) {
}, },
numErrs: 1, numErrs: 1,
}, },
{
name: `valid appProtocol`,
tweakSvc: func(s *core.Service) {
s.Spec.Ports = []core.ServicePort{{
Port: 12345,
TargetPort: intstr.FromInt(12345),
Protocol: "TCP",
AppProtocol: utilpointer.StringPtr("HTTP"),
}}
},
appProtocolEnabled: true,
numErrs: 0,
},
{
name: `valid custom appProtocol`,
tweakSvc: func(s *core.Service) {
s.Spec.Ports = []core.ServicePort{{
Port: 12345,
TargetPort: intstr.FromInt(12345),
Protocol: "TCP",
AppProtocol: utilpointer.StringPtr("example.com/protocol"),
}}
},
appProtocolEnabled: true,
numErrs: 0,
},
{
name: `invalid appProtocol`,
tweakSvc: func(s *core.Service) {
s.Spec.Ports = []core.ServicePort{{
Port: 12345,
TargetPort: intstr.FromInt(12345),
Protocol: "TCP",
AppProtocol: utilpointer.StringPtr("example.com/protocol_with{invalid}[characters]"),
}}
},
appProtocolEnabled: true,
numErrs: 1,
},
} }
for _, tc := range testCases { for _, tc := range testCases {
svc := makeValidService() t.Run(tc.name, func(t *testing.T) {
tc.tweakSvc(&svc) defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceAppProtocol, true)()
errs := ValidateService(&svc) svc := makeValidService()
if len(errs) != tc.numErrs { tc.tweakSvc(&svc)
t.Errorf("Unexpected error list for case %q: %v", tc.name, errs.ToAggregate()) errs := ValidateServiceCreate(&svc)
} if len(errs) != tc.numErrs {
t.Errorf("Unexpected error list for case %q: %v", tc.name, errs.ToAggregate())
}
})
} }
} }
@ -11736,9 +11779,10 @@ func TestValidateNodeUpdate(t *testing.T) {
func TestValidateServiceUpdate(t *testing.T) { func TestValidateServiceUpdate(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
tweakSvc func(oldSvc, newSvc *core.Service) // given basic valid services, each test case can customize them tweakSvc func(oldSvc, newSvc *core.Service) // given basic valid services, each test case can customize them
numErrs int numErrs int
appProtocolEnabled bool
}{ }{
{ {
name: "no change", name: "no change",
@ -12182,16 +12226,54 @@ func TestValidateServiceUpdate(t *testing.T) {
}, },
numErrs: 1, numErrs: 1,
}, },
{
name: "update with valid app protocol, field unset, gate disabled",
tweakSvc: func(oldSvc, newSvc *core.Service) {
oldSvc.Spec.Ports = []core.ServicePort{{Name: "a", Port: 443, TargetPort: intstr.FromInt(3000), Protocol: "TCP"}}
newSvc.Spec.Ports = []core.ServicePort{{Name: "a", Port: 443, TargetPort: intstr.FromInt(3000), Protocol: "TCP", AppProtocol: utilpointer.StringPtr("https")}}
},
numErrs: 1,
},
{
name: "update to valid app protocol, field set, gate disabled",
tweakSvc: func(oldSvc, newSvc *core.Service) {
oldSvc.Spec.Ports = []core.ServicePort{{Name: "a", Port: 443, TargetPort: intstr.FromInt(3000), Protocol: "TCP", AppProtocol: utilpointer.StringPtr("http")}}
newSvc.Spec.Ports = []core.ServicePort{{Name: "a", Port: 443, TargetPort: intstr.FromInt(3000), Protocol: "TCP", AppProtocol: utilpointer.StringPtr("https")}}
},
numErrs: 0,
},
{
name: "update to valid app protocol, gate enabled",
tweakSvc: func(oldSvc, newSvc *core.Service) {
oldSvc.Spec.Ports = []core.ServicePort{{Name: "a", Port: 443, TargetPort: intstr.FromInt(3000), Protocol: "TCP"}}
newSvc.Spec.Ports = []core.ServicePort{{Name: "a", Port: 443, TargetPort: intstr.FromInt(3000), Protocol: "TCP", AppProtocol: utilpointer.StringPtr("https")}}
},
appProtocolEnabled: true,
numErrs: 0,
},
{
name: "update to invalid app protocol, gate enabled",
tweakSvc: func(oldSvc, newSvc *core.Service) {
oldSvc.Spec.Ports = []core.ServicePort{{Name: "a", Port: 443, TargetPort: intstr.FromInt(3000), Protocol: "TCP"}}
newSvc.Spec.Ports = []core.ServicePort{{Name: "a", Port: 443, TargetPort: intstr.FromInt(3000), Protocol: "TCP", AppProtocol: utilpointer.StringPtr("~https")}}
},
appProtocolEnabled: true,
numErrs: 1,
},
} }
for _, tc := range testCases { for _, tc := range testCases {
oldSvc := makeValidService() t.Run(tc.name, func(t *testing.T) {
newSvc := makeValidService() defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceAppProtocol, tc.appProtocolEnabled)()
tc.tweakSvc(&oldSvc, &newSvc)
errs := ValidateServiceUpdate(&newSvc, &oldSvc) oldSvc := makeValidService()
if len(errs) != tc.numErrs { newSvc := makeValidService()
t.Errorf("Unexpected error list for case %q: %v", tc.name, errs.ToAggregate()) tc.tweakSvc(&oldSvc, &newSvc)
} errs := ValidateServiceUpdate(&newSvc, &oldSvc)
if len(errs) != tc.numErrs {
t.Errorf("Expected %d errors, got %d: %v", tc.numErrs, len(errs), errs.ToAggregate())
}
})
} }
} }
@ -13478,53 +13560,82 @@ func TestValidateSSHAuthSecret(t *testing.T) {
} }
} }
func TestValidateEndpoints(t *testing.T) { func TestValidateEndpointsCreate(t *testing.T) {
successCases := map[string]core.Endpoints{ successCases := map[string]struct {
endpoints core.Endpoints
appProtocolEnabled bool
}{
"simple endpoint": { "simple endpoint": {
ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace"}, endpoints: core.Endpoints{
Subsets: []core.EndpointSubset{ ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace"},
{ Subsets: []core.EndpointSubset{
Addresses: []core.EndpointAddress{{IP: "10.10.1.1"}, {IP: "10.10.2.2"}}, {
Ports: []core.EndpointPort{{Name: "a", Port: 8675, Protocol: "TCP"}, {Name: "b", Port: 309, Protocol: "TCP"}}, Addresses: []core.EndpointAddress{{IP: "10.10.1.1"}, {IP: "10.10.2.2"}},
}, Ports: []core.EndpointPort{{Name: "a", Port: 8675, Protocol: "TCP"}, {Name: "b", Port: 309, Protocol: "TCP"}},
{ },
Addresses: []core.EndpointAddress{{IP: "10.10.3.3"}}, {
Ports: []core.EndpointPort{{Name: "a", Port: 93, Protocol: "TCP"}, {Name: "b", Port: 76, Protocol: "TCP"}}, Addresses: []core.EndpointAddress{{IP: "10.10.3.3"}},
Ports: []core.EndpointPort{{Name: "a", Port: 93, Protocol: "TCP"}, {Name: "b", Port: 76, Protocol: "TCP"}},
},
}, },
}, },
}, },
"empty subsets": { "empty subsets": {
ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace"}, endpoints: core.Endpoints{
ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace"},
},
}, },
"no name required for singleton port": { "no name required for singleton port": {
ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace"}, endpoints: core.Endpoints{
Subsets: []core.EndpointSubset{ ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace"},
{ Subsets: []core.EndpointSubset{
Addresses: []core.EndpointAddress{{IP: "10.10.1.1"}}, {
Ports: []core.EndpointPort{{Port: 8675, Protocol: "TCP"}}, Addresses: []core.EndpointAddress{{IP: "10.10.1.1"}},
Ports: []core.EndpointPort{{Port: 8675, Protocol: "TCP"}},
},
}, },
}, },
}, },
"valid appProtocol": {
endpoints: core.Endpoints{
ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace"},
Subsets: []core.EndpointSubset{
{
Addresses: []core.EndpointAddress{{IP: "10.10.1.1"}},
Ports: []core.EndpointPort{{Port: 8675, Protocol: "TCP", AppProtocol: utilpointer.StringPtr("HTTP")}},
},
},
},
appProtocolEnabled: true,
},
"empty ports": { "empty ports": {
ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace"}, endpoints: core.Endpoints{
Subsets: []core.EndpointSubset{ ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace"},
{ Subsets: []core.EndpointSubset{
Addresses: []core.EndpointAddress{{IP: "10.10.3.3"}}, {
Addresses: []core.EndpointAddress{{IP: "10.10.3.3"}},
},
}, },
}, },
}, },
} }
for k, v := range successCases { for name, tc := range successCases {
if errs := ValidateEndpoints(&v); len(errs) != 0 { t.Run(name, func(t *testing.T) {
t.Errorf("Expected success for %s, got %v", k, errs) defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceAppProtocol, tc.appProtocolEnabled)()
} errs := ValidateEndpointsCreate(&tc.endpoints)
if len(errs) != 0 {
t.Errorf("Expected no validation errors, got %v", errs)
}
})
} }
errorCases := map[string]struct { errorCases := map[string]struct {
endpoints core.Endpoints endpoints core.Endpoints
errorType field.ErrorType appProtocolEnabled bool
errorDetail string errorType field.ErrorType
errorDetail string
}{ }{
"missing namespace": { "missing namespace": {
endpoints: core.Endpoints{ObjectMeta: metav1.ObjectMeta{Name: "mysvc"}}, endpoints: core.Endpoints{ObjectMeta: metav1.ObjectMeta{Name: "mysvc"}},
@ -13682,12 +13793,100 @@ func TestValidateEndpoints(t *testing.T) {
errorType: "FieldValueInvalid", errorType: "FieldValueInvalid",
errorDetail: "link-local multicast", errorDetail: "link-local multicast",
}, },
"Invalid AppProtocol": {
endpoints: core.Endpoints{
ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace"},
Subsets: []core.EndpointSubset{
{
Addresses: []core.EndpointAddress{{IP: "10.10.1.1"}},
Ports: []core.EndpointPort{{Name: "p", Port: 93, Protocol: "TCP", AppProtocol: utilpointer.StringPtr("lots-of[invalid]-{chars}")}},
},
},
},
appProtocolEnabled: true,
errorType: "FieldValueInvalid",
errorDetail: "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character",
},
} }
for k, v := range errorCases { for k, v := range errorCases {
if errs := ValidateEndpoints(&v.endpoints); len(errs) == 0 || errs[0].Type != v.errorType || !strings.Contains(errs[0].Detail, v.errorDetail) { t.Run(k, func(t *testing.T) {
t.Errorf("[%s] Expected error type %s with detail %q, got %v", k, v.errorType, v.errorDetail, errs) defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceAppProtocol, v.appProtocolEnabled)()
} if errs := ValidateEndpointsCreate(&v.endpoints); len(errs) == 0 || errs[0].Type != v.errorType || !strings.Contains(errs[0].Detail, v.errorDetail) {
t.Errorf("Expected error type %s with detail %q, got %v", v.errorType, v.errorDetail, errs)
}
})
}
}
func TestValidateEndpointsUpdate(t *testing.T) {
baseEndpoints := core.Endpoints{
ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace", ResourceVersion: "1234"},
Subsets: []core.EndpointSubset{{
Addresses: []core.EndpointAddress{{IP: "10.1.2.3"}},
}},
}
testCases := map[string]struct {
tweakOldEndpoints func(ep *core.Endpoints)
tweakNewEndpoints func(ep *core.Endpoints)
appProtocolEnabled bool
numExpectedErrors int
}{
"update with valid app protocol, field unset, gate not enabled": {
tweakOldEndpoints: func(ep *core.Endpoints) {
ep.Subsets[0].Ports = []core.EndpointPort{{Name: "a", Port: 8675, Protocol: "TCP"}}
},
tweakNewEndpoints: func(ep *core.Endpoints) {
ep.Subsets[0].Ports = []core.EndpointPort{{Name: "a", Port: 8675, Protocol: "TCP", AppProtocol: utilpointer.StringPtr("https")}}
},
numExpectedErrors: 1,
},
"update with valid app protocol, field set, gate not enabled": {
tweakOldEndpoints: func(ep *core.Endpoints) {
ep.Subsets[0].Ports = []core.EndpointPort{{Name: "a", Port: 8675, Protocol: "TCP", AppProtocol: utilpointer.StringPtr("http")}}
},
tweakNewEndpoints: func(ep *core.Endpoints) {
ep.Subsets[0].Ports = []core.EndpointPort{{Name: "a", Port: 8675, Protocol: "TCP", AppProtocol: utilpointer.StringPtr("https")}}
},
numExpectedErrors: 0,
},
"update to valid app protocol, gate enabled": {
tweakOldEndpoints: func(ep *core.Endpoints) {
ep.Subsets[0].Ports = []core.EndpointPort{{Name: "a", Port: 8675, Protocol: "TCP"}}
},
tweakNewEndpoints: func(ep *core.Endpoints) {
ep.Subsets[0].Ports = []core.EndpointPort{{Name: "a", Port: 8675, Protocol: "TCP", AppProtocol: utilpointer.StringPtr("https")}}
},
appProtocolEnabled: true,
numExpectedErrors: 0,
},
"update to invalid app protocol, gate enabled": {
tweakOldEndpoints: func(ep *core.Endpoints) {
ep.Subsets[0].Ports = []core.EndpointPort{{Name: "a", Port: 8675, Protocol: "TCP"}}
},
tweakNewEndpoints: func(ep *core.Endpoints) {
ep.Subsets[0].Ports = []core.EndpointPort{{Name: "a", Port: 8675, Protocol: "TCP", AppProtocol: utilpointer.StringPtr("~https")}}
},
appProtocolEnabled: true,
numExpectedErrors: 1,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceAppProtocol, tc.appProtocolEnabled)()
oldEndpoints := baseEndpoints.DeepCopy()
tc.tweakOldEndpoints(oldEndpoints)
newEndpoints := baseEndpoints.DeepCopy()
tc.tweakNewEndpoints(newEndpoints)
errs := ValidateEndpointsUpdate(newEndpoints, oldEndpoints)
if len(errs) != tc.numExpectedErrors {
t.Errorf("Expected %d validation errors, got %d: %v", tc.numExpectedErrors, len(errs), errs)
}
})
} }
} }
@ -14226,7 +14425,7 @@ func TestEndpointAddressNodeNameUpdateRestrictions(t *testing.T) {
updatedEndpoint := newNodeNameEndpoint("kubernetes-changed-nodename") updatedEndpoint := newNodeNameEndpoint("kubernetes-changed-nodename")
// Check that NodeName can be changed during update, this is to accommodate the case where nodeIP or PodCIDR is reused. // Check that NodeName can be changed during update, this is to accommodate the case where nodeIP or PodCIDR is reused.
// The same ip will now have a different nodeName. // The same ip will now have a different nodeName.
errList := ValidateEndpoints(updatedEndpoint) errList := ValidateEndpoints(updatedEndpoint, false)
errList = append(errList, ValidateEndpointsUpdate(updatedEndpoint, oldEndpoint)...) errList = append(errList, ValidateEndpointsUpdate(updatedEndpoint, oldEndpoint)...)
if len(errList) != 0 { if len(errList) != 0 {
t.Error("Endpoint should allow changing of Subset.Addresses.NodeName on update") t.Error("Endpoint should allow changing of Subset.Addresses.NodeName on update")
@ -14236,7 +14435,7 @@ func TestEndpointAddressNodeNameUpdateRestrictions(t *testing.T) {
func TestEndpointAddressNodeNameInvalidDNSSubdomain(t *testing.T) { func TestEndpointAddressNodeNameInvalidDNSSubdomain(t *testing.T) {
// Check NodeName DNS validation // Check NodeName DNS validation
endpoint := newNodeNameEndpoint("illegal*.nodename") endpoint := newNodeNameEndpoint("illegal*.nodename")
errList := ValidateEndpoints(endpoint) errList := ValidateEndpoints(endpoint, false)
if len(errList) == 0 { if len(errList) == 0 {
t.Error("Endpoint should reject invalid NodeName") t.Error("Endpoint should reject invalid NodeName")
} }
@ -14244,7 +14443,7 @@ func TestEndpointAddressNodeNameInvalidDNSSubdomain(t *testing.T) {
func TestEndpointAddressNodeNameCanBeAnIPAddress(t *testing.T) { func TestEndpointAddressNodeNameCanBeAnIPAddress(t *testing.T) {
endpoint := newNodeNameEndpoint("10.10.1.1") endpoint := newNodeNameEndpoint("10.10.1.1")
errList := ValidateEndpoints(endpoint) errList := ValidateEndpoints(endpoint, false)
if len(errList) != 0 { if len(errList) != 0 {
t.Error("Endpoint should accept a NodeName that is an IP address") t.Error("Endpoint should accept a NodeName that is an IP address")
} }

View File

@ -1096,6 +1096,11 @@ func (in *EndpointAddress) DeepCopy() *EndpointAddress {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *EndpointPort) DeepCopyInto(out *EndpointPort) { func (in *EndpointPort) DeepCopyInto(out *EndpointPort) {
*out = *in *out = *in
if in.AppProtocol != nil {
in, out := &in.AppProtocol, &out.AppProtocol
*out = new(string)
**out = **in
}
return return
} }
@ -1129,7 +1134,9 @@ func (in *EndpointSubset) DeepCopyInto(out *EndpointSubset) {
if in.Ports != nil { if in.Ports != nil {
in, out := &in.Ports, &out.Ports in, out := &in.Ports, &out.Ports
*out = make([]EndpointPort, len(*in)) *out = make([]EndpointPort, len(*in))
copy(*out, *in) for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
} }
return return
} }
@ -5107,6 +5114,11 @@ func (in *ServiceList) DeepCopyObject() runtime.Object {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ServicePort) DeepCopyInto(out *ServicePort) { func (in *ServicePort) DeepCopyInto(out *ServicePort) {
*out = *in *out = *in
if in.AppProtocol != nil {
in, out := &in.AppProtocol, &out.AppProtocol
*out = new(string)
**out = **in
}
out.TargetPort = in.TargetPort out.TargetPort = in.TargetPort
return return
} }
@ -5152,7 +5164,9 @@ func (in *ServiceSpec) DeepCopyInto(out *ServiceSpec) {
if in.Ports != nil { if in.Ports != nil {
in, out := &in.Ports, &out.Ports in, out := &in.Ports, &out.Ports
*out = make([]ServicePort, len(*in)) *out = make([]ServicePort, len(*in))
copy(*out, *in) for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
} }
if in.Selector != nil { if in.Selector != nil {
in, out := &in.Selector, &out.Selector in, out := &in.Selector, &out.Selector

View File

@ -139,8 +139,8 @@ type EndpointPort struct {
// This field follows standard Kubernetes label syntax. // This field follows standard Kubernetes label syntax.
// Un-prefixed names are reserved for IANA standard service names (as per // Un-prefixed names are reserved for IANA standard service names (as per
// RFC-6335 and http://www.iana.org/assignments/service-names). // RFC-6335 and http://www.iana.org/assignments/service-names).
// Non-standard protocols should use prefixed names. // Non-standard protocols should use prefixed names such as
// Default is empty string. // mycompany.com/my-custom-protocol.
// +optional // +optional
AppProtocol *string AppProtocol *string
} }

View File

@ -65,6 +65,7 @@ go_test(
"//staging/src/k8s.io/client-go/tools/cache:go_default_library", "//staging/src/k8s.io/client-go/tools/cache:go_default_library",
"//staging/src/k8s.io/client-go/util/testing:go_default_library", "//staging/src/k8s.io/client-go/util/testing:go_default_library",
"//staging/src/k8s.io/component-base/featuregate/testing:go_default_library", "//staging/src/k8s.io/component-base/featuregate/testing:go_default_library",
"//vendor/k8s.io/utils/pointer:go_default_library",
], ],
) )

View File

@ -444,17 +444,14 @@ func (e *EndpointController) syncService(key string) error {
} else { } else {
for i := range service.Spec.Ports { for i := range service.Spec.Ports {
servicePort := &service.Spec.Ports[i] servicePort := &service.Spec.Ports[i]
portName := servicePort.Name
portProto := servicePort.Protocol
portNum, err := podutil.FindPort(pod, servicePort) portNum, err := podutil.FindPort(pod, servicePort)
if err != nil { if err != nil {
klog.V(4).Infof("Failed to find port for service %s/%s: %v", service.Namespace, service.Name, err) klog.V(4).Infof("Failed to find port for service %s/%s: %v", service.Namespace, service.Name, err)
continue continue
} }
epp := endpointPortFromServicePort(servicePort, portNum)
var readyEps, notReadyEps int var readyEps, notReadyEps int
epp := &v1.EndpointPort{Name: portName, Port: int32(portNum), Protocol: portProto}
subsets, readyEps, notReadyEps = addEndpointSubset(subsets, pod, epa, epp, tolerateUnreadyEndpoints) subsets, readyEps, notReadyEps = addEndpointSubset(subsets, pod, epa, epp, tolerateUnreadyEndpoints)
totalReadyEps = totalReadyEps + readyEps totalReadyEps = totalReadyEps + readyEps
totalNotReadyEps = totalNotReadyEps + notReadyEps totalNotReadyEps = totalNotReadyEps + notReadyEps
@ -608,3 +605,15 @@ func shouldPodBeInEndpoints(pod *v1.Pod) bool {
return true return true
} }
} }
func endpointPortFromServicePort(servicePort *v1.ServicePort, portNum int) *v1.EndpointPort {
epp := &v1.EndpointPort{
Name: servicePort.Name,
Port: int32(portNum),
Protocol: servicePort.Protocol,
}
if utilfeature.DefaultFeatureGate.Enabled(features.ServiceAppProtocol) {
epp.AppProtocol = servicePort.AppProtocol
}
return epp
}

View File

@ -45,6 +45,7 @@ import (
"k8s.io/kubernetes/pkg/controller" "k8s.io/kubernetes/pkg/controller"
endpointutil "k8s.io/kubernetes/pkg/controller/util/endpoint" endpointutil "k8s.io/kubernetes/pkg/controller/util/endpoint"
"k8s.io/kubernetes/pkg/features" "k8s.io/kubernetes/pkg/features"
utilpointer "k8s.io/utils/pointer"
) )
var alwaysReady = func() bool { return true } var alwaysReady = func() bool { return true }
@ -1947,6 +1948,55 @@ func TestSyncEndpointsServiceNotFound(t *testing.T) {
endpointsHandler.ValidateRequest(t, "/api/v1/namespaces/"+ns+"/endpoints/foo", "DELETE", nil) endpointsHandler.ValidateRequest(t, "/api/v1/namespaces/"+ns+"/endpoints/foo", "DELETE", nil)
} }
func TestEndpointPortFromServicePort(t *testing.T) {
http := utilpointer.StringPtr("http")
testCases := map[string]struct {
featureGateEnabled bool
serviceAppProtocol *string
expectedEndpointsAppProtocol *string
}{
"feature gate disabled, empty app protocol": {
featureGateEnabled: false,
serviceAppProtocol: nil,
expectedEndpointsAppProtocol: nil,
},
"feature gate disabled, http app protocol": {
featureGateEnabled: false,
serviceAppProtocol: http,
expectedEndpointsAppProtocol: nil,
},
"feature gate enabled, empty app protocol": {
featureGateEnabled: true,
serviceAppProtocol: nil,
expectedEndpointsAppProtocol: nil,
},
"feature gate enabled, http app protocol": {
featureGateEnabled: true,
serviceAppProtocol: http,
expectedEndpointsAppProtocol: http,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceAppProtocol, tc.featureGateEnabled)()
epp := endpointPortFromServicePort(&v1.ServicePort{Name: "test", AppProtocol: tc.serviceAppProtocol}, 80)
if epp.AppProtocol != tc.expectedEndpointsAppProtocol {
t.Errorf("Expected Endpoints AppProtocol to be %s, got %s", stringVal(tc.expectedEndpointsAppProtocol), stringVal(epp.AppProtocol))
}
})
}
}
func stringVal(str *string) string {
if str == nil {
return "nil"
}
return *str
}
func podChangedHelper(oldPod, newPod *v1.Pod, endpointChanged endpointutil.EndpointsMatch) bool { func podChangedHelper(oldPod, newPod *v1.Pod, endpointChanged endpointutil.EndpointsMatch) bool {
podChanged, _ := endpointutil.PodChanged(oldPod, newPod, endpointChanged) podChanged, _ := endpointutil.PodChanged(oldPod, newPod, endpointChanged)
return podChanged return podChanged

View File

@ -117,9 +117,10 @@ func getEndpointPorts(service *corev1.Service, pod *corev1.Pod) []discovery.Endp
i32PortNum := int32(portNum) i32PortNum := int32(portNum)
endpointPorts = append(endpointPorts, discovery.EndpointPort{ endpointPorts = append(endpointPorts, discovery.EndpointPort{
Name: &portName, Name: &portName,
Port: &i32PortNum, Port: &i32PortNum,
Protocol: &portProto, Protocol: &portProto,
AppProtocol: servicePort.AppProtocol,
}) })
} }

View File

@ -353,6 +353,97 @@ func TestServiceControllerKey(t *testing.T) {
} }
} }
func TestGetEndpointPorts(t *testing.T) {
protoTCP := v1.ProtocolTCP
testCases := map[string]struct {
service *v1.Service
pod *v1.Pod
expectedPorts []*discovery.EndpointPort
}{
"service with AppProtocol on one port": {
service: &v1.Service{
Spec: v1.ServiceSpec{
Ports: []v1.ServicePort{{
Name: "http",
Port: 80,
TargetPort: intstr.FromInt(80),
Protocol: protoTCP,
AppProtocol: utilpointer.StringPtr("example.com/custom-protocol"),
}},
},
},
pod: &v1.Pod{
Spec: v1.PodSpec{
Containers: []v1.Container{{
Ports: []v1.ContainerPort{},
}},
},
},
expectedPorts: []*discovery.EndpointPort{{
Name: utilpointer.StringPtr("http"),
Port: utilpointer.Int32Ptr(80),
Protocol: &protoTCP,
AppProtocol: utilpointer.StringPtr("example.com/custom-protocol"),
}},
},
"service with named port and AppProtocol on one port": {
service: &v1.Service{
Spec: v1.ServiceSpec{
Ports: []v1.ServicePort{{
Name: "http",
Port: 80,
TargetPort: intstr.FromInt(80),
Protocol: protoTCP,
}, {
Name: "https",
Protocol: protoTCP,
TargetPort: intstr.FromString("https"),
AppProtocol: utilpointer.StringPtr("https"),
}},
},
},
pod: &v1.Pod{
Spec: v1.PodSpec{
Containers: []v1.Container{{
Ports: []v1.ContainerPort{{
Name: "https",
ContainerPort: int32(443),
Protocol: protoTCP,
}},
}},
},
},
expectedPorts: []*discovery.EndpointPort{{
Name: utilpointer.StringPtr("http"),
Port: utilpointer.Int32Ptr(80),
Protocol: &protoTCP,
}, {
Name: utilpointer.StringPtr("https"),
Port: utilpointer.Int32Ptr(443),
Protocol: &protoTCP,
AppProtocol: utilpointer.StringPtr("https"),
}},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
actualPorts := getEndpointPorts(tc.service, tc.pod)
if len(actualPorts) != len(tc.expectedPorts) {
t.Fatalf("Expected %d ports, got %d", len(tc.expectedPorts), len(actualPorts))
}
for i, actualPort := range actualPorts {
if !reflect.DeepEqual(&actualPort, tc.expectedPorts[i]) {
t.Errorf("Expected port: %+v, got %+v", tc.expectedPorts[i], &actualPort)
}
}
})
}
}
// Test helpers // Test helpers
func newPod(n int, namespace string, ready bool, nPorts int) *v1.Pod { func newPod(n int, namespace string, ready bool, nPorts int) *v1.Pod {

View File

@ -534,6 +534,12 @@ const (
// Enables topology aware service routing // Enables topology aware service routing
ServiceTopology featuregate.Feature = "ServiceTopology" ServiceTopology featuregate.Feature = "ServiceTopology"
// owner: @robscott
// alpha: v1.18
//
// Enables AppProtocol field for Services and Endpoints.
ServiceAppProtocol featuregate.Feature = "ServiceAppProtocol"
// owner: @wojtek-t // owner: @wojtek-t
// alpha: v1.18 // alpha: v1.18
// //
@ -623,6 +629,7 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
AllowInsecureBackendProxy: {Default: true, PreRelease: featuregate.Beta}, AllowInsecureBackendProxy: {Default: true, PreRelease: featuregate.Beta},
PodDisruptionBudget: {Default: true, PreRelease: featuregate.Beta}, PodDisruptionBudget: {Default: true, PreRelease: featuregate.Beta},
ServiceTopology: {Default: false, PreRelease: featuregate.Alpha}, ServiceTopology: {Default: false, PreRelease: featuregate.Alpha},
ServiceAppProtocol: {Default: false, PreRelease: featuregate.Alpha},
ImmutableEphemeralVolumes: {Default: false, PreRelease: featuregate.Alpha}, ImmutableEphemeralVolumes: {Default: false, PreRelease: featuregate.Alpha},
// inherited features from generic apiserver, relisted here to get a conflict if it is changed // inherited features from generic apiserver, relisted here to get a conflict if it is changed

View File

@ -53,7 +53,7 @@ func (endpointsStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.
// Validate validates a new endpoints. // Validate validates a new endpoints.
func (endpointsStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { func (endpointsStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
allErrs := validation.ValidateEndpoints(obj.(*api.Endpoints)) allErrs := validation.ValidateEndpointsCreate(obj.(*api.Endpoints))
allErrs = append(allErrs, validation.ValidateConditionalEndpoints(obj.(*api.Endpoints), nil)...) allErrs = append(allErrs, validation.ValidateConditionalEndpoints(obj.(*api.Endpoints), nil)...)
return allErrs return allErrs
} }
@ -71,8 +71,7 @@ func (endpointsStrategy) AllowCreateOnUpdate() bool {
// ValidateUpdate is the default update validation for an end user. // ValidateUpdate is the default update validation for an end user.
func (endpointsStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { func (endpointsStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
errorList := validation.ValidateEndpoints(obj.(*api.Endpoints)) errorList := validation.ValidateEndpointsUpdate(obj.(*api.Endpoints), old.(*api.Endpoints))
errorList = append(errorList, validation.ValidateEndpointsUpdate(obj.(*api.Endpoints), old.(*api.Endpoints))...)
errorList = append(errorList, validation.ValidateConditionalEndpoints(obj.(*api.Endpoints), old.(*api.Endpoints))...) errorList = append(errorList, validation.ValidateConditionalEndpoints(obj.(*api.Endpoints), old.(*api.Endpoints))...)
return errorList return errorList
} }

View File

@ -65,7 +65,7 @@ func (svcStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object
// Validate validates a new service. // Validate validates a new service.
func (svcStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { func (svcStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
service := obj.(*api.Service) service := obj.(*api.Service)
allErrs := validation.ValidateService(service) allErrs := validation.ValidateServiceCreate(service)
allErrs = append(allErrs, validation.ValidateConditionalService(service, nil)...) allErrs = append(allErrs, validation.ValidateConditionalService(service, nil)...)
return allErrs return allErrs
} }

File diff suppressed because it is too large Load Diff

View File

@ -1042,6 +1042,16 @@ message EndpointPort {
// Default is TCP. // Default is TCP.
// +optional // +optional
optional string protocol = 3; optional string protocol = 3;
// The application protocol for this port.
// This field follows standard Kubernetes label syntax.
// Un-prefixed names are reserved for IANA standard service names (as per
// RFC-6335 and http://www.iana.org/assignments/service-names).
// Non-standard protocols should use prefixed names such as
// mycompany.com/my-custom-protocol.
// Field can be enabled with ServiceAppProtocol feature gate.
// +optional
optional string appProtocol = 4;
} }
// EndpointSubset is a group of addresses with a common set of ports. The // EndpointSubset is a group of addresses with a common set of ports. The
@ -4609,6 +4619,16 @@ message ServicePort {
// +optional // +optional
optional string protocol = 2; optional string protocol = 2;
// The application protocol for this port.
// This field follows standard Kubernetes label syntax.
// Un-prefixed names are reserved for IANA standard service names (as per
// RFC-6335 and http://www.iana.org/assignments/service-names).
// Non-standard protocols should use prefixed names such as
// mycompany.com/my-custom-protocol.
// Field can be enabled with ServiceAppProtocol feature gate.
// +optional
optional string appProtocol = 6;
// The port that will be exposed by this service. // The port that will be exposed by this service.
optional int32 port = 3; optional int32 port = 3;

View File

@ -3989,6 +3989,16 @@ type ServicePort struct {
// +optional // +optional
Protocol Protocol `json:"protocol,omitempty" protobuf:"bytes,2,opt,name=protocol,casttype=Protocol"` Protocol Protocol `json:"protocol,omitempty" protobuf:"bytes,2,opt,name=protocol,casttype=Protocol"`
// The application protocol for this port.
// This field follows standard Kubernetes label syntax.
// Un-prefixed names are reserved for IANA standard service names (as per
// RFC-6335 and http://www.iana.org/assignments/service-names).
// Non-standard protocols should use prefixed names such as
// mycompany.com/my-custom-protocol.
// Field can be enabled with ServiceAppProtocol feature gate.
// +optional
AppProtocol *string `json:"appProtocol,omitempty" protobuf:"bytes,6,opt,name=appProtocol"`
// The port that will be exposed by this service. // The port that will be exposed by this service.
Port int32 `json:"port" protobuf:"varint,3,opt,name=port"` Port int32 `json:"port" protobuf:"varint,3,opt,name=port"`
@ -4204,6 +4214,16 @@ type EndpointPort struct {
// Default is TCP. // Default is TCP.
// +optional // +optional
Protocol Protocol `json:"protocol,omitempty" protobuf:"bytes,3,opt,name=protocol,casttype=Protocol"` Protocol Protocol `json:"protocol,omitempty" protobuf:"bytes,3,opt,name=protocol,casttype=Protocol"`
// The application protocol for this port.
// This field follows standard Kubernetes label syntax.
// Un-prefixed names are reserved for IANA standard service names (as per
// RFC-6335 and http://www.iana.org/assignments/service-names).
// Non-standard protocols should use prefixed names such as
// mycompany.com/my-custom-protocol.
// Field can be enabled with ServiceAppProtocol feature gate.
// +optional
AppProtocol *string `json:"appProtocol,omitempty" protobuf:"bytes,4,opt,name=appProtocol"`
} }
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

View File

@ -502,10 +502,11 @@ func (EndpointAddress) SwaggerDoc() map[string]string {
} }
var map_EndpointPort = map[string]string{ var map_EndpointPort = map[string]string{
"": "EndpointPort is a tuple that describes a single port.", "": "EndpointPort is a tuple that describes a single port.",
"name": "The name of this port. This must match the 'name' field in the corresponding ServicePort. Must be a DNS_LABEL. Optional only if one port is defined.", "name": "The name of this port. This must match the 'name' field in the corresponding ServicePort. Must be a DNS_LABEL. Optional only if one port is defined.",
"port": "The port number of the endpoint.", "port": "The port number of the endpoint.",
"protocol": "The IP protocol for this port. Must be UDP, TCP, or SCTP. Default is TCP.", "protocol": "The IP protocol for this port. Must be UDP, TCP, or SCTP. Default is TCP.",
"appProtocol": "The application protocol for this port. This field follows standard Kubernetes label syntax. Un-prefixed names are reserved for IANA standard service names (as per RFC-6335 and http://www.iana.org/assignments/service-names). Non-standard protocols should use prefixed names such as mycompany.com/my-custom-protocol. Field can be enabled with ServiceAppProtocol feature gate.",
} }
func (EndpointPort) SwaggerDoc() map[string]string { func (EndpointPort) SwaggerDoc() map[string]string {
@ -2169,12 +2170,13 @@ func (ServiceList) SwaggerDoc() map[string]string {
} }
var map_ServicePort = map[string]string{ var map_ServicePort = map[string]string{
"": "ServicePort contains information on service's port.", "": "ServicePort contains information on service's port.",
"name": "The name of this port within the service. This must be a DNS_LABEL. All ports within a ServiceSpec must have unique names. When considering the endpoints for a Service, this must match the 'name' field in the EndpointPort. Optional if only one ServicePort is defined on this service.", "name": "The name of this port within the service. This must be a DNS_LABEL. All ports within a ServiceSpec must have unique names. When considering the endpoints for a Service, this must match the 'name' field in the EndpointPort. Optional if only one ServicePort is defined on this service.",
"protocol": "The IP protocol for this port. Supports \"TCP\", \"UDP\", and \"SCTP\". Default is TCP.", "protocol": "The IP protocol for this port. Supports \"TCP\", \"UDP\", and \"SCTP\". Default is TCP.",
"port": "The port that will be exposed by this service.", "appProtocol": "The application protocol for this port. This field follows standard Kubernetes label syntax. Un-prefixed names are reserved for IANA standard service names (as per RFC-6335 and http://www.iana.org/assignments/service-names). Non-standard protocols should use prefixed names such as mycompany.com/my-custom-protocol. Field can be enabled with ServiceAppProtocol feature gate.",
"targetPort": "Number or name of the port to access on the pods targeted by the service. Number must be in the range 1 to 65535. Name must be an IANA_SVC_NAME. If this is a string, it will be looked up as a named port in the target Pod's container ports. If this is not specified, the value of the 'port' field is used (an identity map). This field is ignored for services with clusterIP=None, and should be omitted or set equal to the 'port' field. More info: https://kubernetes.io/docs/concepts/services-networking/service/#defining-a-service", "port": "The port that will be exposed by this service.",
"nodePort": "The port on each node on which this service is exposed when type=NodePort or LoadBalancer. Usually assigned by the system. If specified, it will be allocated to the service if unused or else creation of the service will fail. Default is to auto-allocate a port if the ServiceType of this Service requires one. More info: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport", "targetPort": "Number or name of the port to access on the pods targeted by the service. Number must be in the range 1 to 65535. Name must be an IANA_SVC_NAME. If this is a string, it will be looked up as a named port in the target Pod's container ports. If this is not specified, the value of the 'port' field is used (an identity map). This field is ignored for services with clusterIP=None, and should be omitted or set equal to the 'port' field. More info: https://kubernetes.io/docs/concepts/services-networking/service/#defining-a-service",
"nodePort": "The port on each node on which this service is exposed when type=NodePort or LoadBalancer. Usually assigned by the system. If specified, it will be allocated to the service if unused or else creation of the service will fail. Default is to auto-allocate a port if the ServiceType of this Service requires one. More info: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport",
} }
func (ServicePort) SwaggerDoc() map[string]string { func (ServicePort) SwaggerDoc() map[string]string {

View File

@ -1096,6 +1096,11 @@ func (in *EndpointAddress) DeepCopy() *EndpointAddress {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *EndpointPort) DeepCopyInto(out *EndpointPort) { func (in *EndpointPort) DeepCopyInto(out *EndpointPort) {
*out = *in *out = *in
if in.AppProtocol != nil {
in, out := &in.AppProtocol, &out.AppProtocol
*out = new(string)
**out = **in
}
return return
} }
@ -1129,7 +1134,9 @@ func (in *EndpointSubset) DeepCopyInto(out *EndpointSubset) {
if in.Ports != nil { if in.Ports != nil {
in, out := &in.Ports, &out.Ports in, out := &in.Ports, &out.Ports
*out = make([]EndpointPort, len(*in)) *out = make([]EndpointPort, len(*in))
copy(*out, *in) for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
} }
return return
} }
@ -5122,6 +5129,11 @@ func (in *ServiceList) DeepCopyObject() runtime.Object {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ServicePort) DeepCopyInto(out *ServicePort) { func (in *ServicePort) DeepCopyInto(out *ServicePort) {
*out = *in *out = *in
if in.AppProtocol != nil {
in, out := &in.AppProtocol, &out.AppProtocol
*out = new(string)
**out = **in
}
out.TargetPort = in.TargetPort out.TargetPort = in.TargetPort
return return
} }
@ -5167,7 +5179,9 @@ func (in *ServiceSpec) DeepCopyInto(out *ServiceSpec) {
if in.Ports != nil { if in.Ports != nil {
in, out := &in.Ports, &out.Ports in, out := &in.Ports, &out.Ports
*out = make([]ServicePort, len(*in)) *out = make([]ServicePort, len(*in))
copy(*out, *in) for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
} }
if in.Selector != nil { if in.Selector != nil {
in, out := &in.Selector, &out.Selector in, out := &in.Selector, &out.Selector

View File

@ -107,8 +107,9 @@ message EndpointPort {
// This field follows standard Kubernetes label syntax. // This field follows standard Kubernetes label syntax.
// Un-prefixed names are reserved for IANA standard service names (as per // Un-prefixed names are reserved for IANA standard service names (as per
// RFC-6335 and http://www.iana.org/assignments/service-names). // RFC-6335 and http://www.iana.org/assignments/service-names).
// Non-standard protocols should use prefixed names. // Non-standard protocols should use prefixed names such as
// Default is empty string. // mycompany.com/my-custom-protocol.
// +optional
optional string appProtocol = 4; optional string appProtocol = 4;
} }

View File

@ -143,8 +143,9 @@ type EndpointPort struct {
// This field follows standard Kubernetes label syntax. // This field follows standard Kubernetes label syntax.
// Un-prefixed names are reserved for IANA standard service names (as per // Un-prefixed names are reserved for IANA standard service names (as per
// RFC-6335 and http://www.iana.org/assignments/service-names). // RFC-6335 and http://www.iana.org/assignments/service-names).
// Non-standard protocols should use prefixed names. // Non-standard protocols should use prefixed names such as
// Default is empty string. // mycompany.com/my-custom-protocol.
// +optional
AppProtocol *string `json:"appProtocol,omitempty" protobuf:"bytes,4,name=appProtocol"` AppProtocol *string `json:"appProtocol,omitempty" protobuf:"bytes,4,name=appProtocol"`
} }

View File

@ -54,7 +54,7 @@ var map_EndpointPort = map[string]string{
"name": "The name of this port. All ports in an EndpointSlice must have a unique name. If the EndpointSlice is dervied from a Kubernetes service, this corresponds to the Service.ports[].name. Name must either be an empty string or pass DNS_LABEL validation: * must be no more than 63 characters long. * must consist of lower case alphanumeric characters or '-'. * must start and end with an alphanumeric character. Default is empty string.", "name": "The name of this port. All ports in an EndpointSlice must have a unique name. If the EndpointSlice is dervied from a Kubernetes service, this corresponds to the Service.ports[].name. Name must either be an empty string or pass DNS_LABEL validation: * must be no more than 63 characters long. * must consist of lower case alphanumeric characters or '-'. * must start and end with an alphanumeric character. Default is empty string.",
"protocol": "The IP protocol for this port. Must be UDP, TCP, or SCTP. Default is TCP.", "protocol": "The IP protocol for this port. Must be UDP, TCP, or SCTP. Default is TCP.",
"port": "The port number of the endpoint. If this is not specified, ports are not restricted and must be interpreted in the context of the specific consumer.", "port": "The port number of the endpoint. If this is not specified, ports are not restricted and must be interpreted in the context of the specific consumer.",
"appProtocol": "The application protocol for this port. This field follows standard Kubernetes label syntax. Un-prefixed names are reserved for IANA standard service names (as per RFC-6335 and http://www.iana.org/assignments/service-names). Non-standard protocols should use prefixed names. Default is empty string.", "appProtocol": "The application protocol for this port. This field follows standard Kubernetes label syntax. Un-prefixed names are reserved for IANA standard service names (as per RFC-6335 and http://www.iana.org/assignments/service-names). Non-standard protocols should use prefixed names such as mycompany.com/my-custom-protocol.",
} }
func (EndpointPort) SwaggerDoc() map[string]string { func (EndpointPort) SwaggerDoc() map[string]string {

View File

@ -78,7 +78,8 @@
{ {
"name": "37", "name": "37",
"port": 1546792211, "port": 1546792211,
"protocol": "\u003eŽ燹憍峕?狱³-Ǐ忄*" "protocol": "\u003eŽ燹憍峕?狱³-Ǐ忄*",
"appProtocol": "38"
} }
] ]
} }

View File

@ -55,6 +55,7 @@ subsets:
resourceVersion: "35" resourceVersion: "35"
uid: Ă凗蓏Ŋ蛊ĉy uid: Ă凗蓏Ŋ蛊ĉy
ports: ports:
- name: "37" - appProtocol: "38"
name: "37"
port: 1546792211 port: 1546792211
protocol: '>Ž燹憍峕?狱³-Ǐ忄*' protocol: '>Ž燹憍峕?狱³-Ǐ忄*'

View File

@ -45,25 +45,26 @@
{ {
"name": "19", "name": "19",
"protocol": "@Hr鯹)晿", "protocol": "@Hr鯹)晿",
"appProtocol": "20",
"port": 202283346, "port": 202283346,
"targetPort": "20", "targetPort": "21",
"nodePort": -474380055 "nodePort": -474380055
} }
], ],
"selector": { "selector": {
"21": "22" "22": "23"
}, },
"clusterIP": "23", "clusterIP": "24",
"type": ".蘯6ċV夸", "type": ".蘯6ċV夸",
"externalIPs": [ "externalIPs": [
"24" "25"
], ],
"sessionAffinity": "ɑ", "sessionAffinity": "ɑ",
"loadBalancerIP": "25", "loadBalancerIP": "26",
"loadBalancerSourceRanges": [ "loadBalancerSourceRanges": [
"26" "27"
], ],
"externalName": "27", "externalName": "28",
"externalTrafficPolicy": "ʤ脽ěĂ凗蓏Ŋ蛊ĉy緅縕\u003eŽ", "externalTrafficPolicy": "ʤ脽ěĂ凗蓏Ŋ蛊ĉy緅縕\u003eŽ",
"healthCheckNodePort": -1095807277, "healthCheckNodePort": -1095807277,
"publishNotReadyAddresses": true, "publishNotReadyAddresses": true,
@ -74,15 +75,15 @@
}, },
"ipFamily": "³-Ǐ忄*齧獚敆ȎțêɘIJ斬", "ipFamily": "³-Ǐ忄*齧獚敆ȎțêɘIJ斬",
"topologyKeys": [ "topologyKeys": [
"28" "29"
] ]
}, },
"status": { "status": {
"loadBalancer": { "loadBalancer": {
"ingress": [ "ingress": [
{ {
"ip": "29", "ip": "30",
"hostname": "30" "hostname": "31"
} }
] ]
} }

View File

@ -30,34 +30,35 @@ metadata:
selfLink: "5" selfLink: "5"
uid: "7" uid: "7"
spec: spec:
clusterIP: "23" clusterIP: "24"
externalIPs: externalIPs:
- "24" - "25"
externalName: "27" externalName: "28"
externalTrafficPolicy: ʤ脽ěĂ凗蓏Ŋ蛊ĉy緅縕>Ž externalTrafficPolicy: ʤ脽ěĂ凗蓏Ŋ蛊ĉy緅縕>Ž
healthCheckNodePort: -1095807277 healthCheckNodePort: -1095807277
ipFamily: ³-Ǐ忄*齧獚敆ȎțêɘIJ斬 ipFamily: ³-Ǐ忄*齧獚敆ȎțêɘIJ斬
loadBalancerIP: "25" loadBalancerIP: "26"
loadBalancerSourceRanges: loadBalancerSourceRanges:
- "26" - "27"
ports: ports:
- name: "19" - appProtocol: "20"
name: "19"
nodePort: -474380055 nodePort: -474380055
port: 202283346 port: 202283346
protocol: '@Hr鯹)晿' protocol: '@Hr鯹)晿'
targetPort: "20" targetPort: "21"
publishNotReadyAddresses: true publishNotReadyAddresses: true
selector: selector:
"21": "22" "22": "23"
sessionAffinity: ɑ sessionAffinity: ɑ
sessionAffinityConfig: sessionAffinityConfig:
clientIP: clientIP:
timeoutSeconds: -1973740160 timeoutSeconds: -1973740160
topologyKeys: topologyKeys:
- "28" - "29"
type: .蘯6ċV夸 type: .蘯6ċV夸
status: status:
loadBalancer: loadBalancer:
ingress: ingress:
- hostname: "30" - hostname: "31"
ip: "29" ip: "30"