使用类JS语言开发网关插件

AssemblyScript语言是TypeScript语言的子集,您可以使用和JavaScript近似的语法开发网关插件扩展云原生API网关的核心功能。

前提条件

  • 安装Node.js和npm并安装SDK到指定目录。

    # 例如安装在/opt目录下,本文后续示例均安装在该目录下。
    cd /opt
    git clone https://github.com/solo-io/proxy-runtime.git
  • 安装Docker

编写插件

步骤一:初始化工程目录

  1. 新建工程目录文件,例如wasm-demo。

  2. 在所建目录下执行以下命令,进行工程初始化。

    npm install --save-dev assemblyscript
    npx asinit .
  3. 按照如下示例修改package.json的内容。

    {
      "scripts": {
        "asbuild:untouched": "asc assembly/index.ts -b build/untouched.wasm -t build/untouched.wat --use abort=abort_proc_exit --sourceMap --debug",
        "asbuild:optimized": "asc assembly/index.ts -b build/optimized.wasm -t build/optimized.wat --use abort=abort_proc_exit --sourceMap --optimize",
        "asbuild": "npm run asbuild:untouched && npm run asbuild:optimized",
        "test": "node tests"
      },
      "dependencies": {
        "@assemblyscript/loader": "^0.19.22",
        "@solo-io/proxy-runtime": "file:/opt/proxy-runtime",
        "assemblyscript-json": "^1.1.0"
      },
      "devDependencies": {
        "assemblyscript": "^0.19.22"
      }
    }
  4. 执行npm install命令更新依赖。

步骤二:编写index.ts代码

添加index.ts至assembly/目录下,index.ts内容如下:

export * from "@solo-io/proxy-runtime/proxy";
import {
  RootContext,
  Context,
  registerRootContext,
  FilterHeadersStatusValues,
  stream_context,
  send_local_response,
  GrpcStatusValues,
} from "@solo-io/proxy-runtime";
import { JSON } from "assemblyscript-json";

// 插件Context,每个插件有一份,常用于存放插件的配置
class PluginContext extends RootContext {
  // 插件配置字段mock_enable
  private mock_enable_: bool = false;
  // 实现创建HTTP Context的方法,由override继承来的方法,传入解析好的配置字段
  createContext(context_id: u32): Context {
    return new HttpContext(context_id, this, this.mock_enable_);
  }
  // 实现插件配置解析的方法,由override继承来的方法
  onConfigure(configuration_size: u32): bool {
    if (configuration_size == 0) {
        return true;
    }
    super.onConfigure(configuration_size);
    // 使用JSON解析插件配置
    let config = <JSON.Obj>(JSON.parse(this.getConfiguration()));
    let enable_or_null : JSON.Bool | null = config.getBool("mockEnable");
    if (enable_or_null != null) {
      this.mock_enable_ = enable_or_null.valueOf();
    }
    return true;
  }
}

// HTTP Context,每个请求有一份
class HttpContext extends Context {
  private mock_enable_: bool = false;
  constructor(context_id: u32, root_context: PluginContext, mock_enable: bool) {
    super(context_id, root_context);
    this.mock_enable_ = mock_enable;
  }
  // 实现处理HTTP请求头的方法,由override继承来的方法
  onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues {
    // 给请求添加一个请求头"hello:world"
    stream_context.headers.request.add("hello", "world");
    // 判断配置是否开启了mock,若开启直接返回200状态码,以及http应答"hello world"
    if (this.mock_enable_) {
      send_local_response(200, "", String.UTF8.encode("hello world"), [], GrpcStatusValues.Ok);
    }
    return FilterHeadersStatusValues.Continue;
  }
}

registerRootContext((context_id: u32) => { return new PluginContext(context_id); }, "");
说明
  • 代码中的onRequestHeaders方法可以挂载到HTTP请求处理的一个阶段。完整的HTTP处理阶段以及对应的挂载方法,请参见HTTP处理挂载点

  • 代码中的stream_context.headers.request.addsend_local_response是插件SDK提供的两个工具方法。其他主要的工具方法,请参见工具方法

  • 注释中说明了该插件的具体逻辑以及作用。插件开发一般只需关注HttpContext结构体以及相关方法,如果插件需要根据配置进行处理,则需要关注PluginContext结构体。

