-
-
Notifications
You must be signed in to change notification settings - Fork 5.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Create Global Backend Ingress #3404
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello @dtomcej .
Many thanks for this PR.
Few questions/suggestions... 😉
provider/kubernetes/kubernetes.go
Outdated
|
||
service, exists, err := k8sClient.GetService(i.Namespace, i.Spec.Backend.ServiceName) | ||
if err != nil { | ||
log.Errorf("Error while retrieving service information from k8s API %s/%s: %v", i.Namespace, i.Spec.Backend.ServiceName, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because the error seems to be logged in a higher level function, WDYT to replace the log.Errorf
by a fmt.Errorf
and return this new error without logging it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. On second thought, that makes sense
provider/kubernetes/kubernetes.go
Outdated
} | ||
if !exists { | ||
log.Warnf("Endpoints not found for %s/%s", service.Namespace, service.Name) | ||
break |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the service doest not exist, an error
is logged and the continue
instruction is called.
Can you explain to me why is there a Warn
log and a break
instruction when the entrypoint does not exist please?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Service not existing is usually due to someone mistyping something into an ingress hence the Error
being logged, and the continue
skips processing the rest of that ingress, and continues processing the next ingress.
The warn and break are due to endpoints not being found. This is a very common occurrence, as k8s pods are created and killed. These are not user-error prone, and the rest of the ingress can still be processed if there are no endpoints found.
provider/kubernetes/kubernetes.go
Outdated
} | ||
|
||
for _, subset := range endpoints.Subsets { | ||
endpointPort := i.Spec.Backend.ServicePort |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we sure the i.Spec.Backend.ServicePort
is never nil or empty?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes
@timoreimann thoughts? |
4d5e575
to
128ce73
Compare
create example ingress add default route update continues add tests fix tests update docs final cleanup revert frontend name change add new builder
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello @dtomcej .
More suggestions 😉
This will allow users to create a "default backend" that will match all unmatched requests. | ||
|
||
!!! note | ||
Due to traefik'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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/traefik/Træfik please 😉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add this as a linter, so they don't get missed.
### Global Default Backend Ingresses | ||
|
||
Ingresses can be created that look like the following: | ||
```yaml |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add an empty line before and after the yaml
example please?
provider/kubernetes/kubernetes.go
Outdated
|
||
service, exists, err := k8sClient.GetService(i.Namespace, i.Spec.Backend.ServiceName) | ||
if err != nil { | ||
return nil, fmt.Errorf("Error while retrieving service information from k8s API %s/%s: %v", i.Namespace, i.Spec.Backend.ServiceName, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please downcase the Error
word in the error message.
provider/kubernetes/kubernetes.go
Outdated
|
||
endpoints, exists, err := k8sClient.GetEndpoints(service.Namespace, service.Name) | ||
if err != nil { | ||
return nil, fmt.Errorf("Error retrieving endpoint information from k8s API %s/%s: %v", service.Namespace, service.Name, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please downcase the Error
word in the error message.
provider/kubernetes/kubernetes.go
Outdated
@@ -264,8 +363,7 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) | |||
|
|||
service, exists, err := k8sClient.GetService(i.Namespace, pa.Backend.ServiceName) | |||
if err != nil { | |||
log.Errorf("Error while retrieving service information from k8s API %s/%s: %v", i.Namespace, pa.Backend.ServiceName, err) | |||
return nil, err | |||
return nil, fmt.Errorf("Error while retrieving service information from k8s API %s/%s: %v", i.Namespace, pa.Backend.ServiceName, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please downcase the Error
word in the error message.
provider/kubernetes/kubernetes.go
Outdated
@@ -300,8 +398,7 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) | |||
} else { | |||
endpoints, exists, err := k8sClient.GetEndpoints(service.Namespace, service.Name) | |||
if err != nil { | |||
log.Errorf("Error retrieving endpoints %s/%s: %v", service.Namespace, service.Name, err) | |||
return nil, err | |||
return nil, fmt.Errorf("Error retrieving endpoint information from k8s API %s/%s: %v", service.Namespace, service.Name, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please downcase the Error
word in the error message.
if _, err := provider.loadIngresses(client); err != nil { | ||
switch { | ||
case client.apiServiceError != nil: | ||
expected := errors.New("Error while retrieving service information from k8s API testing/service1: failed kube api call") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please downcase the Error
word in the error message.
} | ||
|
||
case client.apiEndpointsError != nil: | ||
expected := errors.New("Error retrieving endpoint information from k8s API testing/service1: failed kube api call") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please downcase the Error
word in the error message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Change my review state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That was a tough one for me to review but it's a nice feature and a nice PR :-) I left a couple of comments.
Regarding the priority: it's a tricky area. I have to think about it more before I can say something qualified :D
provider/kubernetes/kubernetes.go
Outdated
continue | ||
} | ||
|
||
err = p.updateIngressStatus(i, k8sClient) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be combined with the next line.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add an example on how you would set this priority? I think it would be nice to just show it shortly or link to the point where it's documented.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we just add this to example ingress above?
provider/kubernetes/kubernetes.go
Outdated
|
||
func (p *Provider) buildTemplateFromIngress(i *extensionsv1beta1.Ingress, templateObjects types.Configuration, k8sClient Client) (types.Configuration, error) { | ||
if i.Spec.Backend != nil { | ||
// If a global backend is configured, add this as a "default route" by adding a wildcard frontend, with the priority of the ingress. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we extract the whole part inside the if to a method called like: AddGlobalBackendAsDefaultRoute(...)
? Comments tend to become out-dated or forgotten and it would make it a bit better from the readability.
provider/kubernetes/kubernetes.go
Outdated
} | ||
|
||
for _, subset := range endpoints.Subsets { | ||
endpointPort := endpointPortNumber(corev1.ServicePort{Protocol: "TCP", Port: int32(i.Spec.Backend.ServicePort.IntValue())}, subset.Ports) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems to be still valid to specify the service port as string, referencing the actual service port. Therefore, I think we have to write a second function that does that logic for the ingress service port. WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are correct, but its even simpler for this spec! 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests added!
provider/kubernetes/kubernetes.go
Outdated
} | ||
protocol := "http" | ||
for _, address := range subset.Addresses { | ||
if endpointPort == 443 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we also have to extend this check for the https
prefix in the port name? Goes together with the comment above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
provider/kubernetes/kubernetes.go
Outdated
@@ -736,3 +791,58 @@ func getRateLimit(i *extensionsv1beta1.Ingress) *types.RateLimit { | |||
|
|||
return rateLimit | |||
} | |||
|
|||
func createEmptyBackend(name string, templateObjects types.Configuration) (types.Configuration, error) { | |||
if _, exists := templateObjects.Backends[name]; !exists { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you turn around the logic we could save one level of indent :) The comment below would also become obsolete as the error message documents the if then. The same holds true for the functions below.
if _, err := provider.loadIngresses(client); err != apiErr { | ||
t.Errorf("Got error %v, wanted error %v", err, apiErr) | ||
if _, err := provider.loadIngresses(client); err != nil { | ||
switch { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could you use a if
instead of switch
expected := errors.New("error while retrieving service information from k8s API testing/service1: failed kube api call") | ||
if err.Error() != expected.Error() { | ||
t.Errorf("Got error: %+v, wanted error: %+v, provided: %v", err, expected, client.apiServiceError) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
assert.EqualError(t, err, "error while retrieving service information from k8s API testing/service1: failed kube api call")
expected := errors.New("error retrieving endpoint information from k8s API testing/service1: failed kube api call") | ||
if err.Error() != expected.Error() { | ||
t.Errorf("Got error: %+v, wanted error: %+v, provided: %v", err, expected, client.apiServiceError) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
assert.EqualError(t, err, "error retrieving endpoint information from k8s API testing/service1: failed kube api call")
) | ||
assert.Equal(t, expected, actual) | ||
} | ||
func TestLoadGlobalIngressWithHttpsPortNames(t *testing.T) { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @dtomcej .
One last little note before a LGTM 😉
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 the General Annotations table above) on your ingresses accordingly. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One sentence per line please.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 🍰
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 👏 👏
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, nice job :-)
|
||
!!! 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 the General Annotations table above) on your ingresses accordingly. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we create a link out of "General annotations table" and drop "above" then?
provider/kubernetes/kubernetes.go
Outdated
@@ -270,8 +280,7 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) | |||
|
|||
service, exists, err := k8sClient.GetService(i.Namespace, pa.Backend.ServiceName) | |||
if err != nil { | |||
log.Errorf("Error while retrieving service information from k8s API %s/%s: %v", i.Namespace, pa.Backend.ServiceName, err) | |||
return nil, err | |||
return nil, fmt.Errorf("error while retrieving service information from k8s API %s/%s: %v", i.Namespace, pa.Backend.ServiceName, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this change related to what the PR essentially tries to achieve?
I don't mind a good drive-by improvement. What I'm wondering here though is if we had a reason to log the error first. From what I can tell, this line will eventually log the error. I'm not sure if this reads well though as it would show up as
Cannot connect to Provider: error while retrieving service information from k8s API [...]
We are beyond the connection phase at this point already, aren't we?
How about annotating the error returned here like return fmt.Errorf("failed to load Ingress: %s", err)
and log the error from line 159 as-in?
Similar for line 318.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at it in that perspective, I like the original logging, then return pattern.
provider/kubernetes/kubernetes.go
Outdated
@@ -401,6 +409,100 @@ func (p *Provider) loadConfig(templateObjects types.Configuration) *types.Config | |||
return configuration | |||
} | |||
|
|||
func (p *Provider) addGlobalBackend(k8sClient Client, i *extensionsv1beta1.Ingress, templateObjects *types.Configuration) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rename k8sClient
to just cl
.
provider/kubernetes/kubernetes.go
Outdated
func (p *Provider) addGlobalBackend(k8sClient 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: global-default-backend") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You probably mean global-default-frontend
. Why not use the constant here?
provider/kubernetes/kubernetes.go
Outdated
|
||
// Ensure we are not duplicating the backend | ||
if _, exists := templateObjects.Backends[defaultBackendName]; exists { | ||
return errors.New("duplicate backend: global-default-backend") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Constant usage here too?
provider/kubernetes/kubernetes.go
Outdated
@@ -401,6 +409,100 @@ func (p *Provider) loadConfig(templateObjects types.Configuration) *types.Config | |||
return configuration | |||
} | |||
|
|||
func (p *Provider) addGlobalBackend(k8sClient Client, i *extensionsv1beta1.Ingress, templateObjects *types.Configuration) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The method name seems incorrect. To the least, the method also populates the frontend, right?
What is the scope of the method, what's it supposed to do? (Its large size may hint at a smell.)
EDIT: I did not mean to imply that we should refactor this method now -- this could well be done by another PR. We should just think about the method name maybe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason I chose this name is in reference to the backend
section of the spec, which k8s named global default backend
. If the name bothers us, we can change it in an upcoming refactor :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah okay, missed that -- makes sense to me now, so feel free to keep it. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing state just to be sure, because we already have three LGTMs.
provider/kubernetes/kubernetes.go
Outdated
@@ -306,8 +316,7 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) | |||
} else { | |||
endpoints, exists, err := k8sClient.GetEndpoints(service.Namespace, service.Name) | |||
if err != nil { | |||
log.Errorf("Error retrieving endpoints %s/%s: %v", service.Namespace, service.Name, err) | |||
return nil, err | |||
return nil, fmt.Errorf("error retrieving endpoint information from k8s API %s/%s: %v", service.Namespace, service.Name, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you want to undo this one as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Happy!
What does this PR do?
Allows for an ingress with a Global Parameter Backend to create a default backend route.
Motivation
Fixes #1051 , and also allows for traefik to support web frameworks that use global defaults.
More
Additional Notes
Priority is set based on the ingress priority. Enabling this without setting priority on other ingresses may run into issues with overlapping rules...May want to discuss moving the default priority for kubernetes ingresses to 10, to allow for the default route to be 5, and still give some flexibility.