feat: tcp route translation (#401)
diff --git a/docs/en/latest/concepts/apisix_route.md b/docs/en/latest/concepts/apisix_route.md
index 8a16659..68799ae 100644
--- a/docs/en/latest/concepts/apisix_route.md
+++ b/docs/en/latest/concepts/apisix_route.md
@@ -240,3 +240,27 @@
servicePort: 8080
websocket: true
```
+
+TCP Route
+---------
+
+apisix-ingress-controller supports the port-based tcp route.
+
+```yaml
+apiVersion: apisix.apache.org/v2alpha1
+kind: ApisixRoute
+metadata:
+ name: tcp-route
+spec:
+ tcp:
+ - name: tcp-route-rule1
+ match:
+ ingressPort: 9100
+ backend:
+ serviceName: tcp-server
+ servicePort: 8080
+```
+
+The above yaml configuration guides TCP traffic entered to the Ingress proxy server (i.e. [APISIX](https://apisix.apache.org)) port `9100` should be routed to the backend service `tcp-server`.
+
+Note since APISIX doesn't support dynamic listening, so here the `9100` port should be pre-defined in APISIX [configuration](https://github.com/apache/apisix/blob/master/conf/config-default.yaml#L101).
diff --git a/docs/en/latest/references/apisix_route_v2alpha1.md b/docs/en/latest/references/apisix_route_v2alpha1.md
index 1f054d1..b22ec79 100644
--- a/docs/en/latest/references/apisix_route_v2alpha1.md
+++ b/docs/en/latest/references/apisix_route_v2alpha1.md
@@ -44,18 +44,26 @@
| http[].match.exprs[].set | array | Expected expression result set, only used when the operator is `In` or `NotIn`, it's exclusive with `http[].match.exprs[].value`.
| http[].backend | object | The backend service. Deprecated: use http[].backends instead.
| http[].backend.serviceName | string | The backend service name, note the service and ApisixRoute should be created in the same namespace. Cross namespace referencing is not allowed.
-| http[].backend.servicePort | integer or string | The backend service port, can be the port number of the name defined in the service object.
-| http[].backend.resolveGranualrity | string | See [Service Resolve Granularity](#service-resolve-granularity) for the details.
+| http[].backend.servicePort | integer or string | The backend service port, can be the port number or the name defined in the service object.
+| http[].backend.resolveGranularity | string | See [Service Resolve Granularity](#service-resolve-granularity) for the details.
| http[].backends | object | The backend services. When the number of backends more than one, weight based traffic split policy will be applied to shifting traffic between these backends.
| http[].backends[].serviceName | string | The backend service name, note the service and ApisixRoute should be created in the same namespace. Cross namespace referencing is not allowed.
-| http[].backends[].servicePort | integer or string | The backend service port, can be the port number of the name defined in the service object.
-| http[].backends[].resolveGranualrity | string | See [Service Resolve Granularity](#service-resolve-granularity) for the details.
+| http[].backends[].servicePort | integer or string | The backend service port, can be the port number or the name defined in the service object.
+| http[].backends[].resolveGranularity | string | See [Service Resolve Granularity](#service-resolve-granularity) for the details.
| http[].backends[].weight | int | The backend weight, which is critical when shifting traffic between multiple backends, default is `100`. Weight is ignored when there is only one backend.
| http[].plugins | array | A series of APISIX plugins that will be executed once this route rule is matched |
| http[].plugins[].name | string | The plugin name, see [docs](http://apisix.apache.org/docs/apisix/getting-started) for learning the available plugins.
| http[].plugins[].enable | boolean | Whether the plugin is in use |
| http[].plugins[].config | object | The plugin configuration, fields should be same as in APISIX. |
| http[].websocket | boolean | Whether enable websocket proxy. |
+| tcp | array | ApisixRoutes' tcp route rules. |
+| tcp[].name | string (required) | The Route rule name. |
+| tcp[].match | object (required) | The Route match conditions. |
+| tcp[].match.ingressPort | integer (required) | the Ingress proxy server listening port, note since APISIX doesn't support dynamic listening, this port should be defined in [apisix configuration](https://github.com/apache/apisix/blob/master/conf/config-default.yaml#L101).|
+| tcp[].backend | object | The backend service. Deprecated: use http[].backends instead.
+| tcp[].backend.serviceName | string | The backend service name, note the service and ApisixRoute should be created in the same namespace. Cross namespace referencing is not allowed.
+| tcp[].backend.servicePort | integer or string | The backend service port, can be the port number or the name defined in the service object.
+| tcp[].backend.resolveGranularity | string | See [Service Resolve Granularity](#service-resolve-granularity) for the details.
## Expression Operators
diff --git a/pkg/ingress/apisix_route.go b/pkg/ingress/apisix_route.go
index a1b67a2..f6fe291 100644
--- a/pkg/ingress/apisix_route.go
+++ b/pkg/ingress/apisix_route.go
@@ -155,8 +155,9 @@
)
m := &manifest{
- routes: tctx.Routes,
- upstreams: tctx.Upstreams,
+ routes: tctx.Routes,
+ upstreams: tctx.Upstreams,
+ streamRoutes: tctx.StreamRoutes,
}
var (
@@ -187,8 +188,9 @@
}
om := &manifest{
- routes: oldCtx.Routes,
- upstreams: oldCtx.Upstreams,
+ routes: oldCtx.Routes,
+ upstreams: oldCtx.Upstreams,
+ streamRoutes: oldCtx.StreamRoutes,
}
added, updated, deleted = m.diff(om)
}
diff --git a/pkg/ingress/manifest.go b/pkg/ingress/manifest.go
index 8e3034c..42cfb61 100644
--- a/pkg/ingress/manifest.go
+++ b/pkg/ingress/manifest.go
@@ -80,30 +80,60 @@
return
}
+func diffStreamRoutes(olds, news []*apisixv1.StreamRoute) (added, updated, deleted []*apisixv1.StreamRoute) {
+ oldMap := make(map[string]*apisixv1.StreamRoute, len(olds))
+ newMap := make(map[string]*apisixv1.StreamRoute, len(news))
+ for _, sr := range olds {
+ oldMap[sr.ID] = sr
+ }
+ for _, sr := range news {
+ newMap[sr.ID] = sr
+ }
+
+ for _, sr := range news {
+ if ou, ok := oldMap[sr.ID]; !ok {
+ added = append(added, sr)
+ } else if !reflect.DeepEqual(ou, sr) {
+ updated = append(updated, sr)
+ }
+ }
+ for _, sr := range olds {
+ if _, ok := newMap[sr.ID]; !ok {
+ deleted = append(deleted, sr)
+ }
+ }
+ return
+}
+
type manifest struct {
- routes []*apisixv1.Route
- upstreams []*apisixv1.Upstream
+ routes []*apisixv1.Route
+ upstreams []*apisixv1.Upstream
+ streamRoutes []*apisixv1.StreamRoute
}
func (m *manifest) diff(om *manifest) (added, updated, deleted *manifest) {
ar, ur, dr := diffRoutes(om.routes, m.routes)
au, uu, du := diffUpstreams(om.upstreams, m.upstreams)
- if ar != nil || au != nil {
+ asr, usr, dsr := diffStreamRoutes(om.streamRoutes, m.streamRoutes)
+ if ar != nil || au != nil || asr != nil {
added = &manifest{
- routes: ar,
- upstreams: au,
+ routes: ar,
+ upstreams: au,
+ streamRoutes: asr,
}
}
- if ur != nil || uu != nil {
+ if ur != nil || uu != nil || usr != nil {
updated = &manifest{
- routes: ur,
- upstreams: uu,
+ routes: ur,
+ upstreams: uu,
+ streamRoutes: usr,
}
}
- if dr != nil || du != nil {
+ if dr != nil || du != nil || dsr != nil {
deleted = &manifest{
- routes: dr,
- upstreams: du,
+ routes: dr,
+ upstreams: du,
+ streamRoutes: dsr,
}
}
return
@@ -124,6 +154,11 @@
merr = multierror.Append(merr, err)
}
}
+ for _, sr := range deleted.streamRoutes {
+ if err := c.apisix.Cluster(clusterName).StreamRoute().Delete(ctx, sr); err != nil {
+ merr = multierror.Append(merr, err)
+ }
+ }
}
if added != nil {
// Should create upstreams firstly due to the dependencies.
@@ -137,6 +172,11 @@
merr = multierror.Append(merr, err)
}
}
+ for _, sr := range added.streamRoutes {
+ if _, err := c.apisix.Cluster(clusterName).StreamRoute().Create(ctx, sr); err != nil {
+ merr = multierror.Append(merr, err)
+ }
+ }
}
if updated != nil {
for _, r := range updated.upstreams {
@@ -149,6 +189,11 @@
merr = multierror.Append(merr, err)
}
}
+ for _, sr := range updated.streamRoutes {
+ if _, err := c.apisix.Cluster(clusterName).StreamRoute().Create(ctx, sr); err != nil {
+ merr = multierror.Append(merr, err)
+ }
+ }
}
if merr != nil {
return merr
diff --git a/pkg/ingress/manifest_test.go b/pkg/ingress/manifest_test.go
index 9d568e6..3ad5e3a 100644
--- a/pkg/ingress/manifest_test.go
+++ b/pkg/ingress/manifest_test.go
@@ -75,6 +75,51 @@
assert.Equal(t, deleted[0].ID, "2")
}
+func TestDiffStreamRoutes(t *testing.T) {
+ news := []*apisixv1.StreamRoute{
+ {
+ ID: "1",
+ },
+ {
+ ID: "3",
+ ServerPort: 8080,
+ },
+ }
+ added, updated, deleted := diffStreamRoutes(nil, news)
+ assert.Nil(t, updated)
+ assert.Nil(t, deleted)
+ assert.Len(t, added, 2)
+ assert.Equal(t, added[0].ID, "1")
+ assert.Equal(t, added[1].ID, "3")
+ assert.Equal(t, added[1].ServerPort, int32(8080))
+
+ olds := []*apisixv1.StreamRoute{
+ {
+ ID: "2",
+ },
+ {
+ ID: "3",
+ ServerPort: 8081,
+ },
+ }
+ added, updated, deleted = diffStreamRoutes(olds, nil)
+ assert.Nil(t, updated)
+ assert.Nil(t, added)
+ assert.Len(t, deleted, 2)
+ assert.Equal(t, deleted[0].ID, "2")
+ assert.Equal(t, deleted[1].ID, "3")
+ assert.Equal(t, deleted[1].ServerPort, int32(8081))
+
+ added, updated, deleted = diffStreamRoutes(olds, news)
+ assert.Len(t, added, 1)
+ assert.Equal(t, added[0].ID, "1")
+ assert.Len(t, updated, 1)
+ assert.Equal(t, updated[0].ID, "3")
+ assert.Equal(t, updated[0].ServerPort, int32(8080))
+ assert.Len(t, deleted, 1)
+ assert.Equal(t, deleted[0].ID, "2")
+}
+
func TestDiffUpstreams(t *testing.T) {
news := []*apisixv1.Upstream{
{
diff --git a/pkg/kube/translation/apisix_route.go b/pkg/kube/translation/apisix_route.go
index 69cbae4..bbf8705 100644
--- a/pkg/kube/translation/apisix_route.go
+++ b/pkg/kube/translation/apisix_route.go
@@ -294,5 +294,35 @@
}
func (t *translator) translateTCPRoute(ctx *TranslateContext, ar *configv2alpha1.ApisixRoute) error {
+ ruleNameMap := make(map[string]struct{})
+ for _, part := range ar.Spec.TCP {
+ if _, ok := ruleNameMap[part.Name]; ok {
+ return errors.New("duplicated route rule name")
+ }
+ ruleNameMap[part.Name] = struct{}{}
+ backend := &part.Backend
+ svcClusterIP, svcPort, err := t.getTCPServiceClusterIPAndPort(backend, ar)
+ if err != nil {
+ log.Errorw("failed to get service port in backend",
+ zap.Any("backend", backend),
+ zap.Any("apisix_route", ar),
+ zap.Error(err),
+ )
+ return err
+ }
+ sr := apisixv1.NewDefaultStreamRoute()
+ name := apisixv1.ComposeStreamRouteName(ar.Namespace, ar.Name, part.Name)
+ sr.ID = id.GenID(name)
+ sr.ServerPort = part.Match.IngressPort
+ // TODO use upstream id to refer the upstream object.
+ // Currently, APISIX doesn't use upstream_id field in
+ // APISIX, so we have to embed the entire upstream.
+ ups, err := t.translateUpstream(ar.Namespace, backend.ServiceName, backend.ResolveGranularity, svcClusterIP, svcPort)
+ if err != nil {
+ return err
+ }
+ sr.Upstream = ups
+ ctx.addStreamRoute(sr)
+ }
return nil
}
diff --git a/pkg/kube/translation/util.go b/pkg/kube/translation/util.go
index 76af4d3..16ced5a 100644
--- a/pkg/kube/translation/util.go
+++ b/pkg/kube/translation/util.go
@@ -70,6 +70,45 @@
return svc.Spec.ClusterIP, svcPort, nil
}
+func (t *translator) getTCPServiceClusterIPAndPort(backend *configv2alpha1.ApisixRouteTCPBackend, ar *configv2alpha1.ApisixRoute) (string, int32, error) {
+ svc, err := t.ServiceLister.Services(ar.Namespace).Get(backend.ServiceName)
+ if err != nil {
+ return "", 0, err
+ }
+ svcPort := int32(-1)
+loop:
+ for _, port := range svc.Spec.Ports {
+ switch backend.ServicePort.Type {
+ case intstr.Int:
+ if backend.ServicePort.IntVal == port.Port {
+ svcPort = port.Port
+ break loop
+ }
+ case intstr.String:
+ if backend.ServicePort.StrVal == port.Name {
+ svcPort = port.Port
+ break loop
+ }
+ }
+ }
+ if svcPort == -1 {
+ log.Errorw("ApisixRoute refers to non-existent Service port",
+ zap.Any("ApisixRoute", ar),
+ zap.String("port", backend.ServicePort.String()),
+ )
+ return "", 0, err
+ }
+
+ if backend.ResolveGranularity == "service" && svc.Spec.ClusterIP == "" {
+ log.Errorw("ApisixRoute refers to a headless service but want to use the service level resolve granularity",
+ zap.Any("ApisixRoute", ar),
+ zap.Any("service", svc),
+ )
+ return "", 0, errors.New("conflict headless service and backend resolve granularity")
+ }
+ return svc.Spec.ClusterIP, svcPort, nil
+}
+
func (t *translator) translateUpstream(namespace, svcName, svcResolveGranularity, svcClusterIP string, svcPort int32) (*apisixv1.Upstream, error) {
ups, err := t.TranslateUpstream(namespace, svcName, svcPort)
if err != nil {
diff --git a/pkg/types/apisix/v1/types.go b/pkg/types/apisix/v1/types.go
index 846182b..082beec 100644
--- a/pkg/types/apisix/v1/types.go
+++ b/pkg/types/apisix/v1/types.go
@@ -332,6 +332,7 @@
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
ServerPort int32 `json:"server_port,omitempty" yaml:"server_port,omitempty"`
UpstreamId string `json:"upstream_id,omitempty" yaml:"upstream_id,omitempty"`
+ Upstream *Upstream `json:"upstream,omitempty" yaml:"upstream,omitempty"`
}
// GlobalRule represents the global_rule object in APISIX.
@@ -369,6 +370,16 @@
}
}
+// NewDefaultStreamRoute returns an empty StreamRoute with default values.
+func NewDefaultStreamRoute() *StreamRoute {
+ return &StreamRoute{
+ Desc: "Created by apisix-ingress-controller, DO NOT modify it manually",
+ Labels: map[string]string{
+ "managed-by": "apisix-ingress-controller",
+ },
+ }
+}
+
// ComposeUpstreamName uses namespace, name and port info to compose
// the upstream name.
func ComposeUpstreamName(namespace, name string, port int32) string {
@@ -403,3 +414,21 @@
return buf.String()
}
+
+// ComposeStreamRouteName uses namespace, name and rule name to compose
+// the stream_route name.
+func ComposeStreamRouteName(namespace, name string, rule string) string {
+ // FIXME Use sync.Pool to reuse this buffer if the upstream
+ // name composing code path is hot.
+ p := make([]byte, 0, len(namespace)+len(name)+len(rule)+6)
+ buf := bytes.NewBuffer(p)
+
+ buf.WriteString(namespace)
+ buf.WriteByte('_')
+ buf.WriteString(name)
+ buf.WriteByte('_')
+ buf.WriteString(rule)
+ buf.WriteString("_tcp")
+
+ return buf.String()
+}
diff --git a/samples/deploy/crd/v1beta1/ApisixRoute.yaml b/samples/deploy/crd/v1beta1/ApisixRoute.yaml
index d335379..89f4e1a 100644
--- a/samples/deploy/crd/v1beta1/ApisixRoute.yaml
+++ b/samples/deploy/crd/v1beta1/ApisixRoute.yaml
@@ -30,6 +30,12 @@
- JSONPath: .spec.http[].match.backends[].serviceName
name: Target Service
type: string
+ - JSONPath: .spec.tcp[].match.ingressPort
+ name: Ingress Server Port
+ type: integer
+ - JSONPath: .spec.tcp[].match.backend.serviceName
+ name: Target Service
+ type: string
- JSONPath: .metadata.creationTimestamp
name: Age
type: date
@@ -57,6 +63,9 @@
properties:
spec:
type: object
+ anyOf:
+ - required: ["http"]
+ - required: ["tcp"]
properties:
http:
type: array
@@ -197,3 +206,38 @@
required:
- name
- enable
+ tcp:
+ type: array
+ minItems: 1
+ items:
+ type: object
+ required: ["name", "match", "backend"]
+ properties:
+ name:
+ type: string
+ minLength: 1
+ match:
+ type: object
+ properties:
+ ingressPort:
+ type: integer
+ minimum: 1
+ maximum: 65535
+ required:
+ - ingressPort
+ backend:
+ type: object
+ properties:
+ serviceName:
+ type: string
+ minLength: 1
+ servicePort:
+ type: integer
+ minimum: 1
+ maximum: 65535
+ resolveGranualrity:
+ type: string
+ enum: ["endpoint", "service"]
+ required:
+ - serviceName
+ - servicePort
diff --git a/test/e2e/endpoints/endpoints.go b/test/e2e/endpoints/endpoints.go
index 0973a05..9148d02 100644
--- a/test/e2e/endpoints/endpoints.go
+++ b/test/e2e/endpoints/endpoints.go
@@ -26,25 +26,36 @@
)
var _ = ginkgo.Describe("endpoints", func() {
- s := scaffold.NewDefaultScaffold()
+ opts := &scaffold.Options{
+ Name: "default",
+ Kubeconfig: scaffold.GetKubeconfig(),
+ APISIXConfigPath: "testdata/apisix-gw-config.yaml",
+ APISIXDefaultConfigPath: "testdata/apisix-gw-config-default.yaml",
+ IngressAPISIXReplicas: 1,
+ HTTPBinServicePort: 80,
+ APISIXRouteVersion: "apisix.apache.org/v2alpha1",
+ }
+ s := scaffold.NewScaffold(opts)
ginkgo.It("ignore applied only if there is an ApisixRoute referenced", func() {
time.Sleep(5 * time.Second)
assert.Nil(ginkgo.GinkgoT(), s.EnsureNumApisixUpstreamsCreated(0), "checking number of upstreams")
backendSvc, backendSvcPort := s.DefaultHTTPBackend()
ups := fmt.Sprintf(`
-apiVersion: apisix.apache.org/v1
+apiVersion: apisix.apache.org/v2alpha1
kind: ApisixRoute
metadata:
name: httpbin-route
spec:
- rules:
- - host: httpbin.org
- http:
- paths:
- - backend:
- serviceName: %s
- servicePort: %d
- path: /ip
+ http:
+ - name: rule1
+ match:
+ hosts:
+ - httpbin.org
+ paths:
+ - /ip
+ backend:
+ serviceName: %s
+ servicePort: %d
`, backendSvc, backendSvcPort[0])
assert.Nil(ginkgo.GinkgoT(), s.CreateResourceFromString(ups))
assert.Nil(ginkgo.GinkgoT(), s.EnsureNumApisixUpstreamsCreated(1), "checking number of upstreams")
@@ -54,19 +65,20 @@
ginkgo.Skip("now we don't handle endpoints delete event")
backendSvc, backendSvcPort := s.DefaultHTTPBackend()
apisixRoute := fmt.Sprintf(`
-apiVersion: apisix.apache.org/v1
+apiVersion: apisix.apache.org/v2alpha1
kind: ApisixRoute
metadata:
name: httpbin-route
spec:
- rules:
- - host: httpbin.com
- http:
- paths:
- - backend:
- serviceName: %s
- servicePort: %d
- path: /ip
+ http:
+ - name: rule1
+ match:
+ hosts:
+ - httpbin.com
+ paths: /ip
+ backend:
+ serviceName: %s
+ servicePort: %d
`, backendSvc, backendSvcPort[0])
assert.Nil(ginkgo.GinkgoT(), s.CreateResourceFromString(apisixRoute))
assert.Nil(ginkgo.GinkgoT(), s.EnsureNumApisixUpstreamsCreated(1), "checking number of upstreams")
@@ -80,30 +92,34 @@
})
var _ = ginkgo.Describe("port usage", func() {
- s := scaffold.NewScaffold(&scaffold.Options{
+ opts := &scaffold.Options{
Name: "endpoints-port",
Kubeconfig: scaffold.GetKubeconfig(),
APISIXConfigPath: "testdata/apisix-gw-config.yaml",
APISIXDefaultConfigPath: "testdata/apisix-gw-config-default.yaml",
IngressAPISIXReplicas: 1,
- HTTPBinServicePort: 8080, // use service port which is different from target port (80)
- })
+ HTTPBinServicePort: 8080,
+ APISIXRouteVersion: "apisix.apache.org/v2alpha1",
+ }
+ s := scaffold.NewScaffold(opts)
ginkgo.It("service port != target port", func() {
backendSvc, backendSvcPort := s.DefaultHTTPBackend()
apisixRoute := fmt.Sprintf(`
-apiVersion: apisix.apache.org/v1
+apiVersion: apisix.apache.org/v2alpha1
kind: ApisixRoute
metadata:
name: httpbin-route
spec:
- rules:
- - host: httpbin.com
- http:
- paths:
- - backend:
- serviceName: %s
- servicePort: %d
- path: /ip
+ http:
+ - name: rule1
+ match:
+ hosts:
+ - httpbin.com
+ paths:
+ - /ip
+ backend:
+ serviceName: %s
+ servicePort: %d
`, backendSvc, backendSvcPort[0])
assert.Nil(ginkgo.GinkgoT(), s.CreateResourceFromString(apisixRoute))
time.Sleep(5 * time.Second)
diff --git a/test/e2e/features/healthcheck.go b/test/e2e/features/healthcheck.go
index 75c11cd..bf4e7c9 100644
--- a/test/e2e/features/healthcheck.go
+++ b/test/e2e/features/healthcheck.go
@@ -25,7 +25,16 @@
)
var _ = ginkgo.Describe("health check", func() {
- s := scaffold.NewDefaultScaffold()
+ opts := &scaffold.Options{
+ Name: "default",
+ Kubeconfig: scaffold.GetKubeconfig(),
+ APISIXConfigPath: "testdata/apisix-gw-config.yaml",
+ APISIXDefaultConfigPath: "testdata/apisix-gw-config-default.yaml",
+ IngressAPISIXReplicas: 1,
+ HTTPBinServicePort: 80,
+ APISIXRouteVersion: "apisix.apache.org/v2alpha1",
+ }
+ s := scaffold.NewScaffold(opts)
ginkgo.It("active check", func() {
backendSvc, backendPorts := s.DefaultHTTPBackend()
@@ -50,19 +59,21 @@
assert.Nil(ginkgo.GinkgoT(), err, "create ApisixUpstream")
ar := fmt.Sprintf(`
-apiVersion: apisix.apache.org/v1
+apiVersion: apisix.apache.org/v2alpha1
kind: ApisixRoute
metadata:
name: httpbin-route
spec:
- rules:
- - host: httpbin.org
- http:
- paths:
- - backend:
- serviceName: %s
- servicePort: %d
- path: /*
+ http:
+ - name: rule1
+ match:
+ hosts:
+ - httpbin.org
+ paths:
+ - /*
+ backend:
+ serviceName: %s
+ servicePort: %d
`, backendSvc, backendPorts[0])
err = s.CreateResourceFromString(ar)
assert.Nil(ginkgo.GinkgoT(), err)
@@ -110,19 +121,21 @@
assert.Nil(ginkgo.GinkgoT(), err, "create ApisixUpstream")
ar := fmt.Sprintf(`
-apiVersion: apisix.apache.org/v1
+apiVersion: apisix.apache.org/v2alpha1
kind: ApisixRoute
metadata:
name: httpbin-route
spec:
- rules:
- - host: httpbin.org
- http:
- paths:
- - backend:
- serviceName: %s
- servicePort: %d
- path: /*
+ http:
+ - name: rule1
+ match:
+ hosts:
+ - httpbin.org
+ paths:
+ - /*
+ backend:
+ serviceName: %s
+ servicePort: %d
`, backendSvc, backendPorts[0])
err = s.CreateResourceFromString(ar)
assert.Nil(ginkgo.GinkgoT(), err)
diff --git a/test/e2e/features/retries_timeout.go b/test/e2e/features/retries_timeout.go
index 9e3d757..64470ac 100644
--- a/test/e2e/features/retries_timeout.go
+++ b/test/e2e/features/retries_timeout.go
@@ -24,7 +24,16 @@
)
var _ = ginkgo.Describe("retries", func() {
- s := scaffold.NewDefaultScaffold()
+ opts := &scaffold.Options{
+ Name: "default",
+ Kubeconfig: scaffold.GetKubeconfig(),
+ APISIXConfigPath: "testdata/apisix-gw-config.yaml",
+ APISIXDefaultConfigPath: "testdata/apisix-gw-config-default.yaml",
+ IngressAPISIXReplicas: 1,
+ HTTPBinServicePort: 80,
+ APISIXRouteVersion: "apisix.apache.org/v2alpha1",
+ }
+ s := scaffold.NewScaffold(opts)
ginkgo.It("active check", func() {
backendSvc, backendPorts := s.DefaultHTTPBackend()
@@ -41,19 +50,21 @@
time.Sleep(2 * time.Second)
ar := fmt.Sprintf(`
-apiVersion: apisix.apache.org/v1
+apiVersion: apisix.apache.org/v2alpha1
kind: ApisixRoute
metadata:
- name: httpbin-route
+ name: httpbin-route
spec:
- rules:
- - host: httpbin.org
- http:
- paths:
- - backend:
- serviceName: %s
- servicePort: %d
- path: /*
+ http:
+ - name: rule1
+ match:
+ hosts:
+ - httpbin.org
+ paths:
+ - /*
+ backend:
+ serviceName: %s
+ servicePort: %d
`, backendSvc, backendPorts[0])
err = s.CreateResourceFromString(ar)
assert.Nil(ginkgo.GinkgoT(), err)
@@ -67,7 +78,16 @@
})
var _ = ginkgo.Describe("timeout", func() {
- s := scaffold.NewDefaultScaffold()
+ opts := &scaffold.Options{
+ Name: "default",
+ Kubeconfig: scaffold.GetKubeconfig(),
+ APISIXConfigPath: "testdata/apisix-gw-config.yaml",
+ APISIXDefaultConfigPath: "testdata/apisix-gw-config-default.yaml",
+ IngressAPISIXReplicas: 1,
+ HTTPBinServicePort: 80,
+ APISIXRouteVersion: "apisix.apache.org/v2alpha1",
+ }
+ s := scaffold.NewScaffold(opts)
ginkgo.It("active check", func() {
backendSvc, backendPorts := s.DefaultHTTPBackend()
@@ -86,19 +106,21 @@
time.Sleep(2 * time.Second)
ar := fmt.Sprintf(`
-apiVersion: apisix.apache.org/v1
+apiVersion: apisix.apache.org/v2alpha1
kind: ApisixRoute
metadata:
name: httpbin-route
spec:
- rules:
- - host: httpbin.org
- http:
- paths:
- - backend:
- serviceName: %s
- servicePort: %d
- path: /*
+ http:
+ - name: rule1
+ match:
+ hosts:
+ - httpbin.org
+ paths:
+ - /*
+ backend:
+ serviceName: %s
+ servicePort: %d
`, backendSvc, backendPorts[0])
err = s.CreateResourceFromString(ar)
assert.Nil(ginkgo.GinkgoT(), err)
diff --git a/test/e2e/features/scheme.go b/test/e2e/features/scheme.go
index bdcd529..8f2334a 100644
--- a/test/e2e/features/scheme.go
+++ b/test/e2e/features/scheme.go
@@ -25,7 +25,16 @@
)
var _ = ginkgo.Describe("choose scheme", func() {
- s := scaffold.NewDefaultScaffold()
+ opts := &scaffold.Options{
+ Name: "default",
+ Kubeconfig: scaffold.GetKubeconfig(),
+ APISIXConfigPath: "testdata/apisix-gw-config.yaml",
+ APISIXDefaultConfigPath: "testdata/apisix-gw-config-default.yaml",
+ IngressAPISIXReplicas: 1,
+ HTTPBinServicePort: 80,
+ APISIXRouteVersion: "apisix.apache.org/v2alpha1",
+ }
+ s := scaffold.NewScaffold(opts)
ginkgo.It("grpc", func() {
err := s.CreateResourceFromString(`
apiVersion: v1
@@ -64,19 +73,21 @@
scheme: grpc
`))
err = s.CreateResourceFromString(`
-apiVersion: apisix.apache.org/v1
+apiVersion: apisix.apache.org/v2alpha1
kind: ApisixRoute
metadata:
name: grpc-route
spec:
- rules:
- - host: grpc.local
- http:
- paths:
- - backend:
- serviceName: grpc-server-service
- servicePort: 50051
- path: /helloworld.Greeter/SayHello
+ http:
+ - name: rule1
+ match:
+ hosts:
+ - grpc.local
+ paths:
+ - /helloworld.Greeter/SayHello
+ backend:
+ serviceName: grpc-server-service
+ servicePort: 50051
`)
assert.Nil(ginkgo.GinkgoT(), err)
time.Sleep(2 * time.Second)
diff --git a/test/e2e/ingress/namespace.go b/test/e2e/ingress/namespace.go
index 9f4d1f9..57a211a 100644
--- a/test/e2e/ingress/namespace.go
+++ b/test/e2e/ingress/namespace.go
@@ -28,23 +28,34 @@
)
var _ = ginkgo.Describe("namespacing filtering", func() {
- s := scaffold.NewDefaultScaffold()
+ opts := &scaffold.Options{
+ Name: "default",
+ Kubeconfig: scaffold.GetKubeconfig(),
+ APISIXConfigPath: "testdata/apisix-gw-config.yaml",
+ APISIXDefaultConfigPath: "testdata/apisix-gw-config-default.yaml",
+ IngressAPISIXReplicas: 1,
+ HTTPBinServicePort: 80,
+ APISIXRouteVersion: "apisix.apache.org/v2alpha1",
+ }
+ s := scaffold.NewScaffold(opts)
ginkgo.It("resources in other namespaces should be ignored", func() {
backendSvc, backendSvcPort := s.DefaultHTTPBackend()
route := fmt.Sprintf(`
-apiVersion: apisix.apache.org/v1
+apiVersion: apisix.apache.org/v2alpha1
kind: ApisixRoute
metadata:
name: httpbin-route
spec:
- rules:
- - host: httpbin.com
- http:
+ http:
+ - name: rule1
+ match:
+ hosts:
+ - httpbin.com
paths:
- - backend:
- serviceName: %s
- servicePort: %d
- path: /ip
+ - /ip
+ backend:
+ serviceName: %s
+ servicePort: %d
`, backendSvc, backendSvcPort[0])
assert.Nil(ginkgo.GinkgoT(), s.CreateResourceFromString(route), "creating ApisixRoute")
@@ -62,19 +73,21 @@
// Now create another ApisixRoute in default namespace.
route = fmt.Sprintf(`
-apiVersion: apisix.apache.org/v1
+apiVersion: apisix.apache.org/v2alpha1
kind: ApisixRoute
metadata:
name: httpbin-route
spec:
- rules:
- - host: httpbin.com
- http:
- paths:
- - backend:
- serviceName: %s
- servicePort: %d
- path: /headers
+ http:
+ - name: rule1
+ match:
+ hosts:
+ - httpbin.com
+ paths:
+ - /headers
+ backend:
+ serviceName: %s
+ servicePort: %d
`, backendSvc, backendSvcPort[0])
assert.Nil(ginkgo.GinkgoT(), s.CreateResourceFromStringWithNamespace(route, "default"), "creating ApisixRoute")
diff --git a/test/e2e/ingress/sanity.go b/test/e2e/ingress/sanity.go
index 009efb3..7937a55 100644
--- a/test/e2e/ingress/sanity.go
+++ b/test/e2e/ingress/sanity.go
@@ -16,6 +16,7 @@
import (
"encoding/json"
+ "fmt"
"net/http"
"time"
@@ -30,25 +31,34 @@
}
var _ = ginkgo.Describe("single-route", func() {
- s := scaffold.NewDefaultScaffold()
+ opts := &scaffold.Options{
+ Name: "default",
+ Kubeconfig: scaffold.GetKubeconfig(),
+ APISIXConfigPath: "testdata/apisix-gw-config.yaml",
+ APISIXDefaultConfigPath: "testdata/apisix-gw-config-default.yaml",
+ IngressAPISIXReplicas: 1,
+ HTTPBinServicePort: 80,
+ APISIXRouteVersion: "apisix.apache.org/v2alpha1",
+ }
+ s := scaffold.NewScaffold(opts)
ginkgo.It("/ip should return your ip", func() {
backendSvc, backendSvcPort := s.DefaultHTTPBackend()
- s.CreateApisixRoute("httpbin-route", []scaffold.ApisixRouteRule{
- {
- Host: "httpbin.com",
- HTTP: scaffold.ApisixRouteRuleHTTP{
- Paths: []scaffold.ApisixRouteRuleHTTPPath{
- {
- Path: "/ip",
- Backend: scaffold.ApisixRouteRuleHTTPBackend{
- ServiceName: backendSvc,
- ServicePort: backendSvcPort[0],
- },
- },
- },
- },
- },
- })
+ ar := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2alpha1
+kind: ApisixRoute
+metadata:
+ name: httpbin-route
+spec:
+ http:
+ - name: rule1
+ match:
+ paths:
+ - /ip
+ backend:
+ serviceName: %s
+ servicePort: %d
+`, backendSvc, backendSvcPort[0])
+ assert.Nil(ginkgo.GinkgoT(), s.CreateResourceFromString(ar))
err := s.EnsureNumApisixRoutesCreated(1)
assert.Nil(ginkgo.GinkgoT(), err, "checking number of routes")
err = s.EnsureNumApisixUpstreamsCreated(1)
@@ -68,32 +78,43 @@
})
var _ = ginkgo.Describe("double-routes", func() {
- s := scaffold.NewDefaultScaffold()
+ opts := &scaffold.Options{
+ Name: "default",
+ Kubeconfig: scaffold.GetKubeconfig(),
+ APISIXConfigPath: "testdata/apisix-gw-config.yaml",
+ APISIXDefaultConfigPath: "testdata/apisix-gw-config-default.yaml",
+ IngressAPISIXReplicas: 1,
+ HTTPBinServicePort: 80,
+ APISIXRouteVersion: "apisix.apache.org/v2alpha1",
+ }
+ s := scaffold.NewScaffold(opts)
ginkgo.It("double routes work independently", func() {
backendSvc, backendSvcPort := s.DefaultHTTPBackend()
- s.CreateApisixRoute("httpbin-route", []scaffold.ApisixRouteRule{
- {
- Host: "httpbin.com",
- HTTP: scaffold.ApisixRouteRuleHTTP{
- Paths: []scaffold.ApisixRouteRuleHTTPPath{
- {
- Path: "/ip",
- Backend: scaffold.ApisixRouteRuleHTTPBackend{
- ServiceName: backendSvc,
- ServicePort: backendSvcPort[0],
- },
- },
- {
- Path: "/json",
- Backend: scaffold.ApisixRouteRuleHTTPBackend{
- ServiceName: backendSvc,
- ServicePort: backendSvcPort[0],
- },
- },
- },
- },
- },
- })
+ ar := `
+apiVersion: apisix.apache.org/v2alpha1
+kind: ApisixRoute
+metadata:
+ name: httpbin-route
+spec:
+ http:
+ - name: rule1
+ match:
+ paths:
+ - /ip
+ backend:
+ serviceName: %s
+ servicePort: %d
+ - name: rule2
+ match:
+ paths:
+ - /json
+ backend:
+ serviceName: %s
+ servicePort: %d
+`
+ ar = fmt.Sprintf(ar, backendSvc, backendSvcPort[0], backendSvc, backendSvcPort[0])
+ assert.Nil(ginkgo.GinkgoT(), s.CreateResourceFromString(ar))
+
err := s.EnsureNumApisixRoutesCreated(2)
assert.Nil(ginkgo.GinkgoT(), err, "checking number of routes")
err = s.EnsureNumApisixUpstreamsCreated(1)
diff --git a/test/e2e/ingress/tcp.go b/test/e2e/ingress/tcp.go
new file mode 100644
index 0000000..b745527
--- /dev/null
+++ b/test/e2e/ingress/tcp.go
@@ -0,0 +1,72 @@
+// 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 (
+ "fmt"
+ "time"
+
+ "github.com/onsi/ginkgo"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
+)
+
+var _ = ginkgo.Describe("ApisixRoute Testing", func() {
+ opts := &scaffold.Options{
+ Name: "default",
+ Kubeconfig: scaffold.GetKubeconfig(),
+ APISIXConfigPath: "testdata/apisix-gw-config.yaml",
+ APISIXDefaultConfigPath: "testdata/apisix-gw-config-default.yaml",
+ IngressAPISIXReplicas: 1,
+ HTTPBinServicePort: 80,
+ APISIXRouteVersion: "apisix.apache.org/v2alpha1",
+ }
+ s := scaffold.NewScaffold(opts)
+ ginkgo.It("tcp proxy", func() {
+ backendSvc, backendSvcPort := s.DefaultHTTPBackend()
+ apisixRoute := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2alpha1
+kind: ApisixRoute
+metadata:
+ name: httpbin-tcp-route
+spec:
+ tcp:
+ - name: rule1
+ match:
+ ingressPort: 9100
+ backend:
+ serviceName: %s
+ servicePort: %d
+`, backendSvc, backendSvcPort[0])
+
+ assert.Nil(ginkgo.GinkgoT(), s.CreateResourceFromString(apisixRoute))
+ time.Sleep(3 * time.Second)
+
+ err := s.EnsureNumApisixStreamRoutesCreated(1)
+ assert.Nil(ginkgo.GinkgoT(), err, "Checking number of routes")
+
+ sr, err := s.ListApisixStreamRoutes()
+ assert.Nil(ginkgo.GinkgoT(), err)
+ assert.Len(ginkgo.GinkgoT(), sr, 1)
+ assert.Equal(ginkgo.GinkgoT(), sr[0].ServerPort, int32(9100))
+
+ resp := s.NewAPISIXClientWithTCPProxy().GET("/ip").Expect()
+ resp.Body().Contains("origin")
+
+ resp = s.NewAPISIXClientWithTCPProxy().GET("/get").WithHeader("x-my-header", "x-my-value").Expect()
+ resp.Body().Contains("x-my-value")
+ })
+})
diff --git a/test/e2e/scaffold/apisix.go b/test/e2e/scaffold/apisix.go
index 5719f45..84bed8e 100644
--- a/test/e2e/scaffold/apisix.go
+++ b/test/e2e/scaffold/apisix.go
@@ -120,6 +120,10 @@
port: 9443
protocol: TCP
targetPort: 9443
+ - name: tcp
+ port: 9100
+ protocol: TCP
+ targetPort: 9100
type: NodePort
`
)
diff --git a/test/e2e/scaffold/k8s.go b/test/e2e/scaffold/k8s.go
index f165d8a..6fcc05b 100644
--- a/test/e2e/scaffold/k8s.go
+++ b/test/e2e/scaffold/k8s.go
@@ -162,6 +162,17 @@
return ensureNumApisixCRDsCreated(u.String(), desired)
}
+// EnsureNumApisixStreamRoutesCreated waits until desired number of Stream Routes are created in
+// APISIX cluster.
+func (s *Scaffold) EnsureNumApisixStreamRoutesCreated(desired int) error {
+ u := url.URL{
+ Scheme: "http",
+ Host: s.apisixAdminTunnel.Endpoint(),
+ Path: "/apisix/admin/stream_routes",
+ }
+ return ensureNumApisixCRDsCreated(u.String(), desired)
+}
+
// EnsureNumApisixUpstreamsCreated waits until desired number of Upstreams are created in
// APISIX cluster.
func (s *Scaffold) EnsureNumApisixUpstreamsCreated(desired int) error {
@@ -233,6 +244,26 @@
return cli.Cluster("").Route().List(context.TODO())
}
+// ListApisixStreamRoutes list all stream_routes from APISIX.
+func (s *Scaffold) ListApisixStreamRoutes() ([]*v1.StreamRoute, error) {
+ u := url.URL{
+ Scheme: "http",
+ Host: s.apisixAdminTunnel.Endpoint(),
+ Path: "/apisix/admin",
+ }
+ cli, err := apisix.NewClient()
+ if err != nil {
+ return nil, err
+ }
+ err = cli.AddCluster(&apisix.ClusterOptions{
+ BaseURL: u.String(),
+ })
+ if err != nil {
+ return nil, err
+ }
+ return cli.Cluster("").StreamRoute().List(context.TODO())
+}
+
// ListApisixTls list all ssl from APISIX
func (s *Scaffold) ListApisixTls() ([]*v1.Ssl, error) {
u := url.URL{
@@ -261,6 +292,8 @@
adminPort int
httpPort int
httpsPort int
+ tcpPort int
+ tcpNodePort int
)
for _, port := range s.apisixService.Spec.Ports {
if port.Name == "http" {
@@ -272,6 +305,9 @@
} else if port.Name == "http-admin" {
adminNodePort = int(port.NodePort)
adminPort = int(port.Port)
+ } else if port.Name == "tcp" {
+ tcpNodePort = int(port.NodePort)
+ tcpPort = int(port.Port)
}
}
@@ -281,6 +317,8 @@
httpNodePort, httpPort)
s.apisixHttpsTunnel = k8s.NewTunnel(s.kubectlOptions, k8s.ResourceTypeService, "apisix-service-e2e-test",
httpsNodePort, httpsPort)
+ s.apisixTCPTunnel = k8s.NewTunnel(s.kubectlOptions, k8s.ResourceTypeService, "apisix-service-e2e-test",
+ tcpNodePort, tcpPort)
if err := s.apisixAdminTunnel.ForwardPortE(s.t); err != nil {
return err
@@ -294,6 +332,10 @@
return err
}
s.addFinalizers(s.apisixHttpsTunnel.Close)
+ if err := s.apisixTCPTunnel.ForwardPortE(s.t); err != nil {
+ return err
+ }
+ s.addFinalizers(s.apisixTCPTunnel.Close)
return nil
}
diff --git a/test/e2e/scaffold/scaffold.go b/test/e2e/scaffold/scaffold.go
index 8d4d505..ab63cd2 100644
--- a/test/e2e/scaffold/scaffold.go
+++ b/test/e2e/scaffold/scaffold.go
@@ -65,6 +65,7 @@
apisixAdminTunnel *k8s.Tunnel
apisixHttpTunnel *k8s.Tunnel
apisixHttpsTunnel *k8s.Tunnel
+ apisixTCPTunnel *k8s.Tunnel
// Used for template rendering.
EtcdServiceFQDN string
@@ -167,6 +168,26 @@
})
}
+// NewAPISIXClientWithTCPProxy creates the HTTP client but with the TCP proxy of APISIX.
+func (s *Scaffold) NewAPISIXClientWithTCPProxy() *httpexpect.Expect {
+ u := url.URL{
+ Scheme: "http",
+ Host: s.apisixTCPTunnel.Endpoint(),
+ }
+ return httpexpect.WithConfig(httpexpect.Config{
+ BaseURL: u.String(),
+ Client: &http.Client{
+ Transport: &http.Transport{},
+ CheckRedirect: func(req *http.Request, via []*http.Request) error {
+ return http.ErrUseLastResponse
+ },
+ },
+ Reporter: httpexpect.NewAssertReporter(
+ httpexpect.NewAssertReporter(ginkgo.GinkgoT()),
+ ),
+ })
+}
+
// NewAPISIXHttpsClient creates the default HTTPs client.
func (s *Scaffold) NewAPISIXHttpsClient(host string) *httpexpect.Expect {
u := url.URL{
diff --git a/test/e2e/testdata/apisix-gw-config.yaml b/test/e2e/testdata/apisix-gw-config.yaml
index a0ba901..4225a3a 100644
--- a/test/e2e/testdata/apisix-gw-config.yaml
+++ b/test/e2e/testdata/apisix-gw-config.yaml
@@ -53,10 +53,9 @@
http: 'radixtree_uri' # radixtree_uri: match route by uri(base on radixtree)
# radixtree_host_uri: match route by host + uri(base on radixtree)
ssl: 'radixtree_sni' # radixtree_sni: match route by SNI(base on radixtree)
- # stream_proxy: # TCP/UDP proxy
- # tcp: # TCP proxy port list
- # - 9100
- # - 9101
+ stream_proxy: # TCP/UDP proxy
+ tcp: # TCP proxy port list
+ - 9100
# udp: # UDP proxy port list
# - 9200
# - 9211