Implement a validation webhook
In case some ingress have a syntax error in the snippet configuration, the freshly generated configuration will not be reloaded to prevent tearing down existing rules. Although, once inserted, this configuration is preventing from any other valid configuration to be inserted as it remains in the ingresses of the cluster. To solve this problem, implement an optional validation webhook that simulates the addition of the ingress to be added together with the rest of ingresses. In case the generated configuration is not validated by nginx, deny the insertion of the ingress. In case certificates are mounted using kubernetes secrets, when those changes, keys are automatically updated in the container volume, and the controller reloads it using the filewatcher. Related changes: - Update vendors - Extract useful functions to check configuration with an additional ingress - Update documentation for validating webhook - Add validating webhook examples - Add a metric for each syntax check success and errors - Add more certificate generation examples
This commit is contained in:
parent
7283a01b9f
commit
1cd17cd12c
30 changed files with 3314 additions and 131 deletions
|
|
@ -23,23 +23,21 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/log"
|
||||
|
||||
"github.com/mitchellh/hashstructure"
|
||||
"k8s.io/klog"
|
||||
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/class"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/log"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/proxy"
|
||||
ngx_config "k8s.io/ingress-nginx/internal/ingress/controller/config"
|
||||
"k8s.io/ingress-nginx/internal/k8s"
|
||||
"k8s.io/klog"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -96,6 +94,10 @@ type Configuration struct {
|
|||
DynamicCertificatesEnabled bool
|
||||
|
||||
DisableCatchAll bool
|
||||
|
||||
ValidationWebhook string
|
||||
ValidationWebhookCertPath string
|
||||
ValidationWebhookKeyPath string
|
||||
}
|
||||
|
||||
// GetPublishService returns the Service used to set the load-balancer status of Ingresses.
|
||||
|
|
@ -120,47 +122,7 @@ func (n *NGINXController) syncIngress(interface{}) error {
|
|||
|
||||
ings := n.store.ListIngresses()
|
||||
|
||||
upstreams, servers := n.getBackendServers(ings)
|
||||
var passUpstreams []*ingress.SSLPassthroughBackend
|
||||
|
||||
hosts := sets.NewString()
|
||||
|
||||
for _, server := range servers {
|
||||
if !hosts.Has(server.Hostname) {
|
||||
hosts.Insert(server.Hostname)
|
||||
}
|
||||
if server.Alias != "" && !hosts.Has(server.Alias) {
|
||||
hosts.Insert(server.Alias)
|
||||
}
|
||||
|
||||
if !server.SSLPassthrough {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, loc := range server.Locations {
|
||||
if loc.Path != rootLocation {
|
||||
klog.Warningf("Ignoring SSL Passthrough for location %q in server %q", loc.Path, server.Hostname)
|
||||
continue
|
||||
}
|
||||
passUpstreams = append(passUpstreams, &ingress.SSLPassthroughBackend{
|
||||
Backend: loc.Backend,
|
||||
Hostname: server.Hostname,
|
||||
Service: loc.Service,
|
||||
Port: loc.Port,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
pcfg := &ingress.Configuration{
|
||||
Backends: upstreams,
|
||||
Servers: servers,
|
||||
TCPEndpoints: n.getStreamServices(n.cfg.TCPConfigMapName, apiv1.ProtocolTCP),
|
||||
UDPEndpoints: n.getStreamServices(n.cfg.UDPConfigMapName, apiv1.ProtocolUDP),
|
||||
PassthroughBackends: passUpstreams,
|
||||
BackendConfigChecksum: n.store.GetBackendConfiguration().Checksum,
|
||||
ControllerPodsCount: n.store.GetRunningControllerPodsCount(),
|
||||
}
|
||||
hosts, servers, pcfg := n.getConfiguration(ings)
|
||||
|
||||
if n.runningConfig.Equal(pcfg) {
|
||||
klog.V(3).Infof("No configuration change detected, skipping backend reload.")
|
||||
|
|
@ -235,6 +197,60 @@ func (n *NGINXController) syncIngress(interface{}) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// CheckIngress returns an error in case the provided ingress, when added
|
||||
// to the current configuration, generates an invalid configuration
|
||||
func (n *NGINXController) CheckIngress(ing *extensions.Ingress) error {
|
||||
if n == nil {
|
||||
return fmt.Errorf("cannot check ingress on a nil ingress controller")
|
||||
}
|
||||
if ing == nil {
|
||||
// no ingress to add, no state change
|
||||
return nil
|
||||
}
|
||||
if !class.IsValid(ing) {
|
||||
klog.Infof("ignoring ingress %v in %v based on annotation %v", ing.Name, ing.ObjectMeta.Namespace, class.IngressKey)
|
||||
return nil
|
||||
}
|
||||
if n.cfg.Namespace != "" && ing.ObjectMeta.Namespace != n.cfg.Namespace {
|
||||
klog.Infof("ignoring ingress %v in namespace %v different from the namespace watched %s", ing.Name, ing.ObjectMeta.Namespace, n.cfg.Namespace)
|
||||
return nil
|
||||
}
|
||||
|
||||
ings := n.store.ListIngresses()
|
||||
newIngress := &ingress.Ingress{
|
||||
Ingress: *ing,
|
||||
ParsedAnnotations: annotations.NewAnnotationExtractor(n.store).Extract(ing),
|
||||
}
|
||||
|
||||
for i, ingress := range ings {
|
||||
if ingress.Ingress.ObjectMeta.Name == ing.ObjectMeta.Name && ingress.Ingress.ObjectMeta.Namespace == ing.ObjectMeta.Namespace {
|
||||
ings[i] = newIngress
|
||||
newIngress = nil
|
||||
}
|
||||
}
|
||||
if newIngress != nil {
|
||||
ings = append(ings, newIngress)
|
||||
}
|
||||
|
||||
_, _, pcfg := n.getConfiguration(ings)
|
||||
|
||||
cfg := n.store.GetBackendConfiguration()
|
||||
cfg.Resolver = n.resolver
|
||||
|
||||
content, err := n.generateTemplate(cfg, *pcfg)
|
||||
if err != nil {
|
||||
n.metricCollector.IncCheckErrorCount(ing.ObjectMeta.Namespace, ing.Name)
|
||||
return err
|
||||
}
|
||||
err = n.testTemplate(content)
|
||||
if err != nil {
|
||||
n.metricCollector.IncCheckErrorCount(ing.ObjectMeta.Namespace, ing.Name)
|
||||
} else {
|
||||
n.metricCollector.IncCheckCount(ing.ObjectMeta.Namespace, ing.Name)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (n *NGINXController) getStreamServices(configmapName string, proto apiv1.Protocol) []ingress.L4Service {
|
||||
if configmapName == "" {
|
||||
return []ingress.L4Service{}
|
||||
|
|
@ -380,6 +396,51 @@ func (n *NGINXController) getDefaultUpstream() *ingress.Backend {
|
|||
return upstream
|
||||
}
|
||||
|
||||
// getConfiguration returns the configuration matching the standard kubernetes ingress
|
||||
func (n *NGINXController) getConfiguration(ingresses []*ingress.Ingress) (sets.String, []*ingress.Server, *ingress.Configuration) {
|
||||
upstreams, servers := n.getBackendServers(ingresses)
|
||||
var passUpstreams []*ingress.SSLPassthroughBackend
|
||||
|
||||
hosts := sets.NewString()
|
||||
|
||||
for _, server := range servers {
|
||||
if !hosts.Has(server.Hostname) {
|
||||
hosts.Insert(server.Hostname)
|
||||
}
|
||||
if server.Alias != "" && !hosts.Has(server.Alias) {
|
||||
hosts.Insert(server.Alias)
|
||||
}
|
||||
|
||||
if !server.SSLPassthrough {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, loc := range server.Locations {
|
||||
if loc.Path != rootLocation {
|
||||
klog.Warningf("Ignoring SSL Passthrough for location %q in server %q", loc.Path, server.Hostname)
|
||||
continue
|
||||
}
|
||||
passUpstreams = append(passUpstreams, &ingress.SSLPassthroughBackend{
|
||||
Backend: loc.Backend,
|
||||
Hostname: server.Hostname,
|
||||
Service: loc.Service,
|
||||
Port: loc.Port,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return hosts, servers, &ingress.Configuration{
|
||||
Backends: upstreams,
|
||||
Servers: servers,
|
||||
TCPEndpoints: n.getStreamServices(n.cfg.TCPConfigMapName, apiv1.ProtocolTCP),
|
||||
UDPEndpoints: n.getStreamServices(n.cfg.UDPConfigMapName, apiv1.ProtocolUDP),
|
||||
PassthroughBackends: passUpstreams,
|
||||
BackendConfigChecksum: n.store.GetBackendConfiguration().Checksum,
|
||||
ControllerPodsCount: n.store.GetRunningControllerPodsCount(),
|
||||
}
|
||||
}
|
||||
|
||||
// getBackendServers returns a list of Upstream and Server to be used by the
|
||||
// backend. An upstream can be used in multiple servers if the namespace,
|
||||
// service name and port are the same.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue