diff --git a/src/envoy/mixer/integration_test/BUILD b/src/envoy/mixer/integration_test/BUILD index c45865457ce..a6ff4916db5 100644 --- a/src/envoy/mixer/integration_test/BUILD +++ b/src/envoy/mixer/integration_test/BUILD @@ -1,4 +1,4 @@ -# Copyright 2016 Google Inc. All Rights Reserved. +# Copyright 2017 Istio Authors. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,13 +15,15 @@ ################################################################################ # -load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") +load("@io_bazel_rules_go//go:def.bzl", "go_library") +load(":test_suite.bzl", "go_test_suite") go_library( name = "go_default_library", srcs = [ "attributes.go", "envoy.go", + "envoy_conf.go", "http_client.go", "http_server.go", "mixer_server.go", @@ -41,18 +43,22 @@ go_library( ], ) -go_test( - name = "mixer_test", - size = "small", - srcs = [ - "mixer_test.go", - ], +go_test_suite( data = [ - "envoy.conf", "//src/envoy/mixer:envoy", ], library = ":go_default_library", - # shared memory path /envoy_shared_memory_0 used by Envoy - # hot start is not working in sandbox mode. - local = True, + tags = [ + # Use fixed ports, not in sanbbox, have to be run exclusively. + "exclusive", + # shared memory path /envoy_shared_memory_0 used by Envoy + # hot start is not working in sandbox mode. + "local", + ], + tests = [ + "check_cache_test.go", + "check_report_test.go", + "failed_request_test.go", + "quota_test.go", + ], ) diff --git a/src/envoy/mixer/integration_test/attributes.go b/src/envoy/mixer/integration_test/attributes.go index 4c97208fd4b..6aecd20d8bb 100644 --- a/src/envoy/mixer/integration_test/attributes.go +++ b/src/envoy/mixer/integration_test/attributes.go @@ -45,7 +45,21 @@ func verifyStringMap(actual map[string]string, expected map[string]interface{}) return nil } -// Please see the comment at top of mixer_test.go for verification rules +// Attributes verification rules: +// +// 1) If value is *, key must exist, but value is not checked. +// 1) If value is -, key must NOT exist. +// 3) At top level attributes, not inside StringMap, all keys must +// be listed. Extra keys are NOT allowed +// 3) Inside StringMap, not need to list all keys. Extra keys are allowed +// +// Attributes provided from envoy config +// * source.id and source.namespace are forwarded from client proxy +// * target.id and target.namespace are from server proxy +// +// HTTP header "x-istio-attributes" is used to forward attributes between +// proxy. It should be removed before calling mixer and backend. +// func Verify(b *attribute.MutableBag, json_results string) error { var r map[string]interface{} if err := json.Unmarshal([]byte(json_results), &r); err != nil { diff --git a/src/envoy/mixer/integration_test/check_cache_test.go b/src/envoy/mixer/integration_test/check_cache_test.go new file mode 100644 index 00000000000..ab79320ae82 --- /dev/null +++ b/src/envoy/mixer/integration_test/check_cache_test.go @@ -0,0 +1,40 @@ +// Copyright 2017 Istio 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 test + +import ( + "fmt" + "testing" +) + +func TestCheckCache(t *testing.T) { + s, err := SetUp(t, basicConfig+","+checkCacheConfig) + if err != nil { + t.Fatalf("Failed to setup test: %v", err) + } + defer s.TearDown() + + url := fmt.Sprintf("http://localhost:%d/echo", ClientProxyPort) + + // Issues a GET echo request with 0 size body + tag := "OKGet" + for i := 0; i < 10; i++ { + if _, _, err := HTTPGet(url); err != nil { + t.Errorf("Failed in request %s: %v", tag, err) + } + // Only the first check is called. + s.VerifyCheckCount(tag, 1) + } +} diff --git a/src/envoy/mixer/integration_test/check_report_test.go b/src/envoy/mixer/integration_test/check_report_test.go new file mode 100644 index 00000000000..71d8a3f6bb9 --- /dev/null +++ b/src/envoy/mixer/integration_test/check_report_test.go @@ -0,0 +1,154 @@ +// Copyright 2017 Istio 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 test + +import ( + "fmt" + "testing" +) + +// Check attributes from a good GET request +const checkAttributesOkGet = ` +{ + "request.host": "localhost:27070", + "request.path": "/echo", + "request.time": "*", + "source.uid": "POD11", + "source.namespace": "XYZ11", + "target.uid": "POD222", + "target.namespace": "XYZ222", + "request.headers": { + ":method": "GET", + ":path": "/echo", + ":authority": "localhost:27070", + "x-forwarded-proto": "http", + "x-istio-attributes": "-", + "x-request-id": "*" + } +} +` + +// Report attributes from a good GET request +const reportAttributesOkGet = ` +{ + "request.host": "localhost:27070", + "request.path": "/echo", + "request.time": "*", + "source.uid": "POD11", + "source.namespace": "XYZ11", + "target.uid": "POD222", + "target.namespace": "XYZ222", + "request.headers": { + ":method": "GET", + ":path": "/echo", + ":authority": "localhost:27070", + "x-forwarded-proto": "http", + "x-istio-attributes": "-", + "x-request-id": "*" + }, + "request.size": 0, + "response.time": "*", + "response.size": 0, + "response.latency": "*", + "response.http.code": 200, + "response.headers": { + "date": "*", + "content-type": "text/plain; charset=utf-8", + "content-length": "0", + ":status": "200", + "server": "envoy" + } +} +` + +// Check attributes from a good POST request +const checkAttributesOkPost = ` +{ + "request.host": "localhost:27070", + "request.path": "/echo", + "request.time": "*", + "source.uid": "POD11", + "source.namespace": "XYZ11", + "target.uid": "POD222", + "target.namespace": "XYZ222", + "request.headers": { + ":method": "POST", + ":path": "/echo", + ":authority": "localhost:27070", + "x-forwarded-proto": "http", + "x-istio-attributes": "-", + "x-request-id": "*" + } +} +` + +// Report attributes from a good POST request +const reportAttributesOkPost = ` +{ + "request.host": "localhost:27070", + "request.path": "/echo", + "request.time": "*", + "source.uid": "POD11", + "source.namespace": "XYZ11", + "target.uid": "POD222", + "target.namespace": "XYZ222", + "request.headers": { + ":method": "POST", + ":path": "/echo", + ":authority": "localhost:27070", + "x-forwarded-proto": "http", + "x-istio-attributes": "-", + "x-request-id": "*" + }, + "request.size": 12, + "response.time": "*", + "response.size": 12, + "response.latency": "*", + "response.http.code": 200, + "response.headers": { + "date": "*", + "content-type": "text/plain", + "content-length": "12", + ":status": "200", + "server": "envoy" + } +} +` + +func TestCheckReportAttributes(t *testing.T) { + s, err := SetUp(t, basicConfig) + if err != nil { + t.Fatalf("Failed to setup test: %v", err) + } + defer s.TearDown() + + url := fmt.Sprintf("http://localhost:%d/echo", ClientProxyPort) + + // Issues a GET echo request with 0 size body + tag := "OKGet" + if _, _, err := HTTPGet(url); err != nil { + t.Errorf("Failed in request %s: %v", tag, err) + } + s.VerifyCheck(tag, checkAttributesOkGet) + s.VerifyReport(tag, reportAttributesOkGet) + + // Issues a POST request. + tag = "OKPost" + if _, _, err := HTTPPost(url, "text/plain", "Hello World!"); err != nil { + t.Errorf("Failed in request %s: %v", tag, err) + } + s.VerifyCheck(tag, checkAttributesOkPost) + s.VerifyReport(tag, reportAttributesOkPost) +} diff --git a/src/envoy/mixer/integration_test/envoy.go b/src/envoy/mixer/integration_test/envoy.go index 0f9a1fc01c7..b34fb5c240a 100644 --- a/src/envoy/mixer/integration_test/envoy.go +++ b/src/envoy/mixer/integration_test/envoy.go @@ -39,24 +39,6 @@ func getTestBinRootPath() string { } } -func getTestDataRootPath() string { - switch { - // custom path - case os.Getenv("TEST_DATA_ROOT") != "": - return os.Getenv("TEST_DATA_ROOT") - // running under bazel - case os.Getenv("TEST_SRCDIR") != "": - return os.Getenv("TEST_SRCDIR") + "/__main__" - // running with native go - case os.Getenv("GOPATH") != "": - list := strings.Split(os.Getenv("GOPATH"), - string(os.PathListSeparator)) - return list[0] - default: - return "" - } -} - type Envoy struct { cmd *exec.Cmd } @@ -76,14 +58,17 @@ func Run(name string, args ...string) (s string, err error) { return } -func NewEnvoy() (*Envoy, error) { - path := getTestBinRootPath() + "/src/envoy/mixer/envoy" - conf := getTestDataRootPath() + - "/src/envoy/mixer/integration_test/envoy.conf" - log.Printf("Envoy binary: %v\n", path) - log.Printf("Envoy config: %v\n", conf) +func NewEnvoy(conf string) (*Envoy, error) { + bin_path := getTestBinRootPath() + "/src/envoy/mixer/envoy" + log.Printf("Envoy binary: %v\n", bin_path) + + conf_path := "/tmp/envoy.conf" + log.Printf("Envoy config: in %v\n%v\n", conf_path, conf) + if err := CreateEnvoyConf(conf_path, conf); err != nil { + return nil, err + } - cmd := exec.Command(path, "-c", conf, "-l", "debug") + cmd := exec.Command(bin_path, "-c", conf_path, "-l", "debug") cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout return &Envoy{ diff --git a/src/envoy/mixer/integration_test/envoy.conf b/src/envoy/mixer/integration_test/envoy_conf.go similarity index 56% rename from src/envoy/mixer/integration_test/envoy.conf rename to src/envoy/mixer/integration_test/envoy_conf.go index 1e9bd153893..166b11543aa 100644 --- a/src/envoy/mixer/integration_test/envoy.conf +++ b/src/envoy/mixer/integration_test/envoy_conf.go @@ -1,7 +1,83 @@ +// Copyright 2017 Istio Authors. All Rights Reserved. +// +// 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 test + +import ( + "fmt" + "os" + "text/template" +) + +const ( + // These ports should match with used envoy.conf + // Default is using one in this folder. + ServerProxyPort = 29090 + ClientProxyPort = 27070 + MixerPort = 29091 + BackendPort = 28080 + AdminPort = 29001 +) + +type ConfParam struct { + ClientPort int + ServerPort int + AdminPort int + MixerServer string + Backend string + ClientConfig string + ServerConfig string +} + +// A basic config +const basicConfig = ` + "mixer_attributes": { + "target.uid": "POD222", + "target.namespace": "XYZ222" + } +` + +// A config with quota +const quotaConfig = ` + "quota_name": "RequestCount", + "quota_amount": "5" +` + +// A config with check cache keys +const checkCacheConfig = ` + "check_cache_keys": [ + "request.host", + "request.path", + "origin.user" + ] +` + +// The default client proxy mixer config +const defaultClientMixerConfig = ` + "forward_attributes": { + "source.uid": "POD11", + "source.namespace": "XYZ11" + } +` + +// The envoy config template +const envoyConfTempl = ` { "listeners": [ { - "address": "tcp://0.0.0.0:29090", + "address": "tcp://0.0.0.0:{{.ServerPort}}", "bind_to_port": true, "filters": [ { @@ -39,17 +115,8 @@ "type": "decoder", "name": "mixer", "config": { - "mixer_server": "localhost:29091", - "mixer_attributes": { - "target.uid": "POD222", - "target.namespace": "XYZ222" - }, - "quota_name": "RequestCount", - "quota_amount": "5", - "check_cache_keys": [ - "request.host", - "request.path" - ] + "mixer_server": "{{.MixerServer}}", +{{.ServerConfig}} } }, { @@ -63,7 +130,7 @@ ] }, { - "address": "tcp://0.0.0.0:27070", + "address": "tcp://0.0.0.0:{{.ClientPort}}", "bind_to_port": true, "filters": [ { @@ -97,11 +164,8 @@ "type": "decoder", "name": "mixer", "config": { - "mixer_server": "localhost:29091", - "forward_attributes": { - "source.uid": "POD11", - "source.namespace": "XYZ11" - } + "mixer_server": "{{.MixerServer}}", +{{.ClientConfig}} } }, { @@ -117,7 +181,7 @@ ], "admin": { "access_log_path": "/dev/stdout", - "address": "tcp://0.0.0.0:29001" + "address": "tcp://0.0.0.0:{{.AdminPort}}" }, "cluster_manager": { "clusters": [ @@ -128,7 +192,7 @@ "lb_type": "round_robin", "hosts": [ { - "url": "tcp://localhost:28080" + "url": "tcp://{{.Backend}}" } ] }, @@ -139,10 +203,42 @@ "lb_type": "round_robin", "hosts": [ { - "url": "tcp://localhost:29090" + "url": "tcp://localhost:{{.ServerPort}}" } ] } ] } } +` + +func (c *ConfParam) write(path string) error { + tmpl, err := template.New("test").Parse(envoyConfTempl) + if err != nil { + return fmt.Errorf("Failed to parse config template: %v", err) + } + + f, err := os.Create(path) + if err != nil { + return fmt.Errorf("Failed to create file %v: %v", path, err) + } + defer f.Close() + return tmpl.Execute(f, *c) +} + +func getConf() ConfParam { + return ConfParam{ + ClientPort: ClientProxyPort, + ServerPort: ServerProxyPort, + AdminPort: AdminPort, + MixerServer: fmt.Sprintf("localhost:%d", MixerPort), + Backend: fmt.Sprintf("localhost:%d", BackendPort), + ClientConfig: defaultClientMixerConfig, + } +} + +func CreateEnvoyConf(path string, conf string) error { + c := getConf() + c.ServerConfig = conf + return c.write(path) +} diff --git a/src/envoy/mixer/integration_test/failed_request_test.go b/src/envoy/mixer/integration_test/failed_request_test.go new file mode 100644 index 00000000000..9ae99bcf81d --- /dev/null +++ b/src/envoy/mixer/integration_test/failed_request_test.go @@ -0,0 +1,161 @@ +// Copyright 2017 Istio 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 test + +import ( + "fmt" + "testing" + + rpc "github.com/googleapis/googleapis/google/rpc" +) + +const ( + mixerAuthFailMessage = "Unauthenticated by mixer." +) + +// Check attributes from a fail GET request from mixer +const checkAttributesMixerFail = ` +{ + "request.host": "localhost:27070", + "request.path": "/echo", + "request.time": "*", + "source.uid": "POD11", + "source.namespace": "XYZ11", + "target.uid": "POD222", + "target.namespace": "XYZ222", + "request.headers": { + ":method": "GET", + ":path": "/echo", + ":authority": "localhost:27070", + "x-forwarded-proto": "http", + "x-istio-attributes": "-", + "x-request-id": "*" + } +} +` + +// Report attributes from a fail GET request from mixer +const reportAttributesMixerFail = ` +{ + "request.host": "localhost:27070", + "request.path": "/echo", + "request.time": "*", + "source.uid": "POD11", + "source.namespace": "XYZ11", + "target.uid": "POD222", + "target.namespace": "XYZ222", + "request.headers": { + ":method": "GET", + ":path": "/echo", + ":authority": "localhost:27070", + "x-forwarded-proto": "http", + "x-istio-attributes": "-", + "x-request-id": "*" + }, + "request.size": 0, + "response.time": "*", + "response.size": 41, + "response.latency": "*", + "response.http.code": 401, + "response.headers": { + "date": "*", + "content-type": "text/plain", + "content-length": "41", + ":status": "401", + "server": "envoy" + } +} +` + +// Report attributes from a fail GET request from backend +const reportAttributesBackendFail = ` +{ + "request.host": "localhost:27070", + "request.path": "/echo", + "request.time": "*", + "source.uid": "POD11", + "source.namespace": "XYZ11", + "target.uid": "POD222", + "target.namespace": "XYZ222", + "request.headers": { + ":method": "GET", + ":path": "/echo", + ":authority": "localhost:27070", + "x-forwarded-proto": "http", + "x-istio-attributes": "-", + "x-request-id": "*" + }, + "request.size": 0, + "response.time": "*", + "response.size": 25, + "response.latency": "*", + "response.http.code": 400, + "response.headers": { + "date": "*", + "content-type": "text/plain; charset=utf-8", + "content-length": "25", + ":status": "400", + "server": "envoy" + } +} +` + +func TestFailedRequest(t *testing.T) { + s, err := SetUp(t, basicConfig) + if err != nil { + t.Fatalf("Failed to setup test: %v", err) + } + defer s.TearDown() + + url := fmt.Sprintf("http://localhost:%d/echo", ClientProxyPort) + + tag := "MixerFail" + s.mixer.check.r_status = rpc.Status{ + Code: int32(rpc.UNAUTHENTICATED), + Message: mixerAuthFailMessage, + } + code, resp_body, err := HTTPGet(url) + // Make sure to restore r_status for next request. + s.mixer.check.r_status = rpc.Status{} + if err != nil { + t.Errorf("Failed in request %s: %v", tag, err) + } + if code != 401 { + t.Errorf("Status code 401 is expected.") + } + if resp_body != "UNAUTHENTICATED:"+mixerAuthFailMessage { + t.Errorf("Error response body is not expected.") + } + s.VerifyCheck(tag, checkAttributesMixerFail) + s.VerifyReport(tag, reportAttributesMixerFail) + + // Issues a failed request caused by backend + tag = "BackendFail" + headers := map[string]string{} + headers[FailHeader] = "Yes" + code, resp_body, err = HTTPGetWithHeaders(url, headers) + if err != nil { + t.Errorf("Failed in request %s: %v", tag, err) + } + if code != 400 { + t.Errorf("Status code 400 is expected.") + } + if resp_body != FailBody { + t.Errorf("Error response body is not expected.") + } + // Same Check attributes as the first one. + s.VerifyCheck(tag, checkAttributesMixerFail) + s.VerifyReport(tag, reportAttributesBackendFail) +} diff --git a/src/envoy/mixer/integration_test/mixer_server.go b/src/envoy/mixer/integration_test/mixer_server.go index 3e851274819..6e3db085e5d 100644 --- a/src/envoy/mixer/integration_test/mixer_server.go +++ b/src/envoy/mixer/integration_test/mixer_server.go @@ -40,7 +40,7 @@ type Handler struct { func newHandler() *Handler { return &Handler{ bag: nil, - ch: make(chan int, 1), + ch: make(chan int, 10), // Allow maximum 10 requests count: 0, r_status: rpc.Status{}, } diff --git a/src/envoy/mixer/integration_test/mixer_test.go b/src/envoy/mixer/integration_test/mixer_test.go deleted file mode 100644 index 99cef6501b3..00000000000 --- a/src/envoy/mixer/integration_test/mixer_test.go +++ /dev/null @@ -1,340 +0,0 @@ -// Copyright 2017 Istio 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 test - -import ( - "fmt" - "testing" - - rpc "github.com/googleapis/googleapis/google/rpc" -) - -const ( - mixerAuthFailMessage = "Unauthenticated by mixer." - mixerQuotaFailMessage = "Not enough quota by mixer." -) - -// Attributes verification rules -// 1) If value is *, key must exist, but value is not checked. -// 1) If value is -, key must NOT exist. -// 3) At top level attributes, not inside StringMap, all keys must -// be listed. Extra keys are NOT allowed -// 3) Inside StringMap, not need to list all keys. Extra keys are allowed -// -// Attributes provided from envoy config -// * source.id and source.namespace are forwarded from client proxy -// * target.id and target.namespace are from server proxy -// -// HTTP header "x-istio-attributes" is used to forward attributes between -// proxy. It should be removed before calling mixer and backend. -// -// Check attributes from a good GET request -const checkAttributesOkGet = ` -{ - "request.host": "localhost:27070", - "request.path": "/echo", - "request.time": "*", - "source.uid": "POD11", - "source.namespace": "XYZ11", - "target.uid": "POD222", - "target.namespace": "XYZ222", - "request.headers": { - ":method": "GET", - ":path": "/echo", - ":authority": "localhost:27070", - "x-forwarded-proto": "http", - "x-istio-attributes": "-", - "x-request-id": "*" - } -} -` - -// Report attributes from a good GET request -const reportAttributesOkGet = ` -{ - "request.host": "localhost:27070", - "request.path": "/echo", - "request.time": "*", - "source.uid": "POD11", - "source.namespace": "XYZ11", - "target.uid": "POD222", - "target.namespace": "XYZ222", - "request.headers": { - ":method": "GET", - ":path": "/echo", - ":authority": "localhost:27070", - "x-forwarded-proto": "http", - "x-istio-attributes": "-", - "x-request-id": "*" - }, - "request.size": 0, - "response.time": "*", - "response.size": 0, - "response.latency": "*", - "response.http.code": 200, - "response.headers": { - "date": "*", - "content-type": "text/plain; charset=utf-8", - "content-length": "0", - ":status": "200", - "server": "envoy" - } -} -` - -// Report attributes from a good POST request -const reportAttributesOkPost = ` -{ - "request.host": "localhost:27070", - "request.path": "/echo", - "request.time": "*", - "source.uid": "POD11", - "source.namespace": "XYZ11", - "target.uid": "POD222", - "target.namespace": "XYZ222", - "request.headers": { - ":method": "POST", - ":path": "/echo", - ":authority": "localhost:27070", - "x-forwarded-proto": "http", - "x-istio-attributes": "-", - "x-request-id": "*" - }, - "request.size": 12, - "response.time": "*", - "response.size": 45, - "response.latency": "*", - "response.http.code": 429, - "response.headers": { - "date": "*", - "content-type": "text/plain", - "content-length": "45", - ":status": "429", - "server": "envoy" - } -} -` - -// Check attributes from a fail GET request from mixer -const checkAttributesMixerFail = ` -{ - "request.host": "localhost:27070", - "request.path": "/echo111", - "request.time": "*", - "source.uid": "POD11", - "source.namespace": "XYZ11", - "target.uid": "POD222", - "target.namespace": "XYZ222", - "request.headers": { - ":method": "GET", - ":path": "/echo111", - ":authority": "localhost:27070", - "x-forwarded-proto": "http", - "x-istio-attributes": "-", - "x-request-id": "*" - } -} -` - -// Report attributes from a fail GET request from mixer -const reportAttributesMixerFail = ` -{ - "request.host": "localhost:27070", - "request.path": "/echo111", - "request.time": "*", - "source.uid": "POD11", - "source.namespace": "XYZ11", - "target.uid": "POD222", - "target.namespace": "XYZ222", - "request.headers": { - ":method": "GET", - ":path": "/echo111", - ":authority": "localhost:27070", - "x-forwarded-proto": "http", - "x-istio-attributes": "-", - "x-request-id": "*" - }, - "request.size": 0, - "response.time": "*", - "response.size": 41, - "response.latency": "*", - "response.http.code": 401, - "response.headers": { - "date": "*", - "content-type": "text/plain", - "content-length": "41", - ":status": "401", - "server": "envoy" - } -} -` - -// Report attributes from a fail GET request from backend -const reportAttributesBackendFail = ` -{ - "request.host": "localhost:27070", - "request.path": "/echo", - "request.time": "*", - "source.uid": "POD11", - "source.namespace": "XYZ11", - "target.uid": "POD222", - "target.namespace": "XYZ222", - "request.headers": { - ":method": "GET", - ":path": "/echo", - ":authority": "localhost:27070", - "x-forwarded-proto": "http", - "x-istio-attributes": "-", - "x-request-id": "*" - }, - "request.size": 0, - "response.time": "*", - "response.size": 25, - "response.latency": "*", - "response.http.code": 400, - "response.headers": { - "date": "*", - "content-type": "text/plain; charset=utf-8", - "content-length": "25", - ":status": "400", - "server": "envoy" - } -} -` - -func verifyCheckCount(s *TestSetup, tag string, count int, t *testing.T) { - if s.mixer.check.count != count { - t.Fatalf("%s check count doesn't match: %v\n, expected: %+v", - tag, s.mixer.check.count, count) - } -} - -func verifyCheckAttributes(s *TestSetup, tag string, check string, t *testing.T) { - _ = <-s.mixer.check.ch - if err := Verify(s.mixer.check.bag, check); err != nil { - t.Fatalf("Failed to verify %s check: %v\n, Attributes: %+v", - tag, err, s.mixer.check.bag) - } -} - -func verifyReportAttributes(s *TestSetup, tag string, report string, t *testing.T) { - _ = <-s.mixer.report.ch - if err := Verify(s.mixer.report.bag, report); err != nil { - t.Fatalf("Failed to verify %s report: %v\n, Attributes: %+v", - tag, err, s.mixer.report.bag) - } -} - -func verifyQuota(s *TestSetup, tag string, t *testing.T) { - _ = <-s.mixer.quota.ch - if s.mixer.quota_request.Quota != "RequestCount" { - t.Fatalf("Failed to verify %s quota name (=RequestCount): %v\n", - tag, s.mixer.quota_request.Quota) - } - if s.mixer.quota_request.Amount != 5 { - t.Fatalf("Failed to verify %s quota amount (=5): %v\n", - tag, s.mixer.quota_request.Amount) - } -} - -func TestMixer(t *testing.T) { - s, err := SetUp() - if err != nil { - t.Fatalf("Failed to setup test: %v", err) - } - defer s.TearDown() - - // There is a client proxy with filter "forward_attribute" - // and a server proxy with filter "mixer" calling mixer. - // This request will connect to client proxy, to server proxy - // and to the backend. - url := fmt.Sprintf("http://localhost:%d/echo", ClientProxyPort) - - // Issues a GET echo request with 0 size body - tag := "OKGet" - if _, _, err := HTTPGet(url); err != nil { - t.Errorf("Failed in request %s: %v", tag, err) - } - verifyCheckAttributes(&s, tag, checkAttributesOkGet, t) - verifyReportAttributes(&s, tag, reportAttributesOkGet, t) - verifyQuota(&s, tag, t) - - // Issues a failed POST request caused by Mixer Quota - tag = "QuotaFail" - s.mixer.quota.r_status = rpc.Status{ - Code: int32(rpc.RESOURCE_EXHAUSTED), - Message: mixerQuotaFailMessage, - } - code, resp_body, err := HTTPPost(url, "text/plain", "Hello World!") - // Make sure to restore r_status for next request. - s.mixer.quota.r_status = rpc.Status{} - if err != nil { - t.Errorf("Failed in request %s: %v", tag, err) - } - if code != 429 { - t.Errorf("Status code 429 is expected.") - } - if resp_body != "RESOURCE_EXHAUSTED:"+mixerQuotaFailMessage { - t.Errorf("Error response body is not expected.") - } - // Use cached check. so server check count should remain 1. - verifyCheckCount(&s, tag, 1, t) - verifyReportAttributes(&s, tag, reportAttributesOkPost, t) - verifyQuota(&s, tag, t) - - // Issues a failed request caused by mixer - // Use a different path to avoid check cache - url = fmt.Sprintf("http://localhost:%d/echo111", ClientProxyPort) - tag = "MixerFail" - s.mixer.check.r_status = rpc.Status{ - Code: int32(rpc.UNAUTHENTICATED), - Message: mixerAuthFailMessage, - } - code, resp_body, err = HTTPGet(url) - // Make sure to restore r_status for next request. - s.mixer.check.r_status = rpc.Status{} - if err != nil { - t.Errorf("Failed in request %s: %v", tag, err) - } - if code != 401 { - t.Errorf("Status code 401 is expected.") - } - if resp_body != "UNAUTHENTICATED:"+mixerAuthFailMessage { - t.Errorf("Error response body is not expected.") - } - verifyCheckAttributes(&s, tag, checkAttributesMixerFail, t) - verifyReportAttributes(&s, tag, reportAttributesMixerFail, t) - // Not quota call due to Mixer failure. - - // Issues a failed request caused by backend - // Use the first path to use check cache - url = fmt.Sprintf("http://localhost:%d/echo", ClientProxyPort) - tag = "BackendFail" - headers := map[string]string{} - headers[FailHeader] = "Yes" - code, resp_body, err = HTTPGetWithHeaders(url, headers) - if err != nil { - t.Errorf("Failed in request %s: %v", tag, err) - } - if code != 400 { - t.Errorf("Status code 400 is expected.") - } - if resp_body != FailBody { - t.Errorf("Error response body is not expected.") - } - verifyCheckCount(&s, tag, 2, t) - verifyReportAttributes(&s, tag, reportAttributesBackendFail, t) - verifyQuota(&s, tag, t) -} diff --git a/src/envoy/mixer/integration_test/quota_test.go b/src/envoy/mixer/integration_test/quota_test.go new file mode 100644 index 00000000000..e99afb8e426 --- /dev/null +++ b/src/envoy/mixer/integration_test/quota_test.go @@ -0,0 +1,64 @@ +// Copyright 2017 Istio 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 test + +import ( + "fmt" + "testing" + + rpc "github.com/googleapis/googleapis/google/rpc" +) + +const ( + mixerQuotaFailMessage = "Not enough quota by mixer." +) + +func TestQuotaCall(t *testing.T) { + s, err := SetUp(t, basicConfig+","+quotaConfig) + if err != nil { + t.Fatalf("Failed to setup test: %v", err) + } + defer s.TearDown() + + url := fmt.Sprintf("http://localhost:%d/echo", ClientProxyPort) + + // Issues a GET echo request with 0 size body + tag := "OKGet" + if _, _, err := HTTPGet(url); err != nil { + t.Errorf("Failed in request %s: %v", tag, err) + } + s.VerifyQuota(tag, "RequestCount", 5) + + // Issues a failed POST request caused by Mixer Quota + tag = "QuotaFail" + s.mixer.quota_request = nil + s.mixer.quota.r_status = rpc.Status{ + Code: int32(rpc.RESOURCE_EXHAUSTED), + Message: mixerQuotaFailMessage, + } + code, resp_body, err := HTTPPost(url, "text/plain", "Hello World!") + // Make sure to restore r_status for next request. + s.mixer.quota.r_status = rpc.Status{} + if err != nil { + t.Errorf("Failed in request %s: %v", tag, err) + } + if code != 429 { + t.Errorf("Status code 429 is expected.") + } + if resp_body != "RESOURCE_EXHAUSTED:"+mixerQuotaFailMessage { + t.Errorf("Error response body is not expected.") + } + s.VerifyQuota(tag, "RequestCount", 5) +} diff --git a/src/envoy/mixer/integration_test/setup.go b/src/envoy/mixer/integration_test/setup.go index 7dc093d2449..52bfdd2f711 100644 --- a/src/envoy/mixer/integration_test/setup.go +++ b/src/envoy/mixer/integration_test/setup.go @@ -16,49 +16,78 @@ package test import ( "log" -) - -const ( - // These ports should match with used envoy.conf - // Default is using one in this folder. - ServerProxyPort = 29090 - ClientProxyPort = 27070 - MixerPort = 29091 - BackendPort = 28080 + "testing" ) type TestSetup struct { envoy *Envoy mixer *MixerServer backend *HttpServer + t *testing.T } -func SetUp() (ts TestSetup, err error) { - ts.envoy, err = NewEnvoy() +func SetUp(t *testing.T, conf string) (s TestSetup, err error) { + s.t = t + s.envoy, err = NewEnvoy(conf) if err != nil { log.Printf("unable to create Envoy %v", err) } else { - ts.envoy.Start() + s.envoy.Start() } - ts.mixer, err = NewMixerServer(MixerPort) + s.mixer, err = NewMixerServer(MixerPort) if err != nil { log.Printf("unable to create mixer server %v", err) } else { - ts.mixer.Start() + s.mixer.Start() } - ts.backend, err = NewHttpServer(BackendPort) + s.backend, err = NewHttpServer(BackendPort) if err != nil { log.Printf("unable to create HTTP server %v", err) } else { - ts.backend.Start() + s.backend.Start() + } + return s, err +} + +func (s *TestSetup) TearDown() { + s.envoy.Stop() + s.mixer.Stop() + s.backend.Stop() +} + +func (s *TestSetup) VerifyCheckCount(tag string, expected int) { + if s.mixer.check.count != expected { + s.t.Fatalf("%s check count doesn't match: %v\n, expected: %+v", + tag, s.mixer.check.count, expected) } - return ts, err } -func (ts *TestSetup) TearDown() { - ts.envoy.Stop() - ts.mixer.Stop() - ts.backend.Stop() +func (s *TestSetup) VerifyCheck(tag string, result string) { + _ = <-s.mixer.check.ch + if err := Verify(s.mixer.check.bag, result); err != nil { + s.t.Fatalf("Failed to verify %s check: %v\n, Attributes: %+v", + tag, err, s.mixer.check.bag) + } +} + +func (s *TestSetup) VerifyReport(tag string, result string) { + _ = <-s.mixer.report.ch + if err := Verify(s.mixer.report.bag, result); err != nil { + s.t.Fatalf("Failed to verify %s report: %v\n, Attributes: %+v", + tag, err, s.mixer.report.bag) + } +} + +func (s *TestSetup) VerifyQuota(tag string, name string, amount int64) { + _ = <-s.mixer.quota.ch + if s.mixer.quota_request.Quota != name { + s.t.Fatalf("Failed to verify %s quota name: %v, expected: %v\n", + tag, s.mixer.quota_request.Quota, name) + } + if s.mixer.quota_request.Amount != amount { + s.t.Fatalf("Failed to verify %s quota amount: %v, expected: %v\n", + tag, s.mixer.quota_request.Amount, amount) + } } diff --git a/src/envoy/mixer/integration_test/test_suite.bzl b/src/envoy/mixer/integration_test/test_suite.bzl new file mode 100644 index 00000000000..afa984f4773 --- /dev/null +++ b/src/envoy/mixer/integration_test/test_suite.bzl @@ -0,0 +1,29 @@ +# Copyright 2017 Istio Authors. All Rights Reserved. +# +# 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. +# +################################################################################ +# + +load("@io_bazel_rules_go//go:def.bzl", "go_test") + +def go_test_suite(tests, library, data, tags, size="small"): + for test in tests: + go_test( + name = test.split(".")[0], + size = size, + srcs = [test], + data = data, + library = library, + tags = tags, + )