步骤三:编译生成WASM文件

执行以下命令。编译成功后会在build目录下创建文件optimized.wasm。这个文件在本文后续本地调试的示例中也会被用到。在使用云原生网关插件市场的自定义插件功能时,直接上传该文件即可。

npm run asbuild

本地调试

本示例使用的代码和配置为wasm-demo-js.zip

使用Docker Compose启动验证

  1. 打开在编写插件时创建的wasm-demo目录,确认该目录下已经编译生成了build/optimized.wasm文件。

  2. 在wasm-demo目录下创建文件docker-compose.yaml。

    version: '3.7'
    services:
      envoy:
        image: envoyproxy/envoy:v1.21-latest
        depends_on:
        - httpbin
        networks:
        - wasmtest
        ports:
        - "10000:10000"
        volumes:
        - ./envoy.yaml:/etc/envoy/envoy.yaml
        - ./build/optimized.wasm:/etc/envoy/optimized.wasm
    
      httpbin:
        image: kennethreitz/httpbin:latest
        networks:
        - wasmtest
        ports:
        - "12345:80"
    
    networks:
      wasmtest: {}
  3. 继续在wasm-demo目录下创建文件envoy.yaml。

    admin:
      address:
        socket_address:
          protocol: TCP
          address: 0.0.0.0
          port_value: 9901
    static_resources:
      listeners:
      - name: listener_0
        address:
          socket_address:
            protocol: TCP
            address: 0.0.0.0
            port_value: 10000
        filter_chains:
        - filters:
          - name: envoy.filters.network.http_connection_manager
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
              scheme_header_transformation:
                scheme_to_overwrite: https
              stat_prefix: ingress_http
              route_config:
                name: local_route
                virtual_hosts:
                - name: local_service
                  domains: ["*"]
                  routes:
                  - match:
                      prefix: "/"
                    route:
                      cluster: httpbin
              http_filters:
              - name: wasmdemo
                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: wasmdemo
                      vm_config:
                        runtime: envoy.wasm.runtime.v8
                        code:
                          local:
                            filename: /etc/envoy/optimized.wasm
                      configuration:
                        "@type": "type.googleapis.com/google.protobuf.StringValue"
                        value: |
                          {
                            "mockEnable": false
                          }
              - name: envoy.filters.http.router
      clusters:
      - name: httpbin
        connect_timeout: 30s
        type: LOGICAL_DNS
        # Comment out the following line to test on v6 networks
        dns_lookup_family: V4_ONLY
        lb_policy: ROUND_ROBIN
        load_assignment:
          cluster_name: httpbin
          endpoints:
          - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: httpbin
                    port_value: 80
  4. 启动Docker Compose。

    docker compose up

功能验证

