NGINX Ingress Provider
Co-authored-by: Kevin Pollet <pollet.kevin@gmail.com>
This commit is contained in:
parent
b39ee8ede5
commit
9bd5c61782
32 changed files with 3894 additions and 18 deletions
|
|
@ -92,6 +92,10 @@ func NewProviderAggregator(conf static.Providers) *ProviderAggregator {
|
|||
p.quietAddProvider(conf.KubernetesIngress)
|
||||
}
|
||||
|
||||
if conf.KubernetesIngressNGINX != nil {
|
||||
p.quietAddProvider(conf.KubernetesIngressNGINX)
|
||||
}
|
||||
|
||||
if conf.KubernetesCRD != nil {
|
||||
p.quietAddProvider(conf.KubernetesCRD)
|
||||
}
|
||||
|
|
|
|||
115
pkg/provider/kubernetes/ingress-nginx/annotations.go
Normal file
115
pkg/provider/kubernetes/ingress-nginx/annotations.go
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
package ingressnginx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
netv1 "k8s.io/api/networking/v1"
|
||||
)
|
||||
|
||||
type ingressConfig struct {
|
||||
AuthType *string `annotation:"nginx.ingress.kubernetes.io/auth-type"`
|
||||
AuthSecret *string `annotation:"nginx.ingress.kubernetes.io/auth-secret"`
|
||||
AuthRealm *string `annotation:"nginx.ingress.kubernetes.io/auth-realm"`
|
||||
AuthSecretType *string `annotation:"nginx.ingress.kubernetes.io/auth-secret-type"`
|
||||
|
||||
AuthURL *string `annotation:"nginx.ingress.kubernetes.io/auth-url"`
|
||||
AuthResponseHeaders *string `annotation:"nginx.ingress.kubernetes.io/auth-response-headers"`
|
||||
|
||||
ForceSSLRedirect *bool `annotation:"nginx.ingress.kubernetes.io/force-ssl-redirect"`
|
||||
SSLRedirect *bool `annotation:"nginx.ingress.kubernetes.io/ssl-redirect"`
|
||||
|
||||
SSLPassthrough *bool `annotation:"nginx.ingress.kubernetes.io/ssl-passthrough"`
|
||||
|
||||
UseRegex *bool `annotation:"nginx.ingress.kubernetes.io/use-regex"`
|
||||
|
||||
Affinity *string `annotation:"nginx.ingress.kubernetes.io/affinity"`
|
||||
SessionCookieName *string `annotation:"nginx.ingress.kubernetes.io/session-cookie-name"`
|
||||
SessionCookieSecure *bool `annotation:"nginx.ingress.kubernetes.io/session-cookie-secure"`
|
||||
SessionCookiePath *string `annotation:"nginx.ingress.kubernetes.io/session-cookie-path"`
|
||||
SessionCookieDomain *string `annotation:"nginx.ingress.kubernetes.io/session-cookie-domain"`
|
||||
SessionCookieSameSite *string `annotation:"nginx.ingress.kubernetes.io/session-cookie-samesite"`
|
||||
SessionCookieMaxAge *int `annotation:"nginx.ingress.kubernetes.io/session-cookie-max-age"`
|
||||
|
||||
ServiceUpstream *bool `annotation:"nginx.ingress.kubernetes.io/service-upstream"`
|
||||
|
||||
BackendProtocol *string `annotation:"nginx.ingress.kubernetes.io/backend-protocol"`
|
||||
|
||||
ProxySSLSecret *string `annotation:"nginx.ingress.kubernetes.io/proxy-ssl-secret"`
|
||||
ProxySSLVerify *string `annotation:"nginx.ingress.kubernetes.io/proxy-ssl-verify"`
|
||||
ProxySSLName *string `annotation:"nginx.ingress.kubernetes.io/proxy-ssl-name"`
|
||||
ProxySSLServerName *string `annotation:"nginx.ingress.kubernetes.io/proxy-ssl-server-name"`
|
||||
|
||||
EnableCORS *bool `annotation:"nginx.ingress.kubernetes.io/enable-cors"`
|
||||
EnableCORSAllowCredentials *bool `annotation:"nginx.ingress.kubernetes.io/cors-allow-credentials"`
|
||||
CORSExposeHeaders *[]string `annotation:"nginx.ingress.kubernetes.io/cors-expose-headers"`
|
||||
CORSAllowHeaders *[]string `annotation:"nginx.ingress.kubernetes.io/cors-allow-headers"`
|
||||
CORSAllowMethods *[]string `annotation:"nginx.ingress.kubernetes.io/cors-allow-methods"`
|
||||
CORSAllowOrigin *[]string `annotation:"nginx.ingress.kubernetes.io/cors-allow-origin"`
|
||||
CORSMaxAge *int `annotation:"nginx.ingress.kubernetes.io/cors-max-age"`
|
||||
}
|
||||
|
||||
// parseIngressConfig parses the annotations from an Ingress object into an ingressConfig struct.
|
||||
func parseIngressConfig(ing *netv1.Ingress) (ingressConfig, error) {
|
||||
cfg := ingressConfig{}
|
||||
cfgType := reflect.TypeOf(cfg)
|
||||
cfgValue := reflect.ValueOf(&cfg).Elem()
|
||||
|
||||
for i := range cfgType.NumField() {
|
||||
field := cfgType.Field(i)
|
||||
annotation := field.Tag.Get("annotation")
|
||||
if annotation == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
val, ok := ing.GetAnnotations()[annotation]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
switch field.Type.Elem().Kind() {
|
||||
case reflect.String:
|
||||
cfgValue.Field(i).Set(reflect.ValueOf(&val))
|
||||
case reflect.Bool:
|
||||
parsed, err := strconv.ParseBool(val)
|
||||
if err == nil {
|
||||
cfgValue.Field(i).Set(reflect.ValueOf(&parsed))
|
||||
}
|
||||
case reflect.Int:
|
||||
parsed, err := strconv.Atoi(val)
|
||||
if err == nil {
|
||||
cfgValue.Field(i).Set(reflect.ValueOf(&parsed))
|
||||
}
|
||||
case reflect.Slice:
|
||||
if field.Type.Elem().Elem().Kind() == reflect.String {
|
||||
// Handle slice of strings
|
||||
var slice []string
|
||||
elements := strings.Split(val, ",")
|
||||
for _, elt := range elements {
|
||||
slice = append(slice, strings.TrimSpace(elt))
|
||||
}
|
||||
cfgValue.Field(i).Set(reflect.ValueOf(&slice))
|
||||
} else {
|
||||
return cfg, errors.New("unsupported slice type in annotations")
|
||||
}
|
||||
default:
|
||||
return cfg, errors.New("unsupported kind")
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// parseBackendProtocol parses the backend protocol annotation and returns the corresponding protocol string.
|
||||
func parseBackendProtocol(bp string) string {
|
||||
switch strings.ToUpper(bp) {
|
||||
case "HTTPS", "GRPCS":
|
||||
return "https"
|
||||
case "GRPC":
|
||||
return "h2c"
|
||||
default:
|
||||
return "http"
|
||||
}
|
||||
}
|
||||
76
pkg/provider/kubernetes/ingress-nginx/annotations_test.go
Normal file
76
pkg/provider/kubernetes/ingress-nginx/annotations_test.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package ingressnginx
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
netv1 "k8s.io/api/networking/v1"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func Test_parseIngressConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
annotations map[string]string
|
||||
expected ingressConfig
|
||||
}{
|
||||
{
|
||||
desc: "all fields set",
|
||||
annotations: map[string]string{
|
||||
"nginx.ingress.kubernetes.io/ssl-passthrough": "true",
|
||||
"nginx.ingress.kubernetes.io/affinity": "cookie",
|
||||
"nginx.ingress.kubernetes.io/session-cookie-name": "mycookie",
|
||||
"nginx.ingress.kubernetes.io/session-cookie-secure": "true",
|
||||
"nginx.ingress.kubernetes.io/session-cookie-path": "/foo",
|
||||
"nginx.ingress.kubernetes.io/session-cookie-domain": "example.com",
|
||||
"nginx.ingress.kubernetes.io/session-cookie-samesite": "Strict",
|
||||
"nginx.ingress.kubernetes.io/session-cookie-max-age": "3600",
|
||||
"nginx.ingress.kubernetes.io/backend-protocol": "HTTPS",
|
||||
"nginx.ingress.kubernetes.io/cors-expose-headers": "foo, bar",
|
||||
},
|
||||
expected: ingressConfig{
|
||||
SSLPassthrough: ptr.To(true),
|
||||
Affinity: ptr.To("cookie"),
|
||||
SessionCookieName: ptr.To("mycookie"),
|
||||
SessionCookieSecure: ptr.To(true),
|
||||
SessionCookiePath: ptr.To("/foo"),
|
||||
SessionCookieDomain: ptr.To("example.com"),
|
||||
SessionCookieSameSite: ptr.To("Strict"),
|
||||
SessionCookieMaxAge: ptr.To(3600),
|
||||
BackendProtocol: ptr.To("HTTPS"),
|
||||
CORSExposeHeaders: ptr.To([]string{"foo", "bar"}),
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "missing fields",
|
||||
annotations: map[string]string{
|
||||
"nginx.ingress.kubernetes.io/ssl-passthrough": "false",
|
||||
},
|
||||
expected: ingressConfig{
|
||||
SSLPassthrough: ptr.To(false),
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "invalid bool and int",
|
||||
annotations: map[string]string{
|
||||
"nginx.ingress.kubernetes.io/ssl-passthrough": "notabool",
|
||||
"nginx.ingress.kubernetes.io/session-cookie-max-age (in seconds)": "notanint",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var ing netv1.Ingress
|
||||
ing.SetAnnotations(test.annotations)
|
||||
|
||||
cfg, err := parseIngressConfig(&ing)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, test.expected, cfg)
|
||||
})
|
||||
}
|
||||
}
|
||||
384
pkg/provider/kubernetes/ingress-nginx/client.go
Normal file
384
pkg/provider/kubernetes/ingress-nginx/client.go
Normal file
|
|
@ -0,0 +1,384 @@
|
|||
package ingressnginx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/traefik/traefik/v3/pkg/provider/kubernetes/k8s"
|
||||
"github.com/traefik/traefik/v3/pkg/types"
|
||||
traefikversion "github.com/traefik/traefik/v3/pkg/version"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
discoveryv1 "k8s.io/api/discovery/v1"
|
||||
netv1 "k8s.io/api/networking/v1"
|
||||
kerror "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
kinformers "k8s.io/client-go/informers"
|
||||
kclientset "k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
const (
|
||||
resyncPeriod = 10 * time.Minute
|
||||
defaultTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
type clientWrapper struct {
|
||||
clientset kclientset.Interface
|
||||
clusterScopeFactory kinformers.SharedInformerFactory
|
||||
factoriesKube map[string]kinformers.SharedInformerFactory
|
||||
factoriesSecret map[string]kinformers.SharedInformerFactory
|
||||
factoriesIngress map[string]kinformers.SharedInformerFactory
|
||||
isNamespaceAll bool
|
||||
watchedNamespaces []string
|
||||
|
||||
ignoreIngressClasses bool
|
||||
}
|
||||
|
||||
// newInClusterClient returns a new Provider client that is expected to run
|
||||
// inside the cluster.
|
||||
func newInClusterClient(endpoint string) (*clientWrapper, error) {
|
||||
config, err := rest.InClusterConfig()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create in-cluster configuration: %w", err)
|
||||
}
|
||||
|
||||
if endpoint != "" {
|
||||
config.Host = endpoint
|
||||
}
|
||||
|
||||
return createClientFromConfig(config)
|
||||
}
|
||||
|
||||
func newExternalClusterClientFromFile(file string) (*clientWrapper, error) {
|
||||
configFromFlags, err := clientcmd.BuildConfigFromFlags("", file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return createClientFromConfig(configFromFlags)
|
||||
}
|
||||
|
||||
// newExternalClusterClient returns a new Provider client that may run outside
|
||||
// of the cluster.
|
||||
// The endpoint parameter must not be empty.
|
||||
func newExternalClusterClient(endpoint, caFilePath string, token types.FileOrContent) (*clientWrapper, error) {
|
||||
if endpoint == "" {
|
||||
return nil, errors.New("endpoint missing for external cluster client")
|
||||
}
|
||||
|
||||
tokenData, err := token.Read()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read token: %w", err)
|
||||
}
|
||||
|
||||
config := &rest.Config{
|
||||
Host: endpoint,
|
||||
BearerToken: string(tokenData),
|
||||
}
|
||||
|
||||
if caFilePath != "" {
|
||||
caData, err := os.ReadFile(caFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read CA file %s: %w", caFilePath, err)
|
||||
}
|
||||
|
||||
config.TLSClientConfig = rest.TLSClientConfig{CAData: caData}
|
||||
}
|
||||
return createClientFromConfig(config)
|
||||
}
|
||||
|
||||
func createClientFromConfig(c *rest.Config) (*clientWrapper, error) {
|
||||
c.UserAgent = fmt.Sprintf(
|
||||
"%s/%s (%s/%s) kubernetes/ingress",
|
||||
filepath.Base(os.Args[0]),
|
||||
traefikversion.Version,
|
||||
runtime.GOOS,
|
||||
runtime.GOARCH,
|
||||
)
|
||||
|
||||
clientset, err := kclientset.NewForConfig(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newClient(clientset), nil
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
// WatchAll starts namespace-specific controllers for all relevant kinds.
|
||||
func (c *clientWrapper) WatchAll(ctx context.Context, namespace, namespaceSelector string) (<-chan interface{}, error) {
|
||||
stopCh := ctx.Done()
|
||||
eventCh := make(chan interface{}, 1)
|
||||
eventHandler := &k8s.ResourceEventHandler{Ev: eventCh}
|
||||
|
||||
c.ignoreIngressClasses = false
|
||||
_, err := c.clientset.NetworkingV1().IngressClasses().List(ctx, metav1.ListOptions{Limit: 1})
|
||||
if err != nil {
|
||||
if !kerror.IsNotFound(err) {
|
||||
if kerror.IsForbidden(err) {
|
||||
c.ignoreIngressClasses = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if namespaceSelector != "" {
|
||||
ns, err := c.clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{LabelSelector: namespaceSelector})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listing namespaces: %w", err)
|
||||
}
|
||||
for _, item := range ns.Items {
|
||||
c.watchedNamespaces = append(c.watchedNamespaces, item.Name)
|
||||
}
|
||||
} else {
|
||||
c.isNamespaceAll = namespace == metav1.NamespaceAll
|
||||
c.watchedNamespaces = []string{namespace}
|
||||
}
|
||||
|
||||
notOwnedByHelm := func(opts *metav1.ListOptions) {
|
||||
opts.LabelSelector = "owner!=helm"
|
||||
}
|
||||
|
||||
for _, ns := range c.watchedNamespaces {
|
||||
factoryIngress := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns))
|
||||
|
||||
_, err := factoryIngress.Networking().V1().Ingresses().Informer().AddEventHandler(eventHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.factoriesIngress[ns] = factoryIngress
|
||||
|
||||
factoryKube := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns))
|
||||
_, err = factoryKube.Core().V1().Services().Informer().AddEventHandler(eventHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = factoryKube.Discovery().V1().EndpointSlices().Informer().AddEventHandler(eventHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.factoriesKube[ns] = factoryKube
|
||||
|
||||
factorySecret := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(notOwnedByHelm))
|
||||
_, err = factorySecret.Core().V1().Secrets().Informer().AddEventHandler(eventHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.factoriesSecret[ns] = factorySecret
|
||||
}
|
||||
|
||||
for _, ns := range c.watchedNamespaces {
|
||||
c.factoriesIngress[ns].Start(stopCh)
|
||||
c.factoriesKube[ns].Start(stopCh)
|
||||
c.factoriesSecret[ns].Start(stopCh)
|
||||
}
|
||||
|
||||
for _, ns := range c.watchedNamespaces {
|
||||
for t, ok := range c.factoriesIngress[ns].WaitForCacheSync(stopCh) {
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("timed out waiting for controller caches to sync %s in namespace %q", t.String(), ns)
|
||||
}
|
||||
}
|
||||
|
||||
for t, ok := range c.factoriesKube[ns].WaitForCacheSync(stopCh) {
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("timed out waiting for controller caches to sync %s in namespace %q", t.String(), ns)
|
||||
}
|
||||
}
|
||||
|
||||
for t, ok := range c.factoriesSecret[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)
|
||||
|
||||
if !c.ignoreIngressClasses {
|
||||
_, err = c.clusterScopeFactory.Networking().V1().IngressClasses().Informer().AddEventHandler(eventHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
c.clusterScopeFactory.Start(stopCh)
|
||||
|
||||
for t, ok := range c.clusterScopeFactory.WaitForCacheSync(stopCh) {
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("timed out waiting for controller caches to sync %s", t.String())
|
||||
}
|
||||
}
|
||||
|
||||
return eventCh, nil
|
||||
}
|
||||
|
||||
func (c *clientWrapper) ListIngressClasses() ([]*netv1.IngressClass, error) {
|
||||
if c.ignoreIngressClasses {
|
||||
return []*netv1.IngressClass{}, nil
|
||||
}
|
||||
|
||||
return c.clusterScopeFactory.Networking().V1().IngressClasses().Lister().List(labels.Everything())
|
||||
}
|
||||
|
||||
// ListIngresses returns all Ingresses for observed namespaces in the cluster.
|
||||
func (c *clientWrapper) ListIngresses() []*netv1.Ingress {
|
||||
var results []*netv1.Ingress
|
||||
|
||||
for ns, factory := range c.factoriesIngress {
|
||||
// networking
|
||||
listNew, err := factory.Networking().V1().Ingresses().Lister().List(labels.Everything())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Failed to list ingresses in namespace %s", ns)
|
||||
continue
|
||||
}
|
||||
|
||||
results = append(results, listNew...)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// UpdateIngressStatus updates an Ingress with a provided status.
|
||||
func (c *clientWrapper) UpdateIngressStatus(src *netv1.Ingress, ingStatus []netv1.IngressLoadBalancerIngress) error {
|
||||
if !c.isWatchedNamespace(src.Namespace) {
|
||||
return fmt.Errorf("failed to get ingress %s/%s: namespace is not within watched namespaces", src.Namespace, src.Name)
|
||||
}
|
||||
|
||||
ing, err := c.factoriesIngress[c.lookupNamespace(src.Namespace)].Networking().V1().Ingresses().Lister().Ingresses(src.Namespace).Get(src.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get ingress %s/%s: %w", src.Namespace, src.Name, err)
|
||||
}
|
||||
|
||||
logger := log.With().Str("namespace", ing.Namespace).Str("ingress", ing.Name).Logger()
|
||||
|
||||
if isLoadBalancerIngressEquals(ing.Status.LoadBalancer.Ingress, ingStatus) {
|
||||
logger.Debug().Msg("Skipping ingress status update")
|
||||
return nil
|
||||
}
|
||||
|
||||
ingCopy := ing.DeepCopy()
|
||||
ingCopy.Status = netv1.IngressStatus{LoadBalancer: netv1.IngressLoadBalancerStatus{Ingress: ingStatus}}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
_, err = c.clientset.NetworkingV1().Ingresses(ingCopy.Namespace).UpdateStatus(ctx, ingCopy, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update ingress status %s/%s: %w", src.Namespace, src.Name, err)
|
||||
}
|
||||
|
||||
logger.Info().Msg("Updated ingress status")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetService returns the named service from the given namespace.
|
||||
func (c *clientWrapper) GetService(namespace, name string) (*corev1.Service, error) {
|
||||
if !c.isWatchedNamespace(namespace) {
|
||||
return nil, fmt.Errorf("failed to get service %s/%s: namespace is not within watched namespaces", namespace, name)
|
||||
}
|
||||
|
||||
return c.factoriesKube[c.lookupNamespace(namespace)].Core().V1().Services().Lister().Services(namespace).Get(name)
|
||||
}
|
||||
|
||||
// GetEndpointSlicesForService returns the EndpointSlices for the given service name in the given namespace.
|
||||
func (c *clientWrapper) GetEndpointSlicesForService(namespace, serviceName string) ([]*discoveryv1.EndpointSlice, error) {
|
||||
if !c.isWatchedNamespace(namespace) {
|
||||
return nil, fmt.Errorf("failed to get endpointslices for service %s/%s: namespace is not within watched namespaces", namespace, serviceName)
|
||||
}
|
||||
|
||||
serviceLabelRequirement, err := labels.NewRequirement(discoveryv1.LabelServiceName, selection.Equals, []string{serviceName})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create service label selector requirement: %w", err)
|
||||
}
|
||||
serviceSelector := labels.NewSelector()
|
||||
serviceSelector = serviceSelector.Add(*serviceLabelRequirement)
|
||||
|
||||
return c.factoriesKube[c.lookupNamespace(namespace)].Discovery().V1().EndpointSlices().Lister().EndpointSlices(namespace).List(serviceSelector)
|
||||
}
|
||||
|
||||
// GetSecret returns the named secret from the given namespace.
|
||||
func (c *clientWrapper) GetSecret(namespace, name string) (*corev1.Secret, error) {
|
||||
if !c.isWatchedNamespace(namespace) {
|
||||
return nil, fmt.Errorf("failed to get secret %s/%s: namespace is not within watched namespaces", namespace, name)
|
||||
}
|
||||
|
||||
return c.factoriesSecret[c.lookupNamespace(namespace)].Core().V1().Secrets().Lister().Secrets(namespace).Get(name)
|
||||
}
|
||||
|
||||
// lookupNamespace returns the lookup namespace key for the given namespace.
|
||||
// When listening on all namespaces, it returns the client-go identifier ("")
|
||||
// for all-namespaces. Otherwise, it returns the given namespace.
|
||||
// The distinction is necessary because we index all informers on the special
|
||||
// identifier iff all-namespaces are requested but receive specific namespace
|
||||
// identifiers from the Kubernetes API, so we have to bridge this gap.
|
||||
func (c *clientWrapper) lookupNamespace(ns string) string {
|
||||
if c.isNamespaceAll {
|
||||
return metav1.NamespaceAll
|
||||
}
|
||||
return ns
|
||||
}
|
||||
|
||||
// isWatchedNamespace checks to ensure that the namespace is being watched before we request
|
||||
// it to ensure we don't panic by requesting an out-of-watch object.
|
||||
func (c *clientWrapper) isWatchedNamespace(ns string) bool {
|
||||
if c.isNamespaceAll {
|
||||
return true
|
||||
}
|
||||
|
||||
return slices.Contains(c.watchedNamespaces, ns)
|
||||
}
|
||||
|
||||
// isLoadBalancerIngressEquals returns true if the given slices are equal, false otherwise.
|
||||
func isLoadBalancerIngressEquals(aSlice, bSlice []netv1.IngressLoadBalancerIngress) bool {
|
||||
if len(aSlice) != len(bSlice) {
|
||||
return false
|
||||
}
|
||||
|
||||
aMap := make(map[string]struct{})
|
||||
for _, aIngress := range aSlice {
|
||||
aMap[aIngress.Hostname+aIngress.IP] = struct{}{}
|
||||
}
|
||||
|
||||
for _, bIngress := range bSlice {
|
||||
if _, exists := aMap[bIngress.Hostname+bIngress.IP]; !exists {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// filterIngressClass return a slice containing IngressClass matching either the annotation name or the controller.
|
||||
func filterIngressClass(ingressClasses []*netv1.IngressClass, ingressClassByName bool, ingressClass, controllerClass string) []*netv1.IngressClass {
|
||||
var filteredIngressClasses []*netv1.IngressClass
|
||||
for _, ic := range ingressClasses {
|
||||
if ingressClassByName && ic.Name == ingressClass {
|
||||
return append(filteredIngressClasses, ic)
|
||||
}
|
||||
|
||||
if ic.Spec.Controller == controllerClass {
|
||||
filteredIngressClasses = append(filteredIngressClasses, ic)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return filteredIngressClasses
|
||||
}
|
||||
270
pkg/provider/kubernetes/ingress-nginx/client_test.go
Normal file
270
pkg/provider/kubernetes/ingress-nginx/client_test.go
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
package ingressnginx
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
discoveryv1 "k8s.io/api/discovery/v1"
|
||||
netv1 "k8s.io/api/networking/v1"
|
||||
kerror "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
kversion "k8s.io/apimachinery/pkg/version"
|
||||
discoveryfake "k8s.io/client-go/discovery/fake"
|
||||
kubefake "k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func TestIsLoadBalancerIngressEquals(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
aSlice []netv1.IngressLoadBalancerIngress
|
||||
bSlice []netv1.IngressLoadBalancerIngress
|
||||
expectedEqual bool
|
||||
}{
|
||||
{
|
||||
desc: "both slices are empty",
|
||||
expectedEqual: true,
|
||||
},
|
||||
{
|
||||
desc: "not the same length",
|
||||
bSlice: []netv1.IngressLoadBalancerIngress{
|
||||
{IP: "192.168.1.1", Hostname: "traefik"},
|
||||
},
|
||||
expectedEqual: false,
|
||||
},
|
||||
{
|
||||
desc: "same ordered content",
|
||||
aSlice: []netv1.IngressLoadBalancerIngress{
|
||||
{IP: "192.168.1.1", Hostname: "traefik"},
|
||||
},
|
||||
bSlice: []netv1.IngressLoadBalancerIngress{
|
||||
{IP: "192.168.1.1", Hostname: "traefik"},
|
||||
},
|
||||
expectedEqual: true,
|
||||
},
|
||||
{
|
||||
desc: "same unordered content",
|
||||
aSlice: []netv1.IngressLoadBalancerIngress{
|
||||
{IP: "192.168.1.1", Hostname: "traefik"},
|
||||
{IP: "192.168.1.2", Hostname: "traefik2"},
|
||||
},
|
||||
bSlice: []netv1.IngressLoadBalancerIngress{
|
||||
{IP: "192.168.1.2", Hostname: "traefik2"},
|
||||
{IP: "192.168.1.1", Hostname: "traefik"},
|
||||
},
|
||||
expectedEqual: true,
|
||||
},
|
||||
{
|
||||
desc: "different ordered content",
|
||||
aSlice: []netv1.IngressLoadBalancerIngress{
|
||||
{IP: "192.168.1.1", Hostname: "traefik"},
|
||||
{IP: "192.168.1.2", Hostname: "traefik2"},
|
||||
},
|
||||
bSlice: []netv1.IngressLoadBalancerIngress{
|
||||
{IP: "192.168.1.1", Hostname: "traefik"},
|
||||
{IP: "192.168.1.2", Hostname: "traefik"},
|
||||
},
|
||||
expectedEqual: false,
|
||||
},
|
||||
{
|
||||
desc: "different unordered content",
|
||||
aSlice: []netv1.IngressLoadBalancerIngress{
|
||||
{IP: "192.168.1.1", Hostname: "traefik"},
|
||||
{IP: "192.168.1.2", Hostname: "traefik2"},
|
||||
},
|
||||
bSlice: []netv1.IngressLoadBalancerIngress{
|
||||
{IP: "192.168.1.2", Hostname: "traefik3"},
|
||||
{IP: "192.168.1.1", Hostname: "traefik"},
|
||||
},
|
||||
expectedEqual: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
gotEqual := isLoadBalancerIngressEquals(test.aSlice, test.bSlice)
|
||||
assert.Equal(t, test.expectedEqual, gotEqual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientIgnoresHelmOwnedSecrets(t *testing.T) {
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Name: "secret",
|
||||
},
|
||||
}
|
||||
helmSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Name: "helm-secret",
|
||||
Labels: map[string]string{
|
||||
"owner": "helm",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
kubeClient := kubefake.NewClientset(helmSecret, secret)
|
||||
|
||||
discovery, _ := kubeClient.Discovery().(*discoveryfake.FakeDiscovery)
|
||||
discovery.FakedServerVersion = &kversion.Info{
|
||||
GitVersion: "v1.19",
|
||||
}
|
||||
|
||||
client := newClient(kubeClient)
|
||||
|
||||
eventCh, err := client.WatchAll(t.Context(), "", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case event := <-eventCh:
|
||||
secret, ok := event.(*corev1.Secret)
|
||||
require.True(t, ok)
|
||||
|
||||
assert.NotEqual(t, "helm-secret", secret.Name)
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
assert.Fail(t, "expected to receive event for secret")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-eventCh:
|
||||
assert.Fail(t, "received more than one event")
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
|
||||
_, err = client.GetSecret("default", "secret")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = client.GetSecret("default", "helm-secret")
|
||||
assert.True(t, kerror.IsNotFound(err))
|
||||
}
|
||||
|
||||
func TestClientIgnoresEmptyEndpointSliceUpdates(t *testing.T) {
|
||||
emptyEndpointSlice := &discoveryv1.EndpointSlice{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "empty-endpointslice",
|
||||
Namespace: "test",
|
||||
ResourceVersion: "1244",
|
||||
Annotations: map[string]string{
|
||||
"test-annotation": "_",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
samplePortName := "testing"
|
||||
samplePortNumber := int32(1337)
|
||||
samplePortProtocol := corev1.ProtocolTCP
|
||||
sampleAddressReady := true
|
||||
filledEndpointSlice := &discoveryv1.EndpointSlice{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "filled-endpointslice",
|
||||
Namespace: "test",
|
||||
ResourceVersion: "1234",
|
||||
},
|
||||
AddressType: discoveryv1.AddressTypeIPv4,
|
||||
Endpoints: []discoveryv1.Endpoint{{
|
||||
Addresses: []string{"10.13.37.1"},
|
||||
Conditions: discoveryv1.EndpointConditions{
|
||||
Ready: &sampleAddressReady,
|
||||
},
|
||||
}},
|
||||
Ports: []discoveryv1.EndpointPort{{
|
||||
Name: &samplePortName,
|
||||
Port: &samplePortNumber,
|
||||
Protocol: &samplePortProtocol,
|
||||
}},
|
||||
}
|
||||
|
||||
kubeClient := kubefake.NewClientset(emptyEndpointSlice, filledEndpointSlice)
|
||||
|
||||
discovery, _ := kubeClient.Discovery().(*discoveryfake.FakeDiscovery)
|
||||
discovery.FakedServerVersion = &kversion.Info{
|
||||
GitVersion: "v1.19",
|
||||
}
|
||||
|
||||
client := newClient(kubeClient)
|
||||
|
||||
eventCh, err := client.WatchAll(t.Context(), "", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case event := <-eventCh:
|
||||
ep, ok := event.(*discoveryv1.EndpointSlice)
|
||||
require.True(t, ok)
|
||||
|
||||
assert.True(t, ep.Name == "empty-endpointslice" || ep.Name == "filled-endpointslice")
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
assert.Fail(t, "expected to receive event for endpointslices")
|
||||
}
|
||||
|
||||
emptyEndpointSlice, err = kubeClient.DiscoveryV1().EndpointSlices("test").Get(t.Context(), "empty-endpointslice", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Update endpoint annotation and resource version (apparently not done by fake client itself)
|
||||
// to show an update that should not trigger an update event on our eventCh.
|
||||
// This reflects the behavior of kubernetes controllers which use endpoint annotations for leader election.
|
||||
emptyEndpointSlice.Annotations["test-annotation"] = "___"
|
||||
emptyEndpointSlice.ResourceVersion = "1245"
|
||||
_, err = kubeClient.DiscoveryV1().EndpointSlices("test").Update(t.Context(), emptyEndpointSlice, metav1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case event := <-eventCh:
|
||||
ep, ok := event.(*discoveryv1.EndpointSlice)
|
||||
require.True(t, ok)
|
||||
|
||||
assert.Fail(t, "didn't expect to receive event for empty endpointslice update", ep.Name)
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
|
||||
filledEndpointSlice, err = kubeClient.DiscoveryV1().EndpointSlices("test").Get(t.Context(), "filled-endpointslice", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
filledEndpointSlice.Endpoints[0].Addresses[0] = "10.13.37.2"
|
||||
filledEndpointSlice.ResourceVersion = "1235"
|
||||
_, err = kubeClient.DiscoveryV1().EndpointSlices("test").Update(t.Context(), filledEndpointSlice, metav1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case event := <-eventCh:
|
||||
ep, ok := event.(*discoveryv1.EndpointSlice)
|
||||
require.True(t, ok)
|
||||
|
||||
assert.Equal(t, "filled-endpointslice", ep.Name)
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
assert.Fail(t, "expected to receive event for filled endpointslice")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-eventCh:
|
||||
assert.Fail(t, "received more than one event")
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
|
||||
newPortNumber := int32(42)
|
||||
filledEndpointSlice.Ports[0].Port = &newPortNumber
|
||||
filledEndpointSlice.ResourceVersion = "1236"
|
||||
_, err = kubeClient.DiscoveryV1().EndpointSlices("test").Update(t.Context(), filledEndpointSlice, metav1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case event := <-eventCh:
|
||||
ep, ok := event.(*discoveryv1.EndpointSlice)
|
||||
require.True(t, ok)
|
||||
|
||||
assert.Equal(t, "filled-endpointslice", ep.Name)
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
assert.Fail(t, "expected to receive event for filled endpointslice")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-eventCh:
|
||||
assert.Fail(t, "received more than one event")
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
69
pkg/provider/kubernetes/ingress-nginx/convert.go
Normal file
69
pkg/provider/kubernetes/ingress-nginx/convert.go
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
package ingressnginx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
netv1 "k8s.io/api/networking/v1"
|
||||
)
|
||||
|
||||
type marshaler interface {
|
||||
Marshal() ([]byte, error)
|
||||
}
|
||||
|
||||
type unmarshaler interface {
|
||||
Unmarshal(data []byte) error
|
||||
}
|
||||
|
||||
type LoadBalancerIngress interface {
|
||||
corev1.LoadBalancerIngress | netv1.IngressLoadBalancerIngress
|
||||
}
|
||||
|
||||
// convertSlice converts slice of LoadBalancerIngress to slice of LoadBalancerIngress.
|
||||
// O (Bar), I (Foo) => []Bar.
|
||||
func convertSlice[O LoadBalancerIngress, I LoadBalancerIngress](loadBalancerIngresses []I) ([]O, error) {
|
||||
var results []O
|
||||
|
||||
for _, loadBalancerIngress := range loadBalancerIngresses {
|
||||
mar, ok := any(&loadBalancerIngress).(marshaler)
|
||||
if !ok {
|
||||
// All the pointer of types related to the interface LoadBalancerIngress are compatible with the interface marshaler.
|
||||
continue
|
||||
}
|
||||
|
||||
um, err := convert[O](mar)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v, ok := any(*um).(O)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
results = append(results, v)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// convert must only be used with unmarshaler and marshaler compatible types.
|
||||
func convert[T any](input marshaler) (*T, error) {
|
||||
data, err := input.Marshal()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var output T
|
||||
um, ok := any(&output).(unmarshaler)
|
||||
if !ok {
|
||||
return nil, errors.New("the output type doesn't implement unmarshaler interface")
|
||||
}
|
||||
|
||||
err = um.Unmarshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &output, nil
|
||||
}
|
||||
77
pkg/provider/kubernetes/ingress-nginx/convert_test.go
Normal file
77
pkg/provider/kubernetes/ingress-nginx/convert_test.go
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
package ingressnginx
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
netv1 "k8s.io/api/networking/v1"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func Test_convertSlice_corev1_to_networkingv1(t *testing.T) {
|
||||
g := []corev1.LoadBalancerIngress{
|
||||
{
|
||||
IP: "132456",
|
||||
Hostname: "foo",
|
||||
Ports: []corev1.PortStatus{
|
||||
{
|
||||
Port: 123,
|
||||
Protocol: "https",
|
||||
Error: ptr.To("test"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
actual, err := convertSlice[netv1.IngressLoadBalancerIngress](g)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := []netv1.IngressLoadBalancerIngress{
|
||||
{
|
||||
IP: "132456",
|
||||
Hostname: "foo",
|
||||
Ports: []netv1.IngressPortStatus{
|
||||
{
|
||||
Port: 123,
|
||||
Protocol: "https",
|
||||
Error: ptr.To("test"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
func Test_convert(t *testing.T) {
|
||||
g := &corev1.LoadBalancerIngress{
|
||||
IP: "132456",
|
||||
Hostname: "foo",
|
||||
Ports: []corev1.PortStatus{
|
||||
{
|
||||
Port: 123,
|
||||
Protocol: "https",
|
||||
Error: ptr.To("test"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
actual, err := convert[netv1.IngressLoadBalancerIngress](g)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := &netv1.IngressLoadBalancerIngress{
|
||||
IP: "132456",
|
||||
Hostname: "foo",
|
||||
Ports: []netv1.IngressPortStatus{
|
||||
{
|
||||
Port: 123,
|
||||
Protocol: "https",
|
||||
Error: ptr.To("test"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: IngressClass
|
||||
metadata:
|
||||
name: nginx
|
||||
spec:
|
||||
controller: k8s.io/ingress-nginx
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
kind: Ingress
|
||||
apiVersion: networking.k8s.io/v1
|
||||
metadata:
|
||||
name: ingress-with-basicauth
|
||||
namespace: default
|
||||
annotations:
|
||||
# Configuration basic authentication for the Ingress
|
||||
nginx.ingress.kubernetes.io/auth-type: "basic"
|
||||
nginx.ingress.kubernetes.io/auth-secret-type: "auth-file"
|
||||
nginx.ingress.kubernetes.io/auth-secret: "default/basic-auth"
|
||||
nginx.ingress.kubernetes.io/auth-realm: "Authentication Required"
|
||||
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: whoami.localhost
|
||||
http:
|
||||
paths:
|
||||
- path: /basicauth
|
||||
pathType: Exact
|
||||
backend:
|
||||
service:
|
||||
name: whoami
|
||||
port:
|
||||
number: 80
|
||||
|
||||
---
|
||||
kind: Secret
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: basic-auth
|
||||
namespace: default
|
||||
type: Opaque
|
||||
data:
|
||||
# user:password
|
||||
auth: dXNlcjp7U0hBfVc2cGg1TW01UHo4R2dpVUxiUGd6RzM3bWo5Zz0=
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
kind: Ingress
|
||||
apiVersion: networking.k8s.io/v1
|
||||
metadata:
|
||||
name: ingress-with-forwardauth
|
||||
namespace: default
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/auth-url: "http://whoami.default.svc/"
|
||||
nginx.ingress.kubernetes.io/auth-method: "GET"
|
||||
nginx.ingress.kubernetes.io/auth-response-headers: "X-Foo"
|
||||
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: whoami.localhost
|
||||
http:
|
||||
paths:
|
||||
- path: /forwardauth
|
||||
pathType: Exact
|
||||
backend:
|
||||
service:
|
||||
name: whoami
|
||||
port:
|
||||
number: 80
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
---
|
||||
kind: Ingress
|
||||
apiVersion: networking.k8s.io/v1
|
||||
metadata:
|
||||
name: ingress-with-ssl-redirect
|
||||
namespace: default
|
||||
|
||||
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: sslredirect.localhost
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Exact
|
||||
backend:
|
||||
service:
|
||||
name: whoami
|
||||
port:
|
||||
number: 80
|
||||
tls:
|
||||
- hosts:
|
||||
- sslredirect.localhost
|
||||
secretName: whoami-tls
|
||||
|
||||
---
|
||||
kind: Ingress
|
||||
apiVersion: networking.k8s.io/v1
|
||||
metadata:
|
||||
name: ingress-without-ssl-redirect
|
||||
namespace: default
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "false"
|
||||
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: withoutsslredirect.localhost
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Exact
|
||||
backend:
|
||||
service:
|
||||
name: whoami
|
||||
port:
|
||||
number: 80
|
||||
tls:
|
||||
- hosts:
|
||||
- withoutsslredirect.localhost
|
||||
secretName: whoami-tls
|
||||
|
||||
---
|
||||
kind: Ingress
|
||||
apiVersion: networking.k8s.io/v1
|
||||
metadata:
|
||||
name: ingress-with-force-ssl-redirect
|
||||
namespace: default
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
|
||||
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: forcesslredirect.localhost
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Exact
|
||||
backend:
|
||||
service:
|
||||
name: whoami
|
||||
port:
|
||||
number: 80
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
kind: Ingress
|
||||
apiVersion: networking.k8s.io/v1
|
||||
metadata:
|
||||
name: ingress-with-ssl-passthrough
|
||||
namespace: default
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/ssl-passthrough: "true"
|
||||
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: passthrough.whoami.localhost
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: whoami-tls
|
||||
port:
|
||||
number: 443
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
kind: Ingress
|
||||
apiVersion: networking.k8s.io/v1
|
||||
metadata:
|
||||
name: ingress-with-default-backend
|
||||
namespace: default
|
||||
|
||||
spec:
|
||||
defaultBackend:
|
||||
service:
|
||||
name: whoami-default
|
||||
port:
|
||||
number: 80
|
||||
|
||||
rules:
|
||||
- http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Exact
|
||||
backend:
|
||||
service:
|
||||
name: whoami
|
||||
port:
|
||||
number: 80
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
|
||||
---
|
||||
kind: Ingress
|
||||
apiVersion: networking.k8s.io/v1
|
||||
metadata:
|
||||
name: ingress-with-default-backend2
|
||||
namespace: default
|
||||
# annotations:
|
||||
# nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
|
||||
|
||||
# annotations:
|
||||
## Configuration basic authentication for the Ingress
|
||||
# nginx.ingress.kubernetes.io/auth-type: "basic"
|
||||
# nginx.ingress.kubernetes.io/auth-secret-type: "auth-file"
|
||||
# nginx.ingress.kubernetes.io/auth-secret: "default/basic-auth"
|
||||
# nginx.ingress.kubernetes.io/auth-realm: "Authentication Required"
|
||||
|
||||
spec:
|
||||
defaultBackend:
|
||||
service:
|
||||
name: whoami-default2
|
||||
port:
|
||||
number: 80
|
||||
|
||||
rules:
|
||||
- host: dd.localhost
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Exact
|
||||
backend:
|
||||
service:
|
||||
name: whoami
|
||||
port:
|
||||
number: 443
|
||||
#
|
||||
# tls:
|
||||
# - hosts:
|
||||
# - dd.localhost
|
||||
# secretName: whoami-tls
|
||||
#
|
||||
#---
|
||||
#kind: Secret
|
||||
#apiVersion: v1
|
||||
#metadata:
|
||||
# name: whoami-tls
|
||||
# namespace: default
|
||||
#
|
||||
#type: opaque
|
||||
#stringData:
|
||||
# tls.crt: |
|
||||
# -----BEGIN CERTIFICATE-----
|
||||
# MIIEXjCCAsagAwIBAgIQAJmtU2qHBlD9D2HZFZLMeDANBgkqhkiG9w0BAQsFADCB
|
||||
# jzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMTIwMAYDVQQLDClyb21h
|
||||
# aW5AY29udGFpbm91cy5ob21lIChSb21haW4gVHJpYm90dMOpKTE5MDcGA1UEAwww
|
||||
# bWtjZXJ0IHJvbWFpbkBjb250YWlub3VzLmhvbWUgKFJvbWFpbiBUcmlib3R0w6kp
|
||||
# MB4XDTI1MDYxMDE1NDE0NFoXDTI3MDkxMDE1NDE0NFowXzEnMCUGA1UEChMebWtj
|
||||
# ZXJ0IGRldmVsb3BtZW50IGNlcnRpZmljYXRlMTQwMgYDVQQLDCtyb21haW5ATWFj
|
||||
# Qm9vay1Qcm8ubG9jYWwgKFJvbWFpbiBUcmlib3R0w6kpMIIBIjANBgkqhkiG9w0B
|
||||
# AQEFAAOCAQ8AMIIBCgKCAQEAq3dajz+RgY+VUXvKKtHFFVd+0URcpDRgN+SJOxP/
|
||||
# 1uZG2U57DMvTiVy6zfpYo7QPzyEAUwbRTMMgxZV5oy1JPkGzV5kc08GUT3Lh1Azf
|
||||
# LVPX/K1nA+k7p9+kuMsfkHVABMawRpnWo215T9pjGaTKERA2EaNvrSdq73k6raVn
|
||||
# DnnmvUgWGPvxTetaLu0AVQscGyrTfQNMB8BwC+JEQJKocenJ0ve5l9/yv9543P2G
|
||||
# 6UcOv71lDOBNPyltrc4sXfGC2vB1APbp80BVfkZDiF+8Gr8wGJrkd75Esp/xetFV
|
||||
# yZ6NKO9ZsGZ2E14/qxfvASHGNFNQJafqhnuGbmky8AeaawIDAQABo2UwYzAOBgNV
|
||||
# HQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHwYDVR0jBBgwFoAUjIHl
|
||||
# 1gcu+iVVHCicC14yHQiRojgwGwYDVR0RBBQwEoIQd2hvYW1pLmxvY2FsaG9zdDAN
|
||||
# BgkqhkiG9w0BAQsFAAOCAYEAo/f0ADJwnkOakCHcYCNSqRY/VzRIQSQK3wfDq3bD
|
||||
# 8EDxGrGPYHOIL+u/Up4RO2/9vLEnFpWb30A8z/qZTKKD+rMuU3qTcCJ2tsB3DAIV
|
||||
# T+b2GmJYjURf1gqe/NNXnzZqgkoP+bHx6iNvDr1kmc3pZshayz+FxzNmjgpbKl2G
|
||||
# SgfFLnJDm7hwTC9JFoPyzb586Q0OGQKCJpDMy6pi1MAQl2RWiKyrgo1mhYnSxQmI
|
||||
# qbJbxYlegRRQQPD6YEJcL5lwILVW3TXcGrK+zuMD+xWznDTBg2BxbF2umG8jmXPH
|
||||
# 04gRfjlMNLEYSrNEU8EOa/lXebcxnlz6meFOgfYKmSHxL+kwjTUuppDV/qP9U+VS
|
||||
# /ozJ85VS8iEx1obqZGgqgwcBKMRYzuRnW1XEScGUOK9/cs9mGoXG9uafKb7ekFQc
|
||||
# wU0j0FoUVzc50WWEjCGFU/dS/2HXUXU/Rcf+uULC10ORplwrB5XdXZYxowh7T//U
|
||||
# yh86E4+M0LHyZH0vUwDoBk/1
|
||||
# -----END CERTIFICATE-----
|
||||
#
|
||||
#
|
||||
# tls.key: |
|
||||
# -----BEGIN PRIVATE KEY-----
|
||||
# MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCrd1qPP5GBj5VR
|
||||
# e8oq0cUVV37RRFykNGA35Ik7E//W5kbZTnsMy9OJXLrN+lijtA/PIQBTBtFMwyDF
|
||||
# lXmjLUk+QbNXmRzTwZRPcuHUDN8tU9f8rWcD6Tun36S4yx+QdUAExrBGmdajbXlP
|
||||
# 2mMZpMoREDYRo2+tJ2rveTqtpWcOeea9SBYY+/FN61ou7QBVCxwbKtN9A0wHwHAL
|
||||
# 4kRAkqhx6cnS97mX3/K/3njc/YbpRw6/vWUM4E0/KW2tzixd8YLa8HUA9unzQFV+
|
||||
# RkOIX7wavzAYmuR3vkSyn/F60VXJno0o71mwZnYTXj+rF+8BIcY0U1Alp+qGe4Zu
|
||||
# aTLwB5prAgMBAAECggEAVNpLxnf+2c7kZd6MvYPxtA4IhCcAcYI5228NOl87TG3I
|
||||
# weFEo6B6no91IlmxY9HHwQjj0DKfgQ1POnguKcJPbK+2wLLUwTYa3vZLK1TzXMsR
|
||||
# J8noINda3kiei5R5mlNryvFIaqfWwCl8zzeTsy0JkkgjebcXnOjU0o17rFMeHNsH
|
||||
# A3iFWWnHtJkn2OaVtOOgsyjJ9oAnGX0AE4cVp7ZZTerpaYTXzkCphbwRi00IEbCk
|
||||
# 1bn7gPcBQRoxs12GJUUuy/sopQRA51PE//CnV2pkGuDFWBhFBBKYdsHaTUwmTb5P
|
||||
# l6S5CuCtw44NkTPetTe2sn9DpOIlR7PmojQndmKkgQKBgQDJ6/RDueJCBxSYS6bh
|
||||
# 7dTPRphJvntoJHs9Q/NNjKdQhxv0vIIdtRk88Q2qhjjlCzHb1RtD5Jsl+D+TxOdG
|
||||
# wR1/E8+hdbRKv+WACywa38aBPuZSEj89bnyPyQfzs5TtzD5JsdUHT4l5Eudth6Gv
|
||||
# w14dFKria8WiEd7X2GodnlZX/wKBgQDZY1QBNjAHsi7QJJSvbPKwK8RygvdNJEem
|
||||
# FYxhjtHzOfUttjyDXDSGheY3/VzKi2rGgVAHLi+qbvwkURn4qT3xtV5Lpi+BLWHP
|
||||
# Gwepisd9P5TrN0DGQojWjzYatN9MYRzX0JynIB+alabN2bG7kfPPsHikAA7pRxLH
|
||||
# 7EwMBDGdlQKBgBqd9uoCk9e+VTGqL0py7m2QUbzO1jepL3GpBmZ/lwKffMjrHH/M
|
||||
# ApKs9+81mERhEGZ5FgoCFY2Qxti0yQPjqv64XtNaz7RWzWrujhbQzrr0zqmc7Cct
|
||||
# 7E+L4Xd3gbdDCCbwwTMgge+q1UTz7xVbPIm60rfcGwY9MtHjHkHfQGSDAoGBAIA/
|
||||
# CAT6+dTgepuSqSDg7j+eYnOH7etVlutVVQ8M2bFbJNiF5Sc900L1ZX7seryHCUP4
|
||||
# b8T8q2Qpu5iVO/QlrASXkfyhGu9jXYt4D8omtE+gnfMyEoWkJOQncqzIvd9qf0CW
|
||||
# soQqAFsLJG/WmPLmRObm3hUqb6GRq3PEZIzGQJsNAoGBAJEN0ZkrIkNK+Jjd1oNB
|
||||
# AnwgLA0qyAHqJxPig45Nudhb6Jw4ub/hKG9bCrLpcBM57Lue535e2HtQ5Ed22Pim
|
||||
# 0m7bQkvrIQYjflW99RsfkiH5qJsiTy9O92iKgGtJAJ80vTkIggAbsnzOHlZvR0Fr
|
||||
# +GhYvMt0TxpugicUqguSSUZp
|
||||
# -----END PRIVATE KEY-----
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
kind: Ingress
|
||||
apiVersion: networking.k8s.io/v1
|
||||
metadata:
|
||||
name: ingress-with-sticky
|
||||
namespace: default
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/affinity: cookie
|
||||
nginx.ingress.kubernetes.io/session-cookie-name: foobar
|
||||
nginx.ingress.kubernetes.io/session-cookie-secure: "true"
|
||||
nginx.ingress.kubernetes.io/session-cookie-path: "/foobar"
|
||||
nginx.ingress.kubernetes.io/session-cookie-domain: "foo.localhost"
|
||||
nginx.ingress.kubernetes.io/session-cookie-samesite: "None"
|
||||
nginx.ingress.kubernetes.io/session-cookie-max-age: "42"
|
||||
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: sticky.localhost
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Exact
|
||||
backend:
|
||||
service:
|
||||
name: whoami
|
||||
port:
|
||||
number: 80
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: ingress-with-proxy-ssl
|
||||
namespace: default
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" # HTTP, HTTPS, AUTO_HTTP, GRPC, GRPCS and FCGI
|
||||
nginx.ingress.kubernetes.io/proxy-ssl-secret: "default/ingress-with-proxy-ssl"
|
||||
nginx.ingress.kubernetes.io/proxy-ssl-verify: "on"
|
||||
nginx.ingress.kubernetes.io/proxy-ssl-verify-depth: "1"
|
||||
nginx.ingress.kubernetes.io/proxy-ssl-server-name: "whoami.localhost"
|
||||
nginx.ingress.kubernetes.io/proxy-ssl-name: "whoami.localhost"
|
||||
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: proxy-ssl.localhost
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Exact
|
||||
backend:
|
||||
service:
|
||||
name: whoami-tls
|
||||
port:
|
||||
number: 443
|
||||
|
||||
---
|
||||
kind: Secret
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
namespace: default
|
||||
name: ingress-with-proxy-ssl
|
||||
|
||||
data:
|
||||
ca.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: ingress-with-cors
|
||||
namespace: default
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/enable-cors: "true"
|
||||
nginx.ingress.kubernetes.io/cors-allow-credentials: "true"
|
||||
nginx.ingress.kubernetes.io/cors-expose-headers: "X-Forwarded-For, X-Forwarded-Host"
|
||||
nginx.ingress.kubernetes.io/cors-allow-headers: "X-Foo"
|
||||
nginx.ingress.kubernetes.io/cors-allow-methods: "PUT, GET, POST, OPTIONS"
|
||||
nginx.ingress.kubernetes.io/cors-allow-origin: "*"
|
||||
nginx.ingress.kubernetes.io/cors-max-age: "42"
|
||||
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: cors.localhost
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Exact
|
||||
backend:
|
||||
service:
|
||||
name: whoami
|
||||
port:
|
||||
number: 80
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: ingress-with-service-upstream
|
||||
namespace: default
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/service-upstream: "true"
|
||||
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: service-upstream.localhost
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Exact
|
||||
backend:
|
||||
service:
|
||||
name: whoami
|
||||
port:
|
||||
number: 80
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
kind: Secret
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
namespace: default
|
||||
name: whoami-tls
|
||||
|
||||
data:
|
||||
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t
|
||||
tls.key: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t
|
||||
80
pkg/provider/kubernetes/ingress-nginx/fixtures/services.yml
Normal file
80
pkg/provider/kubernetes/ingress-nginx/fixtures/services.yml
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
kind: Service
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: whoami
|
||||
namespace: default
|
||||
|
||||
spec:
|
||||
clusterIP: 10.10.10.1
|
||||
ports:
|
||||
- name: web2
|
||||
protocol: TCP
|
||||
port: 8000
|
||||
targetPort: web2
|
||||
- name: web
|
||||
protocol: TCP
|
||||
port: 80
|
||||
targetPort: web
|
||||
selector:
|
||||
app: whoami
|
||||
task: whoami
|
||||
|
||||
---
|
||||
kind: EndpointSlice
|
||||
apiVersion: discovery.k8s.io/v1
|
||||
metadata:
|
||||
name: whoami
|
||||
namespace: default
|
||||
labels:
|
||||
kubernetes.io/service-name: whoami
|
||||
|
||||
addressType: IPv4
|
||||
ports:
|
||||
- name: web
|
||||
port: 80
|
||||
- name: web2
|
||||
port: 8000
|
||||
endpoints:
|
||||
- addresses:
|
||||
- 10.10.0.1
|
||||
- 10.10.0.2
|
||||
conditions:
|
||||
ready: true
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: whoami-tls
|
||||
namespace: default
|
||||
|
||||
spec:
|
||||
ports:
|
||||
- name: websecure
|
||||
protocol: TCP
|
||||
appProtocol: https
|
||||
port: 443
|
||||
targetPort: websecure
|
||||
selector:
|
||||
app: whoami-tls
|
||||
task: whoami
|
||||
|
||||
---
|
||||
kind: EndpointSlice
|
||||
apiVersion: discovery.k8s.io/v1
|
||||
metadata:
|
||||
name: whoami-tls
|
||||
namespace: default
|
||||
labels:
|
||||
kubernetes.io/service-name: whoami-tls
|
||||
|
||||
addressType: IPv4
|
||||
ports:
|
||||
- name: websecure
|
||||
port: 8443
|
||||
endpoints:
|
||||
- addresses:
|
||||
- 10.10.0.5
|
||||
- 10.10.0.6
|
||||
conditions:
|
||||
ready: true
|
||||
1118
pkg/provider/kubernetes/ingress-nginx/kubernetes.go
Normal file
1118
pkg/provider/kubernetes/ingress-nginx/kubernetes.go
Normal file
File diff suppressed because it is too large
Load diff
605
pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go
Normal file
605
pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go
Normal file
|
|
@ -0,0 +1,605 @@
|
|||
package ingressnginx
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||
"github.com/traefik/traefik/v3/pkg/provider/kubernetes/k8s"
|
||||
"github.com/traefik/traefik/v3/pkg/tls"
|
||||
"github.com/traefik/traefik/v3/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
kubefake "k8s.io/client-go/kubernetes/fake"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func TestLoadIngresses(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
ingressClass string
|
||||
defaultBackendServiceName string
|
||||
defaultBackendServiceNamespace string
|
||||
paths []string
|
||||
expected *dynamic.Configuration
|
||||
}{
|
||||
{
|
||||
desc: "Empty, no IngressClass",
|
||||
paths: []string{
|
||||
"services.yml",
|
||||
"ingresses/01-ingress-with-basicauth.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{},
|
||||
Middlewares: map[string]*dynamic.Middleware{},
|
||||
Services: map[string]*dynamic.Service{},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Basic Auth",
|
||||
paths: []string{
|
||||
"services.yml",
|
||||
"ingressclasses.yml",
|
||||
"ingresses/01-ingress-with-basicauth.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-basicauth-rule-0-path-0": {
|
||||
Rule: "Host(`whoami.localhost`) && Path(`/basicauth`)",
|
||||
RuleSyntax: "default",
|
||||
Middlewares: []string{"default-ingress-with-basicauth-rule-0-path-0-basic-auth"},
|
||||
Service: "default-whoami-80",
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{
|
||||
"default-ingress-with-basicauth-rule-0-path-0-basic-auth": {
|
||||
BasicAuth: &dynamic.BasicAuth{
|
||||
Users: dynamic.Users{
|
||||
"user:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=",
|
||||
},
|
||||
Realm: "Authentication Required",
|
||||
},
|
||||
},
|
||||
},
|
||||
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: "Forward Auth",
|
||||
paths: []string{
|
||||
"services.yml",
|
||||
"ingressclasses.yml",
|
||||
"ingresses/02-ingress-with-forwardauth.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-forwardauth-rule-0-path-0": {
|
||||
Rule: "Host(`whoami.localhost`) && Path(`/forwardauth`)",
|
||||
RuleSyntax: "default",
|
||||
Middlewares: []string{"default-ingress-with-forwardauth-rule-0-path-0-forward-auth"},
|
||||
Service: "default-whoami-80",
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{
|
||||
"default-ingress-with-forwardauth-rule-0-path-0-forward-auth": {
|
||||
ForwardAuth: &dynamic.ForwardAuth{
|
||||
Address: "http://whoami.default.svc/",
|
||||
AuthResponseHeaders: []string{"X-Foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
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: "SSL Redirect",
|
||||
paths: []string{
|
||||
"services.yml",
|
||||
"secrets.yml",
|
||||
"ingressclasses.yml",
|
||||
"ingresses/03-ingress-with-ssl-redirect.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-ssl-redirect-rule-0-path-0": {
|
||||
Rule: "Host(`sslredirect.localhost`) && Path(`/`)",
|
||||
RuleSyntax: "default",
|
||||
TLS: &dynamic.RouterTLSConfig{},
|
||||
Service: "default-whoami-80",
|
||||
},
|
||||
"default-ingress-with-ssl-redirect-rule-0-path-0-redirect": {
|
||||
Rule: "Host(`sslredirect.localhost`) && Path(`/`)",
|
||||
RuleSyntax: "default",
|
||||
Middlewares: []string{"default-ingress-with-ssl-redirect-rule-0-path-0-redirect-scheme"},
|
||||
Service: "noop@internal",
|
||||
},
|
||||
"default-ingress-without-ssl-redirect-rule-0-path-0-http": {
|
||||
Rule: "Host(`withoutsslredirect.localhost`) && Path(`/`)",
|
||||
RuleSyntax: "default",
|
||||
Service: "default-whoami-80",
|
||||
},
|
||||
"default-ingress-without-ssl-redirect-rule-0-path-0": {
|
||||
Rule: "Host(`withoutsslredirect.localhost`) && Path(`/`)",
|
||||
RuleSyntax: "default",
|
||||
TLS: &dynamic.RouterTLSConfig{},
|
||||
Service: "default-whoami-80",
|
||||
},
|
||||
"default-ingress-with-force-ssl-redirect-rule-0-path-0": {
|
||||
Rule: "Host(`forcesslredirect.localhost`) && Path(`/`)",
|
||||
RuleSyntax: "default",
|
||||
Service: "default-whoami-80",
|
||||
},
|
||||
"default-ingress-with-force-ssl-redirect-rule-0-path-0-redirect": {
|
||||
Rule: "Host(`forcesslredirect.localhost`) && Path(`/`)",
|
||||
RuleSyntax: "default",
|
||||
Middlewares: []string{"default-ingress-with-force-ssl-redirect-rule-0-path-0-redirect-scheme"},
|
||||
Service: "noop@internal",
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{
|
||||
"default-ingress-with-ssl-redirect-rule-0-path-0-redirect-scheme": {
|
||||
RedirectScheme: &dynamic.RedirectScheme{
|
||||
Scheme: "https",
|
||||
Permanent: true,
|
||||
},
|
||||
},
|
||||
"default-ingress-with-force-ssl-redirect-rule-0-path-0-redirect-scheme": {
|
||||
RedirectScheme: &dynamic.RedirectScheme{
|
||||
Scheme: "https",
|
||||
Permanent: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
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{
|
||||
Certificates: []*tls.CertAndStores{
|
||||
{
|
||||
Certificate: tls.Certificate{
|
||||
CertFile: "-----BEGIN CERTIFICATE-----",
|
||||
KeyFile: "-----BEGIN CERTIFICATE-----",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "SSL Passthrough",
|
||||
paths: []string{
|
||||
"services.yml",
|
||||
"secrets.yml",
|
||||
"ingressclasses.yml",
|
||||
"ingresses/04-ingress-with-ssl-passthrough.yml",
|
||||
},
|
||||
expected: &dynamic.Configuration{
|
||||
TCP: &dynamic.TCPConfiguration{
|
||||
Routers: map[string]*dynamic.TCPRouter{
|
||||
"default-ingress-with-ssl-passthrough-passthrough-whoami-localhost": {
|
||||
Rule: "HostSNI(`passthrough.whoami.localhost`)",
|
||||
RuleSyntax: "default",
|
||||
TLS: &dynamic.RouterTCPTLSConfig{
|
||||
Passthrough: true,
|
||||
},
|
||||
Service: "default-whoami-tls-443",
|
||||
},
|
||||
},
|
||||
Services: map[string]*dynamic.TCPService{
|
||||
"default-whoami-tls-443": {
|
||||
LoadBalancer: &dynamic.TCPServersLoadBalancer{
|
||||
Servers: []dynamic.TCPServer{
|
||||
{
|
||||
Address: "10.10.0.5:8443",
|
||||
},
|
||||
{
|
||||
Address: "10.10.0.6:8443",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{},
|
||||
Middlewares: map[string]*dynamic.Middleware{},
|
||||
Services: map[string]*dynamic.Service{},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Sticky Sessions",
|
||||
paths: []string{
|
||||
"services.yml",
|
||||
"secrets.yml",
|
||||
"ingressclasses.yml",
|
||||
"ingresses/06-ingress-with-sticky.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-sticky-rule-0-path-0": {
|
||||
Rule: "Host(`sticky.localhost`) && Path(`/`)",
|
||||
RuleSyntax: "default",
|
||||
Service: "default-whoami-80",
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{},
|
||||
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,
|
||||
},
|
||||
Sticky: &dynamic.Sticky{
|
||||
Cookie: &dynamic.Cookie{
|
||||
Name: "foobar",
|
||||
Domain: "foo.localhost",
|
||||
HTTPOnly: true,
|
||||
MaxAge: 42,
|
||||
Path: ptr.To("/foobar"),
|
||||
SameSite: "none",
|
||||
Secure: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Proxy SSL",
|
||||
paths: []string{
|
||||
"services.yml",
|
||||
"secrets.yml",
|
||||
"ingressclasses.yml",
|
||||
"ingresses/07-ingress-with-proxy-ssl.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-proxy-ssl-rule-0-path-0": {
|
||||
Rule: "Host(`proxy-ssl.localhost`) && Path(`/`)",
|
||||
RuleSyntax: "default",
|
||||
Service: "default-whoami-tls-443",
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{},
|
||||
Services: map[string]*dynamic.Service{
|
||||
"default-whoami-tls-443": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{
|
||||
{
|
||||
URL: "https://10.10.0.5:8443",
|
||||
},
|
||||
{
|
||||
URL: "https://10.10.0.6:8443",
|
||||
},
|
||||
},
|
||||
Strategy: "wrr",
|
||||
PassHostHeader: ptr.To(true),
|
||||
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||
FlushInterval: dynamic.DefaultFlushInterval,
|
||||
},
|
||||
ServersTransport: "default-ingress-with-proxy-ssl",
|
||||
},
|
||||
},
|
||||
},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{
|
||||
"default-ingress-with-proxy-ssl": {
|
||||
ServerName: "whoami.localhost",
|
||||
InsecureSkipVerify: true,
|
||||
RootCAs: []types.FileOrContent{"-----BEGIN CERTIFICATE-----"},
|
||||
},
|
||||
},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "CORS",
|
||||
paths: []string{
|
||||
"services.yml",
|
||||
"ingressclasses.yml",
|
||||
"ingresses/08-ingress-with-cors.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-cors-rule-0-path-0": {
|
||||
Rule: "Host(`cors.localhost`) && Path(`/`)",
|
||||
RuleSyntax: "default",
|
||||
Middlewares: []string{"default-ingress-with-cors-rule-0-path-0-cors"},
|
||||
Service: "default-whoami-80",
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{
|
||||
"default-ingress-with-cors-rule-0-path-0-cors": {
|
||||
Headers: &dynamic.Headers{
|
||||
AccessControlAllowCredentials: true,
|
||||
AccessControlAllowHeaders: []string{"X-Foo"},
|
||||
AccessControlAllowMethods: []string{"PUT", "GET", "POST", "OPTIONS"},
|
||||
AccessControlAllowOriginList: []string{"*"},
|
||||
AccessControlExposeHeaders: []string{"X-Forwarded-For", "X-Forwarded-Host"},
|
||||
AccessControlMaxAge: 42,
|
||||
},
|
||||
},
|
||||
},
|
||||
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: "Service Upstream",
|
||||
paths: []string{
|
||||
"services.yml",
|
||||
"ingressclasses.yml",
|
||||
"ingresses/09-ingress-with-service-upstream.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-service-upstream-rule-0-path-0": {
|
||||
Rule: "Host(`service-upstream.localhost`) && Path(`/`)",
|
||||
RuleSyntax: "default",
|
||||
Service: "default-whoami-80",
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{},
|
||||
Services: map[string]*dynamic.Service{
|
||||
"default-whoami-80": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{
|
||||
{
|
||||
URL: "http://10.10.10.1:80",
|
||||
},
|
||||
},
|
||||
Strategy: "wrr",
|
||||
PassHostHeader: ptr.To(true),
|
||||
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||
FlushInterval: dynamic.DefaultFlushInterval,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Default Backend",
|
||||
defaultBackendServiceName: "whoami",
|
||||
defaultBackendServiceNamespace: "default",
|
||||
paths: []string{
|
||||
"services.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-backend": {
|
||||
Rule: "PathPrefix(`/`)",
|
||||
RuleSyntax: "default",
|
||||
Priority: math.MinInt32,
|
||||
Service: "default-backend",
|
||||
},
|
||||
"default-backend-tls": {
|
||||
Rule: "PathPrefix(`/`)",
|
||||
RuleSyntax: "default",
|
||||
Priority: math.MinInt32,
|
||||
TLS: &dynamic.RouterTLSConfig{},
|
||||
Service: "default-backend",
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{},
|
||||
Services: map[string]*dynamic.Service{
|
||||
"default-backend": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{
|
||||
{
|
||||
URL: "http://10.10.0.1:8000",
|
||||
},
|
||||
{
|
||||
URL: "http://10.10.0.2:8000",
|
||||
},
|
||||
},
|
||||
Strategy: "wrr",
|
||||
PassHostHeader: ptr.To(true),
|
||||
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||
FlushInterval: dynamic.DefaultFlushInterval,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
k8sObjects := readResources(t, test.paths)
|
||||
kubeClient := kubefake.NewClientset(k8sObjects...)
|
||||
client := newClient(kubeClient)
|
||||
|
||||
eventCh, err := client.WatchAll(t.Context(), "", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
if len(k8sObjects) > 0 {
|
||||
// just wait for the first event
|
||||
<-eventCh
|
||||
}
|
||||
|
||||
p := Provider{
|
||||
k8sClient: client,
|
||||
defaultBackendServiceName: test.defaultBackendServiceName,
|
||||
defaultBackendServiceNamespace: test.defaultBackendServiceNamespace,
|
||||
}
|
||||
p.SetDefaults()
|
||||
|
||||
conf := p.loadConfiguration(t.Context())
|
||||
assert.Equal(t, test.expected, conf)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func readResources(t *testing.T, paths []string) []runtime.Object {
|
||||
t.Helper()
|
||||
|
||||
var k8sObjects []runtime.Object
|
||||
for _, path := range paths {
|
||||
yamlContent, err := os.ReadFile(filepath.FromSlash("./fixtures/" + path))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
k8sObjects = append(k8sObjects, k8s.MustParseYaml(yamlContent)...)
|
||||
}
|
||||
|
||||
return k8sObjects
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue