diff --git a/api/v1alpha1/validation/envoyproxy_validate.go b/api/v1alpha1/validation/envoyproxy_validate.go index a1f14655996..bb880891e2c 100644 --- a/api/v1alpha1/validation/envoyproxy_validate.go +++ b/api/v1alpha1/validation/envoyproxy_validate.go @@ -9,17 +9,15 @@ import ( "errors" "fmt" "net/netip" - "reflect" bootstrapv3 "github.com/envoyproxy/go-control-plane/envoy/config/bootstrap/v3" clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" "github.com/google/go-cmp/cmp" - "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/testing/protocmp" utilerrors "k8s.io/apimachinery/pkg/util/errors" - "sigs.k8s.io/yaml" egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/envoyproxy/gateway/internal/utils/proto" "github.com/envoyproxy/gateway/internal/xds/bootstrap" _ "github.com/envoyproxy/gateway/internal/xds/extensions" // register the generated types to support protojson unmarshalling ) @@ -140,42 +138,33 @@ func validateService(spec *egv1a1.EnvoyProxySpec) []error { } func validateBootstrap(boostrapConfig *egv1a1.ProxyBootstrap) error { + // Validate user bootstrap config defaultBootstrap := &bootstrapv3.Bootstrap{} // TODO: need validate when enable prometheus? defaultBootstrapStr, err := bootstrap.GetRenderedBootstrapConfig(nil) if err != nil { return err } + if err := proto.FromYAML([]byte(defaultBootstrapStr), defaultBootstrap); err != nil { + return fmt.Errorf("unable to unmarshal default bootstrap: %w", err) + } + if err := defaultBootstrap.Validate(); err != nil { + return fmt.Errorf("default bootstrap validation failed: %w", err) + } + // Validate user bootstrap config userBootstrapStr, err := bootstrap.ApplyBootstrapConfig(boostrapConfig, defaultBootstrapStr) if err != nil { return err } - - jsonData, err := yaml.YAMLToJSON([]byte(userBootstrapStr)) - if err != nil { - return fmt.Errorf("unable to convert user bootstrap to json: %w", err) - } - userBootstrap := &bootstrapv3.Bootstrap{} - if err := protojson.Unmarshal(jsonData, userBootstrap); err != nil { - return fmt.Errorf("unable to unmarshal user bootstrap: %w", err) + if err := proto.FromYAML([]byte(userBootstrapStr), userBootstrap); err != nil { + return fmt.Errorf("failed to parse default bootstrap config: %w", err) } - - // Call Validate method if err := userBootstrap.Validate(); err != nil { return fmt.Errorf("validation failed for user bootstrap: %w", err) } - jsonData, err = yaml.YAMLToJSON([]byte(defaultBootstrapStr)) - if err != nil { - return fmt.Errorf("unable to convert default bootstrap to json: %w", err) - } - - if err := protojson.Unmarshal(jsonData, defaultBootstrap); err != nil { - return fmt.Errorf("unable to unmarshal default bootstrap: %w", err) - } - // Ensure dynamic resources config is same if userBootstrap.DynamicResources == nil || cmp.Diff(userBootstrap.DynamicResources, defaultBootstrap.DynamicResources, protocmp.Transform()) != "" { @@ -196,9 +185,8 @@ func validateBootstrap(boostrapConfig *egv1a1.ProxyBootstrap) error { break } } - - // nolint // Circumvents this error "Error: copylocks: call of reflect.DeepEqual copies lock value:" - if userXdsCluster == nil || !reflect.DeepEqual(*userXdsCluster.LoadAssignment, *defaultXdsCluster.LoadAssignment) { + if userXdsCluster == nil || + cmp.Diff(userXdsCluster.LoadAssignment, defaultXdsCluster.LoadAssignment, protocmp.Transform()) != "" { return fmt.Errorf("xds_cluster's loadAssigntment cannot be modified") } diff --git a/internal/utils/proto/google_proto.go b/internal/utils/proto/google_proto.go new file mode 100644 index 00000000000..fea52d070e7 --- /dev/null +++ b/internal/utils/proto/google_proto.go @@ -0,0 +1,128 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +// Copied from https://github.com/kumahq/kuma/tree/9ea78e31147a855ac54a7a2c92c724ee9a75de46/pkg/util/proto +// to avoid importing the entire kuma codebase breaking our go.mod file + +package proto + +import ( + "fmt" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/known/durationpb" +) + +type ( + MergeFunction func(dst, src protoreflect.Message) + mergeOptions struct { + customMergeFn map[protoreflect.FullName]MergeFunction + } +) +type OptionFn func(options mergeOptions) mergeOptions + +func MergeFunctionOptionFn(name protoreflect.FullName, function MergeFunction) OptionFn { + return func(options mergeOptions) mergeOptions { + options.customMergeFn[name] = function + return options + } +} + +// ReplaceMergeFn instead of merging all subfields one by one, takes src and set it to dest +var ReplaceMergeFn MergeFunction = func(dst, src protoreflect.Message) { + dst.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool { + dst.Clear(fd) + return true + }) + src.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool { + dst.Set(fd, v) + return true + }) +} + +func Merge(dst, src proto.Message) { + duration := &durationpb.Duration{} + merge(dst, src, MergeFunctionOptionFn(duration.ProtoReflect().Descriptor().FullName(), ReplaceMergeFn)) +} + +// Merge Code of proto.Merge with modifications to support custom types +func merge(dst, src proto.Message, opts ...OptionFn) { + mo := mergeOptions{customMergeFn: map[protoreflect.FullName]MergeFunction{}} + for _, opt := range opts { + mo = opt(mo) + } + mo.mergeMessage(dst.ProtoReflect(), src.ProtoReflect()) +} + +func (o mergeOptions) mergeMessage(dst, src protoreflect.Message) { + // The regular proto.mergeMessage would have a fast path method option here. + // As we want to have exceptions we always use the slow path. + if !dst.IsValid() { + panic(fmt.Sprintf("cannot merge into invalid %v message", dst.Descriptor().FullName())) + } + + src.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool { + switch { + case fd.IsList(): + o.mergeList(dst.Mutable(fd).List(), v.List(), fd) + case fd.IsMap(): + o.mergeMap(dst.Mutable(fd).Map(), v.Map(), fd.MapValue()) + case fd.Message() != nil: + mergeFn, exists := o.customMergeFn[fd.Message().FullName()] + if exists { + mergeFn(dst.Mutable(fd).Message(), v.Message()) + } else { + o.mergeMessage(dst.Mutable(fd).Message(), v.Message()) + } + case fd.Kind() == protoreflect.BytesKind: + dst.Set(fd, o.cloneBytes(v)) + default: + dst.Set(fd, v) + } + return true + }) + + if len(src.GetUnknown()) > 0 { + dst.SetUnknown(append(dst.GetUnknown(), src.GetUnknown()...)) + } +} + +func (o mergeOptions) mergeList(dst, src protoreflect.List, fd protoreflect.FieldDescriptor) { + // Merge semantics appends to the end of the existing list. + for i, n := 0, src.Len(); i < n; i++ { + switch v := src.Get(i); { + case fd.Message() != nil: + dstv := dst.NewElement() + o.mergeMessage(dstv.Message(), v.Message()) + dst.Append(dstv) + case fd.Kind() == protoreflect.BytesKind: + dst.Append(o.cloneBytes(v)) + default: + dst.Append(v) + } + } +} + +func (o mergeOptions) mergeMap(dst, src protoreflect.Map, fd protoreflect.FieldDescriptor) { + // Merge semantics replaces, rather than merges into existing entries. + src.Range(func(k protoreflect.MapKey, v protoreflect.Value) bool { + switch { + case fd.Message() != nil: + dstv := dst.NewValue() + o.mergeMessage(dstv.Message(), v.Message()) + dst.Set(k, dstv) + case fd.Kind() == protoreflect.BytesKind: + dst.Set(k, o.cloneBytes(v)) + default: + dst.Set(k, v) + } + return true + }) +} + +func (o mergeOptions) cloneBytes(v protoreflect.Value) protoreflect.Value { + return protoreflect.ValueOfBytes(append([]byte{}, v.Bytes()...)) +} diff --git a/internal/utils/proto/proto.go b/internal/utils/proto/proto.go new file mode 100644 index 00000000000..ff05e3a7150 --- /dev/null +++ b/internal/utils/proto/proto.go @@ -0,0 +1,40 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +// Copied from https://github.com/kumahq/kuma/tree/9ea78e31147a855ac54a7a2c92c724ee9a75de46/pkg/util/proto +// to avoid importing the entire kuma codebase breaking our go.mod file + +package proto + +import ( + "bytes" + + "github.com/golang/protobuf/jsonpb" + protov1 "github.com/golang/protobuf/proto" + "google.golang.org/protobuf/proto" + "sigs.k8s.io/yaml" +) + +func FromYAML(content []byte, pb proto.Message) error { + json, err := yaml.YAMLToJSON(content) + if err != nil { + return err + } + return FromJSON(json, pb) +} + +func ToYAML(pb proto.Message) ([]byte, error) { + marshaler := &jsonpb.Marshaler{} + json, err := marshaler.MarshalToString(protov1.MessageV1(pb)) + if err != nil { + return nil, err + } + return yaml.JSONToYAML([]byte(json)) +} + +func FromJSON(content []byte, out proto.Message) error { + unmarshaler := &jsonpb.Unmarshaler{AllowUnknownFields: true} + return unmarshaler.Unmarshal(bytes.NewReader(content), protov1.MessageV1(out)) +} diff --git a/internal/utils/yaml/yaml.go b/internal/utils/yaml/yaml.go deleted file mode 100644 index 42e87f97dd5..00000000000 --- a/internal/utils/yaml/yaml.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright Envoy Gateway Authors -// SPDX-License-Identifier: Apache-2.0 -// The full text of the Apache license is available in the LICENSE file at -// the root of the repo. - -package yaml - -import ( - "reflect" - - "sigs.k8s.io/yaml" -) - -// MergeYAML merges two yaml files. The second yaml file will override the first one if the same key exists. -// This method can add or override a value within a map, or add a new value to a list. -// Please note that this method can't override a value within a list. -func MergeYAML(base, override string) (string, error) { - // declare two map to hold the yaml content - map1 := map[string]interface{}{} - map2 := map[string]interface{}{} - - if err := yaml.Unmarshal([]byte(base), &map1); err != nil { - return "", err - } - - if err := yaml.Unmarshal([]byte(override), &map2); err != nil { - return "", err - } - - // merge both yaml data recursively - result := mergeMaps(map1, map2) - - out, err := yaml.Marshal(result) - if err != nil { - return "", err - } - return string(out), nil -} - -func mergeMaps(map1, map2 map[string]interface{}) map[string]interface{} { - out := make(map[string]interface{}, len(map1)) - for k, v := range map1 { - out[k] = v - } - for k, v := range map2 { - if v, ok := v.(map[string]interface{}); ok { - if bv, ok := out[k]; ok { - if bv, ok := bv.(map[string]interface{}); ok { - out[k] = mergeMaps(bv, v) - continue - } - } - } - value := reflect.ValueOf(v) - if value.Kind() == reflect.Array || value.Kind() == reflect.Slice { - out[k] = append(out[k].([]interface{}), v.([]interface{})...) - } else { - out[k] = v - } - } - return out -} diff --git a/internal/utils/yaml/yaml_test.go b/internal/utils/yaml/yaml_test.go deleted file mode 100644 index 1ba6c90faed..00000000000 --- a/internal/utils/yaml/yaml_test.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright Envoy Gateway Authors -// SPDX-License-Identifier: Apache-2.0 -// The full text of the Apache license is available in the LICENSE file at -// the root of the repo. - -package yaml - -import ( - "reflect" - "testing" -) - -func TestMergeYAML(t *testing.T) { - tests := []struct { - name string - yaml1 string - yaml2 string - want string - }{ - { - name: "test1", - yaml1: ` -a: a -b: - c: - d: d -e: - f: - - g -k: - l: l -`, - yaml2: ` -a: a1 -b: - c: - d: d1 -e: - f: - - h -i: - j: j -`, - want: `a: a1 -b: - c: - d: d1 -e: - f: - - g - - h -i: - j: j -k: - l: l -`, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, _ := MergeYAML(tt.yaml1, tt.yaml2) - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("MergeYAML() got = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/internal/xds/bootstrap/bootstrap_test.go b/internal/xds/bootstrap/bootstrap_test.go index e57708b95ac..06e8f731d50 100644 --- a/internal/xds/bootstrap/bootstrap_test.go +++ b/internal/xds/bootstrap/bootstrap_test.go @@ -87,6 +87,14 @@ func TestGetRenderedBootstrapConfig(t *testing.T) { t.Run(tc.name, func(t *testing.T) { got, err := GetRenderedBootstrapConfig(tc.proxyMetrics) require.NoError(t, err) + + if *overrideTestData { + // nolint:gosec + err = os.WriteFile(path.Join("testdata", "render", fmt.Sprintf("%s.yaml", tc.name)), []byte(got), 0644) + require.NoError(t, err) + return + } + expected, err := readTestData(tc.name) require.NoError(t, err) assert.Equal(t, expected, got) @@ -95,7 +103,7 @@ func TestGetRenderedBootstrapConfig(t *testing.T) { } func readTestData(caseName string) (string, error) { - filename := path.Join("testdata", fmt.Sprintf("%s.yaml", caseName)) + filename := path.Join("testdata", "render", fmt.Sprintf("%s.yaml", caseName)) b, err := os.ReadFile(filename) if err != nil { diff --git a/internal/xds/bootstrap/testdata/merge/default.in.yaml b/internal/xds/bootstrap/testdata/merge/default.in.yaml new file mode 100644 index 00000000000..0f6069e27d5 --- /dev/null +++ b/internal/xds/bootstrap/testdata/merge/default.in.yaml @@ -0,0 +1,13 @@ +admin: + address: + socket_address: + address: 127.0.0.1 + port_value: 20000 +layered_runtime: + layers: + - name: runtime-0 + rtds_layer: + rtds_config: + ads: {} + resource_api_version: V3 + name: runtime-0 diff --git a/internal/xds/bootstrap/testdata/merge/default.out.yaml b/internal/xds/bootstrap/testdata/merge/default.out.yaml new file mode 100644 index 00000000000..d386f8c5bdb --- /dev/null +++ b/internal/xds/bootstrap/testdata/merge/default.out.yaml @@ -0,0 +1,127 @@ +admin: + accessLog: + - name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/null + address: + socketAddress: + address: 127.0.0.1 + portValue: 20000 +dynamicResources: + adsConfig: + apiType: DELTA_GRPC + grpcServices: + - envoyGrpc: + clusterName: xds_cluster + setNodeOnFirstMessageOnly: true + transportApiVersion: V3 + cdsConfig: + ads: {} + resourceApiVersion: V3 + ldsConfig: + ads: {} + resourceApiVersion: V3 +layeredRuntime: + layers: + - name: global_config + staticLayer: + envoy.restart_features.use_eds_cache_for_ads: true + re2.max_program_size.error_level: 4294967295 + re2.max_program_size.warn_level: 1000 + - name: runtime-0 + rtdsLayer: + name: runtime-0 + rtdsConfig: + ads: {} + resourceApiVersion: V3 +staticResources: + clusters: + - connectTimeout: 0.250s + loadAssignment: + clusterName: prometheus_stats + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 127.0.0.1 + portValue: 19000 + name: prometheus_stats + type: STATIC + - connectTimeout: 10s + loadAssignment: + clusterName: xds_cluster + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: envoy-gateway + portValue: 18000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + name: xds_cluster + transportSocket: + name: envoy.transport_sockets.tls + typedConfig: + '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + commonTlsContext: + tlsCertificateSdsSecretConfigs: + - name: xds_certificate + sdsConfig: + pathConfigSource: + path: /sds/xds-certificate.json + resourceApiVersion: V3 + tlsParams: + tlsMaximumProtocolVersion: TLSv1_3 + validationContextSdsSecretConfig: + name: xds_trusted_ca + sdsConfig: + pathConfigSource: + path: /sds/xds-trusted-ca.json + resourceApiVersion: V3 + type: STRICT_DNS + typedExtensionProtocolOptions: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + '@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicitHttpConfig: + http2ProtocolOptions: + connectionKeepalive: + interval: 30s + timeout: 5s + listeners: + - address: + socketAddress: + address: 0.0.0.0 + portValue: 19001 + filterChains: + - filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + httpFilters: + - name: envoy.filters.http.health_check + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.health_check.v3.HealthCheck + headers: + - name: :path + stringMatch: + exact: /ready + passThroughMode: false + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + routeConfig: + name: local_route + virtualHosts: + - domains: + - '*' + name: prometheus_stats + routes: + - match: + prefix: /stats/prometheus + route: + cluster: prometheus_stats + statPrefix: eg-ready-http + name: envoy-gateway-proxy-ready-0.0.0.0-19001 diff --git a/internal/xds/bootstrap/testdata/merge/stats_sinks.in.yaml b/internal/xds/bootstrap/testdata/merge/stats_sinks.in.yaml new file mode 100644 index 00000000000..8b4f9363f91 --- /dev/null +++ b/internal/xds/bootstrap/testdata/merge/stats_sinks.in.yaml @@ -0,0 +1,34 @@ +stats_sinks: + - name: envoy.stat_sinks.metrics_service + typed_config: + "@type": type.googleapis.com/envoy.config.metrics.v3.MetricsServiceConfig + transport_api_version: V3 + grpc_service: + envoy_grpc: + cluster_name: metrics_cluster +static_resources: + clusters: + - connect_timeout: 1s + dns_lookup_family: V4_ONLY + dns_refresh_rate: 30s + lb_policy: ROUND_ROBIN + typed_extension_protocol_options: + "envoy.extensions.upstreams.http.v3.HttpProtocolOptions": + "@type": "type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions" + explicit_http_config: + http2_protocol_options: + connection_keepalive: + interval: 30s + timeout: 5s + load_assignment: + cluster_name: metrics_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: skywalking-oap.skywalking + portValue: 11800 + name: metrics_cluster + respect_dns_ttl: true + type: STRICT_DNS diff --git a/internal/xds/bootstrap/testdata/merge/stats_sinks.out.yaml b/internal/xds/bootstrap/testdata/merge/stats_sinks.out.yaml new file mode 100644 index 00000000000..2471257d4c3 --- /dev/null +++ b/internal/xds/bootstrap/testdata/merge/stats_sinks.out.yaml @@ -0,0 +1,152 @@ +admin: + accessLog: + - name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/null + address: + socketAddress: + address: 127.0.0.1 + portValue: 19000 +dynamicResources: + adsConfig: + apiType: DELTA_GRPC + grpcServices: + - envoyGrpc: + clusterName: xds_cluster + setNodeOnFirstMessageOnly: true + transportApiVersion: V3 + cdsConfig: + ads: {} + resourceApiVersion: V3 + ldsConfig: + ads: {} + resourceApiVersion: V3 +layeredRuntime: + layers: + - name: global_config + staticLayer: + envoy.restart_features.use_eds_cache_for_ads: true + re2.max_program_size.error_level: 4294967295 + re2.max_program_size.warn_level: 1000 +staticResources: + clusters: + - connectTimeout: 0.250s + loadAssignment: + clusterName: prometheus_stats + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 127.0.0.1 + portValue: 19000 + name: prometheus_stats + type: STATIC + - connectTimeout: 10s + loadAssignment: + clusterName: xds_cluster + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: envoy-gateway + portValue: 18000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + name: xds_cluster + transportSocket: + name: envoy.transport_sockets.tls + typedConfig: + '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + commonTlsContext: + tlsCertificateSdsSecretConfigs: + - name: xds_certificate + sdsConfig: + pathConfigSource: + path: /sds/xds-certificate.json + resourceApiVersion: V3 + tlsParams: + tlsMaximumProtocolVersion: TLSv1_3 + validationContextSdsSecretConfig: + name: xds_trusted_ca + sdsConfig: + pathConfigSource: + path: /sds/xds-trusted-ca.json + resourceApiVersion: V3 + type: STRICT_DNS + typedExtensionProtocolOptions: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + '@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicitHttpConfig: + http2ProtocolOptions: + connectionKeepalive: + interval: 30s + timeout: 5s + - connectTimeout: 1s + dnsLookupFamily: V4_ONLY + dnsRefreshRate: 30s + loadAssignment: + clusterName: metrics_cluster + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: skywalking-oap.skywalking + portValue: 11800 + name: metrics_cluster + respectDnsTtl: true + type: STRICT_DNS + typedExtensionProtocolOptions: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + '@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicitHttpConfig: + http2ProtocolOptions: + connectionKeepalive: + interval: 30s + timeout: 5s + listeners: + - address: + socketAddress: + address: 0.0.0.0 + portValue: 19001 + filterChains: + - filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + httpFilters: + - name: envoy.filters.http.health_check + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.health_check.v3.HealthCheck + headers: + - name: :path + stringMatch: + exact: /ready + passThroughMode: false + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + routeConfig: + name: local_route + virtualHosts: + - domains: + - '*' + name: prometheus_stats + routes: + - match: + prefix: /stats/prometheus + route: + cluster: prometheus_stats + statPrefix: eg-ready-http + name: envoy-gateway-proxy-ready-0.0.0.0-19001 +statsSinks: +- name: envoy.stat_sinks.metrics_service + typedConfig: + '@type': type.googleapis.com/envoy.config.metrics.v3.MetricsServiceConfig + grpcService: + envoyGrpc: + clusterName: metrics_cluster + transportApiVersion: V3 diff --git a/internal/xds/bootstrap/testdata/custom-stats-matcher.yaml b/internal/xds/bootstrap/testdata/render/custom-stats-matcher.yaml similarity index 100% rename from internal/xds/bootstrap/testdata/custom-stats-matcher.yaml rename to internal/xds/bootstrap/testdata/render/custom-stats-matcher.yaml diff --git a/internal/xds/bootstrap/testdata/disable-prometheus.yaml b/internal/xds/bootstrap/testdata/render/disable-prometheus.yaml similarity index 100% rename from internal/xds/bootstrap/testdata/disable-prometheus.yaml rename to internal/xds/bootstrap/testdata/render/disable-prometheus.yaml diff --git a/internal/xds/bootstrap/testdata/enable-prometheus.yaml b/internal/xds/bootstrap/testdata/render/enable-prometheus.yaml similarity index 100% rename from internal/xds/bootstrap/testdata/enable-prometheus.yaml rename to internal/xds/bootstrap/testdata/render/enable-prometheus.yaml diff --git a/internal/xds/bootstrap/testdata/otel-metrics.yaml b/internal/xds/bootstrap/testdata/render/otel-metrics.yaml similarity index 100% rename from internal/xds/bootstrap/testdata/otel-metrics.yaml rename to internal/xds/bootstrap/testdata/render/otel-metrics.yaml diff --git a/internal/xds/bootstrap/util.go b/internal/xds/bootstrap/util.go index ca0ffbfa3e8..e00294e2715 100644 --- a/internal/xds/bootstrap/util.go +++ b/internal/xds/bootstrap/util.go @@ -6,15 +6,20 @@ package bootstrap import ( + "fmt" + + bootstrapv3 "github.com/envoyproxy/go-control-plane/envoy/config/bootstrap/v3" + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" - yamlutils "github.com/envoyproxy/gateway/internal/utils/yaml" + "github.com/envoyproxy/gateway/internal/utils/proto" + _ "github.com/envoyproxy/gateway/internal/xds/extensions" // DON'T REMOVE: import of all extensions ) // ApplyBootstrapConfig applies the bootstrap config to the default bootstrap config and return the result config. func ApplyBootstrapConfig(boostrapConfig *egv1a1.ProxyBootstrap, defaultBootstrap string) (string, error) { bootstrapType := boostrapConfig.Type if bootstrapType != nil && *bootstrapType == egv1a1.BootstrapTypeMerge { - mergedBootstrap, err := yamlutils.MergeYAML(defaultBootstrap, boostrapConfig.Value) + mergedBootstrap, err := mergeBootstrap(defaultBootstrap, boostrapConfig.Value) if err != nil { return "", err } @@ -22,3 +27,28 @@ func ApplyBootstrapConfig(boostrapConfig *egv1a1.ProxyBootstrap, defaultBootstra } return boostrapConfig.Value, nil } + +func mergeBootstrap(base, override string) (string, error) { + dst := &bootstrapv3.Bootstrap{} + if err := proto.FromYAML([]byte(base), dst); err != nil { + return "", fmt.Errorf("failed to parse default bootstrap config: %w", err) + } + + src := &bootstrapv3.Bootstrap{} + if err := proto.FromYAML([]byte(override), src); err != nil { + return "", fmt.Errorf("failed to parse override bootstrap config: %w", err) + } + + proto.Merge(dst, src) + + if err := dst.Validate(); err != nil { + return "", fmt.Errorf("failed to validate merged bootstrap config: %w", err) + } + + data, err := proto.ToYAML(dst) + if err != nil { + return "", fmt.Errorf("failed to convert proto message to YAML: %w", err) + } + + return string(data), nil +} diff --git a/internal/xds/bootstrap/util_test.go b/internal/xds/bootstrap/util_test.go new file mode 100644 index 00000000000..b0d27460093 --- /dev/null +++ b/internal/xds/bootstrap/util_test.go @@ -0,0 +1,78 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package bootstrap + +import ( + "flag" + "fmt" + "os" + "path" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/utils/ptr" + + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" +) + +var ( + overrideTestData = flag.Bool("override-testdata", false, "if override the test output data.") +) + +func TestApplyBootstrapConfig(t *testing.T) { + str, _ := readTestData("enable-prometheus") + cases := []struct { + name string + boostrapConfig *egv1a1.ProxyBootstrap + defaultBootstrap string + }{ + { + name: "default", + boostrapConfig: &egv1a1.ProxyBootstrap{ + Type: ptr.To(egv1a1.BootstrapTypeMerge), + }, + defaultBootstrap: str, + }, + { + name: "stats_sinks", + boostrapConfig: &egv1a1.ProxyBootstrap{ + Type: ptr.To(egv1a1.BootstrapTypeMerge), + }, + defaultBootstrap: str, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + in, err := loadData(tc.name, "in") + require.NoError(t, err) + + tc.boostrapConfig.Value = in + data, err := ApplyBootstrapConfig(tc.boostrapConfig, tc.defaultBootstrap) + require.NoError(t, err) + + if *overrideTestData { + // nolint:gosec + err = os.WriteFile(path.Join("testdata", "merge", fmt.Sprintf("%s.out.yaml", tc.name)), []byte(data), 0644) + require.NoError(t, err) + return + } + + expected, err := loadData(tc.name, "out") + require.NoError(t, err) + require.Equal(t, expected, data) + }) + } +} + +func loadData(caseName string, inOrOut string) (string, error) { + filename := path.Join("testdata", "merge", fmt.Sprintf("%s.%s.yaml", caseName, inOrOut)) + b, err := os.ReadFile(filename) + if err != nil { + return "", err + } + return string(b), nil +} diff --git a/tools/make/golang.mk b/tools/make/golang.mk index 96b13012957..8f30d4a667f 100644 --- a/tools/make/golang.mk +++ b/tools/make/golang.mk @@ -52,6 +52,7 @@ go.testdata.complete: ## Override test ouputdata go test -timeout 30s github.com/envoyproxy/gateway/internal/cmd/egctl --override-testdata=true go test -timeout 30s github.com/envoyproxy/gateway/internal/infrastructure/kubernetes/ratelimit --override-testdata=true go test -timeout 30s github.com/envoyproxy/gateway/internal/infrastructure/kubernetes/proxy --override-testdata=true + go test -timeout 30s github.com/envoyproxy/gateway/internal/xds/bootstrap --override-testdata=true go test -timeout 60s github.com/envoyproxy/gateway/internal/gatewayapi --override-testdata=true .PHONY: go.test.coverage