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, such as Media CDN, 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 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, Service Extensions, Certificate Manager, Artifact Registry, 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, Service Extensions, Certificate Manager, Artifact Registry, 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:v2 -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:v2 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. Create a Makefile in the directory to specify the .wasm files to build.

Here's a basic example that builds a myproject.wasm file from source code in myproject.cc. You can modify the all target to specify other .wasm files to be built from .cc files with the same primary name.

  PROXY_WASM_CPP_SDK=/sdk
  all: myproject.wasm
  include ${PROXY_WASM_CPP_SDK}/Makefile.base_lite
  1. 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.

  2. Add your plugin code to the source file.

Here's sample source code for a plugin that logs the request path when request headers are received and also logs a message when the processing of the request and response is complete.

  #include <string>
  #include <unordered_map>

  #include "proxy_wasm_intrinsics.h"

  class ExampleContext : public Context {
   public:
    explicit ExampleContext(uint32_t id, RootContext* root) : Context(id, root) {}

    FilterHeadersStatus onRequestHeaders(uint32_t headers, bool end_of_stream) override;
    void onDone() override;
  };

  static RegisterContextFactory register_ExampleContext(CONTEXT_FACTORY(ExampleContext));

  FilterHeadersStatus ExampleContext::onRequestHeaders(uint32_t headers, bool end_of_stream) {
    logInfo(std::string("onRequestHeaders ") + std::to_string(id()));
    auto path = getRequestHeader(":path");
    logInfo(std::string("header path ") + std::string(path->view()));
    return FilterHeadersStatus::Continue;
  }

  void ExampleContext::onDone() {
    logInfo("onDone " + std::to_string(id()));
  }

The ExampleContext::onRequestHeaders and ExampleContext::onDone methods are callbacks 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.

    For example, for your plugin to check that each HTTP request contains an Authorization header with the value secret and generate an HTTP 403 (Forbidden) response if it doesn't contain such a header and value, add the following lines:

    use log::info;
    use proxy_wasm::traits::*;
    use proxy_wasm::types::*;
    
    #[no_mangle]
    pub fn _start() {
      proxy_wasm::set_log_level(LogLevel::Trace);
      proxy_wasm::set_http_context(|_, _| -> Box<dyn HttpContext> { Box::new(DemoPlugin) });
    }
    
    struct DemoPlugin;
    
    impl HttpContext for DemoPlugin {
       fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {
           if self.get_http_request_header("Authorization") == Some(String::from("secret")) {
               info!("Access granted.");
               Action::Continue
          } else {
               self.send_http_response(403, vec![], Some(b"Access forbidden.\n"));
               Action::Pause
            }
          }
       }
    
    impl Context for DemoPlugin {}
    

    The on_http_request_headers function 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:v2 /build_wasm.sh

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:v2 | awk '{print $1}'` wasmsdk:v2

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.

Package the plugin code in a container image

To package the plugin code by using Docker, follow these steps:

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

  2. Create a file called Dockerfile in the current directory and add the following lines in it:

    FROM scratch
    COPY target/wasm32-wasi/release/my_wasm_plugin.wasm plugin.wasm
    

    This file instructs Docker to create a container image that contains your Proxy-Wasm plugin, copied from the Rust build output directory, target/wasm32-wasi/release.

  3. Build the container image:

    docker build --no-cache --platform wasm -t my-wasm-plugin .
    
  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
    

    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 the image is stored
    • IMAGE: the name of the container image in the repository

    For example:

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

Alternatively, to package the plugin code by using Cloud Build, create a cloudbuild.yaml file in the current directory, with the following contents:

steps:
- name: 'gcr.io/cloud-builders/docker'
  args: [ 'build', '--no-cache', '--platform', 'wasm',
          '-t', '<LOCATION>-docker.pkg.dev/<PROJECT>/<REPOSITORY>/my-wasm-plugin:prod', '.' ]
images: ['<LOCATION>-docker.pkg.dev/<PROJECT>/<REPOSITORY>/my-wasm-plugin:prod']

Publish the image to Artifact Registry

You need to publish the image to Artifact Registry so that Google Cloud services can access them. To publish the image, you can use either Docker or Cloud Build. In both cases, start with the following step:

To publish the image by using Docker, follow these steps:

  1. Configure Docker to authenticate to Artifact Registry Docker repositories.

  2. Publish your tagged container image to Artifact Registry.

    docker push LOCATION-docker.pkg.dev/PROJECT_ID/REPOSITORY/IMAGE:prod
    
  3. 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
    

To publish the image by using Cloud Build, run the following command:

gcloud builds submit --config cloudbuild.yaml .

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.

Update the plugin code for 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 ExampleRootContext and ExampleContext as the classes to use for root and stream context instances. It then reads a secret authorization value from plugin configuration data, which stream context instances can access through the root context object.

#include <string>
#include <string_view>

#include "third_party/proxy_wasm_cpp_sdk/proxy_wasm_intrinsics.h"

class ExampleRootContext : public RootContext {
 public:
  explicit ExampleRootContext(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 ExampleContext : public Context {
 public:
  explicit ExampleContext(uint32_t id, RootContext* root)
      : Context(id, root),
        secret_(static_cast<ExampleRootContext*>(root)->secret()) {}

  FilterHeadersStatus onRequestHeaders(uint32_t headers,
                                       bool end_of_stream) override {
    // use secret_
    return FilterHeadersStatus::Continue;
  }

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

static RegisterContextFactory register_ExampleContext(
    CONTEXT_FACTORY(ExampleContext), ROOT_FACTORY(ExampleRootContext));

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.

To update the plugin code, implement the on_configure function to receive the configuration data on startup. For example, add the following lines of code to your plugin code so that the on_configure method can read the secret authorization value from configuration data and pass it to a newly instantiated HttpContext trait for request processing.

struct DemoPluginRoot {
    secret: Rc<String>,
}

impl RootContext for DemoPluginRoot {
    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(DemoPlugin {
            secret: self.secret.clone(),
        }))
    }

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

impl Context for DemoPluginRoot {}

Upload the configuration file

If the size of the data to be delivered to your plugin in on_configure exceeds 1 MB, 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.

You can upload smaller configuration files from a local folder later, while creating the plugin.

After you publish the plugin code

You're now ready to create a plugin and then deploy the plugin in Media CDN routes.

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_RESPONSE_HEADERS: Invoked to process HTTP response headers. Context::onResponseHeaders HttpContext::on_http_response_headers
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