ingress: add alternate resource backend

Signed-off-by: Christopher M. Luciano <cmluciano@us.ibm.com>
This commit is contained in:
Christopher M. Luciano
2020-02-24 14:15:55 -05:00
parent 8508875e4d
commit 912f05bafb
23 changed files with 1014 additions and 365 deletions

View File

@@ -464,8 +464,16 @@ type HTTPIngressPath struct {
// IngressBackend describes all endpoints for a given service and port.
type IngressBackend struct {
// Specifies the name of the referenced service.
// +optional
ServiceName string
// Specifies the port of the referenced service.
// +optional
ServicePort intstr.IntOrString
// Resource is an ObjectRef to another Kubernetes resource in the namespace
// of the Ingress object. If resource is specified, serviceName and servicePort
// must not be specified.
// +optional
Resource *api.TypedLocalObjectReference
}

View File

@@ -255,6 +255,7 @@ func Convert_networking_Ingress_To_v1beta1_Ingress(in *networking.Ingress, out *
func autoConvert_v1beta1_IngressBackend_To_networking_IngressBackend(in *v1beta1.IngressBackend, out *networking.IngressBackend, s conversion.Scope) error {
out.ServiceName = in.ServiceName
out.ServicePort = in.ServicePort
out.Resource = (*core.TypedLocalObjectReference)(unsafe.Pointer(in.Resource))
return nil
}
@@ -266,6 +267,7 @@ func Convert_v1beta1_IngressBackend_To_networking_IngressBackend(in *v1beta1.Ing
func autoConvert_networking_IngressBackend_To_v1beta1_IngressBackend(in *networking.IngressBackend, out *v1beta1.IngressBackend, s conversion.Scope) error {
out.ServiceName = in.ServiceName
out.ServicePort = in.ServicePort
out.Resource = (*v1.TypedLocalObjectReference)(unsafe.Pointer(in.Resource))
return nil
}

View File

@@ -198,7 +198,8 @@ var ValidateIngressName = apimachineryvalidation.NameIsDNSSubdomain
// IngressValidationOptions cover beta to GA transitions for HTTP PathType
type IngressValidationOptions struct {
requireRegexPath bool
requireRegexPath bool
allowResourceBackend bool
}
// ValidateIngress validates Ingresses on create and update.
@@ -215,6 +216,8 @@ func ValidateIngressCreate(ingress *networking.Ingress, requestGV schema.GroupVe
opts = IngressValidationOptions{
// TODO(robscott): Remove regex validation for 1.19.
requireRegexPath: true,
// TODO(cmluciano): Allow resource backend for 1.19.
allowResourceBackend: false,
}
allErrs = append(allErrs, validateIngress(ingress, opts, requestGV)...)
annotationVal, annotationIsSet := ingress.Annotations[annotationIngressClass]
@@ -233,7 +236,8 @@ func ValidateIngressUpdate(ingress, oldIngress *networking.Ingress, requestGV sc
// TODO(robscott): Remove regex validation for 1.19.
// Only require regex path validation for this Ingress if the previous
// version of the Ingress also passed that validation.
requireRegexPath: allPathsPassRegexValidation(oldIngress),
requireRegexPath: allPathsPassRegexValidation(oldIngress),
allowResourceBackend: resourceBackendPresent(oldIngress),
}
allErrs = append(allErrs, validateIngress(ingress, opts, requestGV)...)
@@ -269,7 +273,7 @@ func ValidateIngressSpec(spec *networking.IngressSpec, fldPath *field.Path, opts
allErrs = append(allErrs, field.Invalid(fldPath, spec.Rules, errMsg))
}
if spec.Backend != nil {
allErrs = append(allErrs, validateIngressBackend(spec.Backend, fldPath.Child("backend"))...)
allErrs = append(allErrs, validateIngressBackend(spec.Backend, fldPath.Child("backend"), opts)...)
}
if len(spec.Rules) > 0 {
allErrs = append(allErrs, validateIngressRules(spec.Rules, fldPath.Child("rules"), opts)...)
@@ -381,22 +385,34 @@ func validateHTTPIngressPath(path *networking.HTTPIngressPath, fldPath *field.Pa
}
}
allErrs = append(allErrs, validateIngressBackend(&path.Backend, fldPath.Child("backend"))...)
allErrs = append(allErrs, validateIngressBackend(&path.Backend, fldPath.Child("backend"), opts)...)
return allErrs
}
// validateIngressBackend tests if a given backend is valid.
func validateIngressBackend(backend *networking.IngressBackend, fldPath *field.Path) field.ErrorList {
func validateIngressBackend(backend *networking.IngressBackend, fldPath *field.Path, opts IngressValidationOptions) field.ErrorList {
allErrs := field.ErrorList{}
// All backends must reference a single local service by name, and a single service port by name or number.
if len(backend.ServiceName) == 0 {
return append(allErrs, field.Required(fldPath.Child("serviceName"), ""))
hasResourceBackend := backend.Resource != nil
hasServiceBackend := len(backend.ServiceName) > 0 || backend.ServicePort.IntVal != 0 || len(backend.ServicePort.StrVal) > 0
switch {
case hasResourceBackend && hasServiceBackend:
return append(allErrs, field.Invalid(fldPath, "", "cannot set both resource and service backends"))
case hasResourceBackend && !opts.allowResourceBackend:
return append(allErrs, field.Forbidden(fldPath.Child("resource"), "not supported; only service backends are supported in this version"))
case hasResourceBackend:
allErrs = append(allErrs, validateIngressTypedLocalObjectReference(backend.Resource, fldPath.Child("resource"))...)
default:
if len(backend.ServiceName) == 0 {
return append(allErrs, field.Required(fldPath.Child("serviceName"), ""))
}
for _, msg := range apivalidation.ValidateServiceName(backend.ServiceName, false) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("serviceName"), backend.ServiceName, msg))
}
allErrs = append(allErrs, apivalidation.ValidatePortNumOrName(backend.ServicePort, fldPath.Child("servicePort"))...)
}
for _, msg := range apivalidation.ValidateServiceName(backend.ServiceName, false) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("serviceName"), backend.ServiceName, msg))
}
allErrs = append(allErrs, apivalidation.ValidatePortNumOrName(backend.ServicePort, fldPath.Child("servicePort"))...)
return allErrs
}
@@ -426,7 +442,7 @@ func validateIngressClassSpec(spec *networking.IngressClassSpec, fldPath *field.
allErrs = append(allErrs, field.TooLong(fldPath.Child("controller"), spec.Controller, maxLenIngressClassController))
}
allErrs = append(allErrs, validation.IsDomainPrefixedPath(fldPath.Child("controller"), spec.Controller)...)
allErrs = append(allErrs, validateIngressClassParameters(spec.Parameters, fldPath.Child("parameters"))...)
allErrs = append(allErrs, validateIngressTypedLocalObjectReference(spec.Parameters, fldPath.Child("parameters"))...)
return allErrs
}
@@ -436,8 +452,8 @@ func validateIngressClassSpecUpdate(newSpec, oldSpec *networking.IngressClassSpe
return apivalidation.ValidateImmutableField(newSpec.Controller, oldSpec.Controller, fldPath.Child("controller"))
}
// validateIngressClassParameters ensures that Parameters fields are valid.
func validateIngressClassParameters(params *api.TypedLocalObjectReference, fldPath *field.Path) field.ErrorList {
// validateIngressTypedLocalObjectReference ensures that Parameters fields are valid.
func validateIngressTypedLocalObjectReference(params *api.TypedLocalObjectReference, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if params == nil {
@@ -487,3 +503,20 @@ func allPathsPassRegexValidation(ingress *networking.Ingress) bool {
}
return true
}
func resourceBackendPresent(ingress *networking.Ingress) bool {
if ingress.Spec.Backend != nil && ingress.Spec.Backend.Resource != nil {
return true
}
for _, rule := range ingress.Spec.Rules {
if rule.HTTP == nil {
continue
}
for _, path := range rule.HTTP.Paths {
if path.Backend.Resource != nil {
return true
}
}
}
return false
}

View File

@@ -1030,6 +1030,88 @@ func TestValidateIngress(t *testing.T) {
"spec.rules[0].host",
},
},
"path resource backend and service name are not allowed together": {
tweakIngress: func(ing *networking.Ingress) {
ing.Spec.Rules[0].IngressRuleValue = networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{
{
Path: "/foo",
PathType: &pathTypeImplementationSpecific,
Backend: networking.IngressBackend{
ServiceName: "default-backend",
Resource: &api.TypedLocalObjectReference{
APIGroup: utilpointer.StringPtr("example.com"),
Kind: "foo",
Name: "bar",
},
},
},
},
},
}
},
expectErrsOnFields: []string{
"spec.rules[0].http.paths[0].backend",
},
},
"path resource backend and service port are not allowed together": {
tweakIngress: func(ing *networking.Ingress) {
ing.Spec.Rules[0].IngressRuleValue = networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{
{
Path: "/foo",
PathType: &pathTypeImplementationSpecific,
Backend: networking.IngressBackend{
ServicePort: intstr.FromInt(80),
Resource: &api.TypedLocalObjectReference{
APIGroup: utilpointer.StringPtr("example.com"),
Kind: "foo",
Name: "bar",
},
},
},
},
},
}
},
expectErrsOnFields: []string{
"spec.rules[0].http.paths[0].backend",
},
},
"spec.backend resource and service name are not allowed together": {
groupVersion: &networkingv1beta1.SchemeGroupVersion,
tweakIngress: func(ing *networking.Ingress) {
ing.Spec.Backend = &networking.IngressBackend{
ServiceName: "default-backend",
Resource: &api.TypedLocalObjectReference{
APIGroup: utilpointer.StringPtr("example.com"),
Kind: "foo",
Name: "bar",
},
}
},
expectErrsOnFields: []string{
"spec.backend",
},
},
"spec.backend resource and service port are not allowed together": {
groupVersion: &networkingv1beta1.SchemeGroupVersion,
tweakIngress: func(ing *networking.Ingress) {
ing.Spec.Backend = &networking.IngressBackend{
ServicePort: intstr.FromInt(80),
Resource: &api.TypedLocalObjectReference{
APIGroup: utilpointer.StringPtr("example.com"),
Kind: "foo",
Name: "bar",
},
}
},
expectErrsOnFields: []string{
"spec.backend",
},
},
}
for name, testCase := range testCases {
@@ -1181,6 +1263,11 @@ func TestValidateIngressCreate(t *testing.T) {
ServiceName: "default-backend",
ServicePort: intstr.FromInt(80),
}
resourceBackend := &api.TypedLocalObjectReference{
APIGroup: utilpointer.StringPtr("example.com"),
Kind: "foo",
Name: "bar",
}
baseIngress := networking.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "test123",
@@ -1250,6 +1337,31 @@ func TestValidateIngressCreate(t *testing.T) {
},
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec.rules[0].http.paths[0].path"), "/([a-z0-9]*)[", "must be a valid regex")},
},
"Spec.Backend.Resource field not allowed on create": {
tweakIngress: func(ingress *networking.Ingress) {
ingress.Spec.Backend = &networking.IngressBackend{
Resource: resourceBackend}
},
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec.backend.resource"), "not supported; only service backends are supported in this version")},
},
"Paths.Backend.Resource field not allowed on create": {
tweakIngress: func(ingress *networking.Ingress) {
ingress.Spec.Rules = []networking.IngressRule{{
Host: "foo.bar.com",
IngressRuleValue: networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{{
Path: "/([a-z0-9]*)",
PathType: &implementationPathType,
Backend: networking.IngressBackend{
Resource: resourceBackend},
}},
},
},
}}
},
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec.rules[0].http.paths[0].backend.resource"), "not supported; only service backends are supported in this version")},
},
}
for name, testCase := range testCases {
@@ -1277,6 +1389,11 @@ func TestValidateIngressUpdate(t *testing.T) {
ServiceName: "default-backend",
ServicePort: intstr.FromInt(80),
}
resourceBackend := &api.TypedLocalObjectReference{
APIGroup: utilpointer.StringPtr("example.com"),
Kind: "foo",
Name: "bar",
}
baseIngress := networking.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "test123",
@@ -1427,6 +1544,151 @@ func TestValidateIngressUpdate(t *testing.T) {
},
expectedErrs: field.ErrorList{},
},
"new Backend.Resource not allowed on update": {
tweakIngresses: func(newIngress, oldIngress *networking.Ingress) {
oldIngress.Spec.Backend = &defaultBackend
newIngress.Spec.Backend = &networking.IngressBackend{
Resource: resourceBackend}
},
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec.backend.resource"), "not supported; only service backends are supported in this version")},
},
"old Backend.Resource allowed on update": {
tweakIngresses: func(newIngress, oldIngress *networking.Ingress) {
oldIngress.Spec.Backend = &networking.IngressBackend{
Resource: resourceBackend}
newIngress.Spec.Backend = &networking.IngressBackend{
Resource: resourceBackend}
},
expectedErrs: field.ErrorList{},
},
"changing spec.backend from resource -> no resource": {
tweakIngresses: func(newIngress, oldIngress *networking.Ingress) {
oldIngress.Spec.Backend = &networking.IngressBackend{
Resource: resourceBackend}
newIngress.Spec.Backend = &defaultBackend
},
expectedErrs: field.ErrorList{},
},
"changing path backend from resource -> no resource": {
tweakIngresses: func(newIngress, oldIngress *networking.Ingress) {
oldIngress.Spec.Rules = []networking.IngressRule{{
Host: "foo.bar.com",
IngressRuleValue: networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{{
Path: "/foo[",
PathType: &implementationPathType,
Backend: networking.IngressBackend{
Resource: resourceBackend},
}},
},
},
}}
newIngress.Spec.Rules = []networking.IngressRule{{
Host: "foo.bar.com",
IngressRuleValue: networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{{
Path: "/bar[",
PathType: &implementationPathType,
Backend: defaultBackend,
}},
},
},
}}
},
expectedErrs: field.ErrorList{},
},
"changing path backend from resource -> resource": {
tweakIngresses: func(newIngress, oldIngress *networking.Ingress) {
oldIngress.Spec.Rules = []networking.IngressRule{{
Host: "foo.bar.com",
IngressRuleValue: networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{{
Path: "/foo[",
PathType: &implementationPathType,
Backend: networking.IngressBackend{
Resource: resourceBackend},
}},
},
},
}}
newIngress.Spec.Rules = []networking.IngressRule{{
Host: "foo.bar.com",
IngressRuleValue: networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{{
Path: "/bar[",
PathType: &implementationPathType,
Backend: networking.IngressBackend{
Resource: resourceBackend},
}},
},
},
}}
},
expectedErrs: field.ErrorList{},
},
"changing path backend from non-resource -> non-resource": {
tweakIngresses: func(newIngress, oldIngress *networking.Ingress) {
oldIngress.Spec.Rules = []networking.IngressRule{{
Host: "foo.bar.com",
IngressRuleValue: networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{{
Path: "/foo[",
PathType: &implementationPathType,
Backend: defaultBackend,
}},
},
},
}}
newIngress.Spec.Rules = []networking.IngressRule{{
Host: "foo.bar.com",
IngressRuleValue: networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{{
Path: "/bar[",
PathType: &implementationPathType,
Backend: defaultBackend,
}},
},
},
}}
},
expectedErrs: field.ErrorList{},
},
"changing path backend from non-resource -> resource": {
tweakIngresses: func(newIngress, oldIngress *networking.Ingress) {
oldIngress.Spec.Rules = []networking.IngressRule{{
Host: "foo.bar.com",
IngressRuleValue: networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{{
Path: "/foo[",
PathType: &implementationPathType,
Backend: defaultBackend,
}},
},
},
}}
newIngress.Spec.Rules = []networking.IngressRule{{
Host: "foo.bar.com",
IngressRuleValue: networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{{
Path: "/bar[",
PathType: &implementationPathType,
Backend: networking.IngressBackend{
Resource: resourceBackend},
}},
},
},
}}
},
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec.rules[0].http.paths[0].backend.resource"), "not supported; only service backends are supported in this version")},
},
}
for name, testCase := range testCases {
@@ -1816,3 +2078,151 @@ func TestValidateIngressStatusUpdate(t *testing.T) {
}
}
}
func TestValidateResourceBackendPresent(t *testing.T) {
implementationPathType := networking.PathTypeImplementationSpecific
defaultBackend := networking.IngressBackend{
ServiceName: "default-backend",
ServicePort: intstr.FromInt(80),
}
resourceBackend := &api.TypedLocalObjectReference{
APIGroup: utilpointer.StringPtr("example.com"),
Kind: "foo",
Name: "bar",
}
baseIngress := networking.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: metav1.NamespaceDefault,
},
Spec: networking.IngressSpec{
Backend: &networking.IngressBackend{
ServiceName: "default-backend",
ServicePort: intstr.FromInt(80),
},
Rules: []networking.IngressRule{
{
Host: "foo.bar.com",
IngressRuleValue: networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{
{
Path: "/foo",
PathType: &implementationPathType,
Backend: defaultBackend,
},
},
},
},
},
},
},
Status: networking.IngressStatus{
LoadBalancer: api.LoadBalancerStatus{
Ingress: []api.LoadBalancerIngress{
{IP: "127.0.0.1"},
},
},
},
}
testCases := map[string]struct {
groupVersion *schema.GroupVersion
tweakIngress func(ing *networking.Ingress)
expectResourceBackend bool
}{
"nil spec.Backend and no paths": {
tweakIngress: func(ing *networking.Ingress) {
ing.Spec.Backend = nil
ing.Spec.Rules[0].IngressRuleValue.HTTP.Paths[0].Path = ""
},
expectResourceBackend: false,
},
"nil spec.Backend.Resource and no paths": {
tweakIngress: func(ing *networking.Ingress) {
ing.Spec.Backend = nil
ing.Spec.Rules[0].IngressRuleValue.HTTP.Paths = []networking.HTTPIngressPath{}
},
expectResourceBackend: false,
},
"non-nil spec.Backend.Resource and no paths": {
tweakIngress: func(ing *networking.Ingress) {
ing.Spec.Backend = &networking.IngressBackend{
Resource: resourceBackend,
}
ing.Spec.Rules[0].IngressRuleValue.HTTP.Paths[0].Path = ""
},
expectResourceBackend: true,
},
"nil spec.Backend, one rule with nil HTTP ": {
tweakIngress: func(ing *networking.Ingress) {
ing.Spec.Backend = nil
ing.Spec.Rules[0].IngressRuleValue.HTTP = nil
},
expectResourceBackend: false,
},
"nil spec.Backend, one rule with non-nil HTTP, no paths": {
tweakIngress: func(ing *networking.Ingress) {
ing.Spec.Backend = nil
ing.Spec.Rules[0].IngressRuleValue = networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{},
},
}
},
expectResourceBackend: false,
},
"nil spec.Backend, one rule with non-nil HTTP, one path with nil Backend.Resource": {
tweakIngress: func(ing *networking.Ingress) {
ing.Spec.Backend = nil
ing.Spec.Rules[0].IngressRuleValue = networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{
{
Path: "/foo",
PathType: &implementationPathType,
Backend: networking.IngressBackend{
Resource: nil,
},
},
},
},
}
},
expectResourceBackend: false,
},
"nil spec.Backend, one rule with non-nil HTTP, one path with non-nil Backend.Resource": {
tweakIngress: func(ing *networking.Ingress) {
ing.Spec.Backend = nil
ing.Spec.Rules[0].IngressRuleValue = networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{
{
Path: "/foo",
PathType: &implementationPathType,
Backend: networking.IngressBackend{
Resource: resourceBackend,
},
},
},
},
}
},
expectResourceBackend: true,
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
ingress := baseIngress.DeepCopy()
testCase.tweakIngress(ingress)
gv := testCase.groupVersion
if gv == nil {
gv = &networkingv1.SchemeGroupVersion
}
isBackendAllowed := resourceBackendPresent(ingress)
if isBackendAllowed != testCase.expectResourceBackend {
t.Errorf("Expected resourceBackendPresent to return: %v, got: %v", testCase.expectResourceBackend, isBackendAllowed)
}
})
}
}

View File

@@ -35,7 +35,7 @@ func (in *HTTPIngressPath) DeepCopyInto(out *HTTPIngressPath) {
*out = new(PathType)
**out = **in
}
out.Backend = in.Backend
in.Backend.DeepCopyInto(&out.Backend)
return
}
@@ -125,6 +125,11 @@ func (in *Ingress) DeepCopyObject() runtime.Object {
func (in *IngressBackend) DeepCopyInto(out *IngressBackend) {
*out = *in
out.ServicePort = in.ServicePort
if in.Resource != nil {
in, out := &in.Resource, &out.Resource
*out = new(core.TypedLocalObjectReference)
(*in).DeepCopyInto(*out)
}
return
}
@@ -301,7 +306,7 @@ func (in *IngressSpec) DeepCopyInto(out *IngressSpec) {
if in.Backend != nil {
in, out := &in.Backend, &out.Backend
*out = new(IngressBackend)
**out = **in
(*in).DeepCopyInto(*out)
}
if in.TLS != nil {
in, out := &in.TLS, &out.TLS