You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by zh...@apache.org on 2022/05/31 08:52:51 UTC
[apisix-ingress-controller] branch master updated: feat: support gateway API HTTPRoute (#1037)
This is an automated email from the ASF dual-hosted git repository.
zhangjintao pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git
The following commit(s) were added to refs/heads/master by this push:
new 6c7452ff feat: support gateway API HTTPRoute (#1037)
6c7452ff is described below
commit 6c7452ffab358c47e25882d802e4cc891e95eb37
Author: Sarasa Kisaragi <li...@gmail.com>
AuthorDate: Tue May 31 16:52:46 2022 +0800
feat: support gateway API HTTPRoute (#1037)
---
go.mod | 1 +
pkg/ingress/controller.go | 29 +-
pkg/ingress/gateway_httproute.go | 216 +++++++++++++
pkg/kube/translation/gateway_httproute.go | 235 ++++++++++++++
pkg/kube/translation/gateway_httproute_test.go | 406 +++++++++++++++++++++++++
pkg/kube/translation/translator.go | 5 +-
pkg/types/event.go | 2 +
7 files changed, 886 insertions(+), 8 deletions(-)
diff --git a/go.mod b/go.mod
index bf2394ac..aefa0e0f 100644
--- a/go.mod
+++ b/go.mod
@@ -6,6 +6,7 @@ require (
github.com/gin-gonic/gin v1.7.7
github.com/hashicorp/go-memdb v1.3.2
github.com/hashicorp/go-multierror v1.1.1
+ github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.11.0
github.com/prometheus/client_model v0.2.0
github.com/slok/kubewebhook/v2 v2.2.0
diff --git a/pkg/ingress/controller.go b/pkg/ingress/controller.go
index 33c273c8..7ab245c6 100644
--- a/pkg/ingress/controller.go
+++ b/pkg/ingress/controller.go
@@ -119,15 +119,18 @@ type Controller struct {
apisixPluginConfigLister kube.ApisixPluginConfigLister
gatewayInformer cache.SharedIndexInformer
gatewayLister gatewaylistersv1alpha2.GatewayLister
+ gatewayHttpRouteInformer cache.SharedIndexInformer
+ gatewayHttpRouteLister gatewaylistersv1alpha2.HTTPRouteLister
// resource controllers
- namespaceController *namespaceController
- podController *podController
- endpointsController *endpointsController
- endpointSliceController *endpointSliceController
- ingressController *ingressController
- secretController *secretController
- gatewayController *gatewayController
+ namespaceController *namespaceController
+ podController *podController
+ endpointsController *endpointsController
+ endpointSliceController *endpointSliceController
+ ingressController *ingressController
+ secretController *secretController
+ gatewayController *gatewayController
+ gatewayHTTPRouteController *gatewayHTTPRouteController
apisixUpstreamController *apisixUpstreamController
apisixRouteController *apisixRouteController
@@ -264,6 +267,9 @@ func (c *Controller) initWhenStartLeading() {
c.gatewayLister = gatewayFactory.Gateway().V1alpha2().Gateways().Lister()
c.gatewayInformer = gatewayFactory.Gateway().V1alpha2().Gateways().Informer()
+ c.gatewayHttpRouteLister = gatewayFactory.Gateway().V1alpha2().HTTPRoutes().Lister()
+ c.gatewayHttpRouteInformer = gatewayFactory.Gateway().V1alpha2().HTTPRoutes().Informer()
+
switch c.cfg.Kubernetes.ApisixRouteVersion {
case config.ApisixRouteV2beta2:
apisixRouteInformer = apisixFactory.Apisix().V2beta2().ApisixRoutes().Informer()
@@ -328,6 +334,7 @@ func (c *Controller) initWhenStartLeading() {
c.apisixConsumerController = c.newApisixConsumerController()
c.apisixPluginConfigController = c.newApisixPluginConfigController()
c.gatewayController = c.newGatewayController()
+ c.gatewayHTTPRouteController = c.newGatewayHTTPRouteController()
}
// recorderEvent recorder events for resources
@@ -550,9 +557,17 @@ func (c *Controller) run(ctx context.Context) {
c.gatewayInformer.Run(ctx.Done())
})
+ c.goAttach(func() {
+ c.gatewayHttpRouteInformer.Run(ctx.Done())
+ })
+
c.goAttach(func() {
c.gatewayController.run(ctx)
})
+
+ c.goAttach(func() {
+ c.gatewayHTTPRouteController.run(ctx)
+ })
}
c.goAttach(func() {
diff --git a/pkg/ingress/gateway_httproute.go b/pkg/ingress/gateway_httproute.go
new file mode 100644
index 00000000..667bcb22
--- /dev/null
+++ b/pkg/ingress/gateway_httproute.go
@@ -0,0 +1,216 @@
+// 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 ingress
+
+import (
+ "context"
+ "time"
+
+ "go.uber.org/zap"
+ k8serrors "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/client-go/tools/cache"
+ "k8s.io/client-go/util/workqueue"
+ gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+
+ "github.com/apache/apisix-ingress-controller/pkg/kube/translation"
+ "github.com/apache/apisix-ingress-controller/pkg/log"
+ "github.com/apache/apisix-ingress-controller/pkg/types"
+)
+
+type gatewayHTTPRouteController struct {
+ controller *Controller
+ workqueue workqueue.RateLimitingInterface
+ workers int
+}
+
+func (c *Controller) newGatewayHTTPRouteController() *gatewayHTTPRouteController {
+ ctrl := &gatewayHTTPRouteController{
+ controller: c,
+ workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.NewItemFastSlowRateLimiter(1*time.Second, 60*time.Second, 5), "GatewayHTTPRoute"),
+ workers: 1,
+ }
+
+ ctrl.controller.gatewayHttpRouteInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
+ AddFunc: ctrl.onAdd,
+ UpdateFunc: ctrl.onUpdate,
+ DeleteFunc: ctrl.OnDelete,
+ })
+ return ctrl
+}
+
+func (c *gatewayHTTPRouteController) run(ctx context.Context) {
+ log.Info("gateway HTTPRoute controller started")
+ defer log.Info("gateway HTTPRoute controller exited")
+ defer c.workqueue.ShutDown()
+
+ if !cache.WaitForCacheSync(ctx.Done(), c.controller.gatewayHttpRouteInformer.HasSynced) {
+ log.Error("sync Gateway HTTPRoute cache failed")
+ return
+ }
+
+ for i := 0; i < c.workers; i++ {
+ go c.runWorker(ctx)
+ }
+ <-ctx.Done()
+}
+
+func (c *gatewayHTTPRouteController) runWorker(ctx context.Context) {
+ for {
+ obj, quit := c.workqueue.Get()
+ if quit {
+ return
+ }
+ err := c.sync(ctx, obj.(*types.Event))
+ c.workqueue.Done(obj)
+ c.handleSyncErr(obj, err)
+ }
+}
+
+func (c *gatewayHTTPRouteController) sync(ctx context.Context, ev *types.Event) error {
+ key := ev.Object.(string)
+ namespace, name, err := cache.SplitMetaNamespaceKey(key)
+ if err != nil {
+ log.Errorw("found Gateway HTTPRoute resource with invalid key",
+ zap.Error(err),
+ zap.String("key", key),
+ )
+ return err
+ }
+
+ httpRoute, err := c.controller.gatewayHttpRouteLister.HTTPRoutes(namespace).Get(name)
+ if err != nil {
+ if !k8serrors.IsNotFound(err) {
+ log.Errorw("failed to get Gateway HTTPRoute",
+ zap.Error(err),
+ zap.String("key", key),
+ )
+ return err
+ }
+ if ev.Type != types.EventDelete {
+ log.Warnw("Gateway HTTPRoute was deleted before process",
+ zap.String("key", key),
+ )
+ // Don't need to retry.
+ return nil
+ }
+ }
+
+ if ev.Type == types.EventDelete {
+ if httpRoute != nil {
+ // We still find the resource while we are processing the DELETE event,
+ // that means object with same namespace and name was created, discarding
+ // this stale DELETE event.
+ log.Warnf("discard the stale Gateway delete event since the %s exists", key)
+ return nil
+ }
+ httpRoute = ev.Tombstone.(*gatewayv1alpha2.HTTPRoute)
+ }
+
+ tctx, err := c.controller.translator.TranslateGatewayHTTPRouteV1Alpha2(httpRoute)
+
+ if err != nil {
+ log.Errorw("failed to translate gateway HTTPRoute",
+ zap.Error(err),
+ zap.Any("object", httpRoute),
+ )
+ return err
+ }
+
+ log.Debugw("translated HTTPRoute",
+ zap.Any("routes", tctx.Routes),
+ zap.Any("upstreams", tctx.Upstreams),
+ )
+ m := &manifest{
+ routes: tctx.Routes,
+ upstreams: tctx.Upstreams,
+ }
+
+ var (
+ added *manifest
+ updated *manifest
+ deleted *manifest
+ )
+
+ if ev.Type == types.EventDelete {
+ deleted = m
+ } else if ev.Type == types.EventAdd {
+ added = m
+ } else {
+ var oldCtx *translation.TranslateContext
+ oldObj := ev.OldObject.(*gatewayv1alpha2.HTTPRoute)
+ oldCtx, err = c.controller.translator.TranslateGatewayHTTPRouteV1Alpha2(oldObj)
+ if err != nil {
+ log.Errorw("failed to translate old HTTPRoute",
+ zap.String("version", oldObj.APIVersion),
+ zap.String("event_type", "update"),
+ zap.Any("HTTPRoute", oldObj),
+ zap.Error(err),
+ )
+ return err
+ }
+
+ om := &manifest{
+ routes: oldCtx.Routes,
+ upstreams: oldCtx.Upstreams,
+ }
+ added, updated, deleted = m.diff(om)
+ }
+
+ return c.controller.syncManifests(ctx, added, updated, deleted)
+}
+
+func (c *gatewayHTTPRouteController) handleSyncErr(obj interface{}, err error) {
+ if err == nil {
+ c.workqueue.Forget(obj)
+ c.controller.MetricsCollector.IncrSyncOperation("gateway_httproute", "success")
+ return
+ }
+ event := obj.(*types.Event)
+ if k8serrors.IsNotFound(err) && event.Type != types.EventDelete {
+ log.Infow("sync gateway HTTPRoute but not found, ignore",
+ zap.String("event_type", event.Type.String()),
+ zap.String("HTTPRoute ", event.Object.(string)),
+ )
+ c.workqueue.Forget(event)
+ return
+ }
+ log.Warnw("sync gateway HTTPRoute failed, will retry",
+ zap.Any("object", obj),
+ zap.Error(err),
+ )
+ c.workqueue.AddRateLimited(obj)
+ c.controller.MetricsCollector.IncrSyncOperation("gateway_httproute", "failure")
+}
+
+func (c *gatewayHTTPRouteController) onAdd(obj interface{}) {
+ key, err := cache.MetaNamespaceKeyFunc(obj)
+ if err != nil {
+ log.Errorf("found gateway HTTPRoute resource with bad meta namespace key: %s", err)
+ return
+ }
+ if !c.controller.isWatchingNamespace(key) {
+ return
+ }
+ log.Debugw("gateway HTTPRoute add event arrived",
+ zap.Any("object", obj),
+ )
+
+ c.workqueue.Add(&types.Event{
+ Type: types.EventAdd,
+ Object: key,
+ })
+}
+func (c *gatewayHTTPRouteController) onUpdate(oldObj, newObj interface{}) {}
+func (c *gatewayHTTPRouteController) OnDelete(obj interface{}) {}
diff --git a/pkg/kube/translation/gateway_httproute.go b/pkg/kube/translation/gateway_httproute.go
new file mode 100644
index 00000000..e096f096
--- /dev/null
+++ b/pkg/kube/translation/gateway_httproute.go
@@ -0,0 +1,235 @@
+// 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"
+ "strings"
+
+ "github.com/pkg/errors"
+ "go.uber.org/zap"
+ gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+
+ "github.com/apache/apisix-ingress-controller/pkg/id"
+ "github.com/apache/apisix-ingress-controller/pkg/log"
+ apisixv1 "github.com/apache/apisix-ingress-controller/pkg/types/apisix/v1"
+)
+
+func (t *translator) TranslateGatewayHTTPRouteV1Alpha2(httpRoute *gatewayv1alpha2.HTTPRoute) (*TranslateContext, error) {
+ ctx := defaultEmptyTranslateContext()
+
+ var hosts []string
+ for _, hostname := range httpRoute.Spec.Hostnames {
+ hosts = append(hosts, string(hostname))
+ }
+
+ rules := httpRoute.Spec.Rules
+
+ for i, rule := range rules {
+ backends := rule.BackendRefs
+ if len(backends) == 0 {
+ continue
+ }
+
+ var ruleUpstreams []*apisixv1.Upstream
+ var weightedUpstreams []apisixv1.TrafficSplitConfigRuleWeightedUpstream
+
+ for j, backend := range backends {
+ //TODO: Support filters
+ //filters := backend.Filters
+ kind := strings.ToLower(string(*backend.Kind))
+ if kind != "service" {
+ log.Warnw(fmt.Sprintf("ignore non-service kind at Rules[%v].BackendRefs[%v]", i, j),
+ zap.String("kind", kind),
+ )
+ continue
+ }
+
+ ns := string(*backend.Namespace)
+ //if ns != httpRoute.Namespace {
+ // TODO: check gatewayv1alpha2.ReferencePolicy
+ //}
+
+ ups, err := t.TranslateUpstream(ns, string(backend.Name), "", int32(*backend.Port))
+ if err != nil {
+ return nil, errors.Wrap(err, fmt.Sprintf("failed to translate Rules[%v].BackendRefs[%v]", i, j))
+ }
+ name := apisixv1.ComposeUpstreamName(ns, string(backend.Name), "", int32(*backend.Port))
+ ups.Labels["id-name"] = name
+ ups.ID = id.GenID(name)
+ ctx.addUpstream(ups)
+ ruleUpstreams = append(ruleUpstreams, ups)
+
+ if backend.Weight == nil {
+ weightedUpstreams = append(weightedUpstreams, apisixv1.TrafficSplitConfigRuleWeightedUpstream{
+ UpstreamID: ups.ID,
+ Weight: 1, // 1 is default value of BackendRef
+ })
+ } else {
+ weightedUpstreams = append(weightedUpstreams, apisixv1.TrafficSplitConfigRuleWeightedUpstream{
+ UpstreamID: ups.ID,
+ Weight: int(*backend.Weight),
+ })
+ }
+ }
+ if len(ruleUpstreams) == 0 {
+ log.Warnw(fmt.Sprintf("ignore all-failed backend refs at Rules[%v]", i),
+ zap.Any("BackendRefs", rule.BackendRefs),
+ )
+ continue
+ }
+
+ matches := rule.Matches
+ if len(matches) == 0 {
+ defaultType := gatewayv1alpha2.PathMatchPathPrefix
+ defaultValue := "/"
+ matches = []gatewayv1alpha2.HTTPRouteMatch{
+ {
+ Path: &gatewayv1alpha2.HTTPPathMatch{
+ Type: &defaultType,
+ Value: &defaultValue,
+ },
+ },
+ }
+ }
+
+ for j, match := range matches {
+ route, err := t.translateGatewayHTTPRouteMatch(&match)
+ if err != nil {
+ return nil, errors.Wrap(err, fmt.Sprintf("failed to translate Rules[%v].Matches[%v]", i, j))
+ }
+
+ route.Hosts = hosts
+
+ // Bind Upstream
+ if len(ruleUpstreams) == 1 {
+ route.UpstreamId = ruleUpstreams[0].ID
+ } else if len(ruleUpstreams) > 0 {
+ route.Plugins["traffic-split"] = &apisixv1.TrafficSplitConfig{
+ Rules: []apisixv1.TrafficSplitConfigRule{
+ {
+ WeightedUpstreams: weightedUpstreams,
+ },
+ },
+ }
+ }
+
+ ctx.addRoute(route)
+ }
+
+ //TODO: Support filters
+ //filters := rule.Filters
+ }
+
+ return ctx, nil
+}
+
+func (t *translator) translateGatewayHTTPRouteMatch(match *gatewayv1alpha2.HTTPRouteMatch) (*apisixv1.Route, error) {
+ route := apisixv1.NewDefaultRoute()
+
+ if match.Path != nil {
+ switch *match.Path.Type {
+ case gatewayv1alpha2.PathMatchExact:
+ route.Uri = *match.Path.Value
+ case gatewayv1alpha2.PathMatchPathPrefix:
+ route.Uri = *match.Path.Value + "*"
+ case gatewayv1alpha2.PathMatchRegularExpression:
+ var this []apisixv1.StringOrSlice
+ this = append(this, apisixv1.StringOrSlice{
+ StrVal: "uri",
+ })
+ this = append(this, apisixv1.StringOrSlice{
+ StrVal: "~~",
+ })
+ this = append(this, apisixv1.StringOrSlice{
+ StrVal: *match.Path.Value,
+ })
+
+ route.Vars = append(route.Vars, this)
+ default:
+ return nil, errors.New("unknown path match type " + string(*match.Path.Type))
+ }
+ }
+
+ if match.Headers != nil && len(match.Headers) > 0 {
+ for _, header := range match.Headers {
+ name := strings.ToLower(string(header.Name))
+ name = strings.ReplaceAll(name, "-", "_")
+
+ var this []apisixv1.StringOrSlice
+ this = append(this, apisixv1.StringOrSlice{
+ StrVal: "http_" + name,
+ })
+
+ switch *header.Type {
+ case gatewayv1alpha2.HeaderMatchExact:
+ this = append(this, apisixv1.StringOrSlice{
+ StrVal: "==",
+ })
+ case gatewayv1alpha2.HeaderMatchRegularExpression:
+ this = append(this, apisixv1.StringOrSlice{
+ StrVal: "~~",
+ })
+ default:
+ return nil, errors.New("unknown header match type " + string(*header.Type))
+ }
+
+ this = append(this, apisixv1.StringOrSlice{
+ StrVal: header.Value,
+ })
+
+ route.Vars = append(route.Vars, this)
+ }
+ }
+
+ if match.QueryParams != nil && len(match.QueryParams) > 0 {
+ for _, query := range match.QueryParams {
+ var this []apisixv1.StringOrSlice
+ this = append(this, apisixv1.StringOrSlice{
+ StrVal: "arg_" + strings.ToLower(query.Name),
+ })
+
+ switch *query.Type {
+ case gatewayv1alpha2.QueryParamMatchExact:
+ this = append(this, apisixv1.StringOrSlice{
+ StrVal: "==",
+ })
+ case gatewayv1alpha2.QueryParamMatchRegularExpression:
+ this = append(this, apisixv1.StringOrSlice{
+ StrVal: "~~",
+ })
+ default:
+ return nil, errors.New("unknown query match type " + string(*query.Type))
+ }
+
+ this = append(this, apisixv1.StringOrSlice{
+ StrVal: query.Value,
+ })
+
+ route.Vars = append(route.Vars, this)
+ }
+ }
+
+ if match.Method != nil {
+ route.Methods = []string{
+ string(*match.Method),
+ }
+ }
+
+ return route, nil
+}
diff --git a/pkg/kube/translation/gateway_httproute_test.go b/pkg/kube/translation/gateway_httproute_test.go
new file mode 100644
index 00000000..9b8d98f2
--- /dev/null
+++ b/pkg/kube/translation/gateway_httproute_test.go
@@ -0,0 +1,406 @@
+// 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 (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/util/intstr"
+ "k8s.io/client-go/informers"
+ "k8s.io/client-go/kubernetes/fake"
+ "k8s.io/client-go/tools/cache"
+ gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+
+ "github.com/apache/apisix-ingress-controller/pkg/kube"
+ fakeapisix "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/client/clientset/versioned/fake"
+ apisixinformers "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/client/informers/externalversions"
+ v1 "github.com/apache/apisix-ingress-controller/pkg/types/apisix/v1"
+)
+
+func mockHTTPRouteTranslator(t *testing.T) (*translator, <-chan struct{}) {
+ svc := &corev1.Service{
+ TypeMeta: metav1.TypeMeta{},
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "svc",
+ Namespace: "test",
+ },
+ Spec: corev1.ServiceSpec{
+ Ports: []corev1.ServicePort{
+ {
+ Name: "port1",
+ Port: 80,
+ TargetPort: intstr.IntOrString{
+ Type: intstr.Int,
+ IntVal: 9080,
+ },
+ },
+ {
+ Name: "port2",
+ Port: 443,
+ TargetPort: intstr.IntOrString{
+ Type: intstr.Int,
+ IntVal: 9443,
+ },
+ },
+ },
+ },
+ }
+ endpoints := &corev1.Endpoints{
+ TypeMeta: metav1.TypeMeta{},
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "svc",
+ Namespace: "test",
+ },
+ Subsets: []corev1.EndpointSubset{
+ {
+ Ports: []corev1.EndpointPort{
+ {
+ Name: "port1",
+ Port: 9080,
+ },
+ {
+ Name: "port2",
+ Port: 9443,
+ },
+ },
+ Addresses: []corev1.EndpointAddress{
+ {IP: "192.168.1.1"},
+ {IP: "192.168.1.2"},
+ },
+ },
+ },
+ }
+
+ client := fake.NewSimpleClientset()
+ informersFactory := informers.NewSharedInformerFactory(client, 0)
+ svcInformer := informersFactory.Core().V1().Services().Informer()
+ svcLister := informersFactory.Core().V1().Services().Lister()
+ epLister, epInformer := kube.NewEndpointListerAndInformer(informersFactory, false)
+ apisixClient := fakeapisix.NewSimpleClientset()
+ apisixInformersFactory := apisixinformers.NewSharedInformerFactory(apisixClient, 0)
+
+ _, err := client.CoreV1().Endpoints("test").Create(context.Background(), endpoints, metav1.CreateOptions{})
+ assert.Nil(t, err)
+ _, err = client.CoreV1().Services("test").Create(context.Background(), svc, metav1.CreateOptions{})
+ assert.Nil(t, err)
+
+ tr := &translator{
+ &TranslatorOptions{
+ EndpointLister: epLister,
+ ServiceLister: svcLister,
+ ApisixUpstreamLister: apisixInformersFactory.Apisix().V2beta3().ApisixUpstreams().Lister(),
+ },
+ }
+
+ processCh := make(chan struct{})
+ svcInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
+ AddFunc: func(obj interface{}) {
+ processCh <- struct{}{}
+ },
+ })
+ epInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
+ AddFunc: func(obj interface{}) {
+ processCh <- struct{}{}
+ },
+ })
+
+ stopCh := make(chan struct{})
+ defer close(stopCh)
+ go svcInformer.Run(stopCh)
+ go epInformer.Run(stopCh)
+ cache.WaitForCacheSync(stopCh, svcInformer.HasSynced)
+
+ return tr, processCh
+}
+
+func TestTranslateGatewayHTTPRouteExactMatch(t *testing.T) {
+ refStr := func(str string) *string {
+ return &str
+ }
+ refKind := func(str gatewayv1alpha2.Kind) *gatewayv1alpha2.Kind {
+ return &str
+ }
+ refNamespace := func(str gatewayv1alpha2.Namespace) *gatewayv1alpha2.Namespace {
+ return &str
+ }
+ refMethod := func(str gatewayv1alpha2.HTTPMethod) *gatewayv1alpha2.HTTPMethod {
+ return &str
+ }
+ refPathMatchType := func(str gatewayv1alpha2.PathMatchType) *gatewayv1alpha2.PathMatchType {
+ return &str
+ }
+ refHeaderMatchType := func(str gatewayv1alpha2.HeaderMatchType) *gatewayv1alpha2.HeaderMatchType {
+ return &str
+ }
+ refQueryParamMatchType := func(str gatewayv1alpha2.QueryParamMatchType) *gatewayv1alpha2.QueryParamMatchType {
+ return &str
+ }
+ refInt32 := func(i int32) *int32 {
+ return &i
+ }
+ refPortNumber := func(i gatewayv1alpha2.PortNumber) *gatewayv1alpha2.PortNumber {
+ return &i
+ }
+
+ tr, processCh := mockHTTPRouteTranslator(t)
+ <-processCh
+ <-processCh
+
+ httpRoute := &gatewayv1alpha2.HTTPRoute{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "http_route",
+ Namespace: "test",
+ },
+ Spec: gatewayv1alpha2.HTTPRouteSpec{
+ Hostnames: []gatewayv1alpha2.Hostname{
+ "example.com",
+ },
+ Rules: []gatewayv1alpha2.HTTPRouteRule{
+ {
+ Matches: []gatewayv1alpha2.HTTPRouteMatch{
+ {
+ Path: &gatewayv1alpha2.HTTPPathMatch{
+ Type: refPathMatchType(gatewayv1alpha2.PathMatchPathPrefix),
+ Value: refStr("/path"),
+ },
+ Headers: []gatewayv1alpha2.HTTPHeaderMatch{
+ {
+ Type: refHeaderMatchType(gatewayv1alpha2.HeaderMatchExact),
+ Name: "REFERER",
+ Value: "api7.com",
+ },
+ },
+ QueryParams: []gatewayv1alpha2.HTTPQueryParamMatch{
+ {
+ Type: refQueryParamMatchType(gatewayv1alpha2.QueryParamMatchExact),
+ Name: "user",
+ Value: "api7",
+ },
+ {
+ Type: refQueryParamMatchType(gatewayv1alpha2.QueryParamMatchExact),
+ Name: "title",
+ Value: "ingress",
+ },
+ },
+ Method: refMethod(gatewayv1alpha2.HTTPMethodGet),
+ },
+ },
+ Filters: []gatewayv1alpha2.HTTPRouteFilter{
+ // TODO
+ },
+ BackendRefs: []gatewayv1alpha2.HTTPBackendRef{
+ {
+ BackendRef: gatewayv1alpha2.BackendRef{
+ BackendObjectReference: gatewayv1alpha2.BackendObjectReference{
+ Kind: refKind("Service"),
+ Name: "svc",
+ Namespace: refNamespace("test"),
+ Port: refPortNumber(80),
+ },
+ Weight: refInt32(100), // TODO
+ },
+ Filters: []gatewayv1alpha2.HTTPRouteFilter{
+ // TODO
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ tctx, err := tr.TranslateGatewayHTTPRouteV1Alpha2(httpRoute)
+ assert.Nil(t, err)
+
+ assert.Equal(t, 1, len(tctx.Routes))
+ assert.Equal(t, 1, len(tctx.Upstreams))
+
+ r := tctx.Routes[0]
+ u := tctx.Upstreams[0]
+
+ // Metadata
+ // FIXME
+ assert.NotEqual(t, "", u.ID)
+ assert.Equal(t, u.ID, r.UpstreamId)
+
+ // hosts
+ assert.Equal(t, 1, len(r.Hosts))
+ assert.Equal(t, "example.com", r.Hosts[0])
+
+ // matches
+ assert.Equal(t, "/path*", r.Uri)
+
+ assert.Equal(t, 3, len(r.Vars))
+ referer := r.Vars[0]
+ argUser := r.Vars[1]
+ argTitle := r.Vars[2]
+ assert.Equal(t, []v1.StringOrSlice{{StrVal: "http_referer"}, {StrVal: "=="}, {StrVal: "api7.com"}}, referer)
+ assert.Equal(t, []v1.StringOrSlice{{StrVal: "arg_user"}, {StrVal: "=="}, {StrVal: "api7"}}, argUser)
+ assert.Equal(t, []v1.StringOrSlice{{StrVal: "arg_title"}, {StrVal: "=="}, {StrVal: "ingress"}}, argTitle)
+
+ assert.Equal(t, 1, len(r.Methods))
+ assert.Equal(t, "GET", r.Methods[0])
+
+ // backend refs
+ assert.Equal(t, "http", u.Scheme) // FIXME
+ assert.Equal(t, 2, len(u.Nodes))
+ assert.Equal(t, "192.168.1.1", u.Nodes[0].Host)
+ assert.Equal(t, 9080, u.Nodes[0].Port)
+ assert.Equal(t, "192.168.1.2", u.Nodes[1].Host)
+ assert.Equal(t, 9080, u.Nodes[0].Port)
+}
+
+func TestTranslateGatewayHTTPRouteRegexMatch(t *testing.T) {
+ refStr := func(str string) *string {
+ return &str
+ }
+ refKind := func(str gatewayv1alpha2.Kind) *gatewayv1alpha2.Kind {
+ return &str
+ }
+ refNamespace := func(str gatewayv1alpha2.Namespace) *gatewayv1alpha2.Namespace {
+ return &str
+ }
+ refMethod := func(str gatewayv1alpha2.HTTPMethod) *gatewayv1alpha2.HTTPMethod {
+ return &str
+ }
+ refPathMatchType := func(str gatewayv1alpha2.PathMatchType) *gatewayv1alpha2.PathMatchType {
+ return &str
+ }
+ refHeaderMatchType := func(str gatewayv1alpha2.HeaderMatchType) *gatewayv1alpha2.HeaderMatchType {
+ return &str
+ }
+ refQueryParamMatchType := func(str gatewayv1alpha2.QueryParamMatchType) *gatewayv1alpha2.QueryParamMatchType {
+ return &str
+ }
+ refInt32 := func(i int32) *int32 {
+ return &i
+ }
+ refPortNumber := func(i gatewayv1alpha2.PortNumber) *gatewayv1alpha2.PortNumber {
+ return &i
+ }
+
+ tr, processCh := mockHTTPRouteTranslator(t)
+ <-processCh
+ <-processCh
+
+ httpRoute := &gatewayv1alpha2.HTTPRoute{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "http_route",
+ Namespace: "test",
+ },
+ Spec: gatewayv1alpha2.HTTPRouteSpec{
+ Hostnames: []gatewayv1alpha2.Hostname{
+ "example.com",
+ },
+ Rules: []gatewayv1alpha2.HTTPRouteRule{
+ {
+ Matches: []gatewayv1alpha2.HTTPRouteMatch{
+ {
+ Path: &gatewayv1alpha2.HTTPPathMatch{
+ Type: refPathMatchType(gatewayv1alpha2.PathMatchRegularExpression),
+ Value: refStr("/path"),
+ },
+ Headers: []gatewayv1alpha2.HTTPHeaderMatch{
+ {
+ Type: refHeaderMatchType(gatewayv1alpha2.HeaderMatchRegularExpression),
+ Name: "REFERER",
+ Value: "api7.com",
+ },
+ },
+ QueryParams: []gatewayv1alpha2.HTTPQueryParamMatch{
+ {
+ Type: refQueryParamMatchType(gatewayv1alpha2.QueryParamMatchRegularExpression),
+ Name: "user",
+ Value: "api7",
+ },
+ {
+ Type: refQueryParamMatchType(gatewayv1alpha2.QueryParamMatchRegularExpression),
+ Name: "title",
+ Value: "ingress",
+ },
+ },
+ Method: refMethod(gatewayv1alpha2.HTTPMethodGet),
+ },
+ },
+ Filters: []gatewayv1alpha2.HTTPRouteFilter{
+ // TODO
+ },
+ BackendRefs: []gatewayv1alpha2.HTTPBackendRef{
+ {
+ BackendRef: gatewayv1alpha2.BackendRef{
+ BackendObjectReference: gatewayv1alpha2.BackendObjectReference{
+ Kind: refKind("Service"),
+ Name: "svc",
+ Namespace: refNamespace("test"),
+ Port: refPortNumber(80),
+ },
+ Weight: refInt32(100), // TODO
+ },
+ Filters: []gatewayv1alpha2.HTTPRouteFilter{
+ // TODO
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ tctx, err := tr.TranslateGatewayHTTPRouteV1Alpha2(httpRoute)
+ assert.Nil(t, err)
+
+ assert.Equal(t, 1, len(tctx.Routes))
+ assert.Equal(t, 1, len(tctx.Upstreams))
+
+ r := tctx.Routes[0]
+ u := tctx.Upstreams[0]
+
+ // Metadata
+ // FIXME
+ assert.NotEqual(t, "", u.ID)
+ assert.Equal(t, u.ID, r.UpstreamId)
+
+ // hosts
+ assert.Equal(t, 1, len(r.Hosts))
+ assert.Equal(t, "example.com", r.Hosts[0])
+
+ // matches
+ assert.Equal(t, 4, len(r.Vars))
+ uri := r.Vars[0]
+ referer := r.Vars[1]
+ argUser := r.Vars[2]
+ argTitle := r.Vars[3]
+ assert.Equal(t, []v1.StringOrSlice{{StrVal: "uri"}, {StrVal: "~~"}, {StrVal: "/path"}}, uri)
+ assert.Equal(t, []v1.StringOrSlice{{StrVal: "http_referer"}, {StrVal: "~~"}, {StrVal: "api7.com"}}, referer)
+ assert.Equal(t, []v1.StringOrSlice{{StrVal: "arg_user"}, {StrVal: "~~"}, {StrVal: "api7"}}, argUser)
+ assert.Equal(t, []v1.StringOrSlice{{StrVal: "arg_title"}, {StrVal: "~~"}, {StrVal: "ingress"}}, argTitle)
+
+ assert.Equal(t, 1, len(r.Methods))
+ assert.Equal(t, "GET", r.Methods[0])
+
+ // backend refs
+ assert.Equal(t, "http", u.Scheme) // FIXME
+ assert.Equal(t, 2, len(u.Nodes))
+ assert.Equal(t, "192.168.1.1", u.Nodes[0].Host)
+ assert.Equal(t, 9080, u.Nodes[0].Port)
+ assert.Equal(t, "192.168.1.2", u.Nodes[1].Host)
+ assert.Equal(t, 9080, u.Nodes[0].Port)
+}
+
+// TODO: Multiple BackendRefs, Multiple Rules, Multiple Matches
diff --git a/pkg/kube/translation/translator.go b/pkg/kube/translation/translator.go
index d0291abe..8ec4cbb7 100644
--- a/pkg/kube/translation/translator.go
+++ b/pkg/kube/translation/translator.go
@@ -20,6 +20,7 @@ import (
corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
listerscorev1 "k8s.io/client-go/listers/core/v1"
+ gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
"github.com/apache/apisix-ingress-controller/pkg/kube"
configv2 "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/apis/config/v2"
@@ -108,6 +109,8 @@ type Translator interface {
// 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)
+ // TranslateGatewayHTTPRouteV1Alpha2 translates Gateway API HTTPRoute to APISIX resources
+ TranslateGatewayHTTPRouteV1Alpha2(httpRoute *gatewayv1alpha2.HTTPRoute) (*TranslateContext, error)
}
// TranslatorOptions contains options to help Translator
@@ -173,7 +176,7 @@ func (t *translator) TranslateUpstream(namespace, name, subset string, port int3
ups := apisixv1.NewDefaultUpstream()
if err != nil {
if k8serrors.IsNotFound(err) {
- // If subset in ApisixRoute is not empty but the ApisixUpstream resouce not found,
+ // 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{}
diff --git a/pkg/types/event.go b/pkg/types/event.go
index 1d68553d..7d523bcd 100644
--- a/pkg/types/event.go
+++ b/pkg/types/event.go
@@ -46,6 +46,8 @@ type Event struct {
Type EventType
// Object is the event subject.
Object interface{}
+ // OldObject is the old object in update event.
+ OldObject interface{}
// Tombstone is the final state before object was delete,
// it's useful for DELETE event.
Tombstone interface{}