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

Kubernetes support externalname service #1149

Merged
61 changes: 41 additions & 20 deletions provider/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ func (provider *Kubernetes) loadIngresses(k8sClient k8s.Client) (*types.Configur
map[string]*types.Backend{},
map[string]*types.Frontend{},
}
PassHostHeader := provider.getPassHostHeader()
for _, i := range ingresses {
for _, r := range i.Spec.Rules {
if r.HTTP == nil {
Expand All @@ -124,6 +123,18 @@ func (provider *Kubernetes) loadIngresses(k8sClient k8s.Client) (*types.Configur
},
}
}

PassHostHeader := provider.getPassHostHeader()

passHostHeaderAnnotation := i.Annotations["traefik.frontend.passHostHeader"]
switch passHostHeaderAnnotation {
case "true":
PassHostHeader = true
case "false":
PassHostHeader = false

Copy link
Contributor

Choose a reason for hiding this comment

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

You could add a default case to log a warning. I'd then also add a test making sure that an invalid value does not change the PassHostHeader value.

}

if _, exists := templateObjects.Frontends[r.Host+pa.Path]; !exists {
templateObjects.Frontends[r.Host+pa.Path] = &types.Frontend{
Backend: r.Host + pa.Path,
Expand Down Expand Up @@ -193,28 +204,38 @@ func (provider *Kubernetes) loadIngresses(k8sClient k8s.Client) (*types.Configur
if port.Port == 443 {
protocol = "https"
}
endpoints, exists, err := k8sClient.GetEndpoints(service.ObjectMeta.Namespace, service.ObjectMeta.Name)
if err != nil || !exists {
log.Errorf("Error retrieving endpoints %s/%s: %v", service.ObjectMeta.Namespace, service.ObjectMeta.Name, err)
continue
}
if len(endpoints.Subsets) == 0 {
log.Warnf("Endpoints not found for %s/%s, falling back to Service ClusterIP", service.ObjectMeta.Namespace, service.ObjectMeta.Name)
templateObjects.Backends[r.Host+pa.Path].Servers[string(service.UID)] = types.Server{
URL: protocol + "://" + service.Spec.ClusterIP + ":" + strconv.Itoa(int(port.Port)),
if service.Spec.Type == "ExternalName" {
url := protocol + "://" + service.Spec.ExternalName
name := url

templateObjects.Backends[r.Host+pa.Path].Servers[name] = types.Server{
URL: url,
Weight: 1,
}
} else {
for _, subset := range endpoints.Subsets {
for _, address := range subset.Addresses {
url := protocol + "://" + address.IP + ":" + strconv.Itoa(endpointPortNumber(port, subset.Ports))
name := url
if address.TargetRef != nil && address.TargetRef.Name != "" {
name = address.TargetRef.Name
}
templateObjects.Backends[r.Host+pa.Path].Servers[name] = types.Server{
URL: url,
Weight: 1,
endpoints, exists, err := k8sClient.GetEndpoints(service.ObjectMeta.Namespace, service.ObjectMeta.Name)
if err != nil || !exists {
log.Errorf("Error retrieving endpoints %s/%s: %v", service.ObjectMeta.Namespace, service.ObjectMeta.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.

The log message won't be very useful if we hit the !exists case.

I'd rather use an if/else-if or switch construct to produce two distinct messages.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Happy to make the change, but that is how it was before I touched anything. Mostly I don't fully understand what the different between the two is.

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 say let's be good boy scouts and leave the place a bit tidier than how we found it. :-)

Which two do you mean? If/else-if and select?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I meant the error and exists. Not sure what the log message for each should be as I don't fully understand what they mean.

After a quick look it seems err would mean there was an error in contacting the k8s API and exists would mean there are endpoints in the response. Not 100% sure on that though.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, that's basically right: If some error occurs during the GetEndpoints call (which could be prior, during, or after reaching out to the k8s API), the err will be non-nil.

If the API call succeeds but no endpoints were found for the given namespace and name, exists will be false.

One last note: If you split up the OR'ed statement in order to make the error message more specific, be sure to check and handle the err != nil case first: It's conventional in Go that if an error occurs, the remaining return parameters have either arbitrary or zero values.

continue
}
if len(endpoints.Subsets) == 0 {
log.Warnf("Endpoints not found for %s/%s, falling back to Service ClusterIP", service.ObjectMeta.Namespace, service.ObjectMeta.Name)
templateObjects.Backends[r.Host+pa.Path].Servers[string(service.UID)] = types.Server{
URL: protocol + "://" + service.Spec.ClusterIP + ":" + strconv.Itoa(int(port.Port)),
Weight: 1,
}
} else {
for _, subset := range endpoints.Subsets {
for _, address := range subset.Addresses {
url := protocol + "://" + address.IP + ":" + strconv.Itoa(endpointPortNumber(port, subset.Ports))
name := url
if address.TargetRef != nil && address.TargetRef.Name != "" {
name = address.TargetRef.Name
}
templateObjects.Backends[r.Host+pa.Path].Servers[name] = types.Server{
URL: url,
Weight: 1,
}
}
}
}
Expand Down
212 changes: 212 additions & 0 deletions provider/kubernetes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ func TestLoadIngresses(t *testing.T) {
ServicePort: intstr.FromInt(80),
},
},
{
Path: "/namedthing",
Backend: v1beta1.IngressBackend{
ServiceName: "service4",
ServicePort: intstr.FromString("https"),
},
},
},
},
},
Expand Down Expand Up @@ -110,6 +117,24 @@ func TestLoadIngresses(t *testing.T) {
},
},
},
{
ObjectMeta: v1.ObjectMeta{
Name: "service4",
UID: "4",
Namespace: "testing",
},
Spec: v1.ServiceSpec{
ClusterIP: "10.0.0.4",
Type: "ExternalName",
ExternalName: "example.com",
Ports: []v1.ServicePort{
{
Name: "https",
Port: 443,
},
},
},
},
}
endpoints := []*v1.Endpoints{
{
Expand Down Expand Up @@ -221,6 +246,19 @@ func TestLoadIngresses(t *testing.T) {
Method: "wrr",
},
},
"foo/namedthing": {
Servers: map[string]types.Server{
"https://example.com": {
URL: "https://example.com",
Weight: 1,
},
},
CircuitBreaker: nil,
LoadBalancer: &types.LoadBalancer{
Sticky: false,
Method: "wrr",
},
},
"bar": {
Servers: map[string]types.Server{
"2": {
Expand Down Expand Up @@ -257,6 +295,19 @@ func TestLoadIngresses(t *testing.T) {
},
},
},
"foo/namedthing": {
Backend: "foo/namedthing",
PassHostHeader: true,
Priority: len("/namedthing"),
Routes: map[string]types.Route{
"/namedthing": {
Rule: "PathPrefix:/namedthing",
},
"foo": {
Rule: "Host:foo",
},
},
},
"bar": {
Backend: "bar",
PassHostHeader: true,
Expand Down Expand Up @@ -1524,6 +1575,167 @@ func TestServiceAnnotations(t *testing.T) {
}
}

func TestIngressAnnotations(t *testing.T) {
ingresses := []*v1beta1.Ingress{
{
ObjectMeta: v1.ObjectMeta{
Namespace: "testing",
Annotations: map[string]string{
"traefik.frontend.passHostHeader": "false",
},
},
Spec: v1beta1.IngressSpec{
Rules: []v1beta1.IngressRule{
{
Host: "foo",
IngressRuleValue: v1beta1.IngressRuleValue{
HTTP: &v1beta1.HTTPIngressRuleValue{
Paths: []v1beta1.HTTPIngressPath{
{
Path: "/bar",
Backend: v1beta1.IngressBackend{
ServiceName: "service1",
ServicePort: intstr.FromInt(80),
},
},
},
},
},
},
},
},
},
{
ObjectMeta: v1.ObjectMeta{
Namespace: "testing",
Annotations: map[string]string{
"traefik.frontend.passHostHeader": "true",
},
},
Spec: v1beta1.IngressSpec{
Rules: []v1beta1.IngressRule{
{
Host: "other",
IngressRuleValue: v1beta1.IngressRuleValue{
HTTP: &v1beta1.HTTPIngressRuleValue{
Paths: []v1beta1.HTTPIngressPath{
{
Path: "/stuff",
Backend: v1beta1.IngressBackend{
ServiceName: "service1",
ServicePort: intstr.FromInt(80),
},
},
},
},
},
},
},
},
},
}
services := []*v1.Service{
{
ObjectMeta: v1.ObjectMeta{
Name: "service1",
UID: "1",
Namespace: "testing",
},
Spec: v1.ServiceSpec{
ClusterIP: "10.0.0.1",
Type: "ExternalName",
ExternalName: "example.com",
Ports: []v1.ServicePort{
{
Name: "http",
Port: 80,
},
},
},
},
}

endpoints := []*v1.Endpoints{}
watchChan := make(chan interface{})
client := clientMock{
ingresses: ingresses,
services: services,
endpoints: endpoints,
watchChan: watchChan,
}
provider := Kubernetes{}
actual, err := provider.loadIngresses(client)
if err != nil {
t.Fatalf("error %+v", err)
}

expected := &types.Configuration{
Backends: map[string]*types.Backend{
"foo/bar": {
Servers: map[string]types.Server{
"http://example.com": {
URL: "http://example.com",
Weight: 1,
},
},
CircuitBreaker: nil,
LoadBalancer: &types.LoadBalancer{
Sticky: false,
Method: "wrr",
},
},
"other/stuff": {
Servers: map[string]types.Server{
"http://example.com": {
URL: "http://example.com",
Weight: 1,
},
},
CircuitBreaker: nil,
LoadBalancer: &types.LoadBalancer{
Sticky: false,
Method: "wrr",
},
},
},
Frontends: map[string]*types.Frontend{
"foo/bar": {
Backend: "foo/bar",
PassHostHeader: false,
Priority: len("/bar"),
Routes: map[string]types.Route{
"/bar": {
Rule: "PathPrefix:/bar",
},
"foo": {
Rule: "Host:foo",
},
},
},
"other/stuff": {
Backend: "other/stuff",
PassHostHeader: true,
Priority: len("/stuff"),
Routes: map[string]types.Route{
"/stuff": {
Rule: "PathPrefix:/stuff",
},
"other": {
Rule: "Host:other",
},
},
},
},
}

actualJSON, _ := json.Marshal(actual)
expectedJSON, _ := json.Marshal(expected)

if !reflect.DeepEqual(actual, expected) {
t.Fatalf("expected %+v, got %+v", string(expectedJSON), string(actualJSON))
}
}

type clientMock struct {
ingresses []*v1beta1.Ingress
services []*v1.Service
Expand Down