mirror of https://github.com/hashicorp/consul
121 lines
4.2 KiB
Go
121 lines
4.2 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package wasm
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
|
|
envoy_http_wasm_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/wasm/v3"
|
|
envoy_wasm_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/wasm/v3"
|
|
|
|
"github.com/hashicorp/consul/api"
|
|
cmn "github.com/hashicorp/consul/envoyextensions/extensioncommon"
|
|
)
|
|
|
|
// wasm is a built-in Envoy extension that can patch filter chains to insert Wasm plugins.
|
|
type wasm struct {
|
|
cmn.BasicExtensionAdapter
|
|
|
|
name string
|
|
wasmConfig *wasmConfig
|
|
}
|
|
|
|
var supportedRuntimes = []string{"v8", "wamr", "wavm", "wasmtime"}
|
|
|
|
var _ cmn.BasicExtension = (*wasm)(nil)
|
|
|
|
func Constructor(ext api.EnvoyExtension) (cmn.EnvoyExtender, error) {
|
|
w, err := construct(ext)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &cmn.BasicEnvoyExtender{
|
|
Extension: &w,
|
|
}, nil
|
|
}
|
|
|
|
func construct(ext api.EnvoyExtension) (wasm, error) {
|
|
w := wasm{name: ext.Name}
|
|
|
|
if w.name != api.BuiltinWasmExtension {
|
|
return w, fmt.Errorf("expected extension name %q but got %q", api.BuiltinWasmExtension, w.name)
|
|
}
|
|
|
|
if err := w.fromArguments(ext.Arguments); err != nil {
|
|
return w, err
|
|
}
|
|
|
|
// Configure the failure behavior for the filter. If the plugin is required,
|
|
// then filter runtime errors result in a failed request (fail "closed").
|
|
// Otherwise, runtime errors result in the filter being skipped (fail "open").
|
|
w.wasmConfig.PluginConfig.failOpen = !ext.Required
|
|
|
|
return w, nil
|
|
}
|
|
|
|
func (w *wasm) fromArguments(args map[string]any) error {
|
|
var err error
|
|
w.wasmConfig, err = newWasmConfig(args)
|
|
if err != nil {
|
|
return fmt.Errorf("error decoding extension arguments: %w", err)
|
|
}
|
|
return w.wasmConfig.validate()
|
|
}
|
|
|
|
// CanApply indicates if the WASM extension can be applied to the given extension configuration.
|
|
func (w wasm) CanApply(cfg *cmn.RuntimeConfig) bool {
|
|
return cfg.Kind == w.wasmConfig.ProxyType &&
|
|
cfg.Protocol == w.wasmConfig.Protocol
|
|
}
|
|
|
|
func (w wasm) matchesConfigDirection(isInboundListener bool) bool {
|
|
return (isInboundListener && w.wasmConfig.ListenerType == "inbound") ||
|
|
(!isInboundListener && w.wasmConfig.ListenerType == "outbound")
|
|
}
|
|
|
|
// PatchFilters adds a Wasm HTTP or TCP filter to the filter chain.
|
|
func (w wasm) PatchFilters(cfg *cmn.RuntimeConfig, filters []*envoy_listener_v3.Filter, isInboundListener bool) ([]*envoy_listener_v3.Filter, error) {
|
|
if !w.matchesConfigDirection(isInboundListener) {
|
|
return filters, nil
|
|
}
|
|
|
|
// Check that the Wasm plugin protocol matches the service protocol.
|
|
// It is a runtime error if the extension is configured to apply a Wasm plugin
|
|
// that doesn't match the service's protocol.
|
|
// This shouldn't happen because the caller should check CanApply first, but just in case.
|
|
if cfg.Protocol != w.wasmConfig.Protocol {
|
|
return filters, fmt.Errorf("failed to apply Wasm filter: service protocol for %q is %q but protocol for the Wasm extension is %q. Please ensure the protocols match",
|
|
cfg.ServiceName.Name, cfg.Protocol, w.wasmConfig.Protocol)
|
|
}
|
|
|
|
// Generate the Wasm plugin configuration. It is the same config for HTTP and network filters.
|
|
wasmPluginConfig, err := w.wasmConfig.PluginConfig.envoyPluginConfig(cfg)
|
|
if err != nil {
|
|
return filters, fmt.Errorf("failed to encode Envoy Wasm plugin configuration: %w", err)
|
|
}
|
|
|
|
// Insert the filter immediately before the terminal filter.
|
|
insertOptions := cmn.InsertOptions{Location: cmn.InsertBeforeFirstMatch}
|
|
|
|
switch cfg.Protocol {
|
|
case "grpc", "http2", "http":
|
|
insertOptions.FilterName = "envoy.filters.http.router"
|
|
filter, err := cmn.MakeEnvoyHTTPFilter("envoy.filters.http.wasm", &envoy_http_wasm_v3.Wasm{Config: wasmPluginConfig})
|
|
if err != nil {
|
|
return filters, fmt.Errorf("failed to make Wasm HTTP filter: %w", err)
|
|
}
|
|
return cmn.InsertHTTPFilter(filters, filter, insertOptions)
|
|
case "tcp":
|
|
fallthrough
|
|
default:
|
|
insertOptions.FilterName = "envoy.filters.network.tcp_proxy"
|
|
filter, err := cmn.MakeFilter("envoy.filters.network.wasm", &envoy_wasm_v3.Wasm{Config: wasmPluginConfig})
|
|
if err != nil {
|
|
return filters, fmt.Errorf("failed to make Wasm network filter: %w", err)
|
|
}
|
|
return cmn.InsertNetworkFilter(filters, filter, insertOptions)
|
|
}
|
|
}
|