diff --git a/docs/content/en/docs/configuration/keys.md b/docs/content/en/docs/configuration/keys.md index 40bec6fa0..798e06fb0 100644 --- a/docs/content/en/docs/configuration/keys.md +++ b/docs/content/en/docs/configuration/keys.md @@ -148,6 +148,7 @@ The table below describes all supported configuration keys. | [`dynamic-scaling`](#dynamic-scaling) | [true\|false] | Backend | `true` | | [`forwardfor`](#forwardfor) | [add\|ignore\|ifmissing] | Global | `add` | | [`fronting-proxy-port`](#fronting-proxy-port) | port number | Global | 0 (do not listen) | +| [`headers`](#headers) | multiline header:value pair | Backend | | | [`health-check-addr`](#health-check) | address for health checks | Backend | | | [`health-check-fall-count`](#health-check) | number of failures | Backend | | | [`health-check-interval`](#health-check) | time with suffix | Backend | | @@ -963,6 +964,28 @@ See also: * [Bind](#bind) * [Bind port](#bind-port) +## Headers + +| Configuration key | Scope | Default | Since | +|-------------------|-----------|---------|--------| +| `headers` | `Backend` | | v0.11 | + +Configures a list of HTTP header names and the value it should be configured with. More than one header can be configured using a multi-line configuration value. The name of the header and its value should be separated with a colon and/or any amount of spaces. + +The following variables can be used in the value: + +* `%[namespace]`: namespace of the ingress or service +* `%[service]`: name of the service which received the request + +Configuration example: + +```yaml + annotations: + ingress.kubernetes.io/headers: | + x-path: / + host: %[service].%[namespace].svc.cluster.local +``` + --- ## Health check diff --git a/pkg/converters/ingress/annotations/backend.go b/pkg/converters/ingress/annotations/backend.go index 2099e43a2..e77f1941e 100644 --- a/pkg/converters/ingress/annotations/backend.go +++ b/pkg/converters/ingress/annotations/backend.go @@ -490,6 +490,33 @@ func (c *updater) buildBackendHealthCheck(d *backData) { d.backend.HealthCheck.URI = d.mapper.Get(ingtypes.BackHealthCheckURI).Value } +func (c *updater) buildBackendHeaders(d *backData) { + headers := d.mapper.Get(ingtypes.BackHeaders) + if headers.Value == "" { + return + } + for _, header := range utils.LineToSlice(headers.Value) { + header = strings.TrimSpace(header) + if header == "" { + continue + } + idx := strings.IndexAny(header, ": ") + if idx <= 0 { + c.logger.Warn("ignored missing header name or value on %v: %s", headers.Source, header) + continue + } + name := strings.TrimRight(header[:idx], ":") + value := strings.TrimSpace(header[idx+1:]) + // TODO this should use a structured type and a smart match/replace if growing a bit more + value = strings.ReplaceAll(value, "%[service]", d.backend.Name) + value = strings.ReplaceAll(value, "%[namespace]", d.backend.Namespace) + d.backend.Headers = append(d.backend.Headers, &hatypes.BackendHeader{ + Name: name, + Value: value, + }) + } +} + func (c *updater) buildBackendHSTS(d *backData) { rawHSTSList := d.mapper.GetBackendConfig( d.backend, diff --git a/pkg/converters/ingress/annotations/backend_test.go b/pkg/converters/ingress/annotations/backend_test.go index b3befaca1..1bb0ff8d5 100644 --- a/pkg/converters/ingress/annotations/backend_test.go +++ b/pkg/converters/ingress/annotations/backend_test.go @@ -911,6 +911,77 @@ func TestCors(t *testing.T) { } } +func TestHeaders(t *testing.T) { + testCases := []struct { + headers string + expected []*hatypes.BackendHeader + logging string + }{ + // 0 + { + headers: `invalid`, + logging: `WARN ignored missing header name or value on ingress 'ing1/app': invalid`, + }, + // 1 + { + headers: `key value`, + expected: []*hatypes.BackendHeader{ + {Name: "key", Value: "value"}, + }, + }, + // 2 + { + headers: `name: content`, + expected: []*hatypes.BackendHeader{ + {Name: "name", Value: "content"}, + }, + }, + // 3 + { + headers: `k:v`, + expected: []*hatypes.BackendHeader{ + {Name: "k", Value: "v"}, + }, + }, + // 4 + { + headers: `host: %[service].%[namespace].svc.cluster.local`, + expected: []*hatypes.BackendHeader{ + {Name: "host", Value: "app.default.svc.cluster.local"}, + }, + }, + // 5 + { + headers: ` +k8snamespace: %[namespace] +k8sservice: %[service] +host: %[service].%[namespace].svc.cluster.local +`, + expected: []*hatypes.BackendHeader{ + {Name: "k8snamespace", Value: "default"}, + {Name: "k8sservice", Value: "app"}, + {Name: "host", Value: "app.default.svc.cluster.local"}, + }, + }, + } + source := &Source{ + Namespace: "ing1", + Name: "app", + Type: "ingress", + } + for i, test := range testCases { + c := setup(t) + ann := map[string]map[string]string{ + "/": {ingtypes.BackHeaders: test.headers}, + } + d := c.createBackendMappingData("default/app", source, map[string]string{}, ann, []string{"/"}) + c.createUpdater().buildBackendHeaders(d) + c.compareObjects("headers", i, d.backend.Headers, test.expected) + c.logger.CompareLogging(test.logging) + c.teardown() + } +} + func TestHSTS(t *testing.T) { testCases := []struct { paths []string diff --git a/pkg/converters/ingress/annotations/updater.go b/pkg/converters/ingress/annotations/updater.go index 2615424ae..1e94fa1c2 100644 --- a/pkg/converters/ingress/annotations/updater.go +++ b/pkg/converters/ingress/annotations/updater.go @@ -165,6 +165,7 @@ func (c *updater) UpdateBackendConfig(backend *hatypes.Backend, mapper *Mapper) c.buildBackendDNS(data) c.buildBackendDynamic(data) c.buildBackendAgentCheck(data) + c.buildBackendHeaders(data) c.buildBackendHealthCheck(data) c.buildBackendHSTS(data) c.buildBackendLimit(data) diff --git a/pkg/converters/ingress/types/annotations.go b/pkg/converters/ingress/types/annotations.go index d9316f120..509492eb1 100644 --- a/pkg/converters/ingress/types/annotations.go +++ b/pkg/converters/ingress/types/annotations.go @@ -78,6 +78,7 @@ const ( BackCorsExposeHeaders = "cors-expose-headers" BackCorsMaxAge = "cors-max-age" BackDynamicScaling = "dynamic-scaling" + BackHeaders = "headers" BackHealthCheckAddr = "health-check-addr" BackHealthCheckFallCount = "health-check-fall-count" BackHealthCheckInterval = "health-check-interval" diff --git a/pkg/haproxy/instance_test.go b/pkg/haproxy/instance_test.go index e28807a60..7974c3f9d 100644 --- a/pkg/haproxy/instance_test.go +++ b/pkg/haproxy/instance_test.go @@ -326,6 +326,26 @@ func TestBackends(t *testing.T) { # path02 = d1.local/app http-request set-var(txn.pathID) base,lower,map_beg(/etc/haproxy/maps/_back_d1_app_8080_idpath.map) http-request use-service lua.send-413 if { var(txn.pathID) path02 } { req.body_size,sub(2048) gt 0 }`, + }, + { + doconfig: func(g *hatypes.Global, h *hatypes.Host, b *hatypes.Backend) { + b.Headers = []*hatypes.BackendHeader{ + {Name: "Name", Value: "Value"}, + } + }, + expected: ` + http-request set-header Name Value`, + }, + { + doconfig: func(g *hatypes.Global, h *hatypes.Host, b *hatypes.Backend) { + b.Headers = []*hatypes.BackendHeader{ + {Name: "X-ID", Value: "abc"}, + {Name: "Host", Value: "app.domain"}, + } + }, + expected: ` + http-request set-header X-ID abc + http-request set-header Host app.domain`, }, { doconfig: func(g *hatypes.Global, h *hatypes.Host, b *hatypes.Backend) { diff --git a/pkg/haproxy/types/backend.go b/pkg/haproxy/types/backend.go index 973178306..f1ede83d3 100644 --- a/pkg/haproxy/types/backend.go +++ b/pkg/haproxy/types/backend.go @@ -262,6 +262,11 @@ func (p *BackendPath) String() string { return fmt.Sprintf("%+v", *p) } +// String ... +func (h *BackendHeader) String() string { + return fmt.Sprintf("%+v", *h) +} + // String ... func (b *BackendConfigAuth) String() string { return fmt.Sprintf("%+v", *b) diff --git a/pkg/haproxy/types/types.go b/pkg/haproxy/types/types.go index 10c89f508..5730ea009 100644 --- a/pkg/haproxy/types/types.go +++ b/pkg/haproxy/types/types.go @@ -383,6 +383,7 @@ type Backend struct { Cookie Cookie CustomConfig []string Dynamic DynBackendConfig + Headers []*BackendHeader HealthCheck HealthCheck Limit BackendLimit ModeTCP bool @@ -455,6 +456,12 @@ type BackendPath struct { Path string } +// BackendHeader ... +type BackendHeader struct { + Name string + Value string +} + // BackendConfigBool ... type BackendConfigBool struct { Paths BackendPaths diff --git a/rootfs/etc/haproxy/template/haproxy.tmpl b/rootfs/etc/haproxy/template/haproxy.tmpl index ad0b739ae..40aaffe6b 100644 --- a/rootfs/etc/haproxy/template/haproxy.tmpl +++ b/rootfs/etc/haproxy/template/haproxy.tmpl @@ -378,6 +378,11 @@ backend {{ $backend.ID }} {{- end }} {{- end }} +{{- /*------------------------------------*/}} +{{- range $header := $backend.Headers }} + http-request set-header {{ $header.Name }} {{ $header.Value }} +{{- end }} + {{- /*------------------------------------*/}} {{- if $backend.TLS.HasTLSAuth }} {{- $needSSLACL := not $backend.HasSSLRedirect }}