Skip to content

Sharing data

This lab will add code to the existing Wasm extension that initializes the shared value and then increments the shared value in the OnHttpRequestHeaders function. We'll also use a second (pre-built) extension that will only read the value - that's how we'll demonstrate the data is being shared between the extensions using the same VM ID.

Defining the shared data key

Sharing and retrieving data between different plugins is done through the SetSharedData(string, []byte, uint32) and GetSharedData(string) : ([]byte, unint32, error) API.

We'll start by defining the shared key name and the initial value at the top of the main.go file:

const (
  sharedDataKey                 = "my_key"
  sharedDataInitialValue uint64 = 1
)

We'll create the OnVMStart function where we'll set the initial shared value:

// Override types.VMContext.
func (*vmContext) OnVMStart(vmConfigurationSize int) types.OnVMStartStatus {
  initialValueBuf := make([]byte, 8)
  binary.LittleEndian.PutUint64(initialValueBuf, sharedDataInitialValue) // (1)
  if err := proxywasm.SetSharedData(sharedDataKey, initialValueBuf, 0); err != nil {
    proxywasm.LogWarnf("error setting shared data on OnVMStart: %v", err)
  }
  proxywasm.LogInfof("[wasm-extension]: Setting initial shared value %v", sharedDataInitialValue)
  return types.OnVMStartStatusOK
}
  1. We're converting the sharedDataInitialValue to a byte array using LittleEndian byte order.

Note

Make sure to add the encoding/binary to the import statements at the top of the file.

Incrementing the value in shared data

Finally, we need the function that increments the shared value by 1 and the OnHttpRequestHeaders function where we increment and output the shared value:

// Override types.DefaultHttpContext.
func (ctx *httpContext) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {
  for {
    value, err := ctx.incrementData()
    if err == nil {
      proxywasm.LogInfof("incremented shared value: %d", value)
    } else if errors.Is(err, types.ErrorStatusCasMismatch) {
      continue
    }
    break
  }
  return types.ActionContinue
}

func (ctx *httpContext) incrementData() (uint64, error) {
  value, cas, err := proxywasm.GetSharedData(sharedDataKey) // (1)
  if err != nil {
    proxywasm.LogWarnf("error getting shared data: %v", err)
    return 0, err
  }

  buf := make([]byte, 8)
  ret := binary.LittleEndian.Uint64(value) + 1 // (2)
  binary.LittleEndian.PutUint64(buf, ret)
  if err := proxywasm.SetSharedData(sharedDataKey, buf, cas); err != nil { // (3)
    proxywasm.LogWarnf("error setting shared data: %v", err)
    return 0, err
  }
  return ret, err
}
  1. We use GetSharedData and the key to read the value
  2. We increment value from the shared data
  3. We write the incremented value back to the shared data using SetSharedData

Note

We'll also have to add errors to the import statements at the top of the file.

Here's the complete main.go file with these changes included.

Complete main.go file
main.go
package main

import (
    "encoding/binary"
    "errors"

    "github.com/valyala/fastjson"

    "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
    "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
)

const (
    sharedDataKey                 = "my_key"
    sharedDataInitialValue uint64 = 1
)

func main() {
    proxywasm.SetVMContext(&vmContext{})
}

// Override types.VMContext.
func (*vmContext) OnVMStart(vmConfigurationSize int) types.OnVMStartStatus {
    initialValueBuf := make([]byte, 8)
    binary.LittleEndian.PutUint64(initialValueBuf, sharedDataInitialValue)
    if err := proxywasm.SetSharedData(sharedDataKey, initialValueBuf, 0); err != nil {
        proxywasm.LogWarnf("error setting shared data on OnVMStart: %v", err)
    }
    proxywasm.LogInfof("[wasm-extension]: Setting initial shared value %v", sharedDataInitialValue)
    return types.OnVMStartStatusOK
}

