Merge pull request #1149 from Regner/kubernetes-support-externalname-service
Kubernetes support externalname service
This commit is contained in:
commit
d77ad42326
3 changed files with 423 additions and 20 deletions
|
@ -428,3 +428,59 @@ You should now be able to visit the websites in your browser.
|
||||||
* [cheeses.local/stilton](http://cheeses.local/stilton/)
|
* [cheeses.local/stilton](http://cheeses.local/stilton/)
|
||||||
* [cheeses.local/cheddar](http://cheeses.local/cheddar/)
|
* [cheeses.local/cheddar](http://cheeses.local/cheddar/)
|
||||||
* [cheeses.local/wensleydale](http://cheeses.local/wensleydale/)
|
* [cheeses.local/wensleydale](http://cheeses.local/wensleydale/)
|
||||||
|
|
||||||
|
## Disable passing the Host header
|
||||||
|
By default Træfɪk will pass the incoming Host header on to the upstream resource. There
|
||||||
|
are times however where you may not want this to be the case. For example if your service
|
||||||
|
is of the ExternalName type.
|
||||||
|
|
||||||
|
### Disable entirely
|
||||||
|
Add the following to your toml config:
|
||||||
|
```toml
|
||||||
|
disablePassHostHeaders = true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Disable per ingress
|
||||||
|
To disable passing the Host header per ingress resource set the "traefik.frontend.passHostHeader"
|
||||||
|
annotation on your ingress to "false".
|
||||||
|
|
||||||
|
Here is an example ingress definition:
|
||||||
|
```yaml
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: example
|
||||||
|
annotations:
|
||||||
|
traefik.frontend.passHostHeader: "false"
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: example.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /static
|
||||||
|
backend:
|
||||||
|
serviceName: static
|
||||||
|
servicePort: https
|
||||||
|
```
|
||||||
|
|
||||||
|
And an example service definition:
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: static
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- name: https
|
||||||
|
port: 443
|
||||||
|
type: ExternalName
|
||||||
|
externalName: static.otherdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
If you were to visit example.com/static the request would then be passed onto
|
||||||
|
static.otherdomain.com/static and static.otherdomain.com would receive the
|
||||||
|
request with the Host header being static.otherdomain.com.
|
||||||
|
|
||||||
|
Note: The per ingress annotation overides whatever the global value is set to. So you
|
||||||
|
could set `disablePassHostHeaders` to true in your toml file and then enable passing
|
||||||
|
the host header per ingress if you wanted.
|
|
@ -107,7 +107,6 @@ func (provider *Kubernetes) loadIngresses(k8sClient k8s.Client) (*types.Configur
|
||||||
map[string]*types.Backend{},
|
map[string]*types.Backend{},
|
||||||
map[string]*types.Frontend{},
|
map[string]*types.Frontend{},
|
||||||
}
|
}
|
||||||
PassHostHeader := provider.getPassHostHeader()
|
|
||||||
for _, i := range ingresses {
|
for _, i := range ingresses {
|
||||||
for _, r := range i.Spec.Rules {
|
for _, r := range i.Spec.Rules {
|
||||||
if r.HTTP == nil {
|
if r.HTTP == nil {
|
||||||
|
@ -124,6 +123,19 @@ 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
|
||||||
|
default:
|
||||||
|
log.Warnf("Unknown value of %s for traefik.frontend.passHostHeader, falling back to %s", passHostHeaderAnnotation, PassHostHeader)
|
||||||
|
}
|
||||||
|
|
||||||
if _, exists := templateObjects.Frontends[r.Host+pa.Path]; !exists {
|
if _, exists := templateObjects.Frontends[r.Host+pa.Path]; !exists {
|
||||||
templateObjects.Frontends[r.Host+pa.Path] = &types.Frontend{
|
templateObjects.Frontends[r.Host+pa.Path] = &types.Frontend{
|
||||||
Backend: r.Host + pa.Path,
|
Backend: r.Host + pa.Path,
|
||||||
|
@ -193,28 +205,44 @@ func (provider *Kubernetes) loadIngresses(k8sClient k8s.Client) (*types.Configur
|
||||||
if port.Port == 443 {
|
if port.Port == 443 {
|
||||||
protocol = "https"
|
protocol = "https"
|
||||||
}
|
}
|
||||||
endpoints, exists, err := k8sClient.GetEndpoints(service.ObjectMeta.Namespace, service.ObjectMeta.Name)
|
if service.Spec.Type == "ExternalName" {
|
||||||
if err != nil || !exists {
|
url := protocol + "://" + service.Spec.ExternalName
|
||||||
log.Errorf("Error retrieving endpoints %s/%s: %v", service.ObjectMeta.Namespace, service.ObjectMeta.Name, err)
|
name := url
|
||||||
continue
|
|
||||||
}
|
templateObjects.Backends[r.Host+pa.Path].Servers[name] = types.Server{
|
||||||
if len(endpoints.Subsets) == 0 {
|
URL: url,
|
||||||
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,
|
Weight: 1,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for _, subset := range endpoints.Subsets {
|
endpoints, exists, err := k8sClient.GetEndpoints(service.ObjectMeta.Namespace, service.ObjectMeta.Name)
|
||||||
for _, address := range subset.Addresses {
|
if err != nil {
|
||||||
url := protocol + "://" + address.IP + ":" + strconv.Itoa(endpointPortNumber(port, subset.Ports))
|
log.Errorf("Error while retrieving endpoints from k8s API %s/%s: %v", service.ObjectMeta.Namespace, service.ObjectMeta.Name, err)
|
||||||
name := url
|
continue
|
||||||
if address.TargetRef != nil && address.TargetRef.Name != "" {
|
}
|
||||||
name = address.TargetRef.Name
|
|
||||||
}
|
if !exists {
|
||||||
templateObjects.Backends[r.Host+pa.Path].Servers[name] = types.Server{
|
log.Errorf("Service not found for %s/%s", service.ObjectMeta.Namespace, service.ObjectMeta.Name)
|
||||||
URL: url,
|
continue
|
||||||
Weight: 1,
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,13 @@ func TestLoadIngresses(t *testing.T) {
|
||||||
ServicePort: intstr.FromInt(80),
|
ServicePort: intstr.FromInt(80),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Path: "/namedthing",
|
||||||
|
Backend: v1beta1.IngressBackend{
|
||||||
|
ServiceName: "service4",
|
||||||
|
ServicePort: intstr.FromString("https"),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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{
|
endpoints := []*v1.Endpoints{
|
||||||
{
|
{
|
||||||
|
@ -221,6 +246,19 @@ func TestLoadIngresses(t *testing.T) {
|
||||||
Method: "wrr",
|
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": {
|
"bar": {
|
||||||
Servers: map[string]types.Server{
|
Servers: map[string]types.Server{
|
||||||
"2": {
|
"2": {
|
||||||
|
@ -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": {
|
"bar": {
|
||||||
Backend: "bar",
|
Backend: "bar",
|
||||||
PassHostHeader: true,
|
PassHostHeader: true,
|
||||||
|
@ -1524,6 +1575,274 @@ 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidPassHostHeaderValue(t *testing.T) {
|
||||||
|
ingresses := []*v1beta1.Ingress{
|
||||||
|
{
|
||||||
|
ObjectMeta: v1.ObjectMeta{
|
||||||
|
Namespace: "testing",
|
||||||
|
Annotations: map[string]string{
|
||||||
|
"traefik.frontend.passHostHeader": "herpderp",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Frontends: map[string]*types.Frontend{
|
||||||
|
"foo/bar": {
|
||||||
|
Backend: "foo/bar",
|
||||||
|
PassHostHeader: true,
|
||||||
|
Priority: len("/bar"),
|
||||||
|
Routes: map[string]types.Route{
|
||||||
|
"/bar": {
|
||||||
|
Rule: "PathPrefix:/bar",
|
||||||
|
},
|
||||||
|
"foo": {
|
||||||
|
Rule: "Host:foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
type clientMock struct {
|
||||||
ingresses []*v1beta1.Ingress
|
ingresses []*v1beta1.Ingress
|
||||||
services []*v1.Service
|
services []*v1.Service
|
||||||
|
|
Loading…
Reference in a new issue