wasm功能验证

  1. 使用浏览器直接访问httpbin(http://127.0.0.1:12345/get)。

    可以看到不经过网关时的请求头内容如下:不经过网关

  2. 使用浏览器通过网关访问httpbin(http://127.0.0.1:10000/get)。

    可以看到经过网关处理后的请求头的内容如下,加入了hello: world请求头,说明编写插件的功能已经生效。经过网关

插件配置修改验证

  1. 修改envoy.yaml,将mockEnable配置修改为true。

    envoy

  2. 使用浏览器通过网关访问httpbin(http://127.0.0.1:10000/get)。

    可以看到经过网关处理后的请求头的内容如下,说明插件配置修改生效,开启mock后,应答直接返回“hello world”。helloworld

HTTP处理挂载点

HTTP处理挂载点

触发时机

挂载方法

HTTP请求头处理阶段

网关接收到客户端发送来的请求头数据时

onRequestHeaders

HTTP请求Body处理阶段

网关接收到客户端发送来的请求Body数据时

onRequestBody

HTTP请求Trailer处理阶段

网关接收到客户端发送来的请求Trailer数据时

onRequestTrailers

HTTP应答头处理阶段

网关接收到后端服务响应的应答头数据时

onResponseHeaders

HTTP应答Body处理阶段

网关接收到后端服务响应的应答Body数据时

onResponseBody

HTTP应答Trailer处理阶段

网关接收到后端服务响应的应答Trailer数据时

onResponseTrailers

HTTP请求响应完成阶段

网关完成当前请求的处理流程时

onLog

工具方法

分类

方法名称

用途

可以生效的挂载方法

请求头处理

stream_context.headers.request.get_headers

获取客户端请求的全部请求头

onRequestHeaders

stream_context.headers.request.set_headers

替换客户端请求的全部请求头

onRequestHeaders

stream_context.headers.request.get

获取客户端请求的指定请求头

onRequestHeaders

stream_context.headers.request.remove

移除客户端请求的指定请求头

onRequestHeaders

stream_context.headers.request.replace

替换客户端请求的指定请求头

onRequestHeaders

stream_context.headers.request.add

新增一个客户端请求头

onRequestHeaders

请求Body处理

get_buffer_bytes(BufferTypeValues.HttpRequestBody)

获取客户端请求Body

onRequestBody

请求Trailer处理

stream_context.trailers.request.get_headers

获取客户端请求的全部请求Trailer

onRequestTrailers

stream_context.trailers.request.set_headers

替换客户端请求的全部请求Trailer

onRequestTrailers

stream_context.trailers.request.get

获取客户端请求的指定请求Trailer

onRequestTrailers

stream_context.trailers.request.remove

移除客户端请求的指定请求Trailer

onRequestTrailers

stream_context.trailers.request.replace

替换客户端请求的指定请求Trailer

onRequestTrailers

stream_context.trailers.request.add

新增一个客户端请求Trailer

onRequestTrailers

应答头处理

stream_context.headers.response.get_headers

获取后端响应的全部应答头

onResponseHeaders

stream_context.headers.response.set_headers

替换后端响应的全部应答头

onResponseHeaders

stream_context.headers.response.get

获取后端响应的指定应答头

onResponseHeaders

stream_context.headers.response.remove

移除后端响应的指定应答头

onResponseHeaders

stream_context.headers.response.replace

替换后端响应的指定应答头

onResponseHeaders

stream_context.headers.response.add

新增一个后端响应头

onResponseHeaders

应答Body处理

get_buffer_bytes(BufferTypeValues.HttpResponseBody)

获取客户端请求Body

onResponseBody

应答Trailer处理

stream_context.trailers.response.get_headers

获取后端响应的全部应答Trailer

onResponseTrailers

stream_context.trailers.response.set_headers

替换后端响应的全部应答Trailer

onResponseTrailers

stream_context.trailers.response.get

获取后端响应的指定应答Trailer

onResponseTrailers

stream_context.trailers.response.remove

移除后端响应的指定应答Trailer

onResponseTrailers

stream_context.trailers.response.replace

替换后端响应的指定应答Trailer

onResponseTrailers

stream_context.trailers.response.add

新增一个后端响应

onResponseTrailers

HTTP调用

httpCall

发送一个HTTP请求

-

get_header_map_pairs(HeaderMapTypeValues.HttpCallResponseHeaders)

获取DispatchHttpCall请求响应的应答头

-

get_buffer_bytes(HttpCallResponseBody)

获取DispatchHttpCall请求响应的应答Body

-

get_header_map_pairs(HeaderMapTypeValues.HttpCallResponseTrailers)

获取DispatchHttpCall请求响应的应答Trailer

-

直接响应

send_local_response

直接返回一个特定的HTTP应答

-

上下文切换

setEffectiveContext

切换到指定的HTTP Context,用于恢复HTTP请求或应答处理

-

continue_request

恢复先前被暂停的请求处理流程

-

continue_response

恢复先前被暂停的应答处理流程

-