Merge pull request #4379 from diazjf/mirror
Allow Requests to be Mirrored to different backends
This commit is contained in:
commit
adef152db8
9 changed files with 264 additions and 1 deletions
|
@ -107,6 +107,8 @@ You can add these Kubernetes annotations to specific Ingress objects to customiz
|
|||
|[nginx.ingress.kubernetes.io/enable-owasp-core-rules](#modsecurity)|bool|
|
||||
|[nginx.ingress.kubernetes.io/modsecurity-transaction-id](#modsecurity)|string|
|
||||
|[nginx.ingress.kubernetes.io/modsecurity-snippet](#modsecurity)|string|
|
||||
|[nginx.ingress.kubernetes.io/mirror-uri](#mirror)|string|
|
||||
|[nginx.ingress.kubernetes.io/mirror-request-body](#mirror)|string|
|
||||
|
||||
### Canary
|
||||
|
||||
|
@ -797,3 +799,34 @@ By default, a request would need to satisfy all authentication requirements in o
|
|||
```yaml
|
||||
nginx.ingress.kubernetes.io/satisfy: "any"
|
||||
```
|
||||
|
||||
### Mirror
|
||||
|
||||
Enables a request to be mirrored to a mirror backend. Responses by mirror backends are ignored. This feature is useful, to see how requests will react in "test" backends.
|
||||
|
||||
You can mirror a request to the `/mirror` path on your ingress, by applying the below:
|
||||
|
||||
```yaml
|
||||
nginx.ingress.kubernetes.io/mirror-uri: "/mirror"
|
||||
```
|
||||
|
||||
The mirror path can be defined as a separate ingress resource:
|
||||
|
||||
```
|
||||
location = /mirror {
|
||||
internal;
|
||||
proxy_pass http://test_backend;
|
||||
}
|
||||
```
|
||||
|
||||
By default the request-body is sent to the mirror backend, but can be turned off by applying:
|
||||
|
||||
```yaml
|
||||
nginx.ingress.kubernetes.io/mirror-request-body: "off"
|
||||
```
|
||||
|
||||
**Note:** The mirror directive will be applied to all paths within the ingress resource.
|
||||
|
||||
The request sent to the mirror is linked to the orignial request. If you have a slow mirror backend, then the orignial request will throttle.
|
||||
|
||||
For more information on the mirror module see https://nginx.org/en/docs/http/ngx_http_mirror_module.html
|
||||
|
|
|
@ -45,6 +45,7 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/annotations/loadbalancing"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/log"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/luarestywaf"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/mirror"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/portinredirect"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/proxy"
|
||||
|
@ -109,6 +110,7 @@ type Ingress struct {
|
|||
LuaRestyWAF luarestywaf.Config
|
||||
InfluxDB influxdb.Config
|
||||
ModSecurity modsecurity.Config
|
||||
Mirror mirror.Config
|
||||
}
|
||||
|
||||
// Extractor defines the annotation parsers to be used in the extraction of annotations
|
||||
|
@ -156,6 +158,7 @@ func NewAnnotationExtractor(cfg resolver.Resolver) Extractor {
|
|||
"InfluxDB": influxdb.NewParser(cfg),
|
||||
"BackendProtocol": backendprotocol.NewParser(cfg),
|
||||
"ModSecurity": modsecurity.NewParser(cfg),
|
||||
"Mirror": mirror.NewParser(cfg),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
58
internal/ingress/annotations/mirror/main.go
Normal file
58
internal/ingress/annotations/mirror/main.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed 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 mirror
|
||||
|
||||
import (
|
||||
networking "k8s.io/api/networking/v1beta1"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
// Config returns the mirror to use in a given location
|
||||
type Config struct {
|
||||
URI string `json:"uri"`
|
||||
RequestBody string `json:"requestBody"`
|
||||
}
|
||||
|
||||
type mirror struct {
|
||||
r resolver.Resolver
|
||||
}
|
||||
|
||||
// NewParser creates a new mirror configuration annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return mirror{r}
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
// rule used to configure mirror
|
||||
func (a mirror) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
config := &Config{}
|
||||
var err error
|
||||
|
||||
config.URI, err = parser.GetStringAnnotation("mirror-uri", ing)
|
||||
if err != nil {
|
||||
config.URI = ""
|
||||
}
|
||||
|
||||
config.RequestBody, err = parser.GetStringAnnotation("mirror-request-body", ing)
|
||||
if err != nil || config.RequestBody != "off" {
|
||||
config.RequestBody = "on"
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
86
internal/ingress/annotations/mirror/main_test.go
Normal file
86
internal/ingress/annotations/mirror/main_test.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed 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 mirror
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
api "k8s.io/api/core/v1"
|
||||
networking "k8s.io/api/networking/v1beta1"
|
||||
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
uri := parser.GetAnnotationWithPrefix("mirror-uri")
|
||||
requestBody := parser.GetAnnotationWithPrefix("mirror-request-body")
|
||||
|
||||
ap := NewParser(&resolver.Mock{})
|
||||
if ap == nil {
|
||||
t.Fatalf("expected a parser.IngressAnnotation but returned nil")
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
annotations map[string]string
|
||||
expected *Config
|
||||
}{
|
||||
{map[string]string{uri: "/mirror", requestBody: ""}, &Config{
|
||||
URI: "/mirror",
|
||||
RequestBody: "on",
|
||||
}},
|
||||
{map[string]string{uri: "/mirror", requestBody: "off"}, &Config{
|
||||
URI: "/mirror",
|
||||
RequestBody: "off",
|
||||
}},
|
||||
{map[string]string{uri: "", requestBody: "ahh"}, &Config{
|
||||
URI: "",
|
||||
RequestBody: "on",
|
||||
}},
|
||||
{map[string]string{uri: "", requestBody: ""}, &Config{
|
||||
URI: "",
|
||||
RequestBody: "on",
|
||||
}},
|
||||
{map[string]string{}, &Config{
|
||||
URI: "",
|
||||
RequestBody: "on",
|
||||
}},
|
||||
{nil, &Config{
|
||||
URI: "",
|
||||
RequestBody: "on",
|
||||
}},
|
||||
}
|
||||
|
||||
ing := &networking.Ingress{
|
||||
ObjectMeta: meta_v1.ObjectMeta{
|
||||
Name: "foo",
|
||||
Namespace: api.NamespaceDefault,
|
||||
},
|
||||
Spec: networking.IngressSpec{},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
ing.SetAnnotations(testCase.annotations)
|
||||
result, _ := ap.Parse(ing)
|
||||
fmt.Printf("%t", result)
|
||||
if !reflect.DeepEqual(result, testCase.expected) {
|
||||
t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, result, testCase.annotations)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1169,6 +1169,7 @@ func locationApplyAnnotations(loc *ingress.Location, anns *annotations.Ingress)
|
|||
loc.CustomHTTPErrors = anns.CustomHTTPErrors
|
||||
loc.ModSecurity = anns.ModSecurity
|
||||
loc.Satisfy = anns.Satisfy
|
||||
loc.Mirror = anns.Mirror
|
||||
}
|
||||
|
||||
// OK to merge canary ingresses iff there exists one or more ingresses to potentially merge into
|
||||
|
|
|
@ -32,6 +32,7 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/annotations/ipwhitelist"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/log"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/luarestywaf"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/mirror"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/modsecurity"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/proxy"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/ratelimit"
|
||||
|
@ -320,6 +321,9 @@ type Location struct {
|
|||
ModSecurity modsecurity.Config `json:"modsecurity"`
|
||||
// Satisfy dictates allow access if any or all is set
|
||||
Satisfy string `json:"satisfy"`
|
||||
// Mirror allows you to mirror traffic to a "test" backend
|
||||
// +optional
|
||||
Mirror mirror.Config `json:"mirror,omitempty"`
|
||||
}
|
||||
|
||||
// SSLPassthroughBackend describes a SSL upstream server configured
|
||||
|
|
|
@ -422,6 +422,14 @@ func (l1 *Location) Equal(l2 *Location) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
if l1.Mirror.URI != l2.Mirror.URI {
|
||||
return false
|
||||
}
|
||||
|
||||
if l1.Mirror.RequestBody != l2.Mirror.RequestBody {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
@ -955,6 +955,11 @@ stream {
|
|||
{{ opentracingPropagateContext $location }};
|
||||
{{ end }}
|
||||
|
||||
{{ if $location.Mirror.URI }}
|
||||
mirror {{ $location.Mirror.URI }};
|
||||
mirror_request_body {{ $location.Mirror.RequestBody }};
|
||||
{{ end }}
|
||||
|
||||
rewrite_by_lua_block {
|
||||
lua_ingress.rewrite({{ locationConfigForLua $location $server $all }})
|
||||
balancer.rewrite()
|
||||
|
@ -1047,7 +1052,6 @@ stream {
|
|||
}
|
||||
{{ end }}
|
||||
|
||||
|
||||
{{ if not $location.Logs.Access }}
|
||||
access_log off;
|
||||
{{ end }}
|
||||
|
|
66
test/e2e/annotations/mirror.go
Normal file
66
test/e2e/annotations/mirror.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed 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 annotations
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
|
||||
"k8s.io/ingress-nginx/test/e2e/framework"
|
||||
)
|
||||
|
||||
var _ = framework.IngressNginxDescribe("Annotations - Mirror", func() {
|
||||
f := framework.NewDefaultFramework("mirror")
|
||||
host := "mirror.foo.com"
|
||||
|
||||
BeforeEach(func() {
|
||||
f.NewEchoDeployment()
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
})
|
||||
|
||||
It("should set mirror-uri to /mirror", func() {
|
||||
annotations := map[string]string{
|
||||
"nginx.ingress.kubernetes.io/mirror-uri": "/mirror",
|
||||
}
|
||||
|
||||
ing := framework.NewSingleIngress(host, "/", host, f.Namespace, "http-svc", 80, &annotations)
|
||||
f.EnsureIngress(ing)
|
||||
|
||||
f.WaitForNginxServer(host,
|
||||
func(server string) bool {
|
||||
return strings.Contains(server, "mirror /mirror;") && strings.Contains(server, "mirror_request_body on;")
|
||||
})
|
||||
})
|
||||
|
||||
It("should disable mirror-request-body", func() {
|
||||
annotations := map[string]string{
|
||||
"nginx.ingress.kubernetes.io/mirror-uri": "/mirror",
|
||||
"nginx.ingress.kubernetes.io/mirror-request-body": "off",
|
||||
}
|
||||
|
||||
ing := framework.NewSingleIngress(host, "/", host, f.Namespace, "http-svc", 80, &annotations)
|
||||
f.EnsureIngress(ing)
|
||||
|
||||
f.WaitForNginxServer(host,
|
||||
func(server string) bool {
|
||||
return strings.Contains(server, "mirror /mirror;") && strings.Contains(server, "mirror_request_body off;")
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue