From 7b3deb5e84d257f46ce5c513d72b22a24af1be67 Mon Sep 17 00:00:00 2001 From: ikatlinsky Date: Sat, 26 Aug 2023 16:23:33 +0200 Subject: [PATCH] feat: add support for host pass in upstream crd (#1889) Co-authored-by: Katlinsky, Ilya --- docs/en/latest/references/apisix_upstream.md | 2 + pkg/kube/apisix/apis/config/v2/types.go | 10 ++ pkg/providers/translation/apisix_upstream.go | 21 +++ .../translation/apisix_upstream_test.go | 47 ++++++ pkg/types/apisix/v1/types.go | 87 ++++++---- samples/deploy/crd/v1/ApisixUpstream.yaml | 9 ++ test/e2e/suite-features/upstream_pass_host.go | 151 ++++++++++++++++++ 7 files changed, 292 insertions(+), 35 deletions(-) create mode 100644 test/e2e/suite-features/upstream_pass_host.go diff --git a/docs/en/latest/references/apisix_upstream.md b/docs/en/latest/references/apisix_upstream.md index e954429882..3eefea2788 100644 --- a/docs/en/latest/references/apisix_upstream.md +++ b/docs/en/latest/references/apisix_upstream.md @@ -83,3 +83,5 @@ See the [definition](https://github.com/apache/apisix-ingress-controller/blob/ma | discovery.serviceName | string | Name of the upstream service. | | discovery.type | string | Types of Service Discovery, which indicates what registry in APISIX the discovery uses. Should match the entry in APISIX's config. Can refer to the [doc](https://apisix.apache.org/docs/apisix/discovery/) | | discovery.args | object | Args map for discovery-spcefic parameters. Also can refer to the [doc](https://apisix.apache.org/docs/apisix/discovery/) | +| passHost | string | Configures the host when the request is forwarded to the upstream. Can be one of pass, node or rewrite. Defaults to pass if not specified: pass - transparently passes the client's host to the Upstream, node - uses the host configured in the node of the Upstream, rewrite - uses the value configured in upstreamHost. +| upstreamHost | string | Specifies the host of the Upstream request. This is only valid if the passHost is set to rewrite. diff --git a/pkg/kube/apisix/apis/config/v2/types.go b/pkg/kube/apisix/apis/config/v2/types.go index 59569caead..3e2daf8895 100644 --- a/pkg/kube/apisix/apis/config/v2/types.go +++ b/pkg/kube/apisix/apis/config/v2/types.go @@ -543,6 +543,16 @@ type ApisixUpstreamConfig struct { // +optional Subsets []ApisixUpstreamSubset `json:"subsets,omitempty" yaml:"subsets,omitempty"` + // Configures the host when the request is forwarded to the upstream. + // Can be one of pass, node or rewrite. + // +optional + PassHost string `json:"passHost,omitempty" yaml:"passHost,omitempty"` + + // Specifies the host of the Upstream request. This is only valid if + // the pass_host is set to rewrite + // +optional + UpstreamHost string `json:"upstreamHost,omitempty" yaml:"upstreamHost,omitempty"` + // Discovery is used to configure service discovery for upstream. // +optional Discovery *Discovery `json:"discovery,omitempty" yaml:"discovery,omitempty"` diff --git a/pkg/providers/translation/apisix_upstream.go b/pkg/providers/translation/apisix_upstream.go index 162dd9849a..44aca7ba9b 100644 --- a/pkg/providers/translation/apisix_upstream.go +++ b/pkg/providers/translation/apisix_upstream.go @@ -21,6 +21,11 @@ import ( apisixv1 "github.com/apache/apisix-ingress-controller/pkg/types/apisix/v1" ) +type passHostConfig struct { + passHost string + upstreamHost string +} + func (t *translator) TranslateUpstreamConfigV2(au *configv2.ApisixUpstreamConfig) (*apisixv1.Upstream, error) { ups := apisixv1.NewDefaultUpstream() if err := t.translateUpstreamScheme(au.Scheme, ups); err != nil { @@ -38,6 +43,9 @@ func (t *translator) TranslateUpstreamConfigV2(au *configv2.ApisixUpstreamConfig if err := t.translateClientTLSV2(au.TLSSecret, ups); err != nil { return nil, err } + if err := t.translatePassHost(&passHostConfig{au.PassHost, au.UpstreamHost}, ups); err != nil { + return nil, err + } if err := t.translateUpstreamDiscovery(au.Discovery, ups); err != nil { return nil, err } @@ -368,3 +376,16 @@ func (t *translator) translateUpstreamPassiveHealthCheckV2(config *configv2.Pass } return &passive, nil } + +func (t *translator) translatePassHost(ph *passHostConfig, ups *apisixv1.Upstream) error { + switch ph.passHost { + case "", apisixv1.PassHostPass, apisixv1.PassHostNode, apisixv1.PassHostRewrite: + ups.PassHost = ph.passHost + default: + return &TranslateError{Field: "passHost", Reason: "invalid value"} + } + + ups.UpstreamHost = ph.upstreamHost + + return nil +} diff --git a/pkg/providers/translation/apisix_upstream_test.go b/pkg/providers/translation/apisix_upstream_test.go index 92d8081c6a..6723535e56 100644 --- a/pkg/providers/translation/apisix_upstream_test.go +++ b/pkg/providers/translation/apisix_upstream_test.go @@ -409,3 +409,50 @@ func TestUpstreamRetriesAndTimeoutV2(t *testing.T) { Read: 15, }, ups.Timeout) } + +func TestUpstreamPassHost(t *testing.T) { + tr := &translator{} + tests := []struct { + name string + phc *passHostConfig + wantFunc func(t *testing.T, err error, ups *apisixv1.Upstream, phc *passHostConfig) + }{ + { + name: "should be empty when settings not set explicitly", + phc: &passHostConfig{}, + wantFunc: func(t *testing.T, err error, ups *apisixv1.Upstream, phc *passHostConfig) { + assert.Nil(t, err) + assert.Empty(t, ups.PassHost) + assert.Empty(t, ups.UpstreamHost) + }, + }, + { + name: "should set passHost to pass", + phc: &passHostConfig{passHost: apisixv1.PassHostPass}, + wantFunc: func(t *testing.T, err error, ups *apisixv1.Upstream, phc *passHostConfig) { + assert.Nil(t, err) + assert.Equal(t, phc.passHost, ups.PassHost) + assert.Empty(t, ups.UpstreamHost) + }, + }, + { + name: "should fail when passHost set to invalid value", + phc: &passHostConfig{passHost: "unknown"}, + wantFunc: func(t *testing.T, err error, ups *apisixv1.Upstream, phc *passHostConfig) { + assert.Equal(t, &TranslateError{ + Field: "passHost", + Reason: "invalid value", + }, err) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ups := apisixv1.NewDefaultUpstream() + err := tr.translatePassHost(tt.phc, ups) + + tt.wantFunc(t, err, ups, tt.phc) + }) + } +} diff --git a/pkg/types/apisix/v1/types.go b/pkg/types/apisix/v1/types.go index 04ed3be5c9..d85c386d61 100644 --- a/pkg/types/apisix/v1/types.go +++ b/pkg/types/apisix/v1/types.go @@ -79,6 +79,13 @@ const ( // DefaultUpstreamTimeout represents the default connect, // read and send timeout (in seconds) with upstreams. DefaultUpstreamTimeout = 60 + + // PassHostPass represents pass option for pass_host Upstream settings. + PassHostPass = "pass" + // PassHostPass represents node option for pass_host Upstream settings. + PassHostNode = "node" + // PassHostPass represents rewrite option for pass_host Upstream settings. + PassHostRewrite = "rewrite" ) var ValidSchemes map[string]struct{} = map[string]struct{}{ @@ -196,15 +203,17 @@ func (p *Plugins) DeepCopy() *Plugins { type Upstream struct { Metadata `json:",inline" yaml:",inline"` - Type string `json:"type,omitempty" yaml:"type,omitempty"` - HashOn string `json:"hash_on,omitempty" yaml:"hash_on,omitempty"` - Key string `json:"key,omitempty" yaml:"key,omitempty"` - Checks *UpstreamHealthCheck `json:"checks,omitempty" yaml:"checks,omitempty"` - Nodes UpstreamNodes `json:"nodes" yaml:"nodes"` - Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"` - Retries *int `json:"retries,omitempty" yaml:"retries,omitempty"` - Timeout *UpstreamTimeout `json:"timeout,omitempty" yaml:"timeout,omitempty"` - TLS *ClientTLS `json:"tls,omitempty" yaml:"tls,omitempty"` + Type string `json:"type,omitempty" yaml:"type,omitempty"` + HashOn string `json:"hash_on,omitempty" yaml:"hash_on,omitempty"` + Key string `json:"key,omitempty" yaml:"key,omitempty"` + Checks *UpstreamHealthCheck `json:"checks,omitempty" yaml:"checks,omitempty"` + Nodes UpstreamNodes `json:"nodes" yaml:"nodes"` + Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"` + Retries *int `json:"retries,omitempty" yaml:"retries,omitempty"` + Timeout *UpstreamTimeout `json:"timeout,omitempty" yaml:"timeout,omitempty"` + TLS *ClientTLS `json:"tls,omitempty" yaml:"tls,omitempty"` + PassHost string `json:"pass_host,omitempty" yaml:"pass_host,omitempty"` + UpstreamHost string `json:"upstream_host,omitempty" yaml:"upstream_host,omitempty"` // for Service Discovery ServiceName string `json:"service_name,omitempty" yaml:"service_name,omitempty"` @@ -271,10 +280,12 @@ func (up Upstream) MarshalJSON() ([]byte, error) { Key string `json:"key,omitempty" yaml:"key,omitempty"` Checks *UpstreamHealthCheck `json:"checks,omitempty" yaml:"checks,omitempty"` //Nodes UpstreamNodes `json:"nodes" yaml:"nodes"` - Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"` - Retries *int `json:"retries,omitempty" yaml:"retries,omitempty"` - Timeout *UpstreamTimeout `json:"timeout,omitempty" yaml:"timeout,omitempty"` - TLS *ClientTLS `json:"tls,omitempty" yaml:"tls,omitempty"` + Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"` + Retries *int `json:"retries,omitempty" yaml:"retries,omitempty"` + Timeout *UpstreamTimeout `json:"timeout,omitempty" yaml:"timeout,omitempty"` + HostPass string `json:"pass_host,omitempty" yaml:"pass_host,omitempty"` + UpstreamHost string `json:"upstream_host,omitempty" yaml:"upstream_host,omitempty"` + TLS *ClientTLS `json:"tls,omitempty" yaml:"tls,omitempty"` // for Service Discovery ServiceName string `json:"service_name,omitempty" yaml:"service_name,omitempty"` @@ -288,10 +299,12 @@ func (up Upstream) MarshalJSON() ([]byte, error) { Key: up.Key, Checks: up.Checks, //Nodes: up.Nodes, - Scheme: up.Scheme, - Retries: up.Retries, - Timeout: up.Timeout, - TLS: up.TLS, + Scheme: up.Scheme, + Retries: up.Retries, + Timeout: up.Timeout, + HostPass: up.PassHost, + UpstreamHost: up.UpstreamHost, + TLS: up.TLS, ServiceName: up.ServiceName, DiscoveryType: up.DiscoveryType, @@ -301,15 +314,17 @@ func (up Upstream) MarshalJSON() ([]byte, error) { return json.Marshal(&struct { Metadata `json:",inline" yaml:",inline"` - Type string `json:"type,omitempty" yaml:"type,omitempty"` - HashOn string `json:"hash_on,omitempty" yaml:"hash_on,omitempty"` - Key string `json:"key,omitempty" yaml:"key,omitempty"` - Checks *UpstreamHealthCheck `json:"checks,omitempty" yaml:"checks,omitempty"` - Nodes UpstreamNodes `json:"nodes" yaml:"nodes"` - Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"` - Retries *int `json:"retries,omitempty" yaml:"retries,omitempty"` - Timeout *UpstreamTimeout `json:"timeout,omitempty" yaml:"timeout,omitempty"` - TLS *ClientTLS `json:"tls,omitempty" yaml:"tls,omitempty"` + Type string `json:"type,omitempty" yaml:"type,omitempty"` + HashOn string `json:"hash_on,omitempty" yaml:"hash_on,omitempty"` + Key string `json:"key,omitempty" yaml:"key,omitempty"` + Checks *UpstreamHealthCheck `json:"checks,omitempty" yaml:"checks,omitempty"` + Nodes UpstreamNodes `json:"nodes" yaml:"nodes"` + Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"` + Retries *int `json:"retries,omitempty" yaml:"retries,omitempty"` + Timeout *UpstreamTimeout `json:"timeout,omitempty" yaml:"timeout,omitempty"` + HostPass string `json:"pass_host,omitempty" yaml:"pass_host,omitempty"` + UpstreamHost string `json:"upstream_host,omitempty" yaml:"upstream_host,omitempty"` + TLS *ClientTLS `json:"tls,omitempty" yaml:"tls,omitempty"` // for Service Discovery //ServiceName string `json:"service_name,omitempty" yaml:"service_name,omitempty"` @@ -318,15 +333,17 @@ func (up Upstream) MarshalJSON() ([]byte, error) { }{ Metadata: up.Metadata, - Type: up.Type, - HashOn: up.HashOn, - Key: up.Key, - Checks: up.Checks, - Nodes: up.Nodes, - Scheme: up.Scheme, - Retries: up.Retries, - Timeout: up.Timeout, - TLS: up.TLS, + Type: up.Type, + HashOn: up.HashOn, + Key: up.Key, + Checks: up.Checks, + Nodes: up.Nodes, + Scheme: up.Scheme, + Retries: up.Retries, + Timeout: up.Timeout, + HostPass: up.PassHost, + UpstreamHost: up.UpstreamHost, + TLS: up.TLS, //ServiceName: up.ServiceName, //DiscoveryType: up.DiscoveryType, diff --git a/samples/deploy/crd/v1/ApisixUpstream.yaml b/samples/deploy/crd/v1/ApisixUpstream.yaml index 3bfcd5ac75..ac0b67d619 100644 --- a/samples/deploy/crd/v1/ApisixUpstream.yaml +++ b/samples/deploy/crd/v1/ApisixUpstream.yaml @@ -516,6 +516,15 @@ spec: type: string send: type: string + passHost: + type: string + enum: + - pass + - node + - rewrite + upstreamHost: + type: string + pattern: "^\\*?[0-9a-zA-Z-._]+$" tlsSecret: description: ApisixSecret describes the Kubernetes Secret name and namespace. diff --git a/test/e2e/suite-features/upstream_pass_host.go b/test/e2e/suite-features/upstream_pass_host.go new file mode 100644 index 0000000000..37fd12168a --- /dev/null +++ b/test/e2e/suite-features/upstream_pass_host.go @@ -0,0 +1,151 @@ +// 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 features + +import ( + "fmt" + "time" + + ginkgo "github.com/onsi/ginkgo/v2" + "github.com/stretchr/testify/assert" + + "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" +) + +var _ = ginkgo.Describe("suite-features: upstream pass host", func() { + suites := func(scaffoldFunc func() *scaffold.Scaffold) { + s := scaffoldFunc() + + routeTpl := ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: httpbin-route +spec: + http: + - name: rule1 + match: + hosts: + - httpbin.org + paths: + - /* + backends: + - serviceName: %s + servicePort: %d +` + + ginkgo.It("is set to node", func() { + backendSvc, backendPorts := s.DefaultHTTPBackend() + ar := fmt.Sprintf(routeTpl, backendSvc, backendPorts[0]) + err := s.CreateVersionedApisixResource(ar) + assert.Nil(ginkgo.GinkgoT(), err) + time.Sleep(5 * time.Second) + + au := fmt.Sprintf(` +apiVersion: apisix.apache.org/v2 +kind: ApisixUpstream +metadata: + name: %s +spec: + passHost: node +`, backendSvc) + err = s.CreateVersionedApisixResource(au) + assert.Nil(ginkgo.GinkgoT(), err, "create ApisixUpstream") + time.Sleep(2 * time.Second) + + ups, err := s.ListApisixUpstreams() + assert.Nil(ginkgo.GinkgoT(), err) + assert.Len(ginkgo.GinkgoT(), ups, 1) + assert.Equal(ginkgo.GinkgoT(), "node", ups[0].PassHost) + }) + + ginkgo.It("is set to rewrite with upstream host", func() { + backendSvc, backendPorts := s.DefaultHTTPBackend() + ar := fmt.Sprintf(routeTpl, backendSvc, backendPorts[0]) + err := s.CreateVersionedApisixResource(ar) + assert.Nil(ginkgo.GinkgoT(), err) + time.Sleep(5 * time.Second) + + au := fmt.Sprintf(` +apiVersion: apisix.apache.org/v2 +kind: ApisixUpstream +metadata: + name: %s +spec: + passHost: rewrite + upstreamHost: host +`, backendSvc) + err = s.CreateVersionedApisixResource(au) + assert.Nil(ginkgo.GinkgoT(), err, "create ApisixUpstream") + time.Sleep(2 * time.Second) + + ups, err := s.ListApisixUpstreams() + assert.Nil(ginkgo.GinkgoT(), err) + assert.Len(ginkgo.GinkgoT(), ups, 1) + assert.Equal(ginkgo.GinkgoT(), "rewrite", ups[0].PassHost) + assert.Equal(ginkgo.GinkgoT(), "host", ups[0].UpstreamHost) + }) + + ginkgo.It("is set to node with upstream host", func() { + backendSvc, backendPorts := s.DefaultHTTPBackend() + ar := fmt.Sprintf(routeTpl, backendSvc, backendPorts[0]) + err := s.CreateVersionedApisixResource(ar) + assert.Nil(ginkgo.GinkgoT(), err) + time.Sleep(5 * time.Second) + + au := fmt.Sprintf(` +apiVersion: apisix.apache.org/v2 +kind: ApisixUpstream +metadata: + name: %s +spec: + passHost: node + upstreamHost: host +`, backendSvc) + err = s.CreateVersionedApisixResource(au) + assert.Nil(ginkgo.GinkgoT(), err, "create ApisixUpstream") + time.Sleep(2 * time.Second) + + ups, err := s.ListApisixUpstreams() + assert.Nil(ginkgo.GinkgoT(), err) + assert.Len(ginkgo.GinkgoT(), ups, 1) + assert.Equal(ginkgo.GinkgoT(), "node", ups[0].PassHost) + assert.Equal(ginkgo.GinkgoT(), "host", ups[0].UpstreamHost) + }) + + ginkgo.It("is set to invalid value", func() { + backendSvc, backendPorts := s.DefaultHTTPBackend() + ar := fmt.Sprintf(routeTpl, backendSvc, backendPorts[0]) + err := s.CreateVersionedApisixResource(ar) + assert.Nil(ginkgo.GinkgoT(), err) + time.Sleep(5 * time.Second) + + au := fmt.Sprintf(` +apiVersion: apisix.apache.org/v2 +kind: ApisixUpstream +metadata: + name: %s +spec: + passHost: invalid +`, backendSvc) + err = s.CreateVersionedApisixResource(au) + assert.NotNil(ginkgo.GinkgoT(), err) + }) + } + + ginkgo.Describe("suite-features: scaffold v2", func() { + suites(scaffold.NewDefaultV2Scaffold) + }) +})