| // 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 ( |
| "fmt" |
| |
| "go.uber.org/zap" |
| corev1 "k8s.io/api/core/v1" |
| k8serrors "k8s.io/apimachinery/pkg/api/errors" |
| listerscorev1 "k8s.io/client-go/listers/core/v1" |
| |
| config "github.com/apache/apisix-ingress-controller/pkg/config" |
| "github.com/apache/apisix-ingress-controller/pkg/kube" |
| configv2 "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/apis/config/v2" |
| configv2beta2 "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/apis/config/v2beta2" |
| configv2beta3 "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/apis/config/v2beta3" |
| "github.com/apache/apisix-ingress-controller/pkg/log" |
| "github.com/apache/apisix-ingress-controller/pkg/types" |
| apisixv1 "github.com/apache/apisix-ingress-controller/pkg/types/apisix/v1" |
| ) |
| |
| const ( |
| _defaultWeight = 100 |
| ) |
| |
| type translateError struct { |
| field string |
| reason string |
| } |
| |
| func (te *translateError) Error() string { |
| return fmt.Sprintf("%s: %s", te.field, te.reason) |
| } |
| |
| // Translator translates Apisix* CRD resources to the description in APISIX. |
| type Translator interface { |
| // TranslateUpstreamNodes translate Endpoints resources to APISIX Upstream nodes |
| // according to the give port. Extra labels can be passed to filter the ultimate |
| // upstream nodes. |
| TranslateUpstreamNodes(kube.Endpoint, int32, types.Labels) (apisixv1.UpstreamNodes, error) |
| // TranslateUpstreamConfigV2beta3 translates ApisixUpstreamConfig (part of ApisixUpstream) |
| // to APISIX Upstream, it doesn't fill the the Upstream metadata and nodes. |
| TranslateUpstreamConfigV2beta3(*configv2beta3.ApisixUpstreamConfig) (*apisixv1.Upstream, error) |
| // TranslateUpstreamConfigV2 translates ApisixUpstreamConfig (part of ApisixUpstream) |
| // to APISIX Upstream, it doesn't fill the the Upstream metadata and nodes. |
| TranslateUpstreamConfigV2(*configv2.ApisixUpstreamConfig) (*apisixv1.Upstream, error) |
| // TranslateUpstream composes an upstream according to the |
| // given namespace, name (searching Service/Endpoints) and port (filtering Endpoints). |
| // The returned Upstream doesn't have metadata info. |
| // It doesn't assign any metadata fields, so it's caller's responsibility to decide |
| // the metadata. |
| // Note the subset is used to filter the ultimate node list, only pods whose labels |
| // matching the subset labels (defined in ApisixUpstream) will be selected. |
| // When the subset is not found, the node list will be empty. When the subset is empty, |
| // all pods IP will be filled. |
| TranslateUpstream(string, string, string, int32) (*apisixv1.Upstream, error) |
| // TranslateIngress composes a couple of APISIX Routes and upstreams according |
| // to the given Ingress resource. |
| TranslateIngress(kube.Ingress, ...bool) (*TranslateContext, error) |
| // TranslateRouteV2beta2 translates the configv2beta2.ApisixRoute object into several Route, |
| // and Upstream resources. |
| TranslateRouteV2beta2(*configv2beta2.ApisixRoute) (*TranslateContext, error) |
| // TranslateRouteV2beta2NotStrictly translates the configv2beta2.ApisixRoute object into several Route, |
| // and Upstream resources not strictly, only used for delete event. |
| TranslateRouteV2beta2NotStrictly(*configv2beta2.ApisixRoute) (*TranslateContext, error) |
| // TranslateRouteV2beta3 translates the configv2beta3.ApisixRoute object into several Route, |
| // Upstream and PluginConfig resources. |
| TranslateRouteV2beta3(*configv2beta3.ApisixRoute) (*TranslateContext, error) |
| // TranslateRouteV2beta3NotStrictly translates the configv2beta3.ApisixRoute object into several Route, |
| // Upstream and PluginConfig resources not strictly, only used for delete event. |
| TranslateRouteV2beta3NotStrictly(*configv2beta3.ApisixRoute) (*TranslateContext, error) |
| // TranslateRouteV2 translates the configv2.ApisixRoute object into several Route, |
| // Upstream and PluginConfig resources. |
| TranslateRouteV2(*configv2.ApisixRoute) (*TranslateContext, error) |
| // TranslateRouteV2NotStrictly translates the configv2.ApisixRoute object into several Route, |
| // Upstream and PluginConfig resources not strictly, only used for delete event. |
| TranslateRouteV2NotStrictly(*configv2.ApisixRoute) (*TranslateContext, error) |
| // TranslateSSLV2Beta3 translates the configv2beta3.ApisixTls object into the APISIX SSL resource. |
| TranslateSSLV2Beta3(*configv2beta3.ApisixTls) (*apisixv1.Ssl, error) |
| // TranslateSSLV2 translates the configv2.ApisixTls object into the APISIX SSL resource. |
| TranslateSSLV2(*configv2.ApisixTls) (*apisixv1.Ssl, error) |
| // TranslateClusterConfig translates the configv2beta3.ApisixClusterConfig object into the APISIX |
| // Global Rule resource. |
| TranslateClusterConfigV2beta3(*configv2beta3.ApisixClusterConfig) (*apisixv1.GlobalRule, error) |
| // TranslateClusterConfigV2 translates the configv2.ApisixClusterConfig object into the APISIX |
| // Global Rule resource. |
| TranslateClusterConfigV2(*configv2.ApisixClusterConfig) (*apisixv1.GlobalRule, error) |
| // TranslateApisixConsumer translates the configv2beta3.APisixConsumer object into the APISIX Consumer |
| // resource. |
| TranslateApisixConsumerV2beta3(*configv2beta3.ApisixConsumer) (*apisixv1.Consumer, error) |
| // TranslateApisixConsumerV2 translates the configv2beta3.APisixConsumer object into the APISIX Consumer |
| // resource. |
| TranslateApisixConsumerV2(ac *configv2.ApisixConsumer) (*apisixv1.Consumer, error) |
| // TranslatePluginConfigV2beta3 translates the configv2.ApisixPluginConfig object into several PluginConfig |
| // resources. |
| TranslatePluginConfigV2beta3(*configv2beta3.ApisixPluginConfig) (*TranslateContext, error) |
| // TranslatePluginConfigV2beta3NotStrictly translates the configv2beta3.ApisixPluginConfig object into several PluginConfig |
| // resources not strictly, only used for delete event. |
| TranslatePluginConfigV2beta3NotStrictly(*configv2beta3.ApisixPluginConfig) (*TranslateContext, error) |
| // TranslatePluginConfigV2 translates the configv2.ApisixPluginConfig object into several PluginConfig |
| // resources. |
| TranslatePluginConfigV2(*configv2.ApisixPluginConfig) (*TranslateContext, error) |
| // TranslatePluginConfigV2NotStrictly translates the configv2.ApisixPluginConfig object into several PluginConfig |
| // resources not strictly, only used for delete event. |
| TranslatePluginConfigV2NotStrictly(*configv2.ApisixPluginConfig) (*TranslateContext, error) |
| // ExtractKeyPair extracts certificate and private key pair from secret |
| // Supports APISIX style ("cert" and "key") and Kube style ("tls.crt" and "tls.key) |
| ExtractKeyPair(s *corev1.Secret, hasPrivateKey bool) ([]byte, []byte, error) |
| } |
| |
| // TranslatorOptions contains options to help Translator |
| // work well. |
| type TranslatorOptions struct { |
| PodCache types.PodCache |
| PodLister listerscorev1.PodLister |
| EndpointLister kube.EndpointLister |
| ServiceLister listerscorev1.ServiceLister |
| ApisixUpstreamLister kube.ApisixUpstreamLister |
| SecretLister listerscorev1.SecretLister |
| UseEndpointSlices bool |
| APIVersion string |
| } |
| |
| type translator struct { |
| *TranslatorOptions |
| } |
| |
| // NewTranslator initializes a APISIX CRD resources Translator. |
| func NewTranslator(opts *TranslatorOptions) Translator { |
| return &translator{ |
| TranslatorOptions: opts, |
| } |
| } |
| |
| func (t *translator) TranslateUpstreamConfigV2beta3(au *configv2beta3.ApisixUpstreamConfig) (*apisixv1.Upstream, error) { |
| ups := apisixv1.NewDefaultUpstream() |
| if err := t.translateUpstreamScheme(au.Scheme, ups); err != nil { |
| return nil, err |
| } |
| if err := t.translateUpstreamLoadBalancerV2beta3(au.LoadBalancer, ups); err != nil { |
| return nil, err |
| } |
| if err := t.translateUpstreamHealthCheckV2beta3(au.HealthCheck, ups); err != nil { |
| return nil, err |
| } |
| if err := t.translateUpstreamRetriesAndTimeoutV2beta3(au.Retries, au.Timeout, ups); err != nil { |
| return nil, err |
| } |
| if err := t.translateClientTLSV2beta3(au.TLSSecret, ups); err != nil { |
| return nil, err |
| } |
| return ups, nil |
| } |
| |
| func (t *translator) TranslateUpstreamConfigV2(au *configv2.ApisixUpstreamConfig) (*apisixv1.Upstream, error) { |
| ups := apisixv1.NewDefaultUpstream() |
| if err := t.translateUpstreamScheme(au.Scheme, ups); err != nil { |
| return nil, err |
| } |
| if err := t.translateUpstreamLoadBalancerV2(au.LoadBalancer, ups); err != nil { |
| return nil, err |
| } |
| if err := t.translateUpstreamHealthCheckV2(au.HealthCheck, ups); err != nil { |
| return nil, err |
| } |
| if err := t.translateUpstreamRetriesAndTimeoutV2(au.Retries, au.Timeout, ups); err != nil { |
| return nil, err |
| } |
| if err := t.translateClientTLSV2(au.TLSSecret, ups); err != nil { |
| return nil, err |
| } |
| return ups, nil |
| } |
| |
| func (t *translator) TranslateUpstream(namespace, name, subset string, port int32) (*apisixv1.Upstream, error) { |
| var ( |
| endpoint kube.Endpoint |
| err error |
| ) |
| if t.UseEndpointSlices { |
| endpoint, err = t.EndpointLister.GetEndpointSlices(namespace, name) |
| } else { |
| endpoint, err = t.EndpointLister.GetEndpoint(namespace, name) |
| } |
| if err != nil { |
| return nil, &translateError{ |
| field: "endpoints", |
| reason: err.Error(), |
| } |
| } |
| |
| switch t.APIVersion { |
| case config.ApisixV2beta3: |
| return t.translateUpstreamV2beta3(&endpoint, namespace, name, subset, port) |
| case config.ApisixV2: |
| return t.translateUpstreamV2(&endpoint, namespace, name, subset, port) |
| default: |
| panic(fmt.Errorf("unsupported ApisixUpstream version %v", t.APIVersion)) |
| } |
| } |
| |
| func (t *translator) translateUpstreamV2(ep *kube.Endpoint, namespace, name, subset string, port int32) (*apisixv1.Upstream, error) { |
| au, err := t.ApisixUpstreamLister.V2(namespace, name) |
| ups := apisixv1.NewDefaultUpstream() |
| if err != nil { |
| if k8serrors.IsNotFound(err) { |
| // If subset in ApisixRoute is not empty but the ApisixUpstream resource not found, |
| // just set an empty node list. |
| if subset != "" { |
| ups.Nodes = apisixv1.UpstreamNodes{} |
| return ups, nil |
| } |
| } else { |
| return nil, &translateError{ |
| field: "ApisixUpstream", |
| reason: err.Error(), |
| } |
| } |
| } |
| var labels types.Labels |
| if subset != "" { |
| for _, ss := range au.V2().Spec.Subsets { |
| if ss.Name == subset { |
| labels = ss.Labels |
| break |
| } |
| } |
| } |
| // Filter nodes by subset. |
| nodes, err := t.TranslateUpstreamNodes(*ep, port, labels) |
| if err != nil { |
| return nil, err |
| } |
| if au == nil || au.V2().Spec == nil { |
| ups.Nodes = nodes |
| return ups, nil |
| } |
| |
| upsCfg := &au.V2().Spec.ApisixUpstreamConfig |
| for _, pls := range au.V2().Spec.PortLevelSettings { |
| if pls.Port == port { |
| upsCfg = &pls.ApisixUpstreamConfig |
| break |
| } |
| } |
| ups, err = t.TranslateUpstreamConfigV2(upsCfg) |
| if err != nil { |
| return nil, err |
| } |
| ups.Nodes = nodes |
| return ups, nil |
| } |
| |
| func (t *translator) translateUpstreamV2beta3(ep *kube.Endpoint, namespace, name, subset string, port int32) (*apisixv1.Upstream, error) { |
| au, err := t.ApisixUpstreamLister.V2beta3(namespace, name) |
| ups := apisixv1.NewDefaultUpstream() |
| if err != nil { |
| if k8serrors.IsNotFound(err) { |
| // If subset in ApisixRoute is not empty but the ApisixUpstream resource not found, |
| // just set an empty node list. |
| if subset != "" { |
| ups.Nodes = apisixv1.UpstreamNodes{} |
| return ups, nil |
| } |
| } else { |
| return nil, &translateError{ |
| field: "ApisixUpstream", |
| reason: err.Error(), |
| } |
| } |
| } |
| if err != nil { |
| if k8serrors.IsNotFound(err) { |
| // If subset in ApisixRoute is not empty but the ApisixUpstream resource not found, |
| // just set an empty node list. |
| if subset != "" { |
| ups.Nodes = apisixv1.UpstreamNodes{} |
| return ups, nil |
| } |
| } else { |
| return nil, &translateError{ |
| field: "ApisixUpstream", |
| reason: err.Error(), |
| } |
| } |
| } |
| var labels types.Labels |
| if subset != "" { |
| for _, ss := range au.V2beta3().Spec.Subsets { |
| if ss.Name == subset { |
| labels = ss.Labels |
| break |
| } |
| } |
| } |
| // Filter nodes by subset. |
| nodes, err := t.TranslateUpstreamNodes(*ep, port, labels) |
| if err != nil { |
| return nil, err |
| } |
| if au == nil || au.V2beta3().Spec == nil { |
| ups.Nodes = nodes |
| return ups, nil |
| } |
| |
| upsCfg := &au.V2beta3().Spec.ApisixUpstreamConfig |
| for _, pls := range au.V2beta3().Spec.PortLevelSettings { |
| if pls.Port == port { |
| upsCfg = &pls.ApisixUpstreamConfig |
| break |
| } |
| } |
| ups, err = t.TranslateUpstreamConfigV2beta3(upsCfg) |
| if err != nil { |
| return nil, err |
| } |
| ups.Nodes = nodes |
| return ups, nil |
| } |
| |
| func (t *translator) TranslateUpstreamNodes(endpoint kube.Endpoint, port int32, labels types.Labels) (apisixv1.UpstreamNodes, error) { |
| namespace, err := endpoint.Namespace() |
| if err != nil { |
| log.Errorw("failed to get endpoint namespace", |
| zap.Error(err), |
| zap.Any("endpoint", endpoint), |
| ) |
| return nil, err |
| } |
| svcName := endpoint.ServiceName() |
| svc, err := t.ServiceLister.Services(namespace).Get(svcName) |
| if err != nil { |
| return nil, &translateError{ |
| field: "service", |
| reason: err.Error(), |
| } |
| } |
| |
| var svcPort *corev1.ServicePort |
| for _, exposePort := range svc.Spec.Ports { |
| if exposePort.Port == port { |
| svcPort = &exposePort |
| break |
| } |
| } |
| if svcPort == nil { |
| return nil, &translateError{ |
| field: "service.spec.ports", |
| reason: "port not defined", |
| } |
| } |
| // As nodes is not optional, here we create an empty slice, |
| // not a nil slice. |
| nodes := make(apisixv1.UpstreamNodes, 0) |
| for _, hostport := range endpoint.Endpoints(svcPort) { |
| nodes = append(nodes, apisixv1.UpstreamNode{ |
| Host: hostport.Host, |
| Port: hostport.Port, |
| // FIXME Custom node weight |
| Weight: _defaultWeight, |
| }) |
| } |
| if labels != nil { |
| nodes = t.filterNodesByLabels(nodes, labels, namespace) |
| return nodes, nil |
| } |
| return nodes, nil |
| } |
| |
| func (t *translator) TranslateIngress(ing kube.Ingress, args ...bool) (*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) |
| case kube.IngressExtensionsV1beta1: |
| return t.translateIngressExtensionsV1beta1(ing.ExtensionsV1beta1(), skipVerify) |
| default: |
| return nil, fmt.Errorf("translator: source group version not supported: %s", ing.GroupVersion()) |
| } |
| } |