mirror of https://github.com/hashicorp/consul
144 lines
4.8 KiB
Go
144 lines
4.8 KiB
Go
|
// Copyright (c) HashiCorp, Inc.
|
||
|
// SPDX-License-Identifier: MPL-2.0
|
||
|
|
||
|
package wasm
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
|
||
|
envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
|
||
|
envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
|
||
|
envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
||
|
envoy_http_wasm_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/wasm/v3"
|
||
|
envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
|
||
|
envoy_resource_v3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
|
||
|
|
||
|
"github.com/hashicorp/consul/api"
|
||
|
"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 {
|
||
|
name string
|
||
|
wasmConfig *wasmConfig
|
||
|
}
|
||
|
|
||
|
var supportedRuntimes = []string{"v8", "wamr", "wavm", "wasmtime"}
|
||
|
|
||
|
var _ extensioncommon.BasicExtension = (*wasm)(nil)
|
||
|
|
||
|
func Constructor(ext api.EnvoyExtension) (extensioncommon.EnvoyExtender, error) {
|
||
|
w, err := construct(ext)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return &extensioncommon.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.
|
||
|
// Currently the Wasm extension can be applied if the extension configuration is for an inbound
|
||
|
// listener on the a local connect-proxy.
|
||
|
// It does not patch extensions for service upstreams.
|
||
|
func (w wasm) CanApply(config *extensioncommon.RuntimeConfig) bool {
|
||
|
return config.IsLocal() && w.wasmConfig.ListenerType == "inbound" &&
|
||
|
config.Kind == w.wasmConfig.ProxyType
|
||
|
|
||
|
}
|
||
|
|
||
|
// PatchRoute does nothing for the WASM extension.
|
||
|
func (w wasm) PatchRoute(_ *extensioncommon.RuntimeConfig, r *envoy_route_v3.RouteConfiguration) (*envoy_route_v3.RouteConfiguration, bool, error) {
|
||
|
return r, false, nil
|
||
|
}
|
||
|
|
||
|
// PatchCluster does nothing for the WASM extension.
|
||
|
func (w wasm) PatchCluster(_ *extensioncommon.RuntimeConfig, c *envoy_cluster_v3.Cluster) (*envoy_cluster_v3.Cluster, bool, error) {
|
||
|
return c, false, nil
|
||
|
}
|
||
|
|
||
|
// PatchFilter adds a Wasm filter to the HTTP filter chain.
|
||
|
// TODO (wasm/tcp): Add support for TCP filters.
|
||
|
func (w wasm) PatchFilter(cfg *extensioncommon.RuntimeConfig, filter *envoy_listener_v3.Filter) (*envoy_listener_v3.Filter, bool, error) {
|
||
|
if filter.Name != "envoy.filters.network.http_connection_manager" {
|
||
|
return filter, false, nil
|
||
|
}
|
||
|
if typedConfig := filter.GetTypedConfig(); typedConfig == nil {
|
||
|
return filter, false, errors.New("failed to get typed config for http filter")
|
||
|
}
|
||
|
|
||
|
httpConnMgr := envoy_resource_v3.GetHTTPConnectionManager(filter)
|
||
|
if httpConnMgr == nil {
|
||
|
return filter, false, errors.New("failed to get HTTP connection manager")
|
||
|
}
|
||
|
|
||
|
wasmPluginConfig, err := w.wasmConfig.PluginConfig.envoyPluginConfig(cfg)
|
||
|
if err != nil {
|
||
|
return filter, false, fmt.Errorf("failed to encode Envoy Wasm configuration: %w", err)
|
||
|
}
|
||
|
|
||
|
extHttpFilter, err := extensioncommon.MakeEnvoyHTTPFilter(
|
||
|
"envoy.filters.http.wasm",
|
||
|
&envoy_http_wasm_v3.Wasm{Config: wasmPluginConfig},
|
||
|
)
|
||
|
if err != nil {
|
||
|
return filter, false, err
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
changedFilters = make([]*envoy_http_v3.HttpFilter, 0, len(httpConnMgr.HttpFilters)+1)
|
||
|
changed bool
|
||
|
)
|
||
|
|
||
|
// We need to be careful about overwriting http filters completely because
|
||
|
// http filters validates intentions with the RBAC filter. This inserts the
|
||
|
// filter before `envoy.filters.http.router` while keeping everything
|
||
|
// else intact.
|
||
|
for _, httpFilter := range httpConnMgr.HttpFilters {
|
||
|
if httpFilter.Name == "envoy.filters.http.router" {
|
||
|
changedFilters = append(changedFilters, extHttpFilter)
|
||
|
changed = true
|
||
|
}
|
||
|
changedFilters = append(changedFilters, httpFilter)
|
||
|
}
|
||
|
if changed {
|
||
|
httpConnMgr.HttpFilters = changedFilters
|
||
|
}
|
||
|
|
||
|
newFilter, err := extensioncommon.MakeFilter("envoy.filters.network.http_connection_manager", httpConnMgr)
|
||
|
if err != nil {
|
||
|
return filter, false, errors.New("error making new filter")
|
||
|
}
|
||
|
|
||
|
return newFilter, true, nil
|
||
|
}
|