// Override types.DefaultPluginContext.
func (ctx pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus {
    data, err := proxywasm.GetPluginConfiguration()
    if err != nil {
        proxywasm.LogCriticalf("error reading plugin configuration: %v", err)
    }

    var p fastjson.Parser
    v, err := p.ParseBytes(data)
    if err != nil {
        proxywasm.LogCriticalf("error parsing configuration: %v", err)
    }

    obj, err := v.Object()
    if err != nil {
        proxywasm.LogCriticalf("error getting object from json value: %v", err)
    }

    obj.Visit(func(k []byte, v *fastjson.Value) {
        ctx.additionalHeaders[string(k)] = string(v.GetStringBytes())
    })

    return types.OnPluginStartStatusOK
}

type vmContext struct {
    // Embed the default VM context here,
    // so that we don't need to reimplement all the methods.
    types.DefaultVMContext
}

// Override types.DefaultVMContext.
func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext {
    return &pluginContext{contextID: contextID, additionalHeaders: map[string]string{}}
}

type pluginContext struct {
    // Embed the default plugin context here,
    // so that we don't need to reimplement all the methods.
    types.DefaultPluginContext
    additionalHeaders map[string]string
    contextID         uint32
}

// Override types.DefaultPluginContext.
func (ctx *pluginContext) NewHttpContext(contextID uint32) types.HttpContext {
    proxywasm.LogInfo("NewHttpContext")
    return &httpContext{contextID: contextID, additionalHeaders: ctx.additionalHeaders}
}

type httpContext struct {
    // Embed the default http context here,
    // so that we don't need to reimplement all the methods.
    types.DefaultHttpContext
    contextID         uint32
    additionalHeaders map[string]string
}

func (ctx *httpContext) OnHttpResponseHeaders(numHeaders int, endOfStream bool) types.Action {
    proxywasm.LogInfo("OnHttpResponseHeaders")

    for key, value := range ctx.additionalHeaders {
        if err := proxywasm.AddHttpResponseHeader(key, value); err != nil {
            proxywasm.LogCriticalf("failed to add header: %v", err)
            return types.ActionPause
        }
        proxywasm.LogInfof("header set: %s=%s", key, value)
    }

    return types.ActionContinue
}

// Override types.DefaultHttpContext.
func (ctx *httpContext) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {
    for {
        value, err := ctx.incrementData()
        if err == nil {
            proxywasm.LogInfof("incremented shared value: %d", value)
        } else if errors.Is(err, types.ErrorStatusCasMismatch) {
            continue
        }
        break
    }
    return types.ActionContinue
}

func (ctx *httpContext) incrementData() (uint64, error) {
    value, cas, err := proxywasm.GetSharedData(sharedDataKey)
    if err != nil {
        proxywasm.LogWarnf("error getting shared data: %v", err)
        return 0, err
    }

    buf := make([]byte, 8)
    ret := binary.LittleEndian.Uint64(value) + 1
    binary.LittleEndian.PutUint64(buf, ret)
    if err := proxywasm.SetSharedData(sharedDataKey, buf, cas); err != nil {
        proxywasm.LogWarnf("error setting shared data: %v", err)
        return 0, err
    }
    return ret, err
}

We can rebuild the main extension now:

tinygo build -o main.wasm -scheduler=none -target=wasi main.go

Creating the second extension

We'll also use a second extension that reads the shared value and writes it to the log. You can check out the code below and build the extension yourself.

Code for second-extension.wasm
main.go
package main

import (
    "encoding/binary"

    "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
    "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
)

const (
    sharedDataKey = "my_key"
)

func main() {
    proxywasm.SetVMContext(&vmContext{})
}

type vmContext struct {
    // Embed the default VM context here,
    // so that we don't need to reimplement all the methods.
    types.DefaultVMContext
}

// Override types.DefaultVMContext.
func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext {
    return &pluginContext{}
}

type pluginContext struct {
    // Embed the default plugin context here,
    // so that we don't need to reimplement all the methods.
    types.DefaultPluginContext
}

// Override types.DefaultPluginContext.
func (*pluginContext) NewHttpContext(contextID uint32) types.HttpContext {
    proxywasm.LogInfo("[second-extension]: NewHttpContext")

    value, _, err := proxywasm.GetSharedData(sharedDataKey)
    if err != nil {
        proxywasm.LogWarnf("[second-extension]: error getting shared data on NewHttpContext: %v", err)
    }
    buf := make([]byte, 8)
    ret := binary.LittleEndian.Uint64(value)
    binary.LittleEndian.PutUint64(buf, ret)
    proxywasm.LogInfof("[second-extension]: Reading shared data: %d", ret)

    return &httpContext{contextID: contextID}
}

type httpContext struct {
    // Embed the default http context here,
    // so that we don't need to reimplement all the methods.
    types.DefaultHttpContext
    contextID uint32
}

Alternatively, download the second-extension.wasm to the same folder where you built the main.wasm extension.

Creating the Envoy configuration

Finally, we need to modify the Envoy configuration and include the second extensions like this:

http_filters:
  - name: envoy.filters.http.wasm # (1)
  ...
  - 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:
          vm_config:
            vm_id: "my_vm" # (2)
            runtime: "envoy.wasm.runtime.v8"
            code:
              local:
                filename: "second-extension.wasm"
  - name: envoy.filters.http.router
  1. Configuration for the first extension
  2. The vm_id for both extensions has to match to allow data shared between them
Complete envoy.yaml
envoy.yaml
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: local_service
                      domains:
                        - "*"
                      routes:
                        - match:
                            prefix: "/"
                          direct_response:
                            status: 200
                            body:
                              inline_string: "hello world\n"
                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:
                          configuration:
                            "@type": type.googleapis.com/google.protobuf.StringValue
                            value: |
                                { 
                                  "header_1": "some_value_1", 
                                  "header_2": "another_value"
                                }
                          vm_config:
                            vm_id: "my_vm"
                            runtime: "envoy.wasm.runtime.v8"
                            code:
                              local:
                                filename: "main.wasm"
                  - 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:
                          vm_config:
                            vm_id: "my_vm"
                            runtime: "envoy.wasm.runtime.v8"
                            code:
                              local:
                                filename: "second-extension.wasm"
                  - name: envoy.filters.http.router
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
admin:
  access_log_path: "/dev/null"
  address:
    socket_address:
      address: 0.0.0.0
      port_value: 8001

Note

Note the vm_id value for both extensions is the same - this is what allows us to share the data between two extensions.

Save the above YAML to envoy.yaml and run Envoy:

func-e run -c envoy.yaml &

When Envoy starts, we'll notice the log entry from the wasm-extension that tells us the shared value is initialized to 1:

[2022-01-26 21:38:48.766][1346][info][wasm] [source/extensions/common/wasm/context.cc:1167] wasm log my_vm: [wasm-extension]: Setting initial shared value 1

Here's the output we'll get when we send a request to http://localhost:18000:

curl localhost:18000
curl output
[2022-01-26 21:46:09.517][1345][info][wasm] [source/extensions/common/wasm/context.cc:1167] wasm log my_vm: NewHttpContext
[2022-01-26 21:46:09.517][1345][info][wasm] [source/extensions/common/wasm/context.cc:1167] wasm log my_vm: incremented shared value: 2
[2022-01-26 21:46:09.517][1345][info][wasm] [source/extensions/common/wasm/context.cc:1167] wasm log my_vm: NewHttpContext
[2022-01-26 21:46:09.517][1345][info][wasm] [source/extensions/common/wasm/context.cc:1167] wasm log my_vm: [second-extension]: Reading shared data: 2
[2022-01-26 21:46:09.518][1345][info][wasm] [source/extensions/common/wasm/context.cc:1167] wasm log my_vm: OnHttpResponseHeaders
[2022-01-26 21:46:09.518][1345][info][wasm] [source/extensions/common/wasm/context.cc:1167] wasm log my_vm: header set: header_1=some_value_1
[2022-01-26 21:46:09.518][1345][info][wasm] [source/extensions/common/wasm/context.cc:1167] wasm log my_vm: header set: header_2=another_value
hello world

The first Wasm extension invoked is the one in main.wasm, so the first two log entries from the NewHttpContext are coming from that extension. At startup, we've set the initial value to 1. Then when a new request came in, we incremented the value to 2. The following two lines are coming from the second extension, where we read the shared data with value 2 (as expected). The remaining entries are response headers and, finally, the proxy's hello world response.

To stop running the Envoy proxy, type fg and press Ctrl+C.