diff --git a/.github/workflows/workflow.yaml b/.github/workflows/workflow.yaml index bf06f59b..3207bc6e 100644 --- a/.github/workflows/workflow.yaml +++ b/.github/workflows/workflow.yaml @@ -64,7 +64,7 @@ jobs: id: cache-built-examples with: path: examples - key: ${{ hashFiles('examples/**', 'proxywasm/**') }} + key: examples-${{ hashFiles('examples/**', 'proxywasm/**') }} - name: build examples if: steps.cache-built-examples.outputs.cache-hit != 'true' @@ -73,7 +73,7 @@ jobs: e2e-tests-envoy: strategy: matrix: - envoy-tag: [ 1.17.1, 1.18.2 ] + envoy-tag: [ 1.18.2 ] name: e2e tests on Envoy needs: build-examples runs-on: ubuntu-latest @@ -97,7 +97,7 @@ jobs: uses: actions/cache@v2 with: path: examples - key: ${{ hashFiles('examples/**', 'proxywasm/**') }} + key: examples-${{ hashFiles('examples/**', 'proxywasm/**') }} - name: run e2e test run: make test.e2e @@ -107,7 +107,7 @@ jobs: e2e-tests-istio: strategy: matrix: - istio-version: [ 1.8.5, 1.9.3 ] + istio-version: [ 1.9.3 ] name: e2e tests on Istio needs: build-examples runs-on: ubuntu-latest @@ -131,7 +131,7 @@ jobs: uses: actions/cache@v2 with: path: examples - key: ${{ hashFiles('examples/**', 'proxywasm/**') }} + key: examples-${{ hashFiles('examples/**', 'proxywasm/**') }} - name: run e2e test run: make test.e2e diff --git a/README.md b/README.md index 811f168c..c353b577 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,8 @@ Please follow the official instruction [here](https://tinygo.org/getting-started | proxy-wasm-go-sdk| proxy-wasm ABI version |istio/proxyv2| Envoy upstream| |:-------------:|:-------------:|:-------------:|:-------------:| -| main | 0.2.0| 1.8.x, 1.9.x | 1.17.x, 1.18.x | -| v0.1.1 | 0.2.0| 1.8.x, 1.9.x | 1.17.x | +| main | 0.2.0| 1.9.x | 1.18.x | +| v0.1.1 | 0.2.0| 1.8.x, 1.9.x | 1.17.x | ## Run examples diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index f24fe568..1e41ad57 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -109,6 +109,11 @@ func Test_E2E(t *testing.T) { staticReply: 8011, admin: 28311, }, dispatchCallOnTick)) + t.Run("http_routing", testRunnerGetter(envoyPorts{ + endpoint: 11012, + staticReply: 8012, + admin: 28312, + }, httpRouting)) } type runner = func(t *testing.T, nps envoyPorts, stdErr *bytes.Buffer) @@ -164,6 +169,34 @@ func helloworld(t *testing.T, ps envoyPorts, stdErr *bytes.Buffer) { require.Contains(t, out, "helloworld: It's") } +func httpRouting(t *testing.T, ps envoyPorts, stdErr *bytes.Buffer) { + var primary, canary bool + for i := 0; i < 25; i++ { // TODO: maybe flaky + req, err := http.NewRequest("GET", + fmt.Sprintf("http://localhost:%d", ps.endpoint), nil) + require.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + require.NoError(t, err) + raw, err := ioutil.ReadAll(r.Body) + require.NoError(t, err) + body := string(raw) + if strings.Contains(body, "canary") { + canary = true + } + if strings.Contains(body, "primary") { + primary = true + } + r.Body.Close() + fmt.Println("received body: ", body) + } + + out := stdErr.String() + fmt.Println(out) + require.True(t, primary, "must be routed to primary at least once") + require.True(t, canary, "must be routed to canary at least once") +} + func httpAuthRandom(t *testing.T, ps envoyPorts, stdErr *bytes.Buffer) { key := "this-is-key" value := "this-is-value" @@ -241,7 +274,7 @@ func network(t *testing.T, ps envoyPorts, stdErr *bytes.Buffer) { require.NoError(t, err) r.Body.Close() - time.Sleep(time.Second) + time.Sleep(time.Second * 5) out := stdErr.String() fmt.Println(out) @@ -307,7 +340,7 @@ func sharedQueue(t *testing.T, ps envoyPorts, stdErr *bytes.Buffer) { r.Body.Close() } - time.Sleep(time.Second * 2) + time.Sleep(time.Second * 5) out := stdErr.String() fmt.Println(out) @@ -352,7 +385,7 @@ func accessLogger(t *testing.T, ps envoyPorts, stdErr *bytes.Buffer) { } func dispatchCallOnTick(t *testing.T, ps envoyPorts, stdErr *bytes.Buffer) { - time.Sleep(3 * time.Second) + time.Sleep(5 * time.Second) out := stdErr.String() fmt.Println(out) for i := 1; i < 6; i++ { diff --git a/examples/http_routing/README.md b/examples/http_routing/README.md new file mode 100644 index 00000000..9e5a5a5d --- /dev/null +++ b/examples/http_routing/README.md @@ -0,0 +1,11 @@ +## http_routing + +this example proxies http requests and randomly route them to primary/canary clusters by manipulating :authorty header. + +``` +$ curl localhost:18000 +hello from primary! + +$ curl localhost:18000 +hello from canary! +``` \ No newline at end of file diff --git a/examples/http_routing/envoy.yaml b/examples/http_routing/envoy.yaml new file mode 100644 index 00000000..bc15cd4b --- /dev/null +++ b/examples/http_routing/envoy.yaml @@ -0,0 +1,144 @@ +static_resources: + listeners: + - name: main + address: + socket_address: + address: 0.0.0.0 + port_value: 18000 + filter_chains: + - filters: + - name: envoy.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + codec_type: auto + route_config: + name: local_route + virtual_hosts: + - name: canary_route + domains: + - "*-canary" + routes: + - match: + prefix: "/" + route: + cluster: canary + - name: primary_route + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: primary + http_filters: + - name: envoy.filters.http.wasm + typed_config: + "@type": type.googleapis.com/udpa.type.v1.TypedStruct + type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm + value: + config: + name: "my_plugin" + root_id: "my_root_id" + vm_config: + vm_id: "my_vm_id" + runtime: "envoy.wasm.runtime.v8" + code: + local: + filename: "./examples/http_routing/main.go.wasm" + - name: envoy.filters.http.router + typed_config: {} + + - name: staticreply + address: + socket_address: + address: 127.0.0.1 + port_value: 8099 + filter_chains: + - filters: + - name: envoy.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + codec_type: auto + route_config: + name: local_route + virtual_hosts: + - name: local_service + domains: + - "*" + routes: + - match: + prefix: "/" + direct_response: + status: 200 + body: + inline_string: "hello from primary!\n" + http_filters: + - name: envoy.filters.http.router + typed_config: {} + + - name: staticreply_canary + address: + socket_address: + address: 127.0.0.1 + port_value: 31000 + filter_chains: + - filters: + - name: envoy.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + codec_type: auto + route_config: + name: local_route + virtual_hosts: + - name: local_service + domains: + - "*" + routes: + - match: + prefix: "/" + direct_response: + status: 200 + body: + inline_string: "hello from canary!\n" + http_filters: + - name: envoy.filters.http.router + typed_config: {} + + clusters: + - name: primary + connect_timeout: 0.25s + type: STATIC + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: primary + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 8099 + + - name: canary + connect_timeout: 0.25s + type: STATIC + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: canary + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 31000 + +admin: + access_log_path: "/dev/null" + address: + socket_address: + address: 0.0.0.0 + port_value: 8001 diff --git a/examples/http_routing/main.go b/examples/http_routing/main.go new file mode 100644 index 00000000..54c1e4a5 --- /dev/null +++ b/examples/http_routing/main.go @@ -0,0 +1,74 @@ +// Copyright 2020-2021 Tetrate +// +// 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 main + +import ( + "math/rand" + "time" + + "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm" + "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types" +) + +func main() { + proxywasm.SetNewRootContext(newRootContext) +} + +type rootContext struct { + // You'd better embed the default root context + // so that you don't need to reimplement all the methods by yourself. + proxywasm.DefaultRootContext +} + +func newRootContext(uint32) proxywasm.RootContext { return &rootContext{} } + +// Override DefaultRootContext. +func (*rootContext) NewHttpContext(contextID uint32) proxywasm.HttpContext { + return &httpRouting{} +} + +type httpRouting struct { + // You'd better embed the default root context + // so that you don't need to reimplement all the methods by yourself. + proxywasm.DefaultHttpContext +} + +// Unittest purpose. +var now = func() int { + rand.Seed(time.Now().UnixNano()) + return rand.Int() +} + +// Override DefaultHttpContext. +func (ctx *httpRouting) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action { + // Randomly routing to the canary cluster. + dice := now() + proxywasm.LogInfof("dice: %d\n", dice) + if dice%2 == 0 { + const authorityKey = ":authority" + value, err := proxywasm.GetHttpRequestHeader(authorityKey) + if err != nil { + proxywasm.LogCritical("failed to get request header: ':authority'") + return types.ActionPause + } + // Append "-canary" suffix to route this request to the canary cluster. + value += "-canary" + if err := proxywasm.SetHttpRequestHeader(":authority", value); err != nil { + proxywasm.LogCritical("failed to set request header: test") + return types.ActionPause + } + } + return types.ActionContinue +} diff --git a/examples/http_routing/main_test.go b/examples/http_routing/main_test.go new file mode 100644 index 00000000..c1bac315 --- /dev/null +++ b/examples/http_routing/main_test.go @@ -0,0 +1,48 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/tetratelabs/proxy-wasm-go-sdk/proxytest" + "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types" +) + +func TestHttpRouting_OnHttpRequestHeaders(t *testing.T) { + opt := proxytest.NewEmulatorOption(). + WithNewRootContext(newRootContext) + host := proxytest.NewHostEmulator(opt) + // Release the host emulation lock so that other test cases can insert their own host emulation. + defer host.Done() + + t.Run("canary", func(t *testing.T) { + now = func() int { return 0 } + // Initialize http context. + id := host.InitializeHttpContext() + hs := types.Headers{{":authority", "my-host.com"}} + // Call OnHttpResponseHeaders. + action := host.CallOnRequestHeaders(id, + hs, false) + require.Equal(t, types.ActionContinue, action) + resultHeaders := host.GetCurrentRequestHeaders(id) + require.Len(t, resultHeaders, 1) + require.Equal(t, ":authority", resultHeaders[0][0]) + require.Equal(t, "my-host.com-canary", resultHeaders[0][1]) + }) + + t.Run("non-canary", func(t *testing.T) { + now = func() int { return 1 } + // Initialize http context. + id := host.InitializeHttpContext() + hs := types.Headers{{":authority", "my-host.com"}} + // Call OnHttpResponseHeaders. + action := host.CallOnRequestHeaders(id, + hs, false) + require.Equal(t, types.ActionContinue, action) + resultHeaders := host.GetCurrentRequestHeaders(id) + require.Len(t, resultHeaders, 1) + require.Equal(t, ":authority", resultHeaders[0][0]) + require.Equal(t, "my-host.com", resultHeaders[0][1]) + }) +}