This document explains how you can add your own logic as a CEL API and make it available to the gRPC Federation.
We call this mechanism the CEL Plugin.
This document is based on _examples/15_cel_plugin. If you would like to see a working sample, please refer to that document.
First, define in ProtocolBuffers the types, functions you want to provide.
syntax = "proto3";
package example.regexp;
import "grpc/federation/federation.proto";
option go_package = "example/plugin;pluginpb";
message Regexp {
uint64 ptr = 1; // store raw pointer value.
}
option (grpc.federation.file).plugin.export = {
name: "regexp"
types: [
{
name: "Regexp"
methods: [
{
name: "matchString"
args { type { kind: STRING } }
return { kind: BOOL }
}
]
}
]
functions: [
{
name: "compile"
args { type { kind: STRING } }
return { message: "Regexp" }
}
]
};(grpc.federation.file).plugin.export option is used to define the API. In this example, the plugin is named regexp.
The regexp plugin belongs to the example.regexp package. Also, this provides the Regexp message type and makes matchString available as a method on the Regexp message, and compile function is also added.
In summary, it contains the following definitions.
example.regexp.Regexpmessageexample.regexp.Regexp.matchString(string) boolmethodexample.regexp.compile(string) example.regexp.Regexpfunction
Put this definition in plugin/plugin.proto .
Next, write the code to use the defined API.
The following definition is a sample of adding a gRPC method that compiles the expr specified by the example.regexp.compile function and returns whether the contents of target matches the result.
syntax = "proto3";
package org.federation;
import "grpc/federation/federation.proto";
import "plugin/plugin.proto"; // your CEL API definition
option go_package = "example/federation;federation";
service FederationService {
option (grpc.federation.service) = {};
rpc IsMatch(IsMatchRequest) returns (IsMatchResponse) {};
}
message IsMatchRequest {
string expr = 1;
string target = 2;
}
message IsMatchResponse {
option (grpc.federation.message) = {
def {
name: "matched"
// Use the defined CEL API.
by: "example.regexp.compile($.expr).matchString($.target)"
}
};
bool result = 1 [(grpc.federation.field).by = "matched"];
}Run the gRPC Federation code generator to generate Go language code. At this time, CEL API's schema is verified by the gRPC Federation Compiler, and an error is output if it is used incorrectly.
By the code generation, the library code is generated to write the plugin. In this example, the output is in plugin/plugin_grpc_federation.pb.go.
Using that library, write a plugin as follows.
package main
import (
pluginpb "example/plugin"
"regexp"
"unsafe"
)
var _ pluginpb.RegexpPlugin = new(plugin)
type plugin struct{}
// For example.regexp.compile function.
func (_ *plugin) Example_Regexp_Compile(ctx context.Context, expr string) (*pluginpb.Regexp, error) {
re, err := regexp.Compile(expr)
if err != nil {
return nil, err
}
return &pluginpb.Regexp{
Ptr: uint32(uintptr(unsafe.Pointer(re))),
}, nil
}
// For example.regexp.Regexp.matchString method.
func (_ *plugin) Example_Regexp_Regexp_MatchString(ctx context.Context, re *pluginpb.Regexp, s string) (bool, error) {
return (*regexp.Regexp)(unsafe.Pointer(uintptr(re.Ptr))).MatchString(s), nil
}
func main() {
pluginpb.RegisterRegexpPlugin(new(plugin))
}pluginpb.RegisterRegexpPlugin function for registering developed plugins. Also, pluginpb.RegexpPlugin is interface type for plugin.
$ GOOS=wasip1 GOARCH=wasm go build -o regexp.wasm ./cmd/plugin$ sha256sum regexp.wasm
820f86011519c42da0fe9876bc2ca7fbee5df746acf104d9e2b9bba802ddd2b9 regexp.wasmInitialize the gRPC server with the path to the wasm file and the sha256 value of the file.
federationServer, err := federation.NewFederationService(federation.FederationServiceConfig{
CELPlugin: &federation.FederationServiceCELPluginConfig{
Regexp: federation.FederationServiceCELPluginWasmConfig{
Path: "regexp.wasm",
Sha256: "820f86011519c42da0fe9876bc2ca7fbee5df746acf104d9e2b9bba802ddd2b9",
},
},
})By default, plugins do not have access to Environment variables, File System, or Network. This helps to run plugins securely, but there may be cases where you want to allow access to these resources. In such cases, you can enable access to each resource by configuring as follows:
option (grpc.federation.file).plugin.export = {
name: "regexp"
capability {
network: {} // enable network access via HTTP/HTTPS
env { names: ["foo"] } // enable access to `FOO` environment variable
file_system { mount_path: "/" } // enable access to file system from mount point `/`
}
}For more details, please refer to the sample that uses this configuration in _examples/21_wasm_net.
If the prefix is GRPC_FEDERATION_PLUGIN_ , the environment variable will be set for the plugin with that prefix removed. If an environment variable with the same name is already set, it will be overwritten.
Since setting GOMAXPROCS to 2 or higher causes a panic, it is always set to 1.
For example, if you want to turn off GC for the plugin only, set it like this: GRPC_FEDERATION_PLUGIN_GOGC=off
At the same time, you need to allow all environment variables via capability, or allow them using a whitelist format.
option (grpc.federation.file).plugin.export = {
name: "example"
capability {
env { names: ["gogc"] } // enable access to `GOGC` environment variable
}
}or
option (grpc.federation.file).plugin.export = {
name: "example"
capability {
env { all: true } // enable all environment variable
}
}Host and wasm plugin using stdio to exchange data.
The exchanged data types are CELPluginRequest and CELPluginResponse defined on plugin.proto. Actually, the data is converted to json and then exchanged.