Skip to content
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

Merged
merged 15 commits into from
Jul 3, 2018
Merged

Create Global Backend Ingress #3404

merged 15 commits into from
Jul 3, 2018

Conversation

dtomcej
Copy link
Contributor

@dtomcej dtomcej commented May 28, 2018

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

  • Added/updated tests
  • Added/updated documentation

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.

Copy link
Contributor

@nmengin nmengin left a 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... 😉


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)
Copy link
Contributor

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?

Copy link
Contributor Author

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

}
if !exists {
log.Warnf("Endpoints not found for %s/%s", service.Namespace, service.Name)
break
Copy link
Contributor

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?

Copy link
Contributor Author

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.

}

for _, subset := range endpoints.Subsets {
endpointPort := i.Spec.Backend.ServicePort
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

@dtomcej
Copy link
Contributor Author

dtomcej commented Jun 4, 2018

@timoreimann thoughts?

@dtomcej dtomcej force-pushed the issue-1051 branch 2 times, most recently from 4d5e575 to 128ce73 Compare June 6, 2018 14:43
create example ingress
add default route
update continues
add tests
fix tests
update docs
final cleanup
revert frontend name change
add new builder
Copy link
Contributor

@nmengin nmengin left a 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.
Copy link
Contributor

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 😉

Copy link
Contributor Author

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
Copy link
Contributor

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?


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)
Copy link
Contributor

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.


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)
Copy link
Contributor

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.

@@ -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)
Copy link
Contributor

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.

@@ -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)
Copy link
Contributor

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")
Copy link
Contributor

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")
Copy link
Contributor

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.

Copy link
Contributor

@nmengin nmengin left a 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.

Copy link
Contributor

@m3co-code m3co-code left a 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

continue
}

err = p.updateIngressStatus(i, k8sClient)
Copy link
Contributor

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.
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Copy link
Contributor

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?


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.
Copy link
Contributor

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.

}

for _, subset := range endpoints.Subsets {
endpointPort := endpointPortNumber(corev1.ServicePort{Protocol: "TCP", Port: int32(i.Spec.Backend.ServicePort.IntValue())}, subset.Ports)
Copy link
Contributor

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?

Copy link
Contributor Author

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! 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests added!

}
protocol := "http"
for _, address := range subset.Addresses {
if endpointPort == 443 {
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

@@ -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 {
Copy link
Contributor

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 {
Copy link
Contributor

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)
}
Copy link
Contributor

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)
}
Copy link
Contributor

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.

Copy link
Contributor

@nmengin nmengin left a 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.
Copy link
Contributor

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.

Copy link
Contributor

@ldez ldez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 🍰

Copy link
Contributor

@nmengin nmengin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👏 👏

Copy link
Contributor

@m3co-code m3co-code left a 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.
Copy link
Contributor

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?

@@ -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)
Copy link
Contributor

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.

Copy link
Contributor Author

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.

@@ -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 {
Copy link
Contributor

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.

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")
Copy link
Contributor

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?


// Ensure we are not duplicating the backend
if _, exists := templateObjects.Backends[defaultBackendName]; exists {
return errors.New("duplicate backend: global-default-backend")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Constant usage here too?

@@ -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 {
Copy link
Contributor

@timoreimann timoreimann Jul 3, 2018

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.

Copy link
Contributor Author

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 :)

Copy link
Contributor

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. :)

Copy link
Contributor

@timoreimann timoreimann left a 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.

@@ -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)
Copy link
Contributor

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?

Copy link
Contributor

@timoreimann timoreimann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants