package kubernetes import ( "bufio" "bytes" "errors" "flag" "fmt" "net" "os" "reflect" "strconv" "strings" "text/template" "time" "github.com/cenk/backoff" "github.com/containous/traefik/job" "github.com/containous/traefik/log" "github.com/containous/traefik/provider" "github.com/containous/traefik/provider/label" "github.com/containous/traefik/safe" "github.com/containous/traefik/tls" "github.com/containous/traefik/types" "gopkg.in/yaml.v2" corev1 "k8s.io/api/core/v1" extensionsv1beta1 "k8s.io/api/extensions/v1beta1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/util/intstr" ) var _ provider.Provider = (*Provider)(nil) const ( ruleTypePath = "Path" ruleTypePathPrefix = "PathPrefix" ruleTypePathStrip = "PathStrip" ruleTypePathPrefixStrip = "PathPrefixStrip" ruleTypeAddPrefix = "AddPrefix" ruleTypeReplacePath = "ReplacePath" ruleTypeReplacePathRegex = "ReplacePathRegex" traefikDefaultRealm = "traefik" traefikDefaultIngressClass = "traefik" defaultBackendName = "global-default-backend" defaultFrontendName = "global-default-frontend" allowedProtocolHTTPS = "https" allowedProtocolH2C = "h2c" ) // IngressEndpoint holds the endpoint information for the Kubernetes provider type IngressEndpoint struct { IP string `description:"IP used for Kubernetes Ingress endpoints"` Hostname string `description:"Hostname used for Kubernetes Ingress endpoints"` PublishedService string `description:"Published Kubernetes Service to copy status from"` } // Provider holds configurations of the provider. type Provider struct { provider.BaseProvider `mapstructure:",squash" export:"true"` Endpoint string `description:"Kubernetes server endpoint (required for external cluster client)"` Token string `description:"Kubernetes bearer token (not needed for in-cluster client)"` CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)"` DisablePassHostHeaders bool `description:"Kubernetes disable PassHost Headers" export:"true"` EnablePassTLSCert bool `description:"Kubernetes enable Pass TLS Client Certs" export:"true"` Namespaces Namespaces `description:"Kubernetes namespaces" export:"true"` LabelSelector string `description:"Kubernetes Ingress label selector to use" export:"true"` IngressClass string `description:"Value of kubernetes.io/ingress.class annotation to watch for" export:"true"` IngressEndpoint *IngressEndpoint `description:"Kubernetes Ingress Endpoint"` lastConfiguration safe.Safe } func (p *Provider) newK8sClient(ingressLabelSelector string) (Client, error) { ingLabelSel, err := labels.Parse(ingressLabelSelector) if err != nil { return nil, fmt.Errorf("invalid ingress label selector: %q", ingressLabelSelector) } log.Infof("ingress label selector is: %q", ingLabelSel) withEndpoint := "" if p.Endpoint != "" { withEndpoint = fmt.Sprintf(" with endpoint %v", p.Endpoint) } var cl *clientImpl if os.Getenv("KUBERNETES_SERVICE_HOST") != "" && os.Getenv("KUBERNETES_SERVICE_PORT") != "" { log.Infof("Creating in-cluster Provider client%s", withEndpoint) cl, err = newInClusterClient(p.Endpoint) } else { log.Infof("Creating cluster-external Provider client%s", withEndpoint) cl, err = newExternalClusterClient(p.Endpoint, p.Token, p.CertAuthFilePath) } if err == nil { cl.ingressLabelSelector = ingLabelSel } return cl, err } // Init the provider func (p *Provider) Init(constraints types.Constraints) error { return p.BaseProvider.Init(constraints) } // Provide allows the k8s provider to provide configurations to traefik // using the given configuration channel. func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error { // Tell glog (used by client-go) to log into STDERR. Otherwise, we risk // certain kinds of API errors getting logged into a directory not // available in a `FROM scratch` Docker container, causing glog to abort // hard with an exit code > 0. err := flag.Set("logtostderr", "true") if err != nil { return err } log.Debugf("Using Ingress label selector: %q", p.LabelSelector) k8sClient, err := p.newK8sClient(p.LabelSelector) if err != nil { return err } pool.Go(func(stop chan bool) { operation := func() error { stopWatch := make(chan struct{}, 1) defer close(stopWatch) eventsChan, err := k8sClient.WatchAll(p.Namespaces, stopWatch) if err != nil { log.Errorf("Error watching kubernetes events: %v", err) timer := time.NewTimer(1 * time.Second) select { case <-timer.C: return err case <-stop: return nil } } for { select { case <-stop: return nil case event := <-eventsChan: log.Debugf("Received Kubernetes event kind %T", event) templateObjects, err := p.loadIngresses(k8sClient) if err != nil { return err } if reflect.DeepEqual(p.lastConfiguration.Get(), templateObjects) { log.Debugf("Skipping Kubernetes event kind %T", event) } else { p.lastConfiguration.Set(templateObjects) configurationChan <- types.ConfigMessage{ ProviderName: "kubernetes", Configuration: p.loadConfig(*templateObjects), } } } } } notify := func(err error, time time.Duration) { log.Errorf("Provider connection error: %s; retrying in %s", err, time) } err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify) if err != nil { log.Errorf("Cannot connect to Provider: %s", err) } }) return nil } func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) { ingresses := k8sClient.GetIngresses() templateObjects := &types.Configuration{ Backends: map[string]*types.Backend{}, Frontends: map[string]*types.Frontend{}, } for _, i := range ingresses { annotationIngressClass := getAnnotationName(i.Annotations, annotationKubernetesIngressClass) ingressClass := i.Annotations[annotationIngressClass] if !p.shouldProcessIngress(ingressClass) { continue } tlsSection, err := getTLS(i, k8sClient) if err != nil { log.Errorf("Error configuring TLS for ingress %s/%s: %v", i.Namespace, i.Name, err) continue } templateObjects.TLS = append(templateObjects.TLS, tlsSection...) if i.Spec.Backend != nil { err := p.addGlobalBackend(k8sClient, i, templateObjects) if err != nil { log.Errorf("Error creating global backend for ingress %s/%s: %v", i.Namespace, i.Name, err) continue } } var weightAllocator weightAllocator = &defaultWeightAllocator{} annotationPercentageWeights := getAnnotationName(i.Annotations, annotationKubernetesServiceWeights) if _, ok := i.Annotations[annotationPercentageWeights]; ok { fractionalAllocator, err := newFractionalWeightAllocator(i, k8sClient) if err != nil { log.Errorf("failed to create fractional weight allocator for ingress %s/%s: %v", i.Namespace, i.Name, err) continue } log.Debugf("Created custom weight allocator for %s/%s: %s", i.Namespace, i.Name, fractionalAllocator) weightAllocator = fractionalAllocator } for _, r := range i.Spec.Rules { if r.HTTP == nil { log.Warn("Error in ingress: HTTP is nil") continue } for _, pa := range r.HTTP.Paths { priority := getIntValue(i.Annotations, annotationKubernetesPriority, 0) baseName := r.Host + pa.Path if priority > 0 { baseName = strconv.Itoa(priority) + "-" + baseName } if _, exists := templateObjects.Backends[baseName]; !exists { templateObjects.Backends[baseName] = &types.Backend{ Servers: make(map[string]types.Server), LoadBalancer: &types.LoadBalancer{ Method: "wrr", }, } } annotationAuthRealm := getAnnotationName(i.Annotations, annotationKubernetesAuthRealm) if realm := i.Annotations[annotationAuthRealm]; realm != "" && realm != traefikDefaultRealm { log.Errorf("Value for annotation %q on ingress %s/%s invalid: no realm customization supported", annotationAuthRealm, i.Namespace, i.Name) delete(templateObjects.Backends, baseName) continue } if _, exists := templateObjects.Frontends[baseName]; !exists { auth, err := getAuthConfig(i, k8sClient) if err != nil { log.Errorf("Failed to retrieve auth configuration for ingress %s/%s: %s", i.Namespace, i.Name, err) continue } passHostHeader := getBoolValue(i.Annotations, annotationKubernetesPreserveHost, !p.DisablePassHostHeaders) passTLSCert := getBoolValue(i.Annotations, annotationKubernetesPassTLSCert, p.EnablePassTLSCert) entryPoints := getSliceStringValue(i.Annotations, annotationKubernetesFrontendEntryPoints) templateObjects.Frontends[baseName] = &types.Frontend{ Backend: baseName, PassHostHeader: passHostHeader, PassTLSCert: passTLSCert, Routes: make(map[string]types.Route), Priority: priority, WhiteList: getWhiteList(i), Redirect: getFrontendRedirect(i, baseName, pa.Path), EntryPoints: entryPoints, Headers: getHeader(i), Errors: getErrorPages(i), RateLimit: getRateLimit(i), Auth: auth, } } if len(r.Host) > 0 { if _, exists := templateObjects.Frontends[baseName].Routes[r.Host]; !exists { templateObjects.Frontends[baseName].Routes[r.Host] = types.Route{ Rule: getRuleForHost(r.Host), } } } rule, err := getRuleForPath(pa, i) if err != nil { log.Errorf("Failed to get rule for ingress %s/%s: %s", i.Namespace, i.Name, err) delete(templateObjects.Frontends, baseName) continue } if rule != "" { templateObjects.Frontends[baseName].Routes[pa.Path] = types.Route{ Rule: rule, } } service, exists, err := k8sClient.GetService(i.Namespace, pa.Backend.ServiceName) if err != nil { log.Errorf("Error while retrieving service information from k8s API %s/%s: %v", i.Namespace, pa.Backend.ServiceName, err) return nil, err } if !exists { log.Errorf("Service not found for %s/%s", i.Namespace, pa.Backend.ServiceName) delete(templateObjects.Frontends, baseName) continue } templateObjects.Backends[baseName].CircuitBreaker = getCircuitBreaker(service) templateObjects.Backends[baseName].LoadBalancer = getLoadBalancer(service) templateObjects.Backends[baseName].MaxConn = getMaxConn(service) templateObjects.Backends[baseName].Buffering = getBuffering(service) protocol := label.DefaultProtocol for _, port := range service.Spec.Ports { if equalPorts(port, pa.Backend.ServicePort) { if port.Port == 443 || strings.HasPrefix(port.Name, "https") { protocol = "https" } protocol = getStringValue(i.Annotations, annotationKubernetesProtocol, protocol) switch protocol { case allowedProtocolHTTPS: case allowedProtocolH2C: case label.DefaultProtocol: default: log.Errorf("Invalid protocol %s/%s specified for Ingress %s - skipping", annotationKubernetesProtocol, i.Namespace, i.Name) continue } if service.Spec.Type == "ExternalName" { url := protocol + "://" + service.Spec.ExternalName if port.Port != 443 && port.Port != 80 { url = fmt.Sprintf("%s:%d", url, port.Port) } templateObjects.Backends[baseName].Servers[url] = types.Server{ URL: url, Weight: label.DefaultWeight, } } else { endpoints, exists, err := k8sClient.GetEndpoints(service.Namespace, service.Name) if err != nil { log.Errorf("Error retrieving endpoints %s/%s: %v", service.Namespace, service.Name, err) return nil, err } if !exists { log.Warnf("Endpoints not found for %s/%s", service.Namespace, service.Name) break } if len(endpoints.Subsets) == 0 { log.Warnf("Endpoints not available for %s/%s", service.Namespace, service.Name) break } for _, subset := range endpoints.Subsets { endpointPort := endpointPortNumber(port, subset.Ports) if endpointPort == 0 { // endpoint port does not match service. continue } for _, address := range subset.Addresses { url := protocol + "://" + net.JoinHostPort(address.IP, strconv.FormatInt(int64(endpointPort), 10)) name := url if address.TargetRef != nil && address.TargetRef.Name != "" { name = address.TargetRef.Name } templateObjects.Backends[baseName].Servers[name] = types.Server{ URL: url, Weight: weightAllocator.getWeight(r.Host, pa.Path, pa.Backend.ServiceName), } } } } break } } } } err = p.updateIngressStatus(i, k8sClient) if err != nil { log.Errorf("Cannot update Ingress %s/%s due to error: %v", i.Namespace, i.Name, err) } } return templateObjects, nil } func (p *Provider) updateIngressStatus(i *extensionsv1beta1.Ingress, k8sClient Client) error { // Only process if an IngressEndpoint has been configured if p.IngressEndpoint == nil { return nil } if len(p.IngressEndpoint.PublishedService) == 0 { if len(p.IngressEndpoint.IP) == 0 && len(p.IngressEndpoint.Hostname) == 0 { return errors.New("publishedService or ip or hostname must be defined") } return k8sClient.UpdateIngressStatus(i.Namespace, i.Name, p.IngressEndpoint.IP, p.IngressEndpoint.Hostname) } serviceInfo := strings.Split(p.IngressEndpoint.PublishedService, "/") if len(serviceInfo) != 2 { return fmt.Errorf("invalid publishedService format (expected 'namespace/service' format): %s", p.IngressEndpoint.PublishedService) } serviceNamespace, serviceName := serviceInfo[0], serviceInfo[1] service, exists, err := k8sClient.GetService(serviceNamespace, serviceName) if err != nil { return fmt.Errorf("cannot get service %s, received error: %s", p.IngressEndpoint.PublishedService, err) } if exists && service.Status.LoadBalancer.Ingress == nil { // service exists, but has no Load Balancer status log.Debugf("Skipping updating Ingress %s/%s due to service %s having no status set", i.Namespace, i.Name, p.IngressEndpoint.PublishedService) return nil } if !exists { return fmt.Errorf("missing service: %s", p.IngressEndpoint.PublishedService) } return k8sClient.UpdateIngressStatus(i.Namespace, i.Name, service.Status.LoadBalancer.Ingress[0].IP, service.Status.LoadBalancer.Ingress[0].Hostname) } func (p *Provider) loadConfig(templateObjects types.Configuration) *types.Configuration { var FuncMap = template.FuncMap{} configuration, err := p.GetConfiguration("templates/kubernetes.tmpl", FuncMap, templateObjects) if err != nil { log.Error(err) } return configuration } func (p *Provider) addGlobalBackend(cl Client, i *extensionsv1beta1.Ingress, templateObjects *types.Configuration) error { // Ensure that we are not duplicating the frontend if _, exists := templateObjects.Frontends[defaultFrontendName]; exists { return errors.New("duplicate frontend: " + defaultFrontendName) } // Ensure we are not duplicating the backend if _, exists := templateObjects.Backends[defaultBackendName]; exists { return errors.New("duplicate backend: " + defaultBackendName) } templateObjects.Backends[defaultBackendName] = &types.Backend{ Servers: make(map[string]types.Server), LoadBalancer: &types.LoadBalancer{ Method: "wrr", }, } service, exists, err := cl.GetService(i.Namespace, i.Spec.Backend.ServiceName) if err != nil { return fmt.Errorf("error while retrieving service information from k8s API %s/%s: %v", i.Namespace, i.Spec.Backend.ServiceName, err) } if !exists { return fmt.Errorf("service not found for %s/%s", i.Namespace, i.Spec.Backend.ServiceName) } templateObjects.Backends[defaultBackendName].CircuitBreaker = getCircuitBreaker(service) templateObjects.Backends[defaultBackendName].LoadBalancer = getLoadBalancer(service) templateObjects.Backends[defaultBackendName].MaxConn = getMaxConn(service) templateObjects.Backends[defaultBackendName].Buffering = getBuffering(service) endpoints, exists, err := cl.GetEndpoints(service.Namespace, service.Name) if err != nil { return fmt.Errorf("error retrieving endpoint information from k8s API %s/%s: %v", service.Namespace, service.Name, err) } if !exists { return fmt.Errorf("endpoints not found for %s/%s", service.Namespace, service.Name) } if len(endpoints.Subsets) == 0 { return fmt.Errorf("endpoints not available for %s/%s", service.Namespace, service.Name) } for _, subset := range endpoints.Subsets { endpointPort := endpointPortNumber(corev1.ServicePort{Protocol: "TCP", Port: int32(i.Spec.Backend.ServicePort.IntValue())}, subset.Ports) if endpointPort == 0 { // endpoint port does not match service. continue } protocol := "http" for _, address := range subset.Addresses { if endpointPort == 443 || strings.HasPrefix(i.Spec.Backend.ServicePort.String(), "https") { protocol = "https" } url := fmt.Sprintf("%s://%s", protocol, net.JoinHostPort(address.IP, strconv.FormatInt(int64(endpointPort), 10))) name := url if address.TargetRef != nil && address.TargetRef.Name != "" { name = address.TargetRef.Name } templateObjects.Backends[defaultBackendName].Servers[name] = types.Server{ URL: url, Weight: label.DefaultWeight, } } } passHostHeader := getBoolValue(i.Annotations, annotationKubernetesPreserveHost, !p.DisablePassHostHeaders) passTLSCert := getBoolValue(i.Annotations, annotationKubernetesPassTLSCert, p.EnablePassTLSCert) priority := getIntValue(i.Annotations, annotationKubernetesPriority, 0) entryPoints := getSliceStringValue(i.Annotations, annotationKubernetesFrontendEntryPoints) templateObjects.Frontends[defaultFrontendName] = &types.Frontend{ Backend: defaultBackendName, PassHostHeader: passHostHeader, PassTLSCert: passTLSCert, Routes: make(map[string]types.Route), Priority: priority, WhiteList: getWhiteList(i), Redirect: getFrontendRedirect(i, defaultFrontendName, "/"), EntryPoints: entryPoints, Headers: getHeader(i), Errors: getErrorPages(i), RateLimit: getRateLimit(i), } templateObjects.Frontends[defaultFrontendName].Routes["/"] = types.Route{ Rule: "PathPrefix:/", } return nil } func getRuleForPath(pa extensionsv1beta1.HTTPIngressPath, i *extensionsv1beta1.Ingress) (string, error) { if len(pa.Path) == 0 { return "", nil } ruleType := getStringValue(i.Annotations, annotationKubernetesRuleType, ruleTypePathPrefix) switch ruleType { case ruleTypePath, ruleTypePathPrefix, ruleTypePathStrip, ruleTypePathPrefixStrip: case ruleTypeReplacePath: log.Warnf("Using %s as %s will be deprecated in the future. Please use the %s annotation instead", ruleType, annotationKubernetesRuleType, annotationKubernetesRequestModifier) default: return "", fmt.Errorf("cannot use non-matcher rule: %q", ruleType) } rules := []string{ruleType + ":" + pa.Path} if rewriteTarget := getStringValue(i.Annotations, annotationKubernetesRewriteTarget, ""); rewriteTarget != "" { if ruleType == ruleTypeReplacePath { return "", fmt.Errorf("rewrite-target must not be used together with annotation %q", annotationKubernetesRuleType) } rewriteTargetRule := fmt.Sprintf("ReplacePathRegex: ^%s(.*) %s$1", pa.Path, strings.TrimRight(rewriteTarget, "/")) rules = append(rules, rewriteTargetRule) } if requestModifier := getStringValue(i.Annotations, annotationKubernetesRequestModifier, ""); requestModifier != "" { rule, err := parseRequestModifier(requestModifier, ruleType) if err != nil { return "", err } rules = append(rules, rule) } return strings.Join(rules, ";"), nil } func parseRequestModifier(requestModifier, ruleType string) (string, error) { trimmedRequestModifier := strings.TrimRight(requestModifier, " :") if trimmedRequestModifier == "" { return "", fmt.Errorf("rule %q is empty", requestModifier) } // Split annotation to determine modifier type modifierParts := strings.Split(trimmedRequestModifier, ":") if len(modifierParts) < 2 { return "", fmt.Errorf("rule %q is missing type or value", requestModifier) } modifier := strings.TrimSpace(modifierParts[0]) value := strings.TrimSpace(modifierParts[1]) switch modifier { case ruleTypeAddPrefix, ruleTypeReplacePath, ruleTypeReplacePathRegex: if ruleType == ruleTypeReplacePath { return "", fmt.Errorf("cannot use '%s: %s' and '%s: %s', as this leads to rule duplication, and unintended behavior", annotationKubernetesRuleType, ruleTypeReplacePath, annotationKubernetesRequestModifier, modifier) } case "": return "", errors.New("cannot use empty rule") default: return "", fmt.Errorf("cannot use non-modifier rule: %q", modifier) } return modifier + ":" + value, nil } func getRuleForHost(host string) string { if strings.Contains(host, "*") { return "HostRegexp:" + strings.Replace(host, "*", "{subdomain:[A-Za-z0-9-_]+}", 1) } return "Host:" + host } func getTLS(ingress *extensionsv1beta1.Ingress, k8sClient Client) ([]*tls.Configuration, error) { var tlsConfigs []*tls.Configuration for _, t := range ingress.Spec.TLS { secret, exists, err := k8sClient.GetSecret(ingress.Namespace, t.SecretName) if err != nil { return nil, fmt.Errorf("failed to fetch secret %s/%s: %v", ingress.Namespace, t.SecretName, err) } if !exists { return nil, fmt.Errorf("secret %s/%s does not exist", ingress.Namespace, t.SecretName) } cert, key, err := getCertificateBlocks(secret, ingress.Namespace, t.SecretName) if err != nil { return nil, err } entryPoints := getSliceStringValue(ingress.Annotations, annotationKubernetesFrontendEntryPoints) tlsConfig := &tls.Configuration{ EntryPoints: entryPoints, Certificate: &tls.Certificate{ CertFile: tls.FileOrContent(cert), KeyFile: tls.FileOrContent(key), }, } tlsConfigs = append(tlsConfigs, tlsConfig) } return tlsConfigs, nil } func getCertificateBlocks(secret *corev1.Secret, namespace, secretName string) (string, string, error) { var missingEntries []string tlsCrtData, tlsCrtExists := secret.Data["tls.crt"] if !tlsCrtExists { missingEntries = append(missingEntries, "tls.crt") } tlsKeyData, tlsKeyExists := secret.Data["tls.key"] if !tlsKeyExists { missingEntries = append(missingEntries, "tls.key") } if len(missingEntries) > 0 { return "", "", fmt.Errorf("secret %s/%s is missing the following TLS data entries: %s", namespace, secretName, strings.Join(missingEntries, ", ")) } cert := string(tlsCrtData[:]) if cert == "" { missingEntries = append(missingEntries, "tls.crt") } key := string(tlsKeyData[:]) if key == "" { missingEntries = append(missingEntries, "tls.key") } if len(missingEntries) > 0 { return "", "", fmt.Errorf("secret %s/%s contains the following empty TLS data entries: %s", namespace, secretName, strings.Join(missingEntries, ", ")) } return cert, key, nil } // endpointPortNumber returns the port to be used for this endpoint. It is zero // if the endpoint does not match the given service port. func endpointPortNumber(servicePort corev1.ServicePort, endpointPorts []corev1.EndpointPort) int32 { // Is this reasonable to assume? if len(endpointPorts) == 0 { return servicePort.Port } for _, endpointPort := range endpointPorts { // For matching endpoints, the port names must correspond, either by // being empty or non-empty. Multi-port services mandate non-empty // names and allow us to filter for the right addresses. if servicePort.Name == endpointPort.Name { return endpointPort.Port } } return 0 } func equalPorts(servicePort corev1.ServicePort, ingressPort intstr.IntOrString) bool { if int(servicePort.Port) == ingressPort.IntValue() { return true } if servicePort.Name != "" && servicePort.Name == ingressPort.String() { return true } return false } func (p *Provider) shouldProcessIngress(annotationIngressClass string) bool { if len(p.IngressClass) == 0 { return len(annotationIngressClass) == 0 || annotationIngressClass == traefikDefaultIngressClass } return annotationIngressClass == p.IngressClass } func getAuthConfig(i *extensionsv1beta1.Ingress, k8sClient Client) (*types.Auth, error) { authType := getStringValue(i.Annotations, annotationKubernetesAuthType, "") if len(authType) == 0 { return nil, nil } auth := &types.Auth{ HeaderField: getStringValue(i.Annotations, annotationKubernetesAuthHeaderField, ""), } switch strings.ToLower(authType) { case "basic": basic, err := getBasicAuthConfig(i, k8sClient) if err != nil { return nil, err } auth.Basic = basic case "digest": digest, err := getDigestAuthConfig(i, k8sClient) if err != nil { return nil, err } auth.Digest = digest case "forward": forward, err := getForwardAuthConfig(i, k8sClient) if err != nil { return nil, err } auth.Forward = forward default: return nil, fmt.Errorf("unsupported auth-type on annotation %s: %s", annotationKubernetesAuthType, authType) } return auth, nil } func getBasicAuthConfig(i *extensionsv1beta1.Ingress, k8sClient Client) (*types.Basic, error) { credentials, err := getAuthCredentials(i, k8sClient) if err != nil { return nil, err } return &types.Basic{ Users: credentials, RemoveHeader: getBoolValue(i.Annotations, annotationKubernetesAuthRemoveHeader, false), }, nil } func getDigestAuthConfig(i *extensionsv1beta1.Ingress, k8sClient Client) (*types.Digest, error) { credentials, err := getAuthCredentials(i, k8sClient) if err != nil { return nil, err } return &types.Digest{Users: credentials, RemoveHeader: getBoolValue(i.Annotations, annotationKubernetesAuthRemoveHeader, false), }, nil } func getAuthCredentials(i *extensionsv1beta1.Ingress, k8sClient Client) ([]string, error) { authSecret := getStringValue(i.Annotations, annotationKubernetesAuthSecret, "") if authSecret == "" { return nil, fmt.Errorf("auth-secret annotation %s must be set", annotationKubernetesAuthSecret) } auth, err := loadAuthCredentials(i.Namespace, authSecret, k8sClient) if err != nil { return nil, fmt.Errorf("failed to load auth credentials: %s", err) } return auth, nil } func loadAuthCredentials(namespace, secretName string, k8sClient Client) ([]string, error) { secret, ok, err := k8sClient.GetSecret(namespace, secretName) if err != nil { return nil, fmt.Errorf("failed to fetch secret %q/%q: %s", namespace, secretName, err) } if !ok { return nil, fmt.Errorf("secret %q/%q not found", namespace, secretName) } if secret == nil { return nil, fmt.Errorf("data for secret %q/%q must not be nil", namespace, secretName) } if len(secret.Data) != 1 { return nil, fmt.Errorf("found %d elements for secret %q/%q, must be single element exactly", len(secret.Data), namespace, secretName) } var firstSecret []byte for _, v := range secret.Data { firstSecret = v break } var credentials []string scanner := bufio.NewScanner(bytes.NewReader(firstSecret)) for scanner.Scan() { if cred := scanner.Text(); len(cred) > 0 { credentials = append(credentials, cred) } } if len(credentials) == 0 { return nil, fmt.Errorf("secret %q/%q does not contain any credentials", namespace, secretName) } return credentials, nil } func getForwardAuthConfig(i *extensionsv1beta1.Ingress, k8sClient Client) (*types.Forward, error) { authURL := getStringValue(i.Annotations, annotationKubernetesAuthForwardURL, "") if len(authURL) == 0 { return nil, fmt.Errorf("forward authentication requires a url") } forwardAuth := &types.Forward{ Address: authURL, TrustForwardHeader: getBoolValue(i.Annotations, annotationKubernetesAuthForwardTrustHeaders, false), AuthResponseHeaders: getSliceStringValue(i.Annotations, annotationKubernetesAuthForwardResponseHeaders), } authSecretName := getStringValue(i.Annotations, annotationKubernetesAuthForwardTLSSecret, "") if len(authSecretName) > 0 { authSecretCert, authSecretKey, err := loadAuthTLSSecret(i.Namespace, authSecretName, k8sClient) if err != nil { return nil, fmt.Errorf("failed to load auth secret: %s", err) } forwardAuth.TLS = &types.ClientTLS{ Cert: authSecretCert, Key: authSecretKey, InsecureSkipVerify: getBoolValue(i.Annotations, annotationKubernetesAuthForwardTLSInsecure, false), } } return forwardAuth, nil } func loadAuthTLSSecret(namespace, secretName string, k8sClient Client) (string, string, error) { secret, exists, err := k8sClient.GetSecret(namespace, secretName) if err != nil { return "", "", fmt.Errorf("failed to fetch secret %q/%q: %s", namespace, secretName, err) } if !exists { return "", "", fmt.Errorf("secret %q/%q does not exist", namespace, secretName) } if secret == nil { return "", "", fmt.Errorf("data for secret %q/%q must not be nil", namespace, secretName) } if len(secret.Data) != 2 { return "", "", fmt.Errorf("found %d elements for secret %q/%q, must be two elements exactly", len(secret.Data), namespace, secretName) } return getCertificateBlocks(secret, namespace, secretName) } func getFrontendRedirect(i *extensionsv1beta1.Ingress, baseName, path string) *types.Redirect { permanent := getBoolValue(i.Annotations, annotationKubernetesRedirectPermanent, false) if appRoot := getStringValue(i.Annotations, annotationKubernetesAppRoot, ""); appRoot != "" && path == "/" { return &types.Redirect{ Regex: fmt.Sprintf("%s$", baseName), Replacement: fmt.Sprintf("%s/%s", strings.TrimRight(baseName, "/"), strings.TrimLeft(appRoot, "/")), Permanent: permanent, } } redirectEntryPoint := getStringValue(i.Annotations, annotationKubernetesRedirectEntryPoint, "") if len(redirectEntryPoint) > 0 { return &types.Redirect{ EntryPoint: redirectEntryPoint, Permanent: permanent, } } redirectRegex := getStringValue(i.Annotations, annotationKubernetesRedirectRegex, "") _, err := strconv.Unquote(`"` + redirectRegex + `"`) if err != nil { log.Debugf("Skipping Redirect on Ingress %s/%s due to invalid regex: %s", i.Namespace, i.Name, redirectRegex) return nil } redirectReplacement := getStringValue(i.Annotations, annotationKubernetesRedirectReplacement, "") _, err = strconv.Unquote(`"` + redirectReplacement + `"`) if err != nil { log.Debugf("Skipping Redirect on Ingress %s/%s due to invalid replacement: %q", i.Namespace, i.Name, redirectRegex) return nil } if len(redirectRegex) > 0 && len(redirectReplacement) > 0 { return &types.Redirect{ Regex: redirectRegex, Replacement: redirectReplacement, Permanent: permanent, } } return nil } func getWhiteList(i *extensionsv1beta1.Ingress) *types.WhiteList { ranges := getSliceStringValue(i.Annotations, annotationKubernetesWhiteListSourceRange) if len(ranges) <= 0 { return nil } return &types.WhiteList{ SourceRange: ranges, IPStrategy: getIPStrategy(i.Annotations), } } func getIPStrategy(annotations map[string]string) *types.IPStrategy { ipStrategy := getBoolValue(annotations, annotationKubernetesWhiteListIPStrategy, false) depth := getIntValue(annotations, annotationKubernetesWhiteListIPStrategyDepth, 0) excludedIPs := getSliceStringValue(annotations, annotationKubernetesWhiteListIPStrategyExcludedIPs) if depth == 0 && len(excludedIPs) == 0 && !ipStrategy { return nil } return &types.IPStrategy{ Depth: depth, ExcludedIPs: excludedIPs, } } func getBuffering(service *corev1.Service) *types.Buffering { var buffering *types.Buffering bufferingRaw := getStringValue(service.Annotations, annotationKubernetesBuffering, "") if len(bufferingRaw) > 0 { buffering = &types.Buffering{} err := yaml.Unmarshal([]byte(bufferingRaw), buffering) if err != nil { log.Error(err) return nil } } return buffering } func getLoadBalancer(service *corev1.Service) *types.LoadBalancer { loadBalancer := &types.LoadBalancer{ Method: "wrr", } if getStringValue(service.Annotations, annotationKubernetesLoadBalancerMethod, "") == "drr" { loadBalancer.Method = "drr" } if stickiness := getStickiness(service); stickiness != nil { loadBalancer.Stickiness = stickiness } return loadBalancer } func getStickiness(service *corev1.Service) *types.Stickiness { if getBoolValue(service.Annotations, annotationKubernetesAffinity, false) { stickiness := &types.Stickiness{} if cookieName := getStringValue(service.Annotations, annotationKubernetesSessionCookieName, ""); len(cookieName) > 0 { stickiness.CookieName = cookieName } return stickiness } return nil } func getHeader(i *extensionsv1beta1.Ingress) *types.Headers { headers := &types.Headers{ CustomRequestHeaders: getMapValue(i.Annotations, annotationKubernetesCustomRequestHeaders), CustomResponseHeaders: getMapValue(i.Annotations, annotationKubernetesCustomResponseHeaders), AllowedHosts: getSliceStringValue(i.Annotations, annotationKubernetesAllowedHosts), HostsProxyHeaders: getSliceStringValue(i.Annotations, annotationKubernetesProxyHeaders), SSLForceHost: getBoolValue(i.Annotations, annotationKubernetesSSLForceHost, false), SSLRedirect: getBoolValue(i.Annotations, annotationKubernetesSSLRedirect, false), SSLTemporaryRedirect: getBoolValue(i.Annotations, annotationKubernetesSSLTemporaryRedirect, false), SSLHost: getStringValue(i.Annotations, annotationKubernetesSSLHost, ""), SSLProxyHeaders: getMapValue(i.Annotations, annotationKubernetesSSLProxyHeaders), STSSeconds: getInt64Value(i.Annotations, annotationKubernetesHSTSMaxAge, 0), STSIncludeSubdomains: getBoolValue(i.Annotations, annotationKubernetesHSTSIncludeSubdomains, false), STSPreload: getBoolValue(i.Annotations, annotationKubernetesHSTSPreload, false), ForceSTSHeader: getBoolValue(i.Annotations, annotationKubernetesForceHSTSHeader, false), FrameDeny: getBoolValue(i.Annotations, annotationKubernetesFrameDeny, false), CustomFrameOptionsValue: getStringValue(i.Annotations, annotationKubernetesCustomFrameOptionsValue, ""), ContentTypeNosniff: getBoolValue(i.Annotations, annotationKubernetesContentTypeNosniff, false), BrowserXSSFilter: getBoolValue(i.Annotations, annotationKubernetesBrowserXSSFilter, false), CustomBrowserXSSValue: getStringValue(i.Annotations, annotationKubernetesCustomBrowserXSSValue, ""), ContentSecurityPolicy: getStringValue(i.Annotations, annotationKubernetesContentSecurityPolicy, ""), PublicKey: getStringValue(i.Annotations, annotationKubernetesPublicKey, ""), ReferrerPolicy: getStringValue(i.Annotations, annotationKubernetesReferrerPolicy, ""), IsDevelopment: getBoolValue(i.Annotations, annotationKubernetesIsDevelopment, false), } if !headers.HasSecureHeadersDefined() && !headers.HasCustomHeadersDefined() { return nil } return headers } func getMaxConn(service *corev1.Service) *types.MaxConn { amount := getInt64Value(service.Annotations, annotationKubernetesMaxConnAmount, -1) extractorFunc := getStringValue(service.Annotations, annotationKubernetesMaxConnExtractorFunc, "") if amount >= 0 && len(extractorFunc) > 0 { return &types.MaxConn{ ExtractorFunc: extractorFunc, Amount: amount, } } return nil } func getCircuitBreaker(service *corev1.Service) *types.CircuitBreaker { if expression := getStringValue(service.Annotations, annotationKubernetesCircuitBreakerExpression, ""); expression != "" { return &types.CircuitBreaker{ Expression: expression, } } return nil } func getErrorPages(i *extensionsv1beta1.Ingress) map[string]*types.ErrorPage { var errorPages map[string]*types.ErrorPage pagesRaw := getStringValue(i.Annotations, annotationKubernetesErrorPages, "") if len(pagesRaw) > 0 { errorPages = make(map[string]*types.ErrorPage) err := yaml.Unmarshal([]byte(pagesRaw), errorPages) if err != nil { log.Error(err) return nil } } return errorPages } func getRateLimit(i *extensionsv1beta1.Ingress) *types.RateLimit { var rateLimit *types.RateLimit rateRaw := getStringValue(i.Annotations, annotationKubernetesRateLimit, "") if len(rateRaw) > 0 { rateLimit = &types.RateLimit{} err := yaml.Unmarshal([]byte(rateRaw), rateLimit) if err != nil { log.Error(err) return nil } } return rateLimit }