pkg/providers/ingress/translation/translator.go (491 lines of code) (raw):

// Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package translation import ( "bytes" "context" "fmt" "strings" "go.uber.org/zap" networkingv1 "k8s.io/api/networking/v1" networkingv1beta1 "k8s.io/api/networking/v1beta1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" listerscorev1 "k8s.io/client-go/listers/core/v1" "github.com/apache/apisix-ingress-controller/pkg/apisix" "github.com/apache/apisix-ingress-controller/pkg/id" "github.com/apache/apisix-ingress-controller/pkg/kube" kubev2 "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/apis/config/v2" apisixconst "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/const" "github.com/apache/apisix-ingress-controller/pkg/log" apisixtranslation "github.com/apache/apisix-ingress-controller/pkg/providers/apisix/translation" "github.com/apache/apisix-ingress-controller/pkg/providers/translation" "github.com/apache/apisix-ingress-controller/pkg/types" apisixv1 "github.com/apache/apisix-ingress-controller/pkg/types/apisix/v1" ) type TranslatorOptions struct { Apisix apisix.APISIX ClusterName string ServiceLister listerscorev1.ServiceLister } type translator struct { *TranslatorOptions translation.Translator ApisixTranslator apisixtranslation.ApisixTranslator } type IngressTranslator interface { // TranslateIngress composes a couple of APISIX Routes and upstreams according // to the given Ingress resource. // For old objects, you cannot use TranslateIngress to build. Because it needs to parse the latest service, which will cause data inconsistency. TranslateIngress(ing kube.Ingress, args ...bool) (*translation.TranslateContext, error) // TranslateOldIngress get route objects from cache // Build upstream and plugin_config through route TranslateOldIngress(kube.Ingress) (*translation.TranslateContext, error) // TranslateSSLV2 translate networkingv1.IngressTLS to APISIX SSL TranslateIngressTLS(namespace, ingName, secretName string, hosts []string) (*apisixv1.Ssl, error) // TranslateIngressDeleteEvent composes a couple of APISIX Routes and upstreams according // to the given Ingress resource. TranslateIngressDeleteEvent(ing kube.Ingress, args ...bool) (*translation.TranslateContext, error) } func NewIngressTranslator(opts *TranslatorOptions, commonTranslator translation.Translator, apisixTranslator apisixtranslation.ApisixTranslator) IngressTranslator { t := &translator{ TranslatorOptions: opts, Translator: commonTranslator, ApisixTranslator: apisixTranslator, } return t } func (t *translator) TranslateIngressTLS(namespace, ingName, secretName string, hosts []string) (*apisixv1.Ssl, error) { apisixTls := kubev2.ApisixTls{ TypeMeta: metav1.TypeMeta{ Kind: "ApisixTls", APIVersion: "apisix.apache.org/v1", }, ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("%v-%v", ingName, "tls"), Namespace: namespace, }, Spec: &kubev2.ApisixTlsSpec{ Secret: kubev2.ApisixSecret{ Name: secretName, Namespace: namespace, }, }, } for _, host := range hosts { apisixTls.Spec.Hosts = append(apisixTls.Spec.Hosts, kubev2.HostType(host)) } return t.ApisixTranslator.TranslateSSLV2(&apisixTls) } func (t *translator) TranslateIngress(ing kube.Ingress, args ...bool) (*translation.TranslateContext, error) { var skipVerify = false if len(args) != 0 { skipVerify = args[0] } switch ing.GroupVersion() { case kube.IngressV1: return t.translateIngressV1(ing.V1(), skipVerify) case kube.IngressV1beta1: return t.translateIngressV1beta1(ing.V1beta1(), skipVerify) default: return nil, fmt.Errorf("translator: source group version not supported: %s", ing.GroupVersion()) } } func (t *translator) TranslateIngressDeleteEvent(ing kube.Ingress, args ...bool) (*translation.TranslateContext, error) { switch ing.GroupVersion() { case kube.IngressV1: return t.translateOldIngressV1(ing.V1()) case kube.IngressV1beta1: return t.translateOldIngressV1beta1(ing.V1beta1()) default: return nil, fmt.Errorf("translator: source group version not supported: %s", ing.GroupVersion()) } } const ( _regexPriority = 100 ) func (t *translator) translateIngressV1(ing *networkingv1.Ingress, skipVerify bool) (*translation.TranslateContext, error) { ctx := translation.DefaultEmptyTranslateContext() ingress := t.TranslateAnnotations(ing.Annotations) // add https for _, tls := range ing.Spec.TLS { ssl, err := t.TranslateIngressTLS(ing.Namespace, ing.Name, tls.SecretName, tls.Hosts) if err != nil { log.Errorw("failed to translate ingress tls to apisix tls", zap.Error(err), zap.Any("ingress", ing), ) return nil, err } ctx.AddSSL(ssl) } ns := ing.Namespace if ingress.ServiceNamespace != "" { ns = ingress.ServiceNamespace } for _, rule := range ing.Spec.Rules { if rule.HTTP == nil { continue } for _, pathRule := range rule.HTTP.Paths { var ( ups *apisixv1.Upstream err error ) if pathRule.Backend.Service != nil { if skipVerify { ups = t.translateDefaultUpstreamFromIngressV1(ns, pathRule.Backend.Service) } else { ups, err = t.translateUpstreamFromIngressV1(ns, pathRule.Backend.Service) if err != nil { log.Errorw("failed to translate ingress backend to upstream", zap.Error(err), zap.Any("ingress", ing), ) return nil, err } } if ingress.UpstreamScheme != "" { ups.Scheme = ingress.UpstreamScheme } ctx.AddUpstream(ups) } uris := []string{pathRule.Path} var nginxVars []kubev2.ApisixRouteHTTPMatchExpr if pathRule.PathType != nil { if *pathRule.PathType == networkingv1.PathTypePrefix { // As per the specification of Ingress path matching rule: // if the last element of the path is a substring of the // last element in request path, it is not a match, e.g. /foo/bar // matches /foo/bar/baz, but does not match /foo/barbaz. // While in APISIX, /foo/bar matches both /foo/bar/baz and // /foo/barbaz. // In order to be conformant with Ingress specification, here // we create two paths here, the first is the path itself // (exact match), the other is path + "/*" (prefix match). prefix := pathRule.Path if strings.HasSuffix(prefix, "/") { prefix += "*" } else { prefix += "/*" } uris = append(uris, prefix) } else if *pathRule.PathType == networkingv1.PathTypeImplementationSpecific && ingress.UseRegex { nginxVars = append(nginxVars, kubev2.ApisixRouteHTTPMatchExpr{ Subject: kubev2.ApisixRouteHTTPMatchExprSubject{ Scope: apisixconst.ScopePath, }, Op: apisixconst.OpRegexMatch, Value: &pathRule.Path, }) uris = []string{"/*"} } } route := apisixv1.NewDefaultRoute() route.Name = composeIngressRouteName(ing.Namespace, ing.Name, rule.Host, pathRule.Path) route.ID = id.GenID(route.Name) route.Host = rule.Host route.Uris = uris route.EnableWebsocket = ingress.EnableWebSocket if len(nginxVars) > 0 { routeVars, err := t.ApisixTranslator.TranslateRouteMatchExprs(nginxVars) if err != nil { return nil, err } route.Vars = routeVars route.Priority = _regexPriority } if len(ingress.Plugins) > 0 { route.Plugins = *(ingress.Plugins.DeepCopy()) } if ingress.PluginConfigName != "" { route.PluginConfigId = id.GenID(apisixv1.ComposePluginConfigName(ing.Namespace, ingress.PluginConfigName)) } if ups != nil { route.UpstreamId = ups.ID } ctx.AddRoute(route) } } return ctx, nil } func (t *translator) translateIngressV1beta1(ing *networkingv1beta1.Ingress, skipVerify bool) (*translation.TranslateContext, error) { ctx := translation.DefaultEmptyTranslateContext() ingress := t.TranslateAnnotations(ing.Annotations) // add https for _, tls := range ing.Spec.TLS { ssl, err := t.TranslateIngressTLS(ing.Namespace, ing.Name, tls.SecretName, tls.Hosts) if err != nil { log.Errorw("failed to translate ingress tls to apisix tls", zap.Error(err), zap.Any("ingress", ing), ) return nil, err } ctx.AddSSL(ssl) } ns := ing.Namespace if ingress.ServiceNamespace != "" { ns = ingress.ServiceNamespace } for _, rule := range ing.Spec.Rules { for _, pathRule := range rule.HTTP.Paths { var ( ups *apisixv1.Upstream err error ) if pathRule.Backend.ServiceName != "" { if skipVerify { ups = t.translateDefaultUpstreamFromIngressV1beta1(ns, pathRule.Backend.ServiceName, pathRule.Backend.ServicePort) } else { ups, err = t.translateUpstreamFromIngressV1beta1(ns, pathRule.Backend.ServiceName, pathRule.Backend.ServicePort) if err != nil { log.Errorw("failed to translate ingress backend to upstream", zap.Error(err), zap.Any("ingress", ing), ) return nil, err } } if ingress.UpstreamScheme != "" { ups.Scheme = ingress.UpstreamScheme } ctx.AddUpstream(ups) } uris := []string{pathRule.Path} var nginxVars []kubev2.ApisixRouteHTTPMatchExpr if pathRule.PathType != nil { if *pathRule.PathType == networkingv1beta1.PathTypePrefix { // As per the specification of Ingress path matching rule: // if the last element of the path is a substring of the // last element in request path, it is not a match, e.g. /foo/bar // matches /foo/bar/baz, but does not match /foo/barbaz. // While in APISIX, /foo/bar matches both /foo/bar/baz and // /foo/barbaz. // In order to be conformant with Ingress specification, here // we create two paths here, the first is the path itself // (exact match), the other is path + "/*" (prefix match). prefix := pathRule.Path if strings.HasSuffix(prefix, "/") { prefix += "*" } else { prefix += "/*" } uris = append(uris, prefix) } else if *pathRule.PathType == networkingv1beta1.PathTypeImplementationSpecific && ingress.UseRegex { nginxVars = append(nginxVars, kubev2.ApisixRouteHTTPMatchExpr{ Subject: kubev2.ApisixRouteHTTPMatchExprSubject{ Scope: apisixconst.ScopePath, }, Op: apisixconst.OpRegexMatch, Value: &pathRule.Path, }) uris = []string{"/*"} } } route := apisixv1.NewDefaultRoute() route.Name = composeIngressRouteName(ing.Namespace, ing.Name, rule.Host, pathRule.Path) route.ID = id.GenID(route.Name) route.Host = rule.Host route.Uris = uris route.EnableWebsocket = ingress.EnableWebSocket if len(nginxVars) > 0 { routeVars, err := t.ApisixTranslator.TranslateRouteMatchExprs(nginxVars) if err != nil { return nil, err } route.Vars = routeVars route.Priority = _regexPriority } if len(ingress.Plugins) > 0 { route.Plugins = *(ingress.Plugins.DeepCopy()) } if ingress.PluginConfigName != "" { route.PluginConfigId = id.GenID(apisixv1.ComposePluginConfigName(ing.Namespace, ingress.PluginConfigName)) } if ups != nil { route.UpstreamId = ups.ID } ctx.AddRoute(route) } } return ctx, nil } func (t *translator) translateDefaultUpstreamFromIngressV1(namespace string, backend *networkingv1.IngressServiceBackend) *apisixv1.Upstream { var portNumber int32 if backend.Port.Name != "" { svc, err := t.ServiceLister.Services(namespace).Get(backend.Name) if err != nil { portNumber = 0 } else { for _, port := range svc.Spec.Ports { if port.Name == backend.Port.Name { portNumber = port.Port break } } } } else { portNumber = backend.Port.Number } ups := apisixv1.NewDefaultUpstream() ups.Name = apisixv1.ComposeUpstreamName(namespace, backend.Name, "", portNumber, types.ResolveGranularity.Endpoint) ups.ID = id.GenID(ups.Name) return ups } func (t *translator) translateUpstreamFromIngressV1(namespace string, backend *networkingv1.IngressServiceBackend) (*apisixv1.Upstream, error) { var svcPort int32 if backend.Port.Name != "" { svc, err := t.ServiceLister.Services(namespace).Get(backend.Name) if err != nil { return nil, err } for _, port := range svc.Spec.Ports { if port.Name == backend.Port.Name { svcPort = port.Port break } } if svcPort == 0 { return nil, &translation.TranslateError{ Field: "service", Reason: "port not found", } } } else { svcPort = backend.Port.Number } ups, err := t.TranslateService(namespace, backend.Name, "", svcPort) if err != nil { return nil, err } ups.Name = apisixv1.ComposeUpstreamName(namespace, backend.Name, "", svcPort, types.ResolveGranularity.Endpoint) ups.ID = id.GenID(ups.Name) return ups, nil } func (t *translator) translateDefaultUpstreamFromIngressV1beta1(namespace string, svcName string, svcPort intstr.IntOrString) *apisixv1.Upstream { var portNumber int32 if svcPort.Type == intstr.String { svc, err := t.ServiceLister.Services(namespace).Get(svcName) if err != nil { portNumber = 0 } else { for _, port := range svc.Spec.Ports { if port.Name == svcPort.StrVal { portNumber = port.Port break } } } } else { portNumber = svcPort.IntVal } ups := apisixv1.NewDefaultUpstream() ups.Name = apisixv1.ComposeUpstreamName(namespace, svcName, "", portNumber, types.ResolveGranularity.Endpoint) ups.ID = id.GenID(ups.Name) return ups } func (t *translator) translateUpstreamFromIngressV1beta1(namespace string, svcName string, svcPort intstr.IntOrString) (*apisixv1.Upstream, error) { var portNumber int32 if svcPort.Type == intstr.String { svc, err := t.ServiceLister.Services(namespace).Get(svcName) if err != nil { return nil, err } for _, port := range svc.Spec.Ports { if port.Name == svcPort.StrVal { portNumber = port.Port break } } if portNumber == 0 { return nil, &translation.TranslateError{ Field: "service", Reason: "port not found", } } } else { portNumber = svcPort.IntVal } ups, err := t.TranslateService(namespace, svcName, "", portNumber) if err != nil { return nil, err } ups.Name = apisixv1.ComposeUpstreamName(namespace, svcName, "", portNumber, types.ResolveGranularity.Endpoint) ups.ID = id.GenID(ups.Name) return ups, nil } func (t *translator) TranslateOldIngress(ing kube.Ingress) (*translation.TranslateContext, error) { switch ing.GroupVersion() { case kube.IngressV1: return t.translateOldIngressV1(ing.V1()) case kube.IngressV1beta1: return t.translateOldIngressV1beta1(ing.V1beta1()) default: return nil, fmt.Errorf("translator: source group version not supported: %s", ing.GroupVersion()) } } func (t *translator) translateOldIngressTLS(namespace, ingName, secretName string, hosts []string) (*apisixv1.Ssl, error) { ssl, err := t.TranslateIngressTLS(namespace, ingName, secretName, hosts) if err != nil && k8serrors.IsNotFound(err) { return &apisixv1.Ssl{ ID: id.GenID(namespace + "_" + fmt.Sprintf("%v-%v", ingName, "tls")), }, nil } return ssl, err } func (t *translator) translateOldIngressV1(ing *networkingv1.Ingress) (*translation.TranslateContext, error) { oldCtx := translation.DefaultEmptyTranslateContext() for _, tls := range ing.Spec.TLS { ssl, err := t.translateOldIngressTLS(ing.Namespace, ing.Name, tls.SecretName, tls.Hosts) if err != nil { log.Errorw("failed to translate ingress tls to apisix tls", zap.Error(err), zap.Any("ingress", ing), ) continue } oldCtx.AddSSL(ssl) } for _, rule := range ing.Spec.Rules { for _, pathRule := range rule.HTTP.Paths { name := composeIngressRouteName(ing.Namespace, ing.Name, rule.Host, pathRule.Path) r, err := t.Apisix.Cluster(t.ClusterName).Route().Get(context.Background(), name) if err != nil { continue } if r.UpstreamId != "" { ups := apisixv1.NewDefaultUpstream() ups.ID = r.UpstreamId oldCtx.AddUpstream(ups) } if r.PluginConfigId != "" { pc := apisixv1.NewDefaultPluginConfig() pc.ID = r.PluginConfigId oldCtx.AddPluginConfig(pc) } oldCtx.AddRoute(r) } } return oldCtx, nil } func (t *translator) translateOldIngressV1beta1(ing *networkingv1beta1.Ingress) (*translation.TranslateContext, error) { oldCtx := translation.DefaultEmptyTranslateContext() for _, tls := range ing.Spec.TLS { ssl, err := t.translateOldIngressTLS(ing.Namespace, ing.Name, tls.SecretName, tls.Hosts) if err != nil { log.Errorw("failed to translate ingress tls to apisix tls", zap.Error(err), zap.Any("ingress", ing), ) continue } oldCtx.AddSSL(ssl) } for _, rule := range ing.Spec.Rules { for _, pathRule := range rule.HTTP.Paths { name := composeIngressRouteName(ing.Namespace, ing.Name, rule.Host, pathRule.Path) r, err := t.Apisix.Cluster(t.ClusterName).Route().Get(context.Background(), name) if err != nil { continue } if r.UpstreamId != "" { ups := apisixv1.NewDefaultUpstream() ups.ID = r.UpstreamId oldCtx.AddUpstream(ups) } if r.PluginConfigId != "" { pc := apisixv1.NewDefaultPluginConfig() pc.ID = r.PluginConfigId oldCtx.AddPluginConfig(pc) } oldCtx.AddRoute(r) } } return oldCtx, nil } // In the past, we used host + path directly to form its route name for readability, // but this method can cause problems in some scenarios. // For example, the generated name is too long. // The current APISIX limit its maximum length to 100. // ref: https://github.com/apache/apisix-ingress-controller/issues/781 // We will construct the following structure for easy reading and debugging. // ing_namespace_ingressName_id func composeIngressRouteName(namespace, name, host, path string) string { pID := id.GenID(host + path) p := make([]byte, 0, len(namespace)+len(name)+len("ing")+len(pID)+3) buf := bytes.NewBuffer(p) buf.WriteString("ing") buf.WriteByte('_') buf.WriteString(namespace) buf.WriteByte('_') buf.WriteString(name) buf.WriteByte('_') buf.WriteString(pID) return buf.String() }