diff --git a/BUILD b/BUILD index ac06d245289..510620e8246 100644 --- a/BUILD +++ b/BUILD @@ -24,3 +24,7 @@ config_setting( }, visibility = ["//visibility:public"], ) + +load("@io_bazel_rules_go//go:def.bzl", "go_prefix") + +go_prefix("istio.io/proxy") diff --git a/WORKSPACE b/WORKSPACE index ddfa05d578b..4bc7137304c 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -96,3 +96,19 @@ http_file( url = "https://storage.googleapis.com/istio-build/manager/ubuntu_xenial_debug-" + DEBUG_BASE_IMAGE_SHA + ".tar.gz", sha256 = DEBUG_BASE_IMAGE_SHA, ) + +# Following go repositories are for building go integration test for mixer filter. +git_repository( + name = "io_bazel_rules_go", + commit = "9496d79880a7d55b8e4a96f04688d70a374eaaf4", # Mar 3, 2017 (v0.4.1) + remote = "https://github.com/bazelbuild/rules_go.git", +) + +git_repository( + name = "org_pubref_rules_protobuf", + commit = "d42e895387c658eda90276aea018056fcdcb30e4", # Mar 07 2017 (gogo* support) + remote = "https://github.com/pubref/rules_protobuf", +) + +load("//src/envoy/mixer/integration_test:repositories.bzl", "go_mixer_repositories") +go_mixer_repositories() diff --git a/src/envoy/mixer/BUILD b/src/envoy/mixer/BUILD index 6bf9ea64d72..4d57f801626 100644 --- a/src/envoy/mixer/BUILD +++ b/src/envoy/mixer/BUILD @@ -15,7 +15,6 @@ ################################################################################ # - load("@bazel_tools//tools/build_defs/pkg:pkg.bzl", "pkg_tar") load("//src/envoy/mixer:proxy_docker.bzl", "proxy_docker_build") load("@protobuf_git//:protobuf.bzl", "cc_proto_library") @@ -48,6 +47,7 @@ cc_library( cc_binary( name = "envoy", linkstatic = 1, + visibility = [":__subpackages__"], deps = [ ":filter_lib", "@envoy_git//:envoy-main", @@ -80,10 +80,6 @@ pkg_tar( ) proxy_docker_build( - images = [ - {"name": "proxy", "base": "@docker_ubuntu//:xenial"}, - {"name": "proxy_debug", "base": "@ubuntu_xenial_debug//file"}, - ], entrypoint = [ "/usr/local/bin/start_envoy", "-e", @@ -93,6 +89,16 @@ proxy_docker_build( "-t", "/etc/opt/proxy/envoy.conf.template", ], + images = [ + { + "name": "proxy", + "base": "@docker_ubuntu//:xenial", + }, + { + "name": "proxy_debug", + "base": "@ubuntu_xenial_debug//file", + }, + ], ports = ["9090"], repository = "istio", tags = ["manual"], diff --git a/src/envoy/mixer/integration_test/BUILD b/src/envoy/mixer/integration_test/BUILD new file mode 100644 index 00000000000..c45865457ce --- /dev/null +++ b/src/envoy/mixer/integration_test/BUILD @@ -0,0 +1,58 @@ +# Copyright 2016 Google Inc. 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_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "attributes.go", + "envoy.go", + "http_client.go", + "http_server.go", + "mixer_server.go", + "setup.go", + ], + deps = [ + "@com_github_gogo_protobuf//types:go_default_library", + "@com_github_golang_glog//:go_default_library", + "@com_github_golang_protobuf//proto:go_default_library", + "@com_github_googleapis_googleapis//:google/rpc", + "@com_github_istio_api//:mixer/v1", + "@com_github_istio_mixer//pkg/api:go_default_library", + "@com_github_istio_mixer//pkg/attribute:go_default_library", + "@com_github_istio_mixer//pkg/pool:go_default_library", + "@com_github_istio_mixer//pkg/tracing:go_default_library", + "@org_golang_google_grpc//:go_default_library", + ], +) + +go_test( + name = "mixer_test", + size = "small", + srcs = [ + "mixer_test.go", + ], + 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, +) diff --git a/src/envoy/mixer/integration_test/attributes.go b/src/envoy/mixer/integration_test/attributes.go new file mode 100644 index 00000000000..4c97208fd4b --- /dev/null +++ b/src/envoy/mixer/integration_test/attributes.go @@ -0,0 +1,114 @@ +// 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 ( + "encoding/json" + "fmt" + "reflect" + + "istio.io/mixer/pkg/attribute" +) + +func verifyStringMap(actual map[string]string, expected map[string]interface{}) error { + for k, v := range expected { + vstring := v.(string) + // "-" make sure the key does not exist. + if vstring == "-" { + if _, ok := actual[k]; ok { + return fmt.Errorf("key %+v is NOT expected", k) + } + } else { + if val, ok := actual[k]; ok { + // "*" only check key exist + if val != vstring && vstring != "*" { + return fmt.Errorf("key %+v value doesn't match. Actual %+v, expected %+v", + k, val, vstring) + } + } else { + return fmt.Errorf("key %+v is expected", k) + } + } + } + return nil +} + +// Please see the comment at top of mixer_test.go for verification rules +func Verify(b *attribute.MutableBag, json_results string) error { + var r map[string]interface{} + if err := json.Unmarshal([]byte(json_results), &r); err != nil { + return fmt.Errorf("unable to decode json %v", err) + } + + all_keys := make(map[string]bool) + for _, k := range b.Names() { + all_keys[k] = true + } + + for k, v := range r { + switch vv := v.(type) { + case string: + // "*" means only checking key. + if vv == "*" { + if _, ok := b.Get(k); !ok { + return fmt.Errorf("attribute %+v is expected", k) + } + } else { + if val, ok := b.Get(k); ok { + if val.(string) != v.(string) { + return fmt.Errorf("attribute %+v value doesn't match. Actual %+v, expected %+v", + k, val.(string), v.(string)) + } + } else { + return fmt.Errorf("attribute %+v is expected", k) + } + } + case float64: + // Json converts all integers to float64, + // Our tests only verify size related attributes which are int64 type + if val, ok := b.Get(k); ok { + vint64 := int64(vv) + if val.(int64) != vint64 { + return fmt.Errorf("attribute %+v value doesn't match. Actual %+v, expected %+v", + k, val.(int64), vint64) + } + } else { + return fmt.Errorf("attribute %+v is expected", k) + } + case map[string]interface{}: + if val, ok := b.Get(k); ok { + if err := verifyStringMap(val.(map[string]string), v.(map[string]interface{})); err != nil { + return fmt.Errorf("attribute %+v StringMap doesn't match: %+v", k, err) + } + } else { + return fmt.Errorf("attribute %+v is expected", k) + } + default: + return fmt.Errorf("attribute %+v is of a type %+v that I don't know how to handle ", + k, reflect.TypeOf(v)) + } + delete(all_keys, k) + + } + + if len(all_keys) > 0 { + var s string + for k, _ := range all_keys { + s += k + ", " + } + return fmt.Errorf("Following attributes are not expected: %s", s) + } + return nil +} diff --git a/src/envoy/mixer/integration_test/envoy.conf b/src/envoy/mixer/integration_test/envoy.conf new file mode 100644 index 00000000000..2083d7f4e5a --- /dev/null +++ b/src/envoy/mixer/integration_test/envoy.conf @@ -0,0 +1,142 @@ +{ + "listeners": [ + { + "port": 29090, + "bind_to_port": true, + "filters": [ + { + "type": "read", + "name": "http_connection_manager", + "config": { + "codec_type": "auto", + "stat_prefix": "ingress_http", + "route_config": { + "virtual_hosts": [ + { + "name": "backend", + "domains": ["*"], + "routes": [ + { + "timeout_ms": 0, + "prefix": "/", + "cluster": "service1", + "opaque_config": { + "mixer_control": "on", + "mixer_forward": "off" + } + } + ] + } + ] + }, + "access_log": [ + { + "path": "/dev/stdout" + } + ], + "filters": [ + { + "type": "decoder", + "name": "mixer", + "config": { + "mixer_server": "localhost:29091", + "mixer_attributes": { + "target.uid": "POD222", + "target.namespace": "XYZ222" + } + } + }, + { + "type": "decoder", + "name": "router", + "config": {} + } + ] + } + } + ] + }, + { + "port": 27070, + "bind_to_port": true, + "filters": [ + { + "type": "read", + "name": "http_connection_manager", + "config": { + "codec_type": "auto", + "stat_prefix": "ingress_http", + "route_config": { + "virtual_hosts": [ + { + "name": "backend", + "domains": ["*"], + "routes": [ + { + "timeout_ms": 0, + "prefix": "/", + "cluster": "service2" + } + ] + } + ] + }, + "access_log": [ + { + "path": "/dev/stdout" + } + ], + "filters": [ + { + "type": "decoder", + "name": "mixer", + "config": { + "mixer_server": "localhost:29091", + "forward_attributes": { + "source.uid": "POD11", + "source.namespace": "XYZ11" + } + } + }, + { + "type": "decoder", + "name": "router", + "config": {} + } + ] + } + } + ] + } + ], + "admin": { + "access_log_path": "/dev/stdout", + "port": 29001 + }, + "cluster_manager": { + "clusters": [ + { + "name": "service1", + "connect_timeout_ms": 5000, + "type": "strict_dns", + "lb_type": "round_robin", + "hosts": [ + { + "url": "tcp://localhost:28080" + } + ] + }, + { + "name": "service2", + "connect_timeout_ms": 5000, + "type": "strict_dns", + "lb_type": "round_robin", + "hosts": [ + { + "url": "tcp://localhost:29090" + } + ] + } + ] + } +} diff --git a/src/envoy/mixer/integration_test/envoy.go b/src/envoy/mixer/integration_test/envoy.go new file mode 100644 index 00000000000..0f9a1fc01c7 --- /dev/null +++ b/src/envoy/mixer/integration_test/envoy.go @@ -0,0 +1,103 @@ +// 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 ( + "log" + "os" + "os/exec" + "strings" +) + +func getTestBinRootPath() string { + switch { + // custom path + case os.Getenv("TEST_BIN_ROOT") != "": + return os.Getenv("TEST_BIN_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] + "/bazel-bin" + default: + return "bazel-bin" + } +} + +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 +} + +// Run command and return the merged output from stderr and stdout, error code +func Run(name string, args ...string) (s string, err error) { + log.Println(">", name, strings.Join(args, " ")) + c := exec.Command(name, args...) + bytes, err := c.CombinedOutput() + s = string(bytes) + for _, line := range strings.Split(s, "\n") { + log.Println(line) + } + if err != nil { + log.Println(err) + } + 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) + + cmd := exec.Command(path, "-c", conf, "-l", "debug") + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + return &Envoy{ + cmd: cmd, + }, nil +} + +func (s *Envoy) Start() error { + return s.cmd.Start() +} + +func (s *Envoy) Stop() error { + log.Printf("Kill Envoy ...\n") + err := s.cmd.Process.Kill() + log.Printf("Kill Envoy ... Done\n") + return err +} diff --git a/src/envoy/mixer/integration_test/http_client.go b/src/envoy/mixer/integration_test/http_client.go new file mode 100644 index 00000000000..0212ddf775e --- /dev/null +++ b/src/envoy/mixer/integration_test/http_client.go @@ -0,0 +1,80 @@ +// 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 ( + "io/ioutil" + "log" + "net/http" + "net/url" + "strings" +) + +func HTTPGet(url string) (code int, resp_body string, err error) { + log.Println("HTTP GET", url) + client := &http.Client{} + resp, err := client.Get(url) + if err != nil { + log.Println(err) + return 0, "", err + } + defer resp.Body.Close() + body, _ := ioutil.ReadAll(resp.Body) + resp_body = string(body) + code = resp.StatusCode + log.Println(resp_body) + return code, resp_body, nil +} + +func HTTPPost(url string, content_type string, req_body string) (code int, resp_body string, err error) { + log.Println("HTTP POST", url) + client := &http.Client{} + resp, err := client.Post(url, content_type, strings.NewReader(req_body)) + if err != nil { + log.Println(err) + return 0, "", err + } + defer resp.Body.Close() + body, _ := ioutil.ReadAll(resp.Body) + resp_body = string(body) + code = resp.StatusCode + log.Println(resp_body) + return code, resp_body, nil +} + +func HTTPGetWithHeaders(l string, headers map[string]string) (code int, resp_body string, err error) { + log.Println("HTTP GET with headers: ", l) + client := &http.Client{} + req := http.Request{} + + req.Header = map[string][]string{} + for k, v := range headers { + req.Header[k] = []string{v} + } + req.Method = http.MethodGet + req.URL, _ = url.Parse(l) + + resp, err := client.Do(&req) + if err != nil { + log.Println(err) + return 0, "", err + } + defer resp.Body.Close() + body, _ := ioutil.ReadAll(resp.Body) + resp_body = string(body) + code = resp.StatusCode + log.Println(resp_body) + return code, resp_body, nil +} diff --git a/src/envoy/mixer/integration_test/http_server.go b/src/envoy/mixer/integration_test/http_server.go new file mode 100644 index 00000000000..77677286b58 --- /dev/null +++ b/src/envoy/mixer/integration_test/http_server.go @@ -0,0 +1,111 @@ +// 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" + "io/ioutil" + "log" + "net" + "net/http" + "strings" + "time" +) + +const ( + FailHeader = "x-istio-backend-fail" + FailBody = "Bad request from backend." +) + +type HttpServer struct { + port uint16 + lis net.Listener +} + +func handler(w http.ResponseWriter, r *http.Request) { + log.Printf("%v %v %v %v\n", r.Method, r.URL, r.Proto, r.RemoteAddr) + for name, headers := range r.Header { + for _, h := range headers { + log.Printf("%v: %v\n", name, h) + } + } + body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Fail if there is such header. + if r.Header.Get(FailHeader) != "" { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(FailBody)) + return + } + + // echo back the Content-Type and Content-Length in the response + for _, k := range []string{"Content-Type", "Content-Length"} { + if v := r.Header.Get(k); v != "" { + w.Header().Set(k, v) + } + } + w.WriteHeader(http.StatusOK) + w.Write(body) +} + +func NewHttpServer(port uint16) (*HttpServer, error) { + log.Printf("Http server listening on port %v\n", port) + lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) + if err != nil { + log.Fatal(err) + return nil, err + } + return &HttpServer{ + port: port, + lis: lis, + }, nil +} + +func (s *HttpServer) Start() { + go func() { + http.HandleFunc("/", handler) + http.Serve(s.lis, nil) + }() + + addr := fmt.Sprintf("http://localhost:%s", s.port) + + const maxAttempts = 10 + for i := 0; i < maxAttempts; i++ { + time.Sleep(time.Second) + client := http.Client{} + log.Println("Pinging the server...") + rsp, err := client.Post( + addr+"/echo", "text/plain", strings.NewReader("PING")) + if err == nil && rsp.StatusCode == http.StatusOK { + log.Println("Got a response...") + png, err := ioutil.ReadAll(rsp.Body) + if err == nil && string(png) == "PING" { + log.Println("Server is up and running...") + return + } + } + log.Println("Will wait a second and try again.") + } +} + +func (s *HttpServer) Stop() { + log.Printf("Close HTTP server\n") + s.lis.Close() + log.Printf("Close HTTP server -- Done\n") +} diff --git a/src/envoy/mixer/integration_test/mixer_server.go b/src/envoy/mixer/integration_test/mixer_server.go new file mode 100644 index 00000000000..e36f04b484a --- /dev/null +++ b/src/envoy/mixer/integration_test/mixer_server.go @@ -0,0 +1,122 @@ +// 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 ( + "context" + "fmt" + "log" + "net" + + rpc "github.com/googleapis/googleapis/google/rpc" + "google.golang.org/grpc" + + mixerpb "istio.io/api/mixer/v1" + "istio.io/mixer/pkg/api" + "istio.io/mixer/pkg/attribute" + "istio.io/mixer/pkg/pool" + "istio.io/mixer/pkg/tracing" +) + +type Handler struct { + bag *attribute.MutableBag + ch chan int + r_status rpc.Status +} + +func newHandler() *Handler { + return &Handler{ + bag: nil, + ch: make(chan int, 1), + r_status: rpc.Status{}, + } +} + +func (h *Handler) run(bag *attribute.MutableBag) rpc.Status { + h.bag = attribute.CopyBag(bag) + h.ch <- 1 + return h.r_status +} + +type MixerServer struct { + lis net.Listener + gs *grpc.Server + gp *pool.GoroutinePool + s mixerpb.MixerServer + + check *Handler + report *Handler + quota *Handler +} + +func (ts *MixerServer) Check(ctx context.Context, bag *attribute.MutableBag, + request *mixerpb.CheckRequest, response *mixerpb.CheckResponse) { + response.RequestIndex = request.RequestIndex + response.Result = ts.check.run(bag) +} + +func (ts *MixerServer) Report(ctx context.Context, bag *attribute.MutableBag, + request *mixerpb.ReportRequest, response *mixerpb.ReportResponse) { + response.RequestIndex = request.RequestIndex + response.Result = ts.report.run(bag) +} + +func (ts *MixerServer) Quota(ctx context.Context, bag *attribute.MutableBag, + request *mixerpb.QuotaRequest, response *mixerpb.QuotaResponse) { + response.RequestIndex = request.RequestIndex + response.Result = ts.quota.run(bag) + response.Amount = 0 +} + +func NewMixerServer(port uint16) (*MixerServer, error) { + log.Printf("Mixer server listening on port %v\n", port) + s := &MixerServer{ + check: newHandler(), + report: newHandler(), + quota: newHandler(), + } + + var err error + s.lis, err = net.Listen("tcp", fmt.Sprintf(":%d", port)) + if err != nil { + log.Fatalf("failed to listen: %v", err) + return nil, err + } + + var opts []grpc.ServerOption + opts = append(opts, grpc.MaxConcurrentStreams(32)) + opts = append(opts, grpc.MaxMsgSize(1024*1024)) + s.gs = grpc.NewServer(opts...) + + s.gp = pool.NewGoroutinePool(128, false) + s.gp.AddWorkers(32) + + s.s = api.NewGRPCServer(s, tracing.DisabledTracer(), s.gp) + mixerpb.RegisterMixerServer(s.gs, s.s) + return s, nil +} + +func (s *MixerServer) Start() { + go func() { + _ = s.gs.Serve(s.lis) + log.Printf("Mixer server exited\n") + }() +} + +func (s *MixerServer) Stop() { + log.Printf("Stop Mixer server\n") + s.gs.Stop() + log.Printf("Stop Mixer server -- Done\n") +} diff --git a/src/envoy/mixer/integration_test/mixer_test.go b/src/envoy/mixer/integration_test/mixer_test.go new file mode 100644 index 00000000000..b8f382bd58c --- /dev/null +++ b/src/envoy/mixer/integration_test/mixer_test.go @@ -0,0 +1,333 @@ +// 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 ( + mixerFailMessage = "Unauthenticated 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" + } +} +` + +// 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" + } +} +` + +// 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" + } +} +` + +// Check attributes from a fail GET request from backend +const checkAttributesBackendFail = ` +{ + "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 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 verifyAttributes( + s *TestSetup, tag string, check string, report 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) + } + _ = <-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 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 + if _, _, err := HTTPGet(url); err != nil { + t.Errorf("Failed in GET request: %v", err) + } + verifyAttributes(&s, "OkGet", + checkAttributesOkGet, reportAttributesOkGet, t) + + // Issues a POST echo request with + if _, _, err := HTTPPost(url, "text/plain", "Hello World!"); err != nil { + t.Errorf("Failed in POST request: %v", err) + } + verifyAttributes(&s, "OkPost", + checkAttributesOkPost, reportAttributesOkPost, t) + + // Issues a failed request caused by mixer + s.mixer.check.r_status = rpc.Status{ + Code: int32(rpc.UNAUTHENTICATED), + Message: mixerFailMessage, + } + code, resp_body, err := HTTPGet(url) + s.mixer.check.r_status = rpc.Status{} + if err != nil { + t.Errorf("Failed in GET request: error: %v", err) + } + if code != 401 { + t.Errorf("Status code 401 is expected.") + } + if resp_body != "UNAUTHENTICATED:"+mixerFailMessage { + t.Errorf("Error response body is not expected.") + } + verifyAttributes(&s, "MixerFail", + checkAttributesMixerFail, reportAttributesMixerFail, t) + + // Issues a failed request caused by backend + headers := map[string]string{} + headers[FailHeader] = "Yes" + code, resp_body, err = HTTPGetWithHeaders(url, headers) + if err != nil { + t.Errorf("Failed in GET request: error: %v", err) + } + if code != 400 { + t.Errorf("Status code 400 is expected.") + } + if resp_body != FailBody { + t.Errorf("Error response body is not expected.") + } + verifyAttributes(&s, "BackendFail", + checkAttributesBackendFail, reportAttributesBackendFail, t) +} diff --git a/src/envoy/mixer/integration_test/repositories.bzl b/src/envoy/mixer/integration_test/repositories.bzl new file mode 100644 index 00000000000..7eec527fa39 --- /dev/null +++ b/src/envoy/mixer/integration_test/repositories.bzl @@ -0,0 +1,272 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_repositories", "new_go_repository", "go_repository") +load("@org_pubref_rules_protobuf//protobuf:rules.bzl", "proto_repositories") + +load("@org_pubref_rules_protobuf//gogo:rules.bzl", "gogo_proto_repositories") +load("@org_pubref_rules_protobuf//cpp:rules.bzl", "cpp_proto_repositories") + +def go_istio_api_repositories(use_local=False): + ISTIO_API_BUILD_FILE = """ +# build protos from istio.io/api repo + +package(default_visibility = ["//visibility:public"]) + +load("@io_bazel_rules_go//go:def.bzl", "go_prefix") + +go_prefix("istio.io/api") + +load("@org_pubref_rules_protobuf//gogo:rules.bzl", "gogoslick_proto_library") + +gogoslick_proto_library( + name = "mixer/v1", + importmap = { + "gogoproto/gogo.proto": "github.com/gogo/protobuf/gogoproto", + "google/rpc/status.proto": "github.com/googleapis/googleapis/google/rpc", + "google/protobuf/timestamp.proto": "github.com/gogo/protobuf/types", + "google/protobuf/duration.proto": "github.com/gogo/protobuf/types", + }, + imports = [ + "../../external/com_github_gogo_protobuf", + "../../external/com_github_google_protobuf/src", + "../../external/com_github_googleapis_googleapis", + ], + inputs = [ + "@com_github_google_protobuf//:well_known_protos", + "@com_github_googleapis_googleapis//:status_proto", + "@com_github_gogo_protobuf//gogoproto:go_default_library_protos", + ], + protos = [ + "mixer/v1/attributes.proto", + "mixer/v1/check.proto", + "mixer/v1/quota.proto", + "mixer/v1/report.proto", + "mixer/v1/service.proto", + ], + verbose = 0, + visibility = ["//visibility:public"], + with_grpc = True, + deps = [ + "@com_github_gogo_protobuf//gogoproto:go_default_library", + "@com_github_gogo_protobuf//sortkeys:go_default_library", + "@com_github_gogo_protobuf//types:go_default_library", + "@com_github_googleapis_googleapis//:google/rpc", + ], +) + +DESCRIPTOR_FILE_GROUP = [ + "mixer/v1/config/descriptor/attribute_descriptor.proto", + "mixer/v1/config/descriptor/label_descriptor.proto", + "mixer/v1/config/descriptor/log_entry_descriptor.proto", + "mixer/v1/config/descriptor/metric_descriptor.proto", + "mixer/v1/config/descriptor/monitored_resource_descriptor.proto", + "mixer/v1/config/descriptor/principal_descriptor.proto", + "mixer/v1/config/descriptor/quota_descriptor.proto", + "mixer/v1/config/descriptor/value_type.proto", +] + +gogoslick_proto_library( + name = "mixer/v1/config", + importmap = { + "google/protobuf/struct.proto": "github.com/gogo/protobuf/types", + "mixer/v1/config/descriptor/log_entry_descriptor.proto": "istio.io/api/mixer/v1/config/descriptor", + "mixer/v1/config/descriptor/metric_descriptor.proto": "istio.io/api/mixer/v1/config/descriptor", + "mixer/v1/config/descriptor/monitored_resource_descriptor.proto": "istio.io/api/mixer/v1/config/descriptor", + "mixer/v1/config/descriptor/principal_descriptor.proto": "istio.io/api/mixer/v1/config/descriptor", + "mixer/v1/config/descriptor/quota_descriptor.proto": "istio.io/api/mixer/v1/config/descriptor", + }, + imports = [ + "../../external/com_github_google_protobuf/src", + ], + inputs = DESCRIPTOR_FILE_GROUP + [ + "@com_github_google_protobuf//:well_known_protos", + ], + protos = [ + "mixer/v1/config/cfg.proto", + ], + verbose = 0, + visibility = ["//visibility:public"], + with_grpc = False, + deps = [ + ":mixer/v1/config/descriptor", + "@com_github_gogo_protobuf//sortkeys:go_default_library", + "@com_github_gogo_protobuf//types:go_default_library", + "@com_github_googleapis_googleapis//:google/rpc", + ], +) + +gogoslick_proto_library( + name = "mixer/v1/config/descriptor", + importmap = { + "google/protobuf/duration.proto": "github.com/gogo/protobuf/types", + }, + imports = [ + "../../external/com_github_google_protobuf/src", + ], + inputs = [ + "@com_github_google_protobuf//:well_known_protos", + ], + protos = DESCRIPTOR_FILE_GROUP, + verbose = 0, + visibility = ["//visibility:public"], + with_grpc = False, + deps = [ + "@com_github_gogo_protobuf//types:go_default_library", + ], +) +""" + if use_local: + native.new_local_repository( + name = "com_github_istio_api", + build_file_content = ISTIO_API_BUILD_FILE, + path = "../api", + ) + else: + native.new_git_repository( + name = "com_github_istio_api", + build_file_content = ISTIO_API_BUILD_FILE, + commit = "2cb09827d7f09a6e88eac2c2249dcb45c5419f09", # Mar. 14, 2017 (no releases) + remote = "https://github.com/istio/api.git", + ) + +def go_googleapis_repositories(): + GOOGLEAPIS_BUILD_FILE = """ +package(default_visibility = ["//visibility:public"]) + +load("@io_bazel_rules_go//go:def.bzl", "go_prefix") +go_prefix("github.com/googleapis/googleapis") + +load("@org_pubref_rules_protobuf//gogo:rules.bzl", "gogoslick_proto_library") + +gogoslick_proto_library( + name = "google/rpc", + protos = [ + "google/rpc/code.proto", + "google/rpc/error_details.proto", + "google/rpc/status.proto", + ], + importmap = { + "google/protobuf/any.proto": "github.com/gogo/protobuf/types", + "google/protobuf/duration.proto": "github.com/gogo/protobuf/types", + }, + imports = [ + "../../external/com_github_google_protobuf/src", + ], + inputs = [ + "@com_github_google_protobuf//:well_known_protos", + ], + deps = [ + "@com_github_gogo_protobuf//types:go_default_library", + ], + verbose = 0, +) + +load("@org_pubref_rules_protobuf//cpp:rules.bzl", "cc_proto_library") + +cc_proto_library( + name = "cc_status_proto", + protos = [ + "google/rpc/status.proto", + ], + imports = [ + "../../external/com_github_google_protobuf/src", + ], + verbose = 0, +) + +filegroup( + name = "status_proto", + srcs = [ "google/rpc/status.proto" ], +) + +filegroup( + name = "code_proto", + srcs = [ "google/rpc/code.proto" ], +) +""" + native.new_git_repository( + name = "com_github_googleapis_googleapis", + build_file_content = GOOGLEAPIS_BUILD_FILE, + commit = "13ac2436c5e3d568bd0e938f6ed58b77a48aba15", # Oct 21, 2016 (only release pre-dates sha) + remote = "https://github.com/googleapis/googleapis.git", + ) + +def go_mixer_repositories(use_local_api=False): + go_istio_api_repositories(use_local_api) + go_googleapis_repositories() + + go_repositories() + proto_repositories() + + gogo_proto_repositories() + + new_go_repository( + name = "com_github_golang_glog", + commit = "23def4e6c14b4da8ac2ed8007337bc5eb5007998", # Jan 26, 2016 (no releases) + importpath = "github.com/golang/glog", + ) + + new_go_repository( + name = "com_github_ghodss_yaml", + commit = "04f313413ffd65ce25f2541bfd2b2ceec5c0908c", # Dec 6, 2016 (no releases) + importpath = "github.com/ghodss/yaml", + ) + + new_go_repository( + name = "in_gopkg_yaml_v2", + commit = "14227de293ca979cf205cd88769fe71ed96a97e2", # Jan 24, 2017 (no releases) + importpath = "gopkg.in/yaml.v2", + ) + + new_go_repository( + name = "com_github_golang_protobuf", + commit = "8ee79997227bf9b34611aee7946ae64735e6fd93", # Nov 16, 2016 (no releases) + importpath = "github.com/golang/protobuf", + ) + + new_go_repository( + name = "org_golang_google_grpc", + commit = "708a7f9f3283aa2d4f6132d287d78683babe55c8", # Dec 5, 2016 (v1.0.5) + importpath = "google.golang.org/grpc", + ) + + new_go_repository( + name = "com_github_spf13_cobra", + commit = "35136c09d8da66b901337c6e86fd8e88a1a255bd", # Jan 30, 2017 (no releases) + importpath = "github.com/spf13/cobra", + ) + + new_go_repository( + name = "com_github_spf13_pflag", + commit = "9ff6c6923cfffbcd502984b8e0c80539a94968b7", # Jan 30, 2017 (no releases) + importpath = "github.com/spf13/pflag", + ) + + new_go_repository( + name = "com_github_hashicorp_go_multierror", + commit = "ed905158d87462226a13fe39ddf685ea65f1c11f", # Dec 16, 2016 (no releases) + importpath = "github.com/hashicorp/go-multierror", + ) + + new_go_repository( + name = "com_github_hashicorp_errwrap", + commit = "7554cd9344cec97297fa6649b055a8c98c2a1e55", # Oct 27, 2014 (no releases) + importpath = "github.com/hashicorp/errwrap", + ) + + new_go_repository( + name = "com_github_opentracing_opentracing_go", + commit = "0c3154a3c2ce79d3271985848659870599dfb77c", # Sep 26, 2016 (v1.0.0) + importpath = "github.com/opentracing/opentracing-go", + ) + + new_go_repository( + name = "com_github_opentracing_basictracer", + commit = "1b32af207119a14b1b231d451df3ed04a72efebf", # Sep 29, 2016 (no releases) + importpath = "github.com/opentracing/basictracer-go", + ) + + go_repository( + name = "com_github_istio_mixer", + commit = "064001053b51f73adc3a80ff87ef41a15316c300", + importpath = "github.com/istio/mixer", + ) + diff --git a/src/envoy/mixer/integration_test/setup.go b/src/envoy/mixer/integration_test/setup.go new file mode 100644 index 00000000000..7dc093d2449 --- /dev/null +++ b/src/envoy/mixer/integration_test/setup.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 ( + "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 +) + +type TestSetup struct { + envoy *Envoy + mixer *MixerServer + backend *HttpServer +} + +func SetUp() (ts TestSetup, err error) { + ts.envoy, err = NewEnvoy() + if err != nil { + log.Printf("unable to create Envoy %v", err) + } else { + ts.envoy.Start() + } + + ts.mixer, err = NewMixerServer(MixerPort) + if err != nil { + log.Printf("unable to create mixer server %v", err) + } else { + ts.mixer.Start() + } + + ts.backend, err = NewHttpServer(BackendPort) + if err != nil { + log.Printf("unable to create HTTP server %v", err) + } else { + ts.backend.Start() + } + return ts, err +} + +func (ts *TestSetup) TearDown() { + ts.envoy.Stop() + ts.mixer.Stop() + ts.backend.Stop() +}