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{}