diff --git a/controller/internal/istio/envoyfilter.go b/controller/internal/istio/envoyfilter.go index 5b71dce4..b19c9636 100644 --- a/controller/internal/istio/envoyfilter.go +++ b/controller/internal/istio/envoyfilter.go @@ -206,13 +206,36 @@ func GenerateRouteFilter(host *model.VirtualHost, route string, config map[strin } } -func GenerateLDSFilterViaECDS(key string, ldsName string, hasHCM bool, config map[string]interface{}) *istiov1a3.EnvoyFilter { +func GenerateLDSFilter(key string, ldsName string, hasHCM bool, config map[string]interface{}) *istiov1a3.EnvoyFilter { ef := &istiov1a3.EnvoyFilter{ Spec: istioapi.EnvoyFilter{}, } - if config[model.ECDSListenerFilter] != nil { - cfg, _ := config[model.ECDSListenerFilter].([]*fmModel.FilterConfig) + if config[model.CategoryListener] != nil { + cfg, _ := config[model.CategoryListener].([]*fmModel.FilterConfig) + for _, filter := range cfg { + c, _ := filter.Config.(map[string]interface{}) + ef.Spec.ConfigPatches = append(ef.Spec.ConfigPatches, + &istioapi.EnvoyFilter_EnvoyConfigObjectPatch{ + ApplyTo: istioapi.EnvoyFilter_LISTENER, + Match: &istioapi.EnvoyFilter_EnvoyConfigObjectMatch{ + ObjectTypes: &istioapi.EnvoyFilter_EnvoyConfigObjectMatch_Listener{ + Listener: &istioapi.EnvoyFilter_ListenerMatch{ + Name: ldsName, + }, + }, + }, + Patch: &istioapi.EnvoyFilter_Patch{ + Operation: istioapi.EnvoyFilter_Patch_MERGE, + Value: MustNewStruct(c), + }, + }, + ) + } + } + + if config[model.CategoryECDSListener] != nil { + cfg, _ := config[model.CategoryECDSListener].([]*fmModel.FilterConfig) for i := len(cfg) - 1; i >= 0; i-- { filter := cfg[i] ecdsName := key + "-" + filter.Name @@ -257,8 +280,8 @@ func GenerateLDSFilterViaECDS(key string, ldsName string, hasHCM bool, config ma } } - if config[model.ECDSNetworkFilter] != nil { - cfg, _ := config[model.ECDSNetworkFilter].([]*fmModel.FilterConfig) + if config[model.CategoryECDSNetwork] != nil { + cfg, _ := config[model.CategoryECDSNetwork].([]*fmModel.FilterConfig) for i := len(cfg) - 1; i >= 0; i-- { filter := cfg[i] ecdsName := key + "-" + filter.Name @@ -304,11 +327,11 @@ func GenerateLDSFilterViaECDS(key string, ldsName string, hasHCM bool, config ma } if hasHCM { - cfg := config[model.ECDSGolangFilter] + cfg := config[model.CategoryECDSGolang] if cfg == nil { cfg = map[string]interface{}{} } - ecdsName := key + "-" + model.GolangPluginsFilter + ecdsName := key + "-" + model.CategoryGolangPlugins ef.Spec.ConfigPatches = append(ef.Spec.ConfigPatches, &istioapi.EnvoyFilter_EnvoyConfigObjectPatch{ ApplyTo: istioapi.EnvoyFilter_HTTP_FILTER, diff --git a/controller/internal/model/model.go b/controller/internal/model/model.go index f90606fc..e53da5f6 100644 --- a/controller/internal/model/model.go +++ b/controller/internal/model/model.go @@ -47,9 +47,9 @@ type VirtualHost struct { } const ( - ECDSGolangFilter = "golang" - ECDSListenerFilter = "listener" - ECDSNetworkFilter = "network" - - GolangPluginsFilter = "golang-filter" + CategoryECDSGolang = "ecds_golang" + CategoryECDSListener = "ecds_listener" + CategoryECDSNetwork = "ecds_network" + CategoryListener = "listener" + CategoryGolangPlugins = "golang-filter" ) diff --git a/controller/internal/translation/final_state.go b/controller/internal/translation/final_state.go index 4e9f0556..cff79cf8 100644 --- a/controller/internal/translation/final_state.go +++ b/controller/internal/translation/final_state.go @@ -131,7 +131,7 @@ func toFinalState(_ *Ctx, state *mergedState) (*FinalState, error) { info = gateway.Policy.Info } - ef := istio.GenerateLDSFilterViaECDS(key, name, gateway.Gateway.HasHCM, config) + ef := istio.GenerateLDSFilter(key, name, gateway.Gateway.HasHCM, config) ef.SetNamespace(ns) // Put all LDS level filters of the same LDS into the same EnvoyFilter. efName := envoyFilterNameFromLds(name) diff --git a/controller/internal/translation/merged_state.go b/controller/internal/translation/merged_state.go index 6226a09b..2f91771f 100644 --- a/controller/internal/translation/merged_state.go +++ b/controller/internal/translation/merged_state.go @@ -86,7 +86,7 @@ type PolicyKind int const ( PolicyKindRDS PolicyKind = iota - PolicyKindECDS + PolicyKindLDS ) func translateFilterManagerConfigToPolicyInRDS(fmc *filtermanager.FilterManagerConfig, @@ -173,7 +173,7 @@ func translateFilterManagerConfigToPolicyInRDS(fmc *filtermanager.FilterManagerC golangFilterName := "htnn.filters.http.golang" if ctrlcfg.EnableLDSPluginViaECDS() { - golangFilterName = virtualHost.ECDSResourceName + "-" + model.GolangPluginsFilter + golangFilterName = virtualHost.ECDSResourceName + "-" + model.CategoryGolangPlugins } config[golangFilterName] = map[string]interface{}{ "@type": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.ConfigsPerRoute", @@ -196,15 +196,16 @@ func translateFilterManagerConfigToPolicyInRDS(fmc *filtermanager.FilterManagerC return config } -func translateFilterManagerConfigToPolicyInECDS(fmc *filtermanager.FilterManagerConfig, nsName *types.NamespacedName) map[string]interface{} { +func translateFilterManagerConfigToPolicyInLDS(fmc *filtermanager.FilterManagerConfig, nsName *types.NamespacedName) map[string]interface{} { config := map[string]interface{}{} goFilterManager := &filtermanager.FilterManagerConfig{ Plugins: []*fmModel.FilterConfig{}, } nativeFilters := map[string][]*fmModel.FilterConfig{ - model.ECDSListenerFilter: {}, - model.ECDSNetworkFilter: {}, + model.CategoryECDSListener: {}, + model.CategoryECDSNetwork: {}, + model.CategoryListener: {}, } consumerNeeded := false @@ -243,19 +244,28 @@ func translateFilterManagerConfigToPolicyInECDS(fmc *filtermanager.FilterManager continue } - url := nativePlugin.ConfigTypeURL() m, ok := cfg.(map[string]interface{}) if !ok { panic(fmt.Sprintf("unexpected type: %s", reflect.TypeOf(cfg))) } - m["@type"] = url - plugin.Config = m - - if order.Position == plugins.OrderPositionListener { - nativeFilters[model.ECDSListenerFilter] = append(nativeFilters[model.ECDSListenerFilter], plugin) - } else if order.Position == plugins.OrderPositionNetwork { - nativeFilters[model.ECDSNetworkFilter] = append(nativeFilters[model.ECDSNetworkFilter], plugin) + url := nativePlugin.ConfigTypeURL() + if url != "" { + m["@type"] = url + plugin.Config = m + + if order.Position == plugins.OrderPositionListener { + nativeFilters[model.CategoryECDSListener] = append(nativeFilters[model.CategoryECDSListener], plugin) + } else if order.Position == plugins.OrderPositionNetwork { + nativeFilters[model.CategoryECDSNetwork] = append(nativeFilters[model.CategoryECDSNetwork], plugin) + } + } else { + plugin.Config = m + + if order.Position == plugins.OrderPositionListener { + nativeFilters[model.CategoryListener] = append(nativeFilters[model.CategoryListener], plugin) + } + // TODO: support network filter } } } @@ -277,7 +287,7 @@ func translateFilterManagerConfigToPolicyInECDS(fmc *filtermanager.FilterManager } } cfg["plugins"] = plugins - config[model.ECDSGolangFilter] = cfg + config[model.CategoryECDSGolang] = cfg } for category, filters := range nativeFilters { @@ -325,8 +335,8 @@ func toMergedPolicy(nsName *types.NamespacedName, policies []*FilterPolicyWrappe var config map[string]interface{} if policyKind == PolicyKindRDS { config = translateFilterManagerConfigToPolicyInRDS(fmc, nsName, virtualHost) - } else if policyKind == PolicyKindECDS { - config = translateFilterManagerConfigToPolicyInECDS(fmc, nsName) + } else if policyKind == PolicyKindLDS { + config = translateFilterManagerConfigToPolicyInLDS(fmc, nsName) } return &mergedPolicy{ @@ -384,7 +394,7 @@ func toMergedState(ctx *Ctx, state *dataPlaneState) (*FinalState, error) { Gateway: gateway.Gateway, } if len(gateway.Policies) > 0 { - mg.Policy = toMergedPolicy(&gateway.Gateway.GatewaySection.NsName, gateway.Policies, PolicyKindECDS, nil) + mg.Policy = toMergedPolicy(&gateway.Gateway.GatewaySection.NsName, gateway.Policies, PolicyKindLDS, nil) } mergedGateways[name] = mg diff --git a/controller/plugins/listenerpatch/config.go b/controller/plugins/listenerpatch/config.go new file mode 100644 index 00000000..08cf8300 --- /dev/null +++ b/controller/plugins/listenerpatch/config.go @@ -0,0 +1,32 @@ +// Copyright The HTNN Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package listenerpatch + +import ( + "mosn.io/htnn/api/pkg/plugins" + "mosn.io/htnn/types/plugins/listenerpatch" +) + +func init() { + plugins.RegisterPlugin(listenerpatch.Name, &plugin{}) +} + +type plugin struct { + listenerpatch.Plugin +} + +func (p *plugin) ConfigTypeURL() string { + return "" +} diff --git a/controller/plugins/plugins.go b/controller/plugins/plugins.go index 18a5a335..4e5c4d35 100644 --- a/controller/plugins/plugins.go +++ b/controller/plugins/plugins.go @@ -20,6 +20,7 @@ import ( _ "mosn.io/htnn/controller/plugins/cors" _ "mosn.io/htnn/controller/plugins/extproc" _ "mosn.io/htnn/controller/plugins/fault" + _ "mosn.io/htnn/controller/plugins/listenerpatch" _ "mosn.io/htnn/controller/plugins/localratelimit" _ "mosn.io/htnn/controller/plugins/lua" _ "mosn.io/htnn/controller/plugins/networkrbac" diff --git a/controller/plugins/testdata/network/listener_patch.in.yml b/controller/plugins/testdata/network/listener_patch.in.yml new file mode 100644 index 00000000..a38509e2 --- /dev/null +++ b/controller/plugins/testdata/network/listener_patch.in.yml @@ -0,0 +1,21 @@ +apiVersion: htnn.mosn.io/v1 +kind: FilterPolicy +metadata: + name: policy + namespace: default +spec: + targetRef: + group: networking.istio.io + kind: Gateway + name: default + filters: + listenerPatch: + config: + accessLog: + - name: envoy.access_loggers.file + typedConfig: + "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /home/logs/access.log + logFormat: + textFormatSource: + inlineString: "%START_TIME%,%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%" diff --git a/controller/plugins/testdata/network/listener_patch.out.yml b/controller/plugins/testdata/network/listener_patch.out.yml new file mode 100644 index 00000000..3c32b00f --- /dev/null +++ b/controller/plugins/testdata/network/listener_patch.out.yml @@ -0,0 +1,22 @@ +- metadata: + creationTimestamp: null + name: htnn-lds-0.0.0.0-18000 + namespace: default + spec: + configPatches: + - applyTo: LISTENER + match: + listener: + name: 0.0.0.0_18000 + patch: + operation: MERGE + value: + accessLog: + - name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + logFormat: + textFormatSource: + inlineString: '%START_TIME%,%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%' + path: /home/logs/access.log + status: {} diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index c8e392b2..79055817 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -33,10 +33,12 @@ func TestE2E(t *testing.T) { if err != nil { t.Fatalf("Error loading Kubernetes config: %v", err) } + client, err := client.New(cfg, client.Options{}) if err != nil { t.Fatalf("Error initializing Kubernetes client: %v", err) } + clientset, err := kubernetes.NewForConfig(cfg) if err != nil { t.Fatalf("Error initializing Kubernetes REST client: %v", err) diff --git a/e2e/pkg/suite/suite.go b/e2e/pkg/suite/suite.go index 1fc3e4f9..d4dadf58 100644 --- a/e2e/pkg/suite/suite.go +++ b/e2e/pkg/suite/suite.go @@ -31,6 +31,8 @@ import ( "time" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/gateway-api/conformance/utils/roundtripper" @@ -250,3 +252,30 @@ func (suite *Suite) Capture(resp *http.Response) (*roundtripper.CapturedRequest, return cReq, cRes, nil } + +func (suite *Suite) GetLog(namespace string, prefix string) ([]byte, error) { + ctx := context.Background() + clientset := suite.Opt.Clientset + + podName := "" + pods, err := clientset.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, err + } + + for _, pod := range pods.Items { + if strings.HasPrefix(pod.Name, prefix) { + podName = pod.Name + break + } + } + + req := clientset.CoreV1().Pods(namespace).GetLogs(podName, &corev1.PodLogOptions{}) + podLogs, err := req.Stream(ctx) + if err != nil { + return nil, err + } + defer podLogs.Close() + + return io.ReadAll(podLogs) +} diff --git a/e2e/tests/listener_patch.go b/e2e/tests/listener_patch.go new file mode 100644 index 00000000..b2969cc9 --- /dev/null +++ b/e2e/tests/listener_patch.go @@ -0,0 +1,51 @@ +// Copyright The HTNN Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tests + +import ( + "bytes" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "mosn.io/htnn/e2e/pkg/k8s" + "mosn.io/htnn/e2e/pkg/suite" +) + +func init() { + suite.Register(suite.Test{ + Run: func(t *testing.T, suite *suite.Suite) { + hdr := http.Header{} + hdr.Add("Connection", "close") + rsp, err := suite.Get("/echo", hdr) + require.NoError(t, err) + require.Equal(t, 200, rsp.StatusCode) + + time.Sleep(100 * time.Millisecond) + require.Eventually(t, func() bool { + namespace := k8s.DefaultNamespace + + b, err := suite.GetLog(namespace, "default-istio-") + if err != nil { + t.Logf("unexpected error %v", err) + return false + } + return bytes.Contains(b, []byte("added access log: 127.0.0.1:10000")) + }, 10*time.Second, 100*time.Millisecond) + }, + }) +} diff --git a/e2e/tests/listener_patch.yml b/e2e/tests/listener_patch.yml new file mode 100644 index 00000000..0bb02919 --- /dev/null +++ b/e2e/tests/listener_patch.yml @@ -0,0 +1,39 @@ +apiVersion: htnn.mosn.io/v1 +kind: FilterPolicy +metadata: + name: policy +spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: default + filters: + listenerPatch: + config: + accessLog: + - name: envoy.access_loggers.file + typedConfig: + "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + logFormat: + textFormatSource: + inlineString: "added access log: %DOWNSTREAM_LOCAL_ADDRESS% %EMIT_TIME%\n" +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: test +spec: + parentRefs: + - name: default + namespace: e2e + hostnames: ["localhost"] + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: backend + port: 8080 + diff --git a/site/content/en/docs/reference/plugins/listener_patch.md b/site/content/en/docs/reference/plugins/listener_patch.md new file mode 100644 index 00000000..f26a7d7f --- /dev/null +++ b/site/content/en/docs/reference/plugins/listener_patch.md @@ -0,0 +1,59 @@ +--- +title: Listener Patch +--- + +## Description + +The `listenerPatch` plugin allows users to directly patch the Listener resource of Envoy generated from the corresponding Gateway. + +## Attribute + +| | | +|-------|----------| +| Type | General | +| Order | Listener | + +## Configuration + +Please refer to the corresponding [Envoy documentation](https://www.envoyproxy.io/docs/envoy/v1.29.5/api-v3/config/listener/v3/listener.proto#envoy-v3-api-msg-config-listener-v3-listener). + +## Usage + +Assume we have the following Gateway listening on `localhost:10000`: + +```yaml +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: default +spec: + gatewayClassName: istio + listeners: + - name: default + hostname: "*" + port: 10000 + protocol: HTTP +``` + +The following configuration will add a file-based access logger to the gateway: + +```yaml +apiVersion: htnn.mosn.io/v1 +kind: FilterPolicy +metadata: + name: policy +spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: default + filters: + accessLog: + - name: envoy.access_loggers.file + typedConfig: + "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + logFormat: + textFormatSource: + inlineString: "create listener access log: %DOWNSTREAM_LOCAL_ADDRESS% %EMIT_TIME%\n" +``` diff --git a/site/content/zh-hans/docs/reference/plugins/listener_patch.md b/site/content/zh-hans/docs/reference/plugins/listener_patch.md new file mode 100644 index 00000000..a480ea67 --- /dev/null +++ b/site/content/zh-hans/docs/reference/plugins/listener_patch.md @@ -0,0 +1,59 @@ +--- +title: Listener Patch +--- + +## 说明 + +`listenerPatch` 插件允许用户直接给 Gateway 对应的 Envoy Listener 资源打补丁。 + +## 属性 + +| | | +|-------|----------| +| Type | General | +| Order | Listener | + +## 配置 + +请查阅对应的 [Envoy 文档](https://www.envoyproxy.io/docs/envoy/v1.29.5/api-v3/config/listener/v3/listener.proto#envoy-v3-api-msg-config-listener-v3-listener)。 + +## 用法 + +假设我们有以下的 Gateway 在 `localhost:10000` 上监听: + +```yaml +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: default +spec: + gatewayClassName: istio + listeners: + - name: default + hostname: "*" + port: 10000 + protocol: HTTP +``` + +下面的配置将一个基于文件的 access logger 添加至该网关: + +```yaml +apiVersion: htnn.mosn.io/v1 +kind: FilterPolicy +metadata: + name: policy +spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: default + filters: + accessLog: + - name: envoy.access_loggers.file + typedConfig: + "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + logFormat: + textFormatSource: + inlineString: "create listener access log: %DOWNSTREAM_LOCAL_ADDRESS% %EMIT_TIME%\n" +``` diff --git a/types/go.mod b/types/go.mod index bbde3bd1..0a1f36fe 100644 --- a/types/go.mod +++ b/types/go.mod @@ -90,6 +90,7 @@ require ( go.opentelemetry.io/otel/metric v1.21.0 // indirect go.opentelemetry.io/otel/sdk v1.21.0 // indirect go.opentelemetry.io/otel/trace v1.21.0 // indirect + go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect @@ -100,6 +101,7 @@ require ( golang.org/x/time v0.5.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + google.golang.org/grpc v1.63.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/types/plugins/listenerpatch/config.go b/types/plugins/listenerpatch/config.go new file mode 100644 index 00000000..d655ed0d --- /dev/null +++ b/types/plugins/listenerpatch/config.go @@ -0,0 +1,109 @@ +// Copyright The HTNN Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package listenerpatch + +import ( + "fmt" + + envoyapi "github.com/envoyproxy/go-control-plane/envoy/api/v2" + file "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/file/v3" + grpc "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/grpc/v3" + opentelemetry "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/open_telemetry/v3" + stream "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/stream/v3" + wasm "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/wasm/v3" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + + "mosn.io/htnn/api/pkg/filtermanager/api" + "mosn.io/htnn/api/pkg/plugins" +) + +const ( + Name = "listenerPatch" +) + +func init() { + plugins.RegisterPluginType(Name, &Plugin{}) +} + +type Plugin struct { + plugins.PluginMethodDefaultImpl +} + +func (p *Plugin) Order() plugins.PluginOrder { + return plugins.PluginOrder{ + Position: plugins.OrderPositionListener, + } +} + +func (p *Plugin) Type() plugins.PluginType { + return plugins.TypeGeneral +} + +func (p *Plugin) Config() api.PluginConfig { + return &CustomConfig{} +} + +type CustomConfig struct { + envoyapi.Listener +} + +type config interface { + protoreflect.ProtoMessage + + Validate() error + Reset() +} + +var ( + loggerTypedConfigs = map[string]config{ + "type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog": &file.FileAccessLog{}, + "type.googleapis.com/envoy.extensions.access_loggers.open_telemetry.v3.OpenTelemetryAccessLogConfig": &opentelemetry.OpenTelemetryAccessLogConfig{}, + "type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog": &stream.StdoutAccessLog{}, + "type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StderrAccessLog": &stream.StderrAccessLog{}, + "type.googleapis.com/envoy.extensions.access_loggers.grpc.v3.HttpGrpcAccessLogConfig": &grpc.HttpGrpcAccessLogConfig{}, + "type.googleapis.com/envoy.extensions.access_loggers.grpc.v3.TcpGrpcAccessLogConfig": &grpc.TcpGrpcAccessLogConfig{}, + "type.googleapis.com/envoy.extensions.access_loggers.wasm.v3.WasmAccessLog": &wasm.WasmAccessLog{}, + } +) + +func (conf *CustomConfig) Validate() error { + // We can't use the default validation because the listener is not a full listener. + for _, logger := range conf.Listener.AccessLog { + tc := logger.GetTypedConfig() + if tc == nil { + return fmt.Errorf("access log config is nil") + } + + typeURL := tc.TypeUrl + cfg, ok := loggerTypedConfigs[typeURL] + if !ok { + return fmt.Errorf("unknown logger type: %s", typeURL) + } + + // We always call Validate after Unmarshal success + _ = proto.Unmarshal(tc.GetValue(), cfg) + + err := cfg.Validate() + cfg.Reset() + if err != nil { + return err + } + } + + // TODO: support other fields + + return nil +} diff --git a/types/plugins/listenerpatch/config_test.go b/types/plugins/listenerpatch/config_test.go new file mode 100644 index 00000000..e9aa5ba4 --- /dev/null +++ b/types/plugins/listenerpatch/config_test.go @@ -0,0 +1,104 @@ +// Copyright The HTNN Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package listenerpatch + +import ( + "testing" + + _ "github.com/envoyproxy/go-control-plane/envoy/extensions/matching/common_inputs/network/v3" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/encoding/protojson" +) + +func TestConfig(t *testing.T) { + tests := []struct { + name string + input string + err string + }{ + { + name: "bad access log", + input: ` +{ + "accessLog": [{ + }] +}`, + err: `access log config is nil`, + }, + { + name: "validate log format", + input: ` +{ + "accessLog": [{ + "name": "envoy.access_loggers.file", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog", + "logFormat": "%START_TIME%,%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%" + } + }] +}`, + err: `unexpected token "%START_TIME%`, + }, + { + name: "validate file logger", + input: ` +{ + "accessLog": [{ + "name": "envoy.access_loggers.file", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog", + "logFormat": { + "textFormat": "%START_TIME%,%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%" + }, + "path": "" + } + }] +}`, + err: `invalid FileAccessLog.Path: value length must be at least 1 runes`, + }, + { + name: "unknown logger", + input: ` +{ + "accessLog": [{ + "name": "envoy.access_loggers.http_file", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.access_loggers.file.v3.HTTPFileLogger", + "logFormat": { + "textFormat": "%START_TIME%,%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%" + }, + "path": "/xx" + } + }] +}`, + err: `unable to resolve`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conf := &CustomConfig{} + err := protojson.Unmarshal([]byte(tt.input), conf) + if err == nil { + err = conf.Validate() + } + if tt.err == "" { + assert.Nil(t, err) + } else { + assert.ErrorContains(t, err, tt.err) + } + }) + } +} diff --git a/types/plugins/plugins.go b/types/plugins/plugins.go index 2eaa221e..7578c6a5 100644 --- a/types/plugins/plugins.go +++ b/types/plugins/plugins.go @@ -30,6 +30,7 @@ import ( _ "mosn.io/htnn/types/plugins/keyauth" _ "mosn.io/htnn/types/plugins/limitcountredis" _ "mosn.io/htnn/types/plugins/limitreq" + _ "mosn.io/htnn/types/plugins/listenerpatch" _ "mosn.io/htnn/types/plugins/localratelimit" _ "mosn.io/htnn/types/plugins/lua" _ "mosn.io/htnn/types/plugins/networkrbac"