AssemblyScript语言是TypeScript语言的子集,您可以使用和JavaScript近似的语法开发网关插件扩展API网关的核心功能。
前提条件
安装Node.js和npm并安装SDK到指定目录。
# 例如安装在/opt目录下,本文后续示例均安装在该目录下。 cd /opt git clone https://github.com/solo-io/proxy-runtime.git
安装Docker。
编写插件
步骤一:初始化工程目录
新建工程目录文件,例如wasm-demo。
在所建目录下执行以下命令,进行工程初始化。
npm install --save-dev assemblyscript npx asinit .
按照如下示例修改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" } }
执行
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); }, "");
步骤三:编译生成WASM文件
执行以下命令。编译成功后会在build
目录下创建文件optimized.wasm。这个文件在本文后续本地调试的示例中也会被用到。在使用云原生网关插件市场的自定义插件功能时,直接上传该文件即可。
npm run asbuild
本地调试
本示例使用的代码和配置为wasm-demo-js.zip。
使用Docker Compose启动验证
打开在编写插件时创建的wasm-demo目录,确认该目录下已经编译生成了
build/optimized.wasm
文件。在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: {}
继续在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
启动Docker Compose。
docker compose up
功能验证
wasm功能验证
使用浏览器直接访问httpbin(http://127.0.0.1:12345/get)。
可以看到不经过网关时的请求头内容如下:
使用浏览器通过网关访问httpbin(http://127.0.0.1:10000/get)。
可以看到经过网关处理后的请求头的内容如下,加入了
hello: world
请求头,说明编写插件的功能已经生效。
插件配置修改验证
修改envoy.yaml,将
mockEnable
配置修改为true。使用浏览器通过网关访问httpbin(http://127.0.0.1:10000/get)。
可以看到经过网关处理后的请求头的内容如下,说明插件配置修改生效,开启mock后,应答直接返回“hello world”。
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 | 恢复先前被暂停的应答处理流程 | - |