mirror of
https://github.com/traefik/traefik
synced 2026-02-03 11:10:33 +00:00
Support NGINX custom-headers annotation
This commit is contained in:
@@ -287,12 +287,13 @@ The following annotations are organized by category for easier navigation.
|
||||
|
||||
### Load Balancing & Backend
|
||||
|
||||
| Annotation | Limitations / Notes |
|
||||
|-------------------------------------------------------|--------------------------------------------------------------------------------------------|
|
||||
| <a id="opt-nginx-ingress-kubernetes-ioload-balance" href="#opt-nginx-ingress-kubernetes-ioload-balance" title="#opt-nginx-ingress-kubernetes-ioload-balance">`nginx.ingress.kubernetes.io/load-balance`</a> | Only round_robin supported; ewma and IP hash not supported. |
|
||||
| <a id="opt-nginx-ingress-kubernetes-iobackend-protocol" href="#opt-nginx-ingress-kubernetes-iobackend-protocol" title="#opt-nginx-ingress-kubernetes-iobackend-protocol">`nginx.ingress.kubernetes.io/backend-protocol`</a> | FCGI and AUTO_HTTP not supported. |
|
||||
| <a id="opt-nginx-ingress-kubernetes-ioservice-upstream" href="#opt-nginx-ingress-kubernetes-ioservice-upstream" title="#opt-nginx-ingress-kubernetes-ioservice-upstream">`nginx.ingress.kubernetes.io/service-upstream`</a> | |
|
||||
| <a id="opt-nginx-ingress-kubernetes-ioupstream-vhost" href="#opt-nginx-ingress-kubernetes-ioupstream-vhost" title="#opt-nginx-ingress-kubernetes-ioupstream-vhost">`nginx.ingress.kubernetes.io/upstream-vhost`</a> | |
|
||||
| Annotation | Limitations / Notes |
|
||||
|-------------------------------------------------------|--------------------------------------------------------------------------------------------------|
|
||||
| <a id="opt-nginx-ingress-kubernetes-ioload-balance" href="#opt-nginx-ingress-kubernetes-ioload-balance" title="#opt-nginx-ingress-kubernetes-ioload-balance">`nginx.ingress.kubernetes.io/load-balance`</a> | Only round_robin supported; ewma and IP hash not supported. |
|
||||
| <a id="opt-nginx-ingress-kubernetes-iobackend-protocol" href="#opt-nginx-ingress-kubernetes-iobackend-protocol" title="#opt-nginx-ingress-kubernetes-iobackend-protocol">`nginx.ingress.kubernetes.io/backend-protocol`</a> | FCGI and AUTO_HTTP not supported. |
|
||||
| <a id="opt-nginx-ingress-kubernetes-ioservice-upstream" href="#opt-nginx-ingress-kubernetes-ioservice-upstream" title="#opt-nginx-ingress-kubernetes-ioservice-upstream">`nginx.ingress.kubernetes.io/service-upstream`</a> | |
|
||||
| <a id="opt-nginx-ingress-kubernetes-ioupstream-vhost" href="#opt-nginx-ingress-kubernetes-ioupstream-vhost" title="#opt-nginx-ingress-kubernetes-ioupstream-vhost">`nginx.ingress.kubernetes.io/upstream-vhost`</a> | |
|
||||
| <a id="opt-nginx-ingress-kubernetes-iocustom-headers" href="#opt-nginx-ingress-kubernetes-iocustom-headers" title="#opt-nginx-ingress-kubernetes-iocustom-headers">`nginx.ingress.kubernetes.io/custom-headers`</a> | Header whitelisting, similar to `global-allowed-response-headers` NGINX config is not supported. |
|
||||
|
||||
### CORS
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ type ingressConfig struct {
|
||||
|
||||
WhitelistSourceRange *string `annotation:"nginx.ingress.kubernetes.io/whitelist-source-range"`
|
||||
|
||||
CustomHeaders *string `annotation:"nginx.ingress.kubernetes.io/custom-headers"`
|
||||
UpstreamVhost *string `annotation:"nginx.ingress.kubernetes.io/upstream-vhost"`
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ type clientWrapper struct {
|
||||
clusterScopeFactory kinformers.SharedInformerFactory
|
||||
factoriesKube map[string]kinformers.SharedInformerFactory
|
||||
factoriesSecret map[string]kinformers.SharedInformerFactory
|
||||
factoriesConfigMap map[string]kinformers.SharedInformerFactory
|
||||
factoriesIngress map[string]kinformers.SharedInformerFactory
|
||||
isNamespaceAll bool
|
||||
watchedNamespaces []string
|
||||
@@ -115,10 +116,11 @@ func createClientFromConfig(c *rest.Config) (*clientWrapper, error) {
|
||||
|
||||
func newClient(clientSet kclientset.Interface) *clientWrapper {
|
||||
return &clientWrapper{
|
||||
clientset: clientSet,
|
||||
factoriesSecret: make(map[string]kinformers.SharedInformerFactory),
|
||||
factoriesIngress: make(map[string]kinformers.SharedInformerFactory),
|
||||
factoriesKube: make(map[string]kinformers.SharedInformerFactory),
|
||||
clientset: clientSet,
|
||||
factoriesSecret: make(map[string]kinformers.SharedInformerFactory),
|
||||
factoriesConfigMap: make(map[string]kinformers.SharedInformerFactory),
|
||||
factoriesIngress: make(map[string]kinformers.SharedInformerFactory),
|
||||
factoriesKube: make(map[string]kinformers.SharedInformerFactory),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,12 +184,20 @@ func (c *clientWrapper) WatchAll(ctx context.Context, namespace, namespaceSelect
|
||||
return nil, err
|
||||
}
|
||||
c.factoriesSecret[ns] = factorySecret
|
||||
|
||||
factoryConfigMap := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(notOwnedByHelm))
|
||||
_, err = factoryConfigMap.Core().V1().ConfigMaps().Informer().AddEventHandler(eventHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.factoriesConfigMap[ns] = factoryConfigMap
|
||||
}
|
||||
|
||||
for _, ns := range c.watchedNamespaces {
|
||||
c.factoriesIngress[ns].Start(stopCh)
|
||||
c.factoriesKube[ns].Start(stopCh)
|
||||
c.factoriesSecret[ns].Start(stopCh)
|
||||
c.factoriesConfigMap[ns].Start(stopCh)
|
||||
}
|
||||
|
||||
for _, ns := range c.watchedNamespaces {
|
||||
@@ -208,6 +218,12 @@ func (c *clientWrapper) WatchAll(ctx context.Context, namespace, namespaceSelect
|
||||
return nil, fmt.Errorf("timed out waiting for controller caches to sync %s in namespace %q", t.String(), ns)
|
||||
}
|
||||
}
|
||||
|
||||
for t, ok := range c.factoriesConfigMap[ns].WaitForCacheSync(stopCh) {
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("timed out waiting for controller caches to sync %s in namespace %q", t.String(), ns)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.clusterScopeFactory = kinformers.NewSharedInformerFactory(c.clientset, resyncPeriod)
|
||||
@@ -314,6 +330,15 @@ func (c *clientWrapper) GetEndpointSlicesForService(namespace, serviceName strin
|
||||
return c.factoriesKube[c.lookupNamespace(namespace)].Discovery().V1().EndpointSlices().Lister().EndpointSlices(namespace).List(serviceSelector)
|
||||
}
|
||||
|
||||
// GetConfigMap returns the named configMap from the given namespace.
|
||||
func (c *clientWrapper) GetConfigMap(namespace, name string) (*corev1.ConfigMap, error) {
|
||||
if !c.isWatchedNamespace(namespace) {
|
||||
return nil, fmt.Errorf("failed to get configmap %s/%s: namespace is not within watched namespaces", namespace, name)
|
||||
}
|
||||
|
||||
return c.factoriesConfigMap[c.lookupNamespace(namespace)].Core().V1().ConfigMaps().Lister().ConfigMaps(namespace).Get(name)
|
||||
}
|
||||
|
||||
// GetSecret returns the named secret from the given namespace.
|
||||
func (c *clientWrapper) GetSecret(namespace, name string) (*corev1.Secret, error) {
|
||||
if !c.isWatchedNamespace(namespace) {
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
---
|
||||
kind: Ingress
|
||||
apiVersion: networking.k8s.io/v1
|
||||
metadata:
|
||||
name: ingress-with-custom-headers
|
||||
namespace: default
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/custom-headers: default/custom-headers-configmap
|
||||
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: whoami.localhost
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Exact
|
||||
backend:
|
||||
service:
|
||||
name: whoami
|
||||
port:
|
||||
number: 80
|
||||
|
||||
---
|
||||
kind: ConfigMap
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: custom-headers-configmap
|
||||
namespace: default
|
||||
data:
|
||||
X-Custom-Header: "some-random-string"
|
||||
@@ -804,6 +804,40 @@ func (p *Provider) applyMiddlewares(namespace, routerKey string, ingressConfig i
|
||||
|
||||
applyUpstreamVhost(routerKey, ingressConfig, rt, conf)
|
||||
|
||||
if err := p.applyCustomHeaders(routerKey, ingressConfig, rt, conf); err != nil {
|
||||
return fmt.Errorf("applying custom headers: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) applyCustomHeaders(routerName string, ingressConfig ingressConfig, rt *dynamic.Router, conf *dynamic.Configuration) error {
|
||||
customHeaders := ptr.Deref(ingressConfig.CustomHeaders, "")
|
||||
if customHeaders == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
customHeadersParts := strings.Split(customHeaders, "/")
|
||||
if len(customHeadersParts) != 2 {
|
||||
return fmt.Errorf("invalid custom headers config map %q", customHeaders)
|
||||
}
|
||||
|
||||
configMapNamespace := customHeadersParts[0]
|
||||
configMapName := customHeadersParts[1]
|
||||
|
||||
configMap, err := p.k8sClient.GetConfigMap(configMapNamespace, configMapName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting configMap %s: %w", customHeaders, err)
|
||||
}
|
||||
|
||||
customHeadersMiddlewareName := routerName + "-custom-headers"
|
||||
conf.HTTP.Middlewares[customHeadersMiddlewareName] = &dynamic.Middleware{
|
||||
Headers: &dynamic.Headers{
|
||||
CustomResponseHeaders: configMap.Data,
|
||||
},
|
||||
}
|
||||
rt.Middlewares = append(rt.Middlewares, customHeadersMiddlewareName)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,58 @@ func TestLoadIngresses(t *testing.T) {
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Custom Headers",
|
||||
paths: []string{
|
||||
"services.yml",
|
||||
"ingressclasses.yml",
|
||||
"ingresses/11-ingress-with-custom-headers.yml",
|
||||
},
|
||||
expected: &dynamic.Configuration{
|
||||
TCP: &dynamic.TCPConfiguration{
|
||||
Routers: map[string]*dynamic.TCPRouter{},
|
||||
Services: map[string]*dynamic.TCPService{},
|
||||
},
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"default-ingress-with-custom-headers-rule-0-path-0": {
|
||||
Rule: "Host(`whoami.localhost`) && Path(`/`)",
|
||||
RuleSyntax: "default",
|
||||
Middlewares: []string{"default-ingress-with-custom-headers-rule-0-path-0-custom-headers"},
|
||||
Service: "default-whoami-80",
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{
|
||||
"default-ingress-with-custom-headers-rule-0-path-0-custom-headers": {
|
||||
Headers: &dynamic.Headers{
|
||||
CustomResponseHeaders: map[string]string{"X-Custom-Header": "some-random-string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Services: map[string]*dynamic.Service{
|
||||
"default-whoami-80": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{
|
||||
{
|
||||
URL: "http://10.10.0.1:80",
|
||||
},
|
||||
{
|
||||
URL: "http://10.10.0.2:80",
|
||||
},
|
||||
},
|
||||
Strategy: "wrr",
|
||||
PassHostHeader: ptr.To(true),
|
||||
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||
FlushInterval: dynamic.DefaultFlushInterval,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Basic Auth",
|
||||
paths: []string{
|
||||
|
||||
Reference in New Issue
Block a user