Merge pull request #1244 from aledbf/add-custom-backend-annotation

Add custom default backend annotation
This commit is contained in:
Manuel Alejandro de Brito Fontes 2017-08-25 15:15:17 -04:00 committed by GitHub
commit b791460206
10 changed files with 270 additions and 17 deletions

View file

@ -50,6 +50,7 @@ The following annotations are supported:
|[ingress.kubernetes.io/base-url-scheme](#rewrite)|string| |[ingress.kubernetes.io/base-url-scheme](#rewrite)|string|
|[ingress.kubernetes.io/client-body-buffer-size](#client-body-buffer-size)|string| |[ingress.kubernetes.io/client-body-buffer-size](#client-body-buffer-size)|string|
|[ingress.kubernetes.io/configuration-snippet](#configuration-snippet)|string| |[ingress.kubernetes.io/configuration-snippet](#configuration-snippet)|string|
|[ingress.kubernetes.io/default-backend](#default-backend)|string|
|[ingress.kubernetes.io/enable-cors](#enable-cors)|true or false| |[ingress.kubernetes.io/enable-cors](#enable-cors)|true or false|
|[ingress.kubernetes.io/force-ssl-redirect](#server-side-https-enforcement-through-redirect)|true or false| |[ingress.kubernetes.io/force-ssl-redirect](#server-side-https-enforcement-through-redirect)|true or false|
|[ingress.kubernetes.io/from-to-www-redirect](#redirect-from-to-www)|true or false| |[ingress.kubernetes.io/from-to-www-redirect](#redirect-from-to-www)|true or false|
@ -158,6 +159,10 @@ Using this annotation you can add additional configuration to the NGINX location
ingress.kubernetes.io/configuration-snippet: | ingress.kubernetes.io/configuration-snippet: |
more_set_headers "Request-Id: $request_id"; more_set_headers "Request-Id: $request_id";
``` ```
### Default Backend
The ingress controller requires a default backend. This service is handle the response when the service in the Ingress rule does not have endpoints.
This is a global configuration for the ingress controller. In some cases could be required to return a custom content or format. In this scenario we can use the annotation `ingress.kubernetes.io/default-backend: <svc name>` to specify a custom default backend.
### Enable CORS ### Enable CORS

View file

@ -415,7 +415,8 @@ http {
fastcgi_param HTTP_X_Endpoints {{ .DefaultBackendEndpoints }}; fastcgi_param HTTP_X_Endpoints {{ .DefaultBackendEndpoints }};
fastcgi_pass unix:/var/run/go-fastcgi.sock; fastcgi_pass unix:/var/run/go-fastcgi.sock;
{{ else }} {{ else }}
return 404; set $proxy_upstream_name "upstream-default-backend";
proxy_pass http://upstream-default-backend;
{{ end }} {{ end }}
} }
} }

View file

@ -0,0 +1,57 @@
/*
Copyright 2015 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package defaultbackend
import (
"fmt"
"github.com/pkg/errors"
extensions "k8s.io/api/extensions/v1beta1"
"k8s.io/ingress/core/pkg/ingress/annotations/parser"
"k8s.io/ingress/core/pkg/ingress/resolver"
)
const (
defaultBackend = "ingress.kubernetes.io/default-backend"
)
type backend struct {
serviceResolver resolver.Service
}
// NewParser creates a new default backend annotation parser
func NewParser(sr resolver.Service) parser.IngressAnnotation {
return backend{sr}
}
// Parse parses the annotations contained in the ingress to use
// a custom default backend
func (db backend) Parse(ing *extensions.Ingress) (interface{}, error) {
s, err := parser.GetStringAnnotation(defaultBackend, ing)
if err != nil {
return nil, err
}
name := fmt.Sprintf("%v/%v", ing.Namespace, s)
svc, err := db.serviceResolver.GetService(s)
if err != nil {
return nil, errors.Wrapf(err, "unexpected error reading service %v", name)
}
return svc, nil
}

View file

@ -24,6 +24,7 @@ import (
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"fmt" "fmt"
"k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/ingress/core/pkg/ingress/resolver" "k8s.io/ingress/core/pkg/ingress/resolver"
) )

View file

@ -25,6 +25,7 @@ import (
"k8s.io/ingress/core/pkg/ingress/annotations/authtls" "k8s.io/ingress/core/pkg/ingress/annotations/authtls"
"k8s.io/ingress/core/pkg/ingress/annotations/clientbodybuffersize" "k8s.io/ingress/core/pkg/ingress/annotations/clientbodybuffersize"
"k8s.io/ingress/core/pkg/ingress/annotations/cors" "k8s.io/ingress/core/pkg/ingress/annotations/cors"
"k8s.io/ingress/core/pkg/ingress/annotations/defaultbackend"
"k8s.io/ingress/core/pkg/ingress/annotations/healthcheck" "k8s.io/ingress/core/pkg/ingress/annotations/healthcheck"
"k8s.io/ingress/core/pkg/ingress/annotations/ipwhitelist" "k8s.io/ingress/core/pkg/ingress/annotations/ipwhitelist"
"k8s.io/ingress/core/pkg/ingress/annotations/parser" "k8s.io/ingress/core/pkg/ingress/annotations/parser"
@ -46,6 +47,7 @@ type extractorConfig interface {
resolver.AuthCertificate resolver.AuthCertificate
resolver.DefaultBackend resolver.DefaultBackend
resolver.Secret resolver.Secret
resolver.Service
} }
type annotationExtractor struct { type annotationExtractor struct {
@ -75,6 +77,7 @@ func newAnnotationExtractor(cfg extractorConfig) annotationExtractor {
"ConfigurationSnippet": snippet.NewParser(), "ConfigurationSnippet": snippet.NewParser(),
"Alias": alias.NewParser(), "Alias": alias.NewParser(),
"ClientBodyBufferSize": clientbodybuffersize.NewParser(), "ClientBodyBufferSize": clientbodybuffersize.NewParser(),
"DefaultBackend": defaultbackend.NewParser(cfg),
}, },
} }
} }
@ -89,6 +92,10 @@ func (e *annotationExtractor) Extract(ing *extensions.Ingress) map[string]interf
continue continue
} }
if !errors.IsLocationDenied(err) {
continue
}
_, alreadyDenied := anns[DeniedKeyName] _, alreadyDenied := anns[DeniedKeyName]
if !alreadyDenied { if !alreadyDenied {
anns[DeniedKeyName] = err anns[DeniedKeyName] = err

View file

@ -41,6 +41,7 @@ const (
type mockCfg struct { type mockCfg struct {
MockSecrets map[string]*api.Secret MockSecrets map[string]*api.Secret
MockServices map[string]*api.Service
} }
func (m mockCfg) GetDefaultBackend() defaults.Backend { func (m mockCfg) GetDefaultBackend() defaults.Backend {
@ -51,6 +52,10 @@ func (m mockCfg) GetSecret(name string) (*api.Secret, error) {
return m.MockSecrets[name], nil return m.MockSecrets[name], nil
} }
func (m mockCfg) GetService(name string) (*api.Service, error) {
return m.MockServices[name], nil
}
func (m mockCfg) GetAuthCertificate(name string) (*resolver.AuthSSLCert, error) { func (m mockCfg) GetAuthCertificate(name string) (*resolver.AuthSSLCert, error) {
if secret, _ := m.GetSecret(name); secret != nil { if secret, _ := m.GetSecret(name); secret != nil {
return &resolver.AuthSSLCert{ return &resolver.AuthSSLCert{

View file

@ -31,6 +31,7 @@ import (
api "k8s.io/api/core/v1" api "k8s.io/api/core/v1"
extensions "k8s.io/api/extensions/v1beta1" extensions "k8s.io/api/extensions/v1beta1"
"k8s.io/apimachinery/pkg/conversion"
"k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/runtime"
@ -66,6 +67,8 @@ const (
var ( var (
// list of ports that cannot be used by TCP or UDP services // list of ports that cannot be used by TCP or UDP services
reservedPorts = []string{"80", "443", "8181", "18080"} reservedPorts = []string{"80", "443", "8181", "18080"}
cloner = conversion.NewCloner()
) )
// GenericController holds the boilerplate code required to build an Ingress controlller. // GenericController holds the boilerplate code required to build an Ingress controlller.
@ -321,6 +324,8 @@ func newIngressController(config *Configuration) *GenericController {
ConfigMap: ic.mapLister, ConfigMap: ic.mapLister,
}) })
cloner.RegisterDeepCopyFunc(ingress.GetGeneratedDeepCopyFuncs)
return &ic return &ic
} }
@ -340,7 +345,7 @@ func (ic GenericController) GetDefaultBackend() defaults.Backend {
} }
// GetRecorder returns the event recorder // GetRecorder returns the event recorder
func (ic GenericController) GetRecoder() record.EventRecorder { func (ic GenericController) GetRecorder() record.EventRecorder {
return ic.recorder return ic.recorder
} }
@ -356,6 +361,18 @@ func (ic GenericController) GetSecret(name string) (*api.Secret, error) {
return s.(*api.Secret), nil return s.(*api.Secret), nil
} }
// GetService searches for a service in the local secrets Store
func (ic GenericController) GetService(name string) (*api.Service, error) {
s, exists, err := ic.svcLister.Store.GetByKey(name)
if err != nil {
return nil, err
}
if !exists {
return nil, fmt.Errorf("service %v was not found", name)
}
return s.(*api.Service), nil
}
func (ic *GenericController) getConfigMap(ns, name string) (*api.ConfigMap, error) { func (ic *GenericController) getConfigMap(ns, name string) (*api.ConfigMap, error) {
s, exists, err := ic.mapLister.Store.GetByKey(fmt.Sprintf("%v/%v", ns, name)) s, exists, err := ic.mapLister.Store.GetByKey(fmt.Sprintf("%v/%v", ns, name))
if err != nil { if err != nil {
@ -688,6 +705,7 @@ func (ic *GenericController) getBackendServers() ([]*ingress.Backend, []*ingress
loc.Backend = ups.Name loc.Backend = ups.Name
loc.Port = ups.Port loc.Port = ups.Port
loc.Service = ups.Service loc.Service = ups.Service
loc.Ingress = ing
mergeLocationAnnotations(loc, anns) mergeLocationAnnotations(loc, anns)
if loc.Redirect.FromToWWW { if loc.Redirect.FromToWWW {
server.RedirectFromToWWW = true server.RedirectFromToWWW = true
@ -704,6 +722,7 @@ func (ic *GenericController) getBackendServers() ([]*ingress.Backend, []*ingress
IsDefBackend: false, IsDefBackend: false,
Service: ups.Service, Service: ups.Service,
Port: ups.Port, Port: ups.Port,
Ingress: ing,
} }
mergeLocationAnnotations(loc, anns) mergeLocationAnnotations(loc, anns)
if loc.Redirect.FromToWWW { if loc.Redirect.FromToWWW {
@ -731,12 +750,38 @@ func (ic *GenericController) getBackendServers() ([]*ingress.Backend, []*ingress
} }
} }
// Configure Backends[].SSLPassthrough aUpstreams := make([]*ingress.Backend, 0, len(upstreams))
for _, upstream := range upstreams { for _, upstream := range upstreams {
isHTTPSfrom := []*ingress.Server{} isHTTPSfrom := []*ingress.Server{}
for _, server := range servers { for _, server := range servers {
for _, location := range server.Locations { for _, location := range server.Locations {
if upstream.Name == location.Backend { if upstream.Name == location.Backend {
if len(upstream.Endpoints) == 0 {
glog.V(3).Infof("upstream %v does not have any active endpoints. Using default backend", upstream.Name)
location.Backend = "upstream-default-backend"
// check if the location contains endpoints and a custom default backend
if location.DefaultBackend != nil {
sp := location.DefaultBackend.Spec.Ports[0]
endps := ic.getEndpoints(location.DefaultBackend, &sp, api.ProtocolTCP, &healthcheck.Upstream{})
if len(endps) > 0 {
glog.V(3).Infof("using custom default backend in server %v location %v (service %v/%v)",
server.Hostname, location.Path, location.DefaultBackend.Namespace, location.DefaultBackend.Name)
b, err := cloner.DeepCopy(upstream)
if err == nil {
name := fmt.Sprintf("custom-default-backend-%v", upstream.Name)
nb := b.(*ingress.Backend)
nb.Name = name
nb.Endpoints = endps
aUpstreams = append(aUpstreams, nb)
location.Backend = name
}
}
}
}
// Configure Backends[].SSLPassthrough
if server.SSLPassthrough { if server.SSLPassthrough {
if location.Path == rootLocation { if location.Path == rootLocation {
if location.Backend == defUpstreamName { if location.Backend == defUpstreamName {
@ -746,24 +791,24 @@ func (ic *GenericController) getBackendServers() ([]*ingress.Backend, []*ingress
isHTTPSfrom = append(isHTTPSfrom, server) isHTTPSfrom = append(isHTTPSfrom, server)
} }
continue
} }
} }
} }
} }
if len(isHTTPSfrom) > 0 { if len(isHTTPSfrom) > 0 {
upstream.SSLPassthrough = true upstream.SSLPassthrough = true
} }
} }
aUpstreams := make([]*ingress.Backend, 0, len(upstreams)) // create the list of upstreams and skip those without endpoints
for _, value := range upstreams { for _, upstream := range upstreams {
if len(value.Endpoints) == 0 { if len(upstream.Endpoints) == 0 {
glog.V(3).Infof("upstream %v does not have any active endpoints. Using default backend", value.Name) continue
value.Endpoints = append(value.Endpoints, ic.cfg.Backend.DefaultEndpoint())
} }
aUpstreams = append(aUpstreams, value) aUpstreams = append(aUpstreams, upstream)
} }
if ic.cfg.SortBackends { if ic.cfg.SortBackends {
sort.Sort(ingress.BackendByNameServers(aUpstreams)) sort.Sort(ingress.BackendByNameServers(aUpstreams))
} }

View file

@ -41,6 +41,12 @@ type AuthCertificate interface {
GetAuthCertificate(string) (*AuthSSLCert, error) GetAuthCertificate(string) (*AuthSSLCert, error)
} }
// Service has a method that searches for services contenating
// the namespace and name using a the character /
type Service interface {
GetService(string) (*api.Service, error)
}
// AuthSSLCert contains the necessary information to do certificate based // AuthSSLCert contains the necessary information to do certificate based
// authentication of an ingress location // authentication of an ingress location
type AuthSSLCert struct { type AuthSSLCert struct {

View file

@ -150,6 +150,7 @@ type Configuration struct {
} }
// Backend describes one or more remote server/s (endpoints) associated with a service // Backend describes one or more remote server/s (endpoints) associated with a service
// +k8s:deepcopy-gen=true
type Backend struct { type Backend struct {
// Name represents an unique api.Service name formatted as <namespace>-<name>-<port> // Name represents an unique api.Service name formatted as <namespace>-<name>-<port>
Name string `json:"name"` Name string `json:"name"`
@ -177,12 +178,14 @@ type Backend struct {
// restarts. Exactly one of these values will be set on the upstream, since multiple // restarts. Exactly one of these values will be set on the upstream, since multiple
// affinity values are incompatible. Once set, the backend makes no guarantees // affinity values are incompatible. Once set, the backend makes no guarantees
// about honoring updates. // about honoring updates.
// +k8s:deepcopy-gen=true
type SessionAffinityConfig struct { type SessionAffinityConfig struct {
AffinityType string `json:"name"` AffinityType string `json:"name"`
CookieSessionAffinity CookieSessionAffinity `json:"cookieSessionAffinity"` CookieSessionAffinity CookieSessionAffinity `json:"cookieSessionAffinity"`
} }
// CookieSessionAffinity defines the structure used in Affinity configured by Cookies. // CookieSessionAffinity defines the structure used in Affinity configured by Cookies.
// +k8s:deepcopy-gen=true
type CookieSessionAffinity struct { type CookieSessionAffinity struct {
Name string `json:"name"` Name string `json:"name"`
Hash string `json:"hash"` Hash string `json:"hash"`
@ -190,6 +193,7 @@ type CookieSessionAffinity struct {
} }
// Endpoint describes a kubernetes endpoint in a backend // Endpoint describes a kubernetes endpoint in a backend
// +k8s:deepcopy-gen=true
type Endpoint struct { type Endpoint struct {
// Address IP address of the endpoint // Address IP address of the endpoint
Address string `json:"address"` Address string `json:"address"`
@ -261,10 +265,13 @@ type Location struct {
// contains active endpoints or not. Returning true means the location // contains active endpoints or not. Returning true means the location
// uses the default backend. // uses the default backend.
IsDefBackend bool `json:"isDefBackend"` IsDefBackend bool `json:"isDefBackend"`
// Ingress returns the ingress from which this location was generated
Ingress *extensions.Ingress `json:"ingress"`
// Backend describes the name of the backend to use. // Backend describes the name of the backend to use.
Backend string `json:"backend"` Backend string `json:"backend"`
// Service describes the referenced services from the ingress
Service *api.Service `json:"service,omitempty"` Service *api.Service `json:"service,omitempty"`
// Port describes to which port from the service
Port intstr.IntOrString `json:"port"` Port intstr.IntOrString `json:"port"`
// BasicDigestAuth returns authentication configuration for // BasicDigestAuth returns authentication configuration for
// an Ingress rule. // an Ingress rule.
@ -301,14 +308,17 @@ type Location struct {
Proxy proxy.Configuration `json:"proxy,omitempty"` Proxy proxy.Configuration `json:"proxy,omitempty"`
// UsePortInRedirects indicates if redirects must specify the port // UsePortInRedirects indicates if redirects must specify the port
// +optional // +optional
UsePortInRedirects bool `json:"use-port-in-redirects"` UsePortInRedirects bool `json:"usePortInRedirects"`
// ConfigurationSnippet contains additional configuration for the backend // ConfigurationSnippet contains additional configuration for the backend
// to be considered in the configuration of the location // to be considered in the configuration of the location
ConfigurationSnippet string `json:"configuration-snippet"` ConfigurationSnippet string `json:"configurationSnippet"`
// ClientBodyBufferSize allows for the configuration of the client body // ClientBodyBufferSize allows for the configuration of the client body
// buffer size for a specific location. // buffer size for a specific location.
// +optional // +optional
ClientBodyBufferSize string `json:"client-body-buffer-size,omitempty"` ClientBodyBufferSize string `json:"clientBodyBufferSize,omitempty"`
// DefaultBackend allows the use of a custom default backend for this location.
// +optional
DefaultBackend *api.Service `json:"defaultBackend,omitempty"`
} }
// SSLPassthroughBackend describes a SSL upstream server configured // SSLPassthroughBackend describes a SSL upstream server configured

View file

@ -0,0 +1,116 @@
// +build !ignore_autogenerated
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// This file was autogenerated by deepcopy-gen. Do not edit it manually!
package ingress
import (
v1 "k8s.io/api/core/v1"
conversion "k8s.io/apimachinery/pkg/conversion"
reflect "reflect"
)
// GetGeneratedDeepCopyFuncs returns the generated funcs, since we aren't registering them.
func GetGeneratedDeepCopyFuncs() []conversion.GeneratedDeepCopyFunc {
return []conversion.GeneratedDeepCopyFunc{
{Fn: DeepCopy__Backend, InType: reflect.TypeOf(&Backend{})},
{Fn: DeepCopy__CookieSessionAffinity, InType: reflect.TypeOf(&CookieSessionAffinity{})},
{Fn: DeepCopy__Endpoint, InType: reflect.TypeOf(&Endpoint{})},
{Fn: DeepCopy__SessionAffinityConfig, InType: reflect.TypeOf(&SessionAffinityConfig{})},
}
}
// DeepCopy__Backend is an autogenerated deepcopy function.
func DeepCopy__Backend(in interface{}, out interface{}, c *conversion.Cloner) error {
{
in := in.(*Backend)
out := out.(*Backend)
*out = *in
if in.Service != nil {
in, out := &in.Service, &out.Service
if newVal, err := c.DeepCopy(*in); err != nil {
return err
} else {
*out = newVal.(*v1.Service)
}
}
if in.Endpoints != nil {
in, out := &in.Endpoints, &out.Endpoints
*out = make([]Endpoint, len(*in))
for i := range *in {
if err := DeepCopy__Endpoint(&(*in)[i], &(*out)[i], c); err != nil {
return err
}
}
}
if err := DeepCopy__SessionAffinityConfig(&in.SessionAffinity, &out.SessionAffinity, c); err != nil {
return err
}
return nil
}
}
// DeepCopy__CookieSessionAffinity is an autogenerated deepcopy function.
func DeepCopy__CookieSessionAffinity(in interface{}, out interface{}, c *conversion.Cloner) error {
{
in := in.(*CookieSessionAffinity)
out := out.(*CookieSessionAffinity)
*out = *in
if in.Locations != nil {
in, out := &in.Locations, &out.Locations
*out = make(map[string][]string)
for key, val := range *in {
if newVal, err := c.DeepCopy(&val); err != nil {
return err
} else {
(*out)[key] = *newVal.(*[]string)
}
}
}
return nil
}
}
// DeepCopy__Endpoint is an autogenerated deepcopy function.
func DeepCopy__Endpoint(in interface{}, out interface{}, c *conversion.Cloner) error {
{
in := in.(*Endpoint)
out := out.(*Endpoint)
*out = *in
if in.Target != nil {
in, out := &in.Target, &out.Target
*out = new(v1.ObjectReference)
**out = **in
}
return nil
}
}
// DeepCopy__SessionAffinityConfig is an autogenerated deepcopy function.
func DeepCopy__SessionAffinityConfig(in interface{}, out interface{}, c *conversion.Cloner) error {
{
in := in.(*SessionAffinityConfig)
out := out.(*SessionAffinityConfig)
*out = *in
if err := DeepCopy__CookieSessionAffinity(&in.CookieSessionAffinity, &out.CookieSessionAffinity, c); err != nil {
return err
}
return nil
}
}