Create Global Backend Ingress
This commit is contained in:
parent
41eb4f1c70
commit
461ebf6d88
5 changed files with 298 additions and 4 deletions
|
@ -324,3 +324,25 @@ More information are available in the [User Guide](/user-guide/kubernetes/#add-
|
||||||
!!! note
|
!!! note
|
||||||
Only TLS certificates provided by users can be stored in Kubernetes Secrets.
|
Only TLS certificates provided by users can be stored in Kubernetes Secrets.
|
||||||
[Let's Encrypt](https://letsencrypt.org) certificates cannot be managed in Kubernets Secrets yet.
|
[Let's Encrypt](https://letsencrypt.org) certificates cannot be managed in Kubernets Secrets yet.
|
||||||
|
|
||||||
|
### Global Default Backend Ingresses
|
||||||
|
|
||||||
|
Ingresses can be created that look like the following:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: cheese
|
||||||
|
spec:
|
||||||
|
backend:
|
||||||
|
serviceName: stilton
|
||||||
|
servicePort: 80
|
||||||
|
```
|
||||||
|
|
||||||
|
This ingress follows the [Global Default Backend](https://kubernetes.io/docs/concepts/services-networking/ingress/#the-ingress-resource) property of ingresses.
|
||||||
|
This will allow users to create a "default backend" that will match all unmatched requests.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
Due to Træfik's use of priorities, you may have to set this ingress priority lower than other ingresses in your environment, to avoid this global ingress from satisfying requests that _could_ match other ingresses.
|
||||||
|
To do this, use the `traefik.ingress.kubernetes.io/priority` annotation (as seen in [General Annotations](/configuration/backends/kubernetes/#general-annotations)) on your ingresses accordingly.
|
||||||
|
|
8
examples/k8s/cheese-default-ingress.yaml
Normal file
8
examples/k8s/cheese-default-ingress.yaml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: cheese-default
|
||||||
|
spec:
|
||||||
|
backend:
|
||||||
|
serviceName: stilton
|
||||||
|
servicePort: 80
|
|
@ -42,6 +42,33 @@ func iRules(opts ...func(*extensionsv1beta1.IngressSpec)) func(*extensionsv1beta
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func iSpecBackends(opts ...func(*extensionsv1beta1.IngressSpec)) func(*extensionsv1beta1.Ingress) {
|
||||||
|
return func(i *extensionsv1beta1.Ingress) {
|
||||||
|
s := &extensionsv1beta1.IngressSpec{}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(s)
|
||||||
|
}
|
||||||
|
i.Spec = *s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func iSpecBackend(opts ...func(*extensionsv1beta1.IngressBackend)) func(*extensionsv1beta1.IngressSpec) {
|
||||||
|
return func(s *extensionsv1beta1.IngressSpec) {
|
||||||
|
p := &extensionsv1beta1.IngressBackend{}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(p)
|
||||||
|
}
|
||||||
|
s.Backend = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func iIngressBackend(name string, port intstr.IntOrString) func(*extensionsv1beta1.IngressBackend) {
|
||||||
|
return func(p *extensionsv1beta1.IngressBackend) {
|
||||||
|
p.ServiceName = name
|
||||||
|
p.ServicePort = port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func iRule(opts ...func(*extensionsv1beta1.IngressRule)) func(*extensionsv1beta1.IngressSpec) {
|
func iRule(opts ...func(*extensionsv1beta1.IngressRule)) func(*extensionsv1beta1.IngressSpec) {
|
||||||
return func(spec *extensionsv1beta1.IngressSpec) {
|
return func(spec *extensionsv1beta1.IngressSpec) {
|
||||||
r := &extensionsv1beta1.IngressRule{}
|
r := &extensionsv1beta1.IngressRule{}
|
||||||
|
|
|
@ -36,6 +36,8 @@ const (
|
||||||
ruleTypeReplacePath = "ReplacePath"
|
ruleTypeReplacePath = "ReplacePath"
|
||||||
traefikDefaultRealm = "traefik"
|
traefikDefaultRealm = "traefik"
|
||||||
traefikDefaultIngressClass = "traefik"
|
traefikDefaultIngressClass = "traefik"
|
||||||
|
defaultBackendName = "global-default-backend"
|
||||||
|
defaultFrontendName = "global-default-frontend"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IngressEndpoint holds the endpoint information for the Kubernetes provider
|
// IngressEndpoint holds the endpoint information for the Kubernetes provider
|
||||||
|
@ -164,7 +166,7 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
|
||||||
func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) {
|
func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) {
|
||||||
ingresses := k8sClient.GetIngresses()
|
ingresses := k8sClient.GetIngresses()
|
||||||
|
|
||||||
templateObjects := types.Configuration{
|
templateObjects := &types.Configuration{
|
||||||
Backends: map[string]*types.Backend{},
|
Backends: map[string]*types.Backend{},
|
||||||
Frontends: map[string]*types.Frontend{},
|
Frontends: map[string]*types.Frontend{},
|
||||||
}
|
}
|
||||||
|
@ -184,6 +186,14 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
|
||||||
}
|
}
|
||||||
templateObjects.TLS = append(templateObjects.TLS, tlsSection...)
|
templateObjects.TLS = append(templateObjects.TLS, tlsSection...)
|
||||||
|
|
||||||
|
if i.Spec.Backend != nil {
|
||||||
|
err := p.addGlobalBackend(k8sClient, i, templateObjects)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error creating global backend for ingress %s/%s: %v", i.Namespace, i.Name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var weightAllocator weightAllocator = &defaultWeightAllocator{}
|
var weightAllocator weightAllocator = &defaultWeightAllocator{}
|
||||||
annotationPercentageWeights := getAnnotationName(i.Annotations, annotationKubernetesServiceWeights)
|
annotationPercentageWeights := getAnnotationName(i.Annotations, annotationKubernetesServiceWeights)
|
||||||
if _, ok := i.Annotations[annotationPercentageWeights]; ok {
|
if _, ok := i.Annotations[annotationPercentageWeights]; ok {
|
||||||
|
@ -351,7 +361,7 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
|
||||||
log.Errorf("Cannot update Ingress %s/%s due to error: %v", i.Namespace, i.Name, err)
|
log.Errorf("Cannot update Ingress %s/%s due to error: %v", i.Namespace, i.Name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &templateObjects, nil
|
return templateObjects, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) updateIngressStatus(i *extensionsv1beta1.Ingress, k8sClient Client) error {
|
func (p *Provider) updateIngressStatus(i *extensionsv1beta1.Ingress, k8sClient Client) error {
|
||||||
|
@ -401,6 +411,100 @@ func (p *Provider) loadConfig(templateObjects types.Configuration) *types.Config
|
||||||
return configuration
|
return configuration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Provider) addGlobalBackend(cl Client, i *extensionsv1beta1.Ingress, templateObjects *types.Configuration) error {
|
||||||
|
// Ensure that we are not duplicating the frontend
|
||||||
|
if _, exists := templateObjects.Frontends[defaultFrontendName]; exists {
|
||||||
|
return errors.New("duplicate frontend: " + defaultFrontendName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we are not duplicating the backend
|
||||||
|
if _, exists := templateObjects.Backends[defaultBackendName]; exists {
|
||||||
|
return errors.New("duplicate backend: " + defaultBackendName)
|
||||||
|
}
|
||||||
|
|
||||||
|
templateObjects.Backends[defaultBackendName] = &types.Backend{
|
||||||
|
Servers: make(map[string]types.Server),
|
||||||
|
LoadBalancer: &types.LoadBalancer{
|
||||||
|
Method: "wrr",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
service, exists, err := cl.GetService(i.Namespace, i.Spec.Backend.ServiceName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while retrieving service information from k8s API %s/%s: %v", i.Namespace, i.Spec.Backend.ServiceName, err)
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("service not found for %s/%s", i.Namespace, i.Spec.Backend.ServiceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
templateObjects.Backends[defaultBackendName].CircuitBreaker = getCircuitBreaker(service)
|
||||||
|
templateObjects.Backends[defaultBackendName].LoadBalancer = getLoadBalancer(service)
|
||||||
|
templateObjects.Backends[defaultBackendName].MaxConn = getMaxConn(service)
|
||||||
|
templateObjects.Backends[defaultBackendName].Buffering = getBuffering(service)
|
||||||
|
|
||||||
|
endpoints, exists, err := cl.GetEndpoints(service.Namespace, service.Name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error retrieving endpoint information from k8s API %s/%s: %v", service.Namespace, service.Name, err)
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("endpoints not found for %s/%s", service.Namespace, service.Name)
|
||||||
|
}
|
||||||
|
if len(endpoints.Subsets) == 0 {
|
||||||
|
return fmt.Errorf("endpoints not available for %s/%s", service.Namespace, service.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, subset := range endpoints.Subsets {
|
||||||
|
endpointPort := endpointPortNumber(corev1.ServicePort{Protocol: "TCP", Port: int32(i.Spec.Backend.ServicePort.IntValue())}, subset.Ports)
|
||||||
|
if endpointPort == 0 {
|
||||||
|
// endpoint port does not match service.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol := "http"
|
||||||
|
for _, address := range subset.Addresses {
|
||||||
|
if endpointPort == 443 || strings.HasPrefix(i.Spec.Backend.ServicePort.String(), "https") {
|
||||||
|
protocol = "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("%s://%s", protocol, net.JoinHostPort(address.IP, strconv.FormatInt(int64(endpointPort), 10)))
|
||||||
|
name := url
|
||||||
|
if address.TargetRef != nil && address.TargetRef.Name != "" {
|
||||||
|
name = address.TargetRef.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
templateObjects.Backends[defaultBackendName].Servers[name] = types.Server{
|
||||||
|
URL: url,
|
||||||
|
Weight: label.DefaultWeight,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
passHostHeader := getBoolValue(i.Annotations, annotationKubernetesPreserveHost, !p.DisablePassHostHeaders)
|
||||||
|
passTLSCert := getBoolValue(i.Annotations, annotationKubernetesPassTLSCert, p.EnablePassTLSCert)
|
||||||
|
priority := getIntValue(i.Annotations, annotationKubernetesPriority, 0)
|
||||||
|
entryPoints := getSliceStringValue(i.Annotations, annotationKubernetesFrontendEntryPoints)
|
||||||
|
|
||||||
|
templateObjects.Frontends[defaultFrontendName] = &types.Frontend{
|
||||||
|
Backend: defaultBackendName,
|
||||||
|
PassHostHeader: passHostHeader,
|
||||||
|
PassTLSCert: passTLSCert,
|
||||||
|
Routes: make(map[string]types.Route),
|
||||||
|
Priority: priority,
|
||||||
|
WhiteList: getWhiteList(i),
|
||||||
|
Redirect: getFrontendRedirect(i),
|
||||||
|
EntryPoints: entryPoints,
|
||||||
|
Headers: getHeader(i),
|
||||||
|
Errors: getErrorPages(i),
|
||||||
|
RateLimit: getRateLimit(i),
|
||||||
|
}
|
||||||
|
|
||||||
|
templateObjects.Frontends[defaultFrontendName].Routes["/"] = types.Route{
|
||||||
|
Rule: "PathPrefix:/",
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func getRuleForPath(pa extensionsv1beta1.HTTPIngressPath, i *extensionsv1beta1.Ingress) (string, error) {
|
func getRuleForPath(pa extensionsv1beta1.HTTPIngressPath, i *extensionsv1beta1.Ingress) (string, error) {
|
||||||
if len(pa.Path) == 0 {
|
if len(pa.Path) == 0 {
|
||||||
return "", nil
|
return "", nil
|
||||||
|
|
|
@ -221,6 +221,134 @@ func TestLoadIngresses(t *testing.T) {
|
||||||
assert.Equal(t, expected, actual)
|
assert.Equal(t, expected, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadGlobalIngressWithPortNumbers(t *testing.T) {
|
||||||
|
ingresses := []*extensionsv1beta1.Ingress{
|
||||||
|
buildIngress(
|
||||||
|
iNamespace("testing"),
|
||||||
|
iSpecBackends(iSpecBackend(iIngressBackend("service1", intstr.FromInt(80)))),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
services := []*corev1.Service{
|
||||||
|
buildService(
|
||||||
|
sName("service1"),
|
||||||
|
sNamespace("testing"),
|
||||||
|
sUID("1"),
|
||||||
|
sSpec(
|
||||||
|
clusterIP("10.0.0.1"),
|
||||||
|
sPorts(sPort(80, ""))),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoints := []*corev1.Endpoints{
|
||||||
|
buildEndpoint(
|
||||||
|
eNamespace("testing"),
|
||||||
|
eName("service1"),
|
||||||
|
eUID("1"),
|
||||||
|
subset(
|
||||||
|
eAddresses(eAddress("10.10.0.1")),
|
||||||
|
ePorts(ePort(8080, ""))),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
watchChan := make(chan interface{})
|
||||||
|
client := clientMock{
|
||||||
|
ingresses: ingresses,
|
||||||
|
services: services,
|
||||||
|
endpoints: endpoints,
|
||||||
|
watchChan: watchChan,
|
||||||
|
}
|
||||||
|
provider := Provider{}
|
||||||
|
|
||||||
|
actual, err := provider.loadIngresses(client)
|
||||||
|
require.NoError(t, err, "error loading ingresses")
|
||||||
|
|
||||||
|
expected := buildConfiguration(
|
||||||
|
backends(
|
||||||
|
backend("global-default-backend",
|
||||||
|
lbMethod("wrr"),
|
||||||
|
servers(
|
||||||
|
server("http://10.10.0.1:8080", weight(1)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
frontends(
|
||||||
|
frontend("global-default-backend",
|
||||||
|
frontendName("global-default-frontend"),
|
||||||
|
passHostHeader(),
|
||||||
|
routes(
|
||||||
|
route("/", "PathPrefix:/"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert.Equal(t, expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadGlobalIngressWithHttpsPortNames(t *testing.T) {
|
||||||
|
ingresses := []*extensionsv1beta1.Ingress{
|
||||||
|
buildIngress(
|
||||||
|
iNamespace("testing"),
|
||||||
|
iSpecBackends(iSpecBackend(iIngressBackend("service1", intstr.FromString("https-global")))),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
services := []*corev1.Service{
|
||||||
|
buildService(
|
||||||
|
sName("service1"),
|
||||||
|
sNamespace("testing"),
|
||||||
|
sUID("1"),
|
||||||
|
sSpec(
|
||||||
|
clusterIP("10.0.0.1"),
|
||||||
|
sPorts(sPort(8443, "https-global"))),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoints := []*corev1.Endpoints{
|
||||||
|
buildEndpoint(
|
||||||
|
eNamespace("testing"),
|
||||||
|
eName("service1"),
|
||||||
|
eUID("1"),
|
||||||
|
subset(
|
||||||
|
eAddresses(eAddress("10.10.0.1")),
|
||||||
|
ePorts(ePort(8080, ""))),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
watchChan := make(chan interface{})
|
||||||
|
client := clientMock{
|
||||||
|
ingresses: ingresses,
|
||||||
|
services: services,
|
||||||
|
endpoints: endpoints,
|
||||||
|
watchChan: watchChan,
|
||||||
|
}
|
||||||
|
provider := Provider{}
|
||||||
|
|
||||||
|
actual, err := provider.loadIngresses(client)
|
||||||
|
require.NoError(t, err, "error loading ingresses")
|
||||||
|
|
||||||
|
expected := buildConfiguration(
|
||||||
|
backends(
|
||||||
|
backend("global-default-backend",
|
||||||
|
lbMethod("wrr"),
|
||||||
|
servers(
|
||||||
|
server("https://10.10.0.1:8080", weight(1)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
frontends(
|
||||||
|
frontend("global-default-backend",
|
||||||
|
frontendName("global-default-frontend"),
|
||||||
|
passHostHeader(),
|
||||||
|
routes(
|
||||||
|
route("/", "PathPrefix:/"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert.Equal(t, expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
func TestRuleType(t *testing.T) {
|
func TestRuleType(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
desc string
|
desc string
|
||||||
|
@ -1557,8 +1685,13 @@ func TestKubeAPIErrors(t *testing.T) {
|
||||||
|
|
||||||
provider := Provider{}
|
provider := Provider{}
|
||||||
|
|
||||||
if _, err := provider.loadIngresses(client); err != apiErr {
|
if _, err := provider.loadIngresses(client); err != nil {
|
||||||
t.Errorf("Got error %v, wanted error %v", err, apiErr)
|
if client.apiServiceError != nil {
|
||||||
|
assert.EqualError(t, err, "failed kube api call")
|
||||||
|
}
|
||||||
|
if client.apiEndpointsError != nil {
|
||||||
|
assert.EqualError(t, err, "failed kube api call")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue