Prepare the plugin code

The custom code that you create for Service Extensions plugins must be packaged and published to Artifact Registry before other services can access it. This page describes how to create plugin code, package the code in a container image, and upload it to an Artifact Registry repository.

For information about Service Extensions, see Service Extensions overview.

Before you start, review the best practices for writing plugin code.

For more examples, see Code samples for plugins.

Before you begin

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  3. Make sure that billing is enabled for your Google Cloud project.

  4. Enable the Network Services, Network Actions, Artifact Registry, Cloud Build, Cloud Logging, and Cloud Monitoring APIs.

    Enable the APIs

  5. Install the Google Cloud CLI.
  6. To initialize the gcloud CLI, run the following command:

    gcloud init
  7. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  8. Make sure that billing is enabled for your Google Cloud project.

  9. Enable the Network Services, Network Actions, Artifact Registry, Cloud Build, Cloud Logging, and Cloud Monitoring APIs.

    Enable the APIs

  10. Install the Google Cloud CLI.
  11. To initialize the gcloud CLI, run the following command:

    gcloud init

Set up the toolchain

C++

The Proxy-Wasm C++ SDK let developers use C++ to implement WebAssembly (Wasm) plugins for Service Extensions. The SDK uses the C++ WebAssembly toolchain Emscripten, as well as other libraries, such as protobuf and, optionally, Abseil.

Because building plugins written in C++ depends on specific versions of these tools and libraries, we recommend using the Docker image provided by the Proxy-Wasm C++ SDK. The instructions for C++ on this page use the Docker method. To build C++ plugins without using Docker, see the Proxy-Wasm C++ SDK documentation.

  1. Install Docker if it's not already installed. Docker is included in Cloud Shell, the Google Cloud interactive shell environment.

  2. Download a copy of the Proxy-Wasm C++ SDK. The simplest way to do this is to clone the Git repository:

    git clone https://github.com/proxy-wasm/proxy-wasm-cpp-sdk.git
    
  3. Build the Proxy-Wasm C++ SDK Docker image from the Dockerfile provided by the SDK:

    cd proxy-wasm-cpp-sdk
    docker build -t wasmsdk:v3 -f Dockerfile-sdk .
    

    When the command completes building SDK libraries and dependencies, the resulting Docker image is associated with the specified tag, which is wasmsdk:v3 in this example.

Rust

The customization capability of Service Extensions is provided through the use of WebAssembly and Proxy-Wasm. WebAssembly supports a number of programming languages. Google recommends Rust because it provides excellent WebAssembly support and Proxy-Wasm provides a full-featured Rust SDK. Rust also provides good performance and strong type safety.

  1. Install the Rust toolchain.

    At the end of the installation process, follow any instructions printed to the console to finish the configuration process.

  2. Add Wasm support to the Rust toolchain:

    rustup target add wasm32-wasi
    

Create the plugin package

C++

  1. Create a new directory, separate from proxy-wasm-cpp-sdk:

    mkdir myproject
    
  2. In the directory, create a Makefile with the following contents:

    # Express any dependencies
    PROTOBUF=     # full / lite / none
    WASM_DEPS=    # absl_base re2 ...
    
    # Include the SDK Makefile
    PROXY_WASM_CPP_SDK=/sdk
    include ${PROXY_WASM_CPP_SDK}/Makefile
    
  3. Add a C++ source file for the plugin in the same directory. The names of C++ source files must match the WASM files that the Makefile targets, with the .wasm suffix replaced by .cc. In this example, the source file must be named myproject.cc.

  4. Add your plugin code to the source file.

    The following sample source code is a plugin that rewrites the request host, and emits a response header:

    #include "proxy_wasm_intrinsics.h"
    
    class MyHttpContext : public Context {
     public:
      explicit MyHttpContext(uint32_t id, RootContext* root) : Context(id, root) {}
    
      FilterHeadersStatus onRequestHeaders(uint32_t headers,
                                           bool end_of_stream) override {
        LOG_INFO("onRequestHeaders: hello from wasm");
    
        // Route Extension example: host rewrite
        replaceRequestHeader(":host", "service-extensions.com");
        replaceRequestHeader(":path", "/");
        return FilterHeadersStatus::Continue;
      }
    
      FilterHeadersStatus onResponseHeaders(uint32_t headers,
                                            bool end_of_stream) override {
        LOG_INFO("onResponseHeaders: hello from wasm");
    
        // Traffic Extension example: add response header
        addResponseHeader("hello", "service-extensions");
        return FilterHeadersStatus::Continue;
      }
    };
    
    static RegisterContextFactory register_StaticContext(
        CONTEXT_FACTORY(MyHttpContext), ROOT_FACTORY(RootContext));

    The onRequestHeaders method is a callback that Service Extensions invokes.

Rust

  1. Create a Rust package directory by using the cargo new command from Rust's package manager, Cargo:

    cargo new --lib my-wasm-plugin
    

    The command creates a directory that contains a cargo.toml file that you can update to describe how to build the Rust package and an src directory in which you store the plugin code.

  2. Update the cargo.toml file to specify the parameters required to build the package:

    [package]
    name = "my-wasm-plugin"
    version = "0.1.0"
    edition = "2021"
    
  3. To register the Proxy-Wasm Rust SDK and logging support as dependencies, add the dependencies section. For example:

    [dependencies]
    proxy-wasm = "0.2"
    log = "0.4"
    
  4. To build a dynamic library as required for plugins, add the lib section. For example:

    [lib]
    crate-type = ["cdylib"]
    
  5. To reduce the size of the compiled plugin, add the profile.release section. For example:

    [profile.release]
    lto = true
    opt-level = 3
    codegen-units = 1
    panic = "abort"
    strip = "debuginfo"
    
  6. Add your plugin code to the lib.rs file in the src directory.

    The following sample source code is a plugin that rewrites the request host, and emits a response header:

    use log::info;
    use proxy_wasm::traits::*;
    use proxy_wasm::types::*;
    
    proxy_wasm::main! { {
        proxy_wasm::set_log_level(LogLevel::Trace);
        proxy_wasm::set_http_context(|_, _| -> Box<dyn HttpContext> { Box::new(MyHttpContext) });
    } }
    
    struct MyHttpContext;
    
    impl Context for MyHttpContext {}
    
    impl HttpContext for MyHttpContext {
        fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {
            info!("onRequestHeaders: hello from wasm");
    
            // Route extension example: host rewrite
            self.set_http_request_header(":host", Some("service-extensions.com"));
            self.set_http_request_header(":path", Some("/"));
            return Action::Continue;
        }
    
        fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action {
            info!("onResponseHeaders: hello from wasm");
    
            // Traffic extension example: add response header
            self.add_http_response_header("hello", "service-extensions");
            return Action::Continue;
        }
    }

    The on_http_request_headers method is a callback that Service Extensions invokes.

Compile the plugin

C++

To compile the plugin, run the following command from within the directory in which the Makefile and C++ plugin source files are located:

docker run -v $PWD:/work -w /work wasmsdk:v3 /build_wasm.sh myproject.wasm

This command maps the current directory to the work directory within the Docker image, and then runs the build_wasm.sh script provided by the Docker image to build the plugin code. When the compile operation completes successfully, a myproject.wasm file, which contains the compiled Wasm bytecode, is created in the current directory.

The first time that plugin code is compiled using the Docker image, Emscripten generates the standard libraries. To cache these in the Docker image so that they don't need to be regenerated each time, commit the image with the standard libraries after the first successful compilation:

docker commit `docker ps -l | grep wasmsdk:v3 | awk '{print $1}'` wasmsdk:v3

For more information about building C++ plugins, see the Proxy-Wasm C++ SDK documentation.

Rust

To compile the plugin code, run the cargo build command:

cargo build --release --target wasm32-wasi

When the build completes successfully, a Finished release [optimized] target(s) message appears. If you're familiar with Envoy, you might want to load the plugin in Envoy and verify its behavior.

Publish the image to Artifact Registry

Publish the compiled plugin to Artifact Registry so that Google Cloud services can access it.

  1. Store the plugin artifact in an image name with the following format:

    LOCATION-docker.pkg.dev/PROJECT_ID/REPOSITORY/IMAGE
    

    Replace the following:

    • LOCATION: the regional or multi-regional location of the repository. For increased reliability, specify a multi-regional location.
    • PROJECT_ID: your Google Cloud console project ID.
    • REPOSITORY: the name of the repository where you intend to store the image.
    • IMAGE: the name of the container image in the repository.

    For example: us-docker.pkg.dev/my-project/my-repo/my-wasm-plugin

  2. Create a local package/ directory and copy your publishable plugin artifact into it. The following sample copies the plugin artifact built by Rust:

    mkdir -p package && cp -f target/wasm32-wasi/release/my_wasm_plugin.wasm package/plugin.wasm
    
  3. Create a package/Dockerfile file with the following contents:

    FROM scratch
    COPY plugin.wasm plugin.wasm
    
  4. Package the plugin code by using either Cloud Build or Docker.

    Cloud Build

    1. Create a package/cloudbuild.yaml build config file with the following contents:

      steps:
        - name: 'gcr.io/cloud-builders/docker'
          args: [ 'build', '--no-cache', '--platform', 'wasm',
                '-t', 'LOCATION-docker.pkg.dev/PROJECT_ID/REPOSITORY/IMAGE:prod', '.' ]
      images: [ 'LOCATION-docker.pkg.dev/PROJECT_ID/REPOSITORY/IMAGE:prod' ]
      
    2. Trigger the operation to build the plugin container and publish it to Artifact Registry:

      gcloud builds submit --config package/cloudbuild.yaml package/
      

    Docker

    1. Install Docker if it's not already installed. Docker is included in Cloud Shell, the Google Cloud interactive shell environment.

    2. Configure Docker to authenticate to Artifact Registry. For example:

      gcloud auth login
      gcloud auth configure-docker LOCATION-docker.pkg.dev
      gcloud auth print-access-token | docker login -u oauth2accesstoken --password-stdin https://LOCATION-docker.pkg.dev
      
    3. Build the container image:

      docker build --no-cache --platform wasm -t my-wasm-plugin package/
      
    4. To tag the local image with the repository image name, use image tags, such as prod in the following example:

      docker tag my-wasm-plugin LOCATION-docker.pkg.dev/PROJECT_ID/REPOSITORY/IMAGE:prod
      
    5. Publish your tagged container image to Artifact Registry.

      docker push LOCATION-docker.pkg.dev/PROJECT_ID/REPOSITORY/IMAGE:prod
      
  5. To confirm that the image was successfully pushed to Artifact Registry, run the gcloud artifacts docker images list command.

    gcloud artifacts docker images list LOCATION-docker.pkg.dev/PROJECT_ID/REPOSITORY/IMAGE --include-tags
    

Prepare and upload the configuration file

Plugins might optionally receive configuration data, which can affect plugin behavior at runtime. Configuration data can be text or binary and in any format accepted by the plugin. If the size of the configuration data is large, you might need to upload the configuration file to Artifact Registry.

Write plugin code to read configuration data

C++

Configuration data is passed to the onConfigure method of the root context object, which is instantiated once at plugin startup, and remains active for the lifetime of the Wasm runtime that hosts the plugin. The root context object is an instance of the RootContext class or of a subclass of RootContext.

Plugin code can control what class is used for the root context through the RegisterContextFactory value. For example, the following plugin code registers MyRootContext and MyHttpContext as the classes to use for root and stream context instances. It then reads a secret value from plugin configuration data, which stream context instances can access through the root context object.

#include <string>
#include <string_view>

#include "proxy_wasm_intrinsics.h"

class MyRootContext : public RootContext {
 public:
  explicit MyRootContext(uint32_t id, std::string_view root_id)
      : RootContext(id, root_id) {}

  bool onConfigure(size_t config_len) override {
    secret_ = getBufferBytes(WasmBufferType::PluginConfiguration, 0, config_len)
                  ->toString();
    return true;
  }

  const std::string& secret() const { return secret_; }

 private:
  std::string secret_;
};

class MyHttpContext : public Context {
 public:
  explicit MyHttpContext(uint32_t id, RootContext* root)
      : Context(id, root),
        secret_(static_cast<MyRootContext*>(root)->secret()) {}

  FilterHeadersStatus onRequestHeaders(uint32_t headers,
                                       bool end_of_stream) override {
    // Use secret here...
    LOG_INFO("secret: " + secret_);
    return FilterHeadersStatus::Continue;
  }

 private:
  const std::string& secret_;
};

static RegisterContextFactory register_MyHttpContext(
    CONTEXT_FACTORY(MyHttpContext), ROOT_FACTORY(MyRootContext));

Rust

Configuration data is read in from a RootContext trait, which is instantiated once at plugin startup, and remains active for the lifetime of the Wasm runtime that hosts the plugin. RootContext traits are useful for performing operations or maintaining state across many requests.

Plugin code can control what class is used for the root context through the set_root_context method, and the root context instantiates stream contexts. For example, the following plugin code registers MyRootContext, which instantiates MyHttpContext as required. The root context reads a secret value from plugin configuration data, and passes it to stream contexts.

use log::info;
use proxy_wasm::traits::*;
use proxy_wasm::types::*;
use std::rc::Rc;

proxy_wasm::main! { {
    proxy_wasm::set_log_level(LogLevel::Trace);
    proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> {
        Box::new(MyRootContext { secret: Rc::new("missing".to_string()) })
    });
} }

struct MyRootContext {
    secret: Rc<String>,
}

impl Context for MyRootContext {}

impl RootContext for MyRootContext {
    fn on_configure(&mut self, _: usize) -> bool {
        if let Some(config_bytes) = self.get_plugin_configuration() {
            self.secret = Rc::new(String::from_utf8(config_bytes).unwrap())
        }
        true
    }

    fn create_http_context(&self, _: u32) -> Option<Box<dyn HttpContext>> {
        Some(Box::new(MyHttpContext {
            secret: self.secret.clone(), // shallow copy, ref count only
        }))
    }

    fn get_type(&self) -> Option<ContextType> {
        Some(ContextType::HttpContext)
    }
}

struct MyHttpContext {
    secret: Rc<String>,
}

impl Context for MyHttpContext {}

impl HttpContext for MyHttpContext {
    fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {
        // Use secret here...
        info!("secret: {}", self.secret);
        Action::Continue
    }
}

Upload the configuration file

If the size of the data to be delivered to your plugin in on_configure exceeds 900 KiB, publish it to Artifact Registry by following the method described in Publish the image to Artifact Registry. In this case, save the configuration file by the name plugin.config in the container image.

The next step is to create a plugin.

While creating the plugin, you need to provide the URI to the uploaded container image.

Create a new version of plugin code

To create a new version of the plugin code, edit the plugin file. Then, as described in the preceding sections, compile the plugin code, repackage it, and upload it to Artifact Registry.

Callbacks

The code that you compile into Wasm can define arbitrary methods or functions, but some of them have a special significance. These are the methods that are defined in the Proxy-Wasm SDK for the language of your choice and map to the Proxy-Wasm Application Binary Interface (ABI) specifications. Service Extensions invokes these callbacks in response to user requests or in response to plugin lifecycle events.

These callbacks are listed in the following table in the order in which they're typically invoked:

Callback name and description C++ method name Rust method name
START_PLUGIN: Invoked when a plugin is started. RootContext::onStart RootContext::on_vm_start
CONFIGURE_PLUGIN: Invoked after a plugin is started, to provide configuration data to the plugin. RootContext::onConfigure RootContext::on_configure
CREATE_CONTEXT: Invoked when a new stream context is created. Each stream corresponds to a client HTTP request. Context::onCreate RootContext::create_http_context
HTTP_REQUEST_HEADERS: Invoked to process HTTP request headers. Context::onRequestHeaders HttpContext::on_http_request_headers
HTTP_REQUEST_BODY: Invoked repeatedly to process HTTP request body chunks. Context::onRequestBody HttpContext::on_http_request_body
HTTP_RESPONSE_HEADERS: Invoked to process HTTP response headers. Context::onResponseHeaders HttpContext::on_http_response_headers
HTTP_RESPONSE_BODY: Invoked repeatedly to process HTTP response body chunks. Context::onResponseBody HttpContext::on_http_response_body
DONE: Invoked when the processing of a plugin has completed. Context::onDone Context::on_done
DELETE: Invoked when the stream context object corresponding to a client HTTP request is deleted. Context::onDelete (no callback)

What's next