diff --git a/docs/guides/e2e-test-tips.md b/docs/guides/e2e-test-tips.md index f28e24b3b021..0e8593b9afab 100644 --- a/docs/guides/e2e-test-tips.md +++ b/docs/guides/e2e-test-tips.md @@ -97,6 +97,20 @@ Running `make test/e2e/debug` can intentionally leave resources if test fails. C make k3d/stop/all && docker stop $(docker ps -aq) # omit $ for fish ``` +### Tests failing because of disk pressure + +From time to time when running tests your docker environment can run out of space. You will see k8s events like this: +``` +Warning FailedScheduling 63s (x1 over 2m15s) default-scheduler 0/1 nodes are available: +1 node(s) had taint {node.kubernetes.io/disk-pressure:}, that the pod didn't tolerate. +``` + +To fix this issue you need to clean up your docker environment: + +```bash +docker system prune --volumes --all +``` + ### Integration with direnv [direnv](https://direnv.net/) is a useful tool that can populate environment variables in your shell as you change directories. diff --git a/docs/madr/decisions/010-timeout-policy.md b/docs/madr/decisions/010-timeout-policy.md index 61b7abfaf15e..28063a5e75e6 100644 --- a/docs/madr/decisions/010-timeout-policy.md +++ b/docs/madr/decisions/010-timeout-policy.md @@ -100,11 +100,12 @@ In this policy same limitations applies as in `MeshAccessLog` [policy](https://g ```yaml from: - targetRef: - kind: Mesh|MeshSubset|MeshService|MeshServiceSubset + kind: Mesh name: ... ``` -Matching on MeshGatewayRoute and MeshHTTPRoute does not make sense (there is no route that a request originates from). +Since timeouts are mostly configured on clusters and listeners there and we have single inbound in most cases we can only +configure `Mesh` kind in from section. #### To level @@ -239,11 +240,10 @@ spec: targetRef: kind: MeshGatewayRoute name: default-gateway-route - from: + to: - targetRef: kind: MeshService name: backend - mesh: consume default: idleTimeout: 30m http: diff --git a/pkg/plugins/policies/meshtimeout/api/v1alpha1/validator.go b/pkg/plugins/policies/meshtimeout/api/v1alpha1/validator.go index cf1ed69799cf..6cf52ef07f1b 100644 --- a/pkg/plugins/policies/meshtimeout/api/v1alpha1/validator.go +++ b/pkg/plugins/policies/meshtimeout/api/v1alpha1/validator.go @@ -37,9 +37,6 @@ func validateFrom(from []From) validators.ValidationError { verr.AddErrorAt(path.Field("targetRef"), matcher_validators.ValidateTargetRef(fromItem.GetTargetRef(), &matcher_validators.ValidateTargetRefOpts{ SupportedKinds: []common_api.TargetRefKind{ common_api.Mesh, - common_api.MeshSubset, - common_api.MeshService, - common_api.MeshServiceSubset, }, })) @@ -57,6 +54,7 @@ func validateTo(to []To) validators.ValidationError { SupportedKinds: []common_api.TargetRefKind{ common_api.Mesh, common_api.MeshService, + common_api.MeshGatewayRoute, }, })) diff --git a/pkg/plugins/policies/meshtimeout/plugin/testdata/basic_inbound_cluster.golden.yaml b/pkg/plugins/policies/meshtimeout/plugin/testdata/basic_inbound_cluster.golden.yaml new file mode 100644 index 000000000000..d27d6e236f66 --- /dev/null +++ b/pkg/plugins/policies/meshtimeout/plugin/testdata/basic_inbound_cluster.golden.yaml @@ -0,0 +1,9 @@ +connectTimeout: 10s +name: localhost:8080 +typedExtensionProtocolOptions: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + '@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + commonHttpProtocolOptions: + idleTimeout: 3600s + maxConnectionDuration: 600s + maxStreamDuration: 600s diff --git a/pkg/plugins/policies/meshtimeout/plugin/testdata/basic_inbound_listener.golden.yaml b/pkg/plugins/policies/meshtimeout/plugin/testdata/basic_inbound_listener.golden.yaml new file mode 100644 index 000000000000..38155ed5bf24 --- /dev/null +++ b/pkg/plugins/policies/meshtimeout/plugin/testdata/basic_inbound_listener.golden.yaml @@ -0,0 +1,33 @@ +address: + socketAddress: + address: 127.0.0.1 + portValue: 80 +enableReusePort: false +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.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + routeConfig: + name: inbound:backend + requestHeadersToRemove: + - x-kuma-tags + validateClusters: false + virtualHosts: + - domains: + - '*' + name: backend + routes: + - match: + prefix: / + route: + cluster: backend + timeout: 5s + statPrefix: "inbound_127_0_0_1_80" + streamIdleTimeout: 1s +name: inbound:127.0.0.1:80 +trafficDirection: INBOUND diff --git a/pkg/plugins/policies/meshtimeout/plugin/testdata/basic_tcp_cluster.golden.yaml b/pkg/plugins/policies/meshtimeout/plugin/testdata/basic_tcp_cluster.golden.yaml new file mode 100644 index 000000000000..05471d801b1f --- /dev/null +++ b/pkg/plugins/policies/meshtimeout/plugin/testdata/basic_tcp_cluster.golden.yaml @@ -0,0 +1,2 @@ +connectTimeout: 10s +name: second-service diff --git a/pkg/plugins/policies/meshtimeout/plugin/testdata/basic_tcp_listener.golden.yaml b/pkg/plugins/policies/meshtimeout/plugin/testdata/basic_tcp_listener.golden.yaml new file mode 100644 index 000000000000..835f30188893 --- /dev/null +++ b/pkg/plugins/policies/meshtimeout/plugin/testdata/basic_tcp_listener.golden.yaml @@ -0,0 +1,14 @@ +address: + socketAddress: + address: 127.0.0.1 + portValue: 10002 +filterChains: +- filters: + - name: envoy.filters.network.tcp_proxy + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy + cluster: backend + idleTimeout: 30s + statPrefix: "127_0_0_1_10002" +name: outbound:127.0.0.1:10002 +trafficDirection: OUTBOUND diff --git a/pkg/plugins/policies/meshtimeout/plugin/testdata/gateway_cluster.golden.yaml b/pkg/plugins/policies/meshtimeout/plugin/testdata/gateway_cluster.golden.yaml new file mode 100644 index 000000000000..1f145d0b33ee --- /dev/null +++ b/pkg/plugins/policies/meshtimeout/plugin/testdata/gateway_cluster.golden.yaml @@ -0,0 +1,17 @@ +connectTimeout: 10s +edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 +name: backend-26cb64fa4e85e7b7 +perConnectionBufferLimitBytes: 32768 +type: EDS +typedExtensionProtocolOptions: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + '@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + commonHttpProtocolOptions: + idleTimeout: 3600s + maxConnectionDuration: 600s + maxStreamDuration: 600s + explicitHttpConfig: + httpProtocolOptions: {} \ No newline at end of file diff --git a/pkg/plugins/policies/meshtimeout/plugin/testdata/gateway_listener.golden.yaml b/pkg/plugins/policies/meshtimeout/plugin/testdata/gateway_listener.golden.yaml new file mode 100644 index 000000000000..7a4aeecf7464 --- /dev/null +++ b/pkg/plugins/policies/meshtimeout/plugin/testdata/gateway_listener.golden.yaml @@ -0,0 +1,56 @@ +address: + socketAddress: + address: 192.168.0.1 + portValue: 8080 +enableReusePort: true +filterChains: + - filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + commonHttpProtocolOptions: + headersWithUnderscoresAction: REJECT_REQUEST + idleTimeout: 300s + http2ProtocolOptions: + allowConnect: true + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + maxConcurrentStreams: 100 + httpFilters: + - name: envoy.filters.http.local_ratelimit + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit + statPrefix: rate_limit + - name: gzip-compress + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.compressor.v3.Compressor + compressorLibrary: + name: gzip + typedConfig: + '@type': type.googleapis.com/envoy.extensions.compression.gzip.compressor.v3.Gzip + responseDirectionConfig: + disableOnEtagHeader: true + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + mergeSlashes: true + normalizePath: true + pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: sample-gateway:HTTP:8080 + requestHeadersTimeout: 0.500s + serverName: Kuma Gateway + statPrefix: sample-gateway + streamIdleTimeout: 1s + stripAnyHostPort: true + useRemoteAddress: true +listenerFilters: + - name: envoy.filters.listener.tls_inspector + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector +name: sample-gateway:HTTP:8080 +perConnectionBufferLimitBytes: 32768 +trafficDirection: INBOUND \ No newline at end of file diff --git a/pkg/plugins/policies/meshtimeout/plugin/testdata/gateway_route.golden.yaml b/pkg/plugins/policies/meshtimeout/plugin/testdata/gateway_route.golden.yaml new file mode 100644 index 000000000000..fc47de6fbd6d --- /dev/null +++ b/pkg/plugins/policies/meshtimeout/plugin/testdata/gateway_route.golden.yaml @@ -0,0 +1,22 @@ +name: sample-gateway:HTTP:8080 +requestHeadersToRemove: + - x-kuma-tags +validateClusters: false +virtualHosts: + - domains: + - '*' + name: '*' + routes: + - match: + path: / + route: + timeout: 5s + weightedClusters: + clusters: + - name: backend-26cb64fa4e85e7b7 + requestHeadersToAdd: + - header: + key: x-kuma-tags + value: '&kuma.io/service=sample-gateway&' + weight: 1 + totalWeight: 1 \ No newline at end of file diff --git a/pkg/plugins/policies/meshtimeout/plugin/testdata/http_outbound_cluster.golden.yaml b/pkg/plugins/policies/meshtimeout/plugin/testdata/http_outbound_cluster.golden.yaml new file mode 100644 index 000000000000..44685f81bfc3 --- /dev/null +++ b/pkg/plugins/policies/meshtimeout/plugin/testdata/http_outbound_cluster.golden.yaml @@ -0,0 +1,9 @@ +connectTimeout: 10s +name: other-service +typedExtensionProtocolOptions: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + '@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + commonHttpProtocolOptions: + idleTimeout: 3600s + maxConnectionDuration: 600s + maxStreamDuration: 600s diff --git a/pkg/plugins/policies/meshtimeout/plugin/testdata/http_outbound_listener.golden.yaml b/pkg/plugins/policies/meshtimeout/plugin/testdata/http_outbound_listener.golden.yaml new file mode 100644 index 000000000000..fbb85a0380e8 --- /dev/null +++ b/pkg/plugins/policies/meshtimeout/plugin/testdata/http_outbound_listener.golden.yaml @@ -0,0 +1,34 @@ +address: + socketAddress: + address: 127.0.0.1 + portValue: 10001 +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.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + routeConfig: + name: outbound:backend + requestHeadersToAdd: + - header: + key: x-kuma-tags + value: '&kuma.io/service=web&' + validateClusters: false + virtualHosts: + - domains: + - '*' + name: backend + routes: + - match: + prefix: / + route: + cluster: backend + timeout: 5s + statPrefix: "outbound_127_0_0_1_10001" + streamIdleTimeout: 1s +name: outbound:127.0.0.1:10001 +trafficDirection: OUTBOUND diff --git a/pkg/plugins/policies/meshtimeout/plugin/testdata/outbound_with_defaults_cluster.golden.yaml b/pkg/plugins/policies/meshtimeout/plugin/testdata/outbound_with_defaults_cluster.golden.yaml new file mode 100644 index 000000000000..3d8cc5286d14 --- /dev/null +++ b/pkg/plugins/policies/meshtimeout/plugin/testdata/outbound_with_defaults_cluster.golden.yaml @@ -0,0 +1,9 @@ +connectTimeout: 10s +name: other-service +typedExtensionProtocolOptions: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + '@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + commonHttpProtocolOptions: + idleTimeout: 3600s + maxConnectionDuration: 0s + maxStreamDuration: 0s diff --git a/pkg/plugins/policies/meshtimeout/plugin/testdata/outbound_with_defaults_listener.golden.yaml b/pkg/plugins/policies/meshtimeout/plugin/testdata/outbound_with_defaults_listener.golden.yaml new file mode 100644 index 000000000000..b3c6cc71bd99 --- /dev/null +++ b/pkg/plugins/policies/meshtimeout/plugin/testdata/outbound_with_defaults_listener.golden.yaml @@ -0,0 +1,34 @@ +address: + socketAddress: + address: 127.0.0.1 + portValue: 10001 +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.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + routeConfig: + name: outbound:backend + requestHeadersToAdd: + - header: + key: x-kuma-tags + value: '&kuma.io/service=web&' + validateClusters: false + virtualHosts: + - domains: + - '*' + name: backend + routes: + - match: + prefix: / + route: + cluster: backend + timeout: 15s + statPrefix: "outbound_127_0_0_1_10001" + streamIdleTimeout: 1800s +name: outbound:127.0.0.1:10001 +trafficDirection: OUTBOUND diff --git a/pkg/plugins/policies/meshtimeout/plugin/v1alpha1/plugin.go b/pkg/plugins/policies/meshtimeout/plugin/v1alpha1/plugin.go index b6c2e3467ec5..de2be5c10dd7 100644 --- a/pkg/plugins/policies/meshtimeout/plugin/v1alpha1/plugin.go +++ b/pkg/plugins/policies/meshtimeout/plugin/v1alpha1/plugin.go @@ -1,17 +1,28 @@ package v1alpha1 import ( - "github.com/kumahq/kuma/pkg/core" + "context" + + envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" + envoy_listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" + envoy_route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + + mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1" core_plugins "github.com/kumahq/kuma/pkg/core/plugins" core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh" core_xds "github.com/kumahq/kuma/pkg/core/xds" "github.com/kumahq/kuma/pkg/plugins/policies/matchers" api "github.com/kumahq/kuma/pkg/plugins/policies/meshtimeout/api/v1alpha1" + plugin_xds "github.com/kumahq/kuma/pkg/plugins/policies/meshtimeout/plugin/xds" + policies_xds "github.com/kumahq/kuma/pkg/plugins/policies/xds" + gateway_plugin "github.com/kumahq/kuma/pkg/plugins/runtime/gateway" + gateway_route "github.com/kumahq/kuma/pkg/plugins/runtime/gateway/route" xds_context "github.com/kumahq/kuma/pkg/xds/context" + envoy_names "github.com/kumahq/kuma/pkg/xds/envoy/names" + envoy_common "github.com/kumahq/kuma/pkg/xds/generator" ) var _ core_plugins.PolicyPlugin = &plugin{} -var log = core.Log.WithName("MeshTimeout") type plugin struct { } @@ -25,6 +36,215 @@ func (p plugin) MatchedPolicies(dataplane *core_mesh.DataplaneResource, resource } func (p plugin) Apply(rs *core_xds.ResourceSet, ctx xds_context.Context, proxy *core_xds.Proxy) error { - log.Info("apply is not implemented") + policies, ok := proxy.Policies.Dynamic[api.MeshTimeoutType] + if !ok { + return nil + } + + listeners := policies_xds.GatherListeners(rs) + clusters := policies_xds.GatherClusters(rs) + routes := policies_xds.GatherRoutes(rs) + + if err := applyToInbounds(policies.FromRules, listeners.Inbound, clusters.Inbound, proxy.Dataplane); err != nil { + return err + } + if err := applyToOutbounds(policies.ToRules, listeners.Outbound, clusters.Outbound, proxy.Dataplane, proxy.Routing); err != nil { + return err + } + if err := applyToGateway(policies.ToRules, listeners.Gateway, clusters.Gateway, routes.Gateway, proxy.Dataplane, ctx, proxy); err != nil { + return err + } + return nil +} + +func applyToInbounds(fromRules core_xds.FromRules, inboundListeners map[core_xds.InboundListener]*envoy_listener.Listener, inboundClusters map[string]*envoy_cluster.Cluster, dataplane *core_mesh.DataplaneResource) error { + for _, inbound := range dataplane.Spec.Networking.GetInbound() { + iface := dataplane.Spec.Networking.ToInboundInterface(inbound) + + listenerKey := core_xds.InboundListener{ + Address: iface.DataplaneIP, + Port: iface.DataplanePort, + } + + listener, ok := inboundListeners[listenerKey] + if !ok { + continue + } + + rules, ok := fromRules.Rules[listenerKey] + if !ok { + continue + } + + cluster, ok := inboundClusters[createInboundClusterName(inbound.ServicePort, listenerKey.Port)] + if !ok { + continue + } + protocol := core_mesh.ParseProtocol(inbound.GetProtocol()) + if err := configure(rules, core_xds.MeshSubset(), protocol, listener, cluster, nil); err != nil { + return err + } + } + return nil } + +func applyToOutbounds(rules core_xds.ToRules, outboundListeners map[mesh_proto.OutboundInterface]*envoy_listener.Listener, outboundClusters map[string]*envoy_cluster.Cluster, dataplane *core_mesh.DataplaneResource, routing core_xds.Routing) error { + for _, outbound := range dataplane.Spec.Networking.GetOutbound() { + oface := dataplane.Spec.Networking.ToOutboundInterface(outbound) + + listener, ok := outboundListeners[oface] + if !ok { + continue + } + + serviceName := outbound.GetTagsIncludingLegacy()[mesh_proto.ServiceTag] + cluster, ok := outboundClusters[serviceName] + if !ok { + continue + } + + protocol := inferProtocol(routing, serviceName) + if err := configure(rules.Rules, core_xds.MeshService(serviceName), protocol, listener, cluster, nil); err != nil { + return err + } + } + + return nil +} + +func applyToGateway( + toRules core_xds.ToRules, + gatewayListeners map[core_xds.InboundListener]*envoy_listener.Listener, + gatewayClusters map[string]*envoy_cluster.Cluster, + gatewayRoutes map[string]*envoy_route.RouteConfiguration, + dataplane *core_mesh.DataplaneResource, + ctx xds_context.Context, + proxy *core_xds.Proxy, +) error { + gatewayListerInfos, err := gateway_plugin.GatewayListenerInfoFromProxy(context.TODO(), ctx.Mesh, proxy, ctx.ControlPlane.Zone) + if err != nil { + return err + } + for _, listenerInfo := range gatewayListerInfos { + address := dataplane.Spec.GetNetworking().Address + port := listenerInfo.Listener.Port + listenerKey := core_xds.InboundListener{ + Address: address, + Port: port, + } + gatewayListener, ok := gatewayListeners[listenerKey] + if !ok { + continue + } + route, ok := gatewayRoutes[listenerInfo.Listener.ResourceName] + if !ok { + continue + } + routeActionsPerCluster := routeActionPerCluster(route) + + for _, hostInfo := range listenerInfo.HostInfos { + destinations := gateway_plugin.RouteDestinationsMutable(hostInfo.Entries) + for _, dest := range destinations { + clusterName, err := gateway_route.DestinationClusterName(dest, hostInfo.Host.Tags) + if err != nil { + continue + } + cluster, ok := gatewayClusters[clusterName] + if !ok { + continue + } + + routeActions, ok := routeActionsPerCluster[clusterName] + if !ok { + continue + } + + serviceName := dest.Destination[mesh_proto.ServiceTag] + + if err := configure( + toRules.Rules, + core_xds.MeshService(serviceName), + toProtocol(listenerInfo.Listener.Protocol), + gatewayListener, + cluster, + routeActions, + ); err != nil { + return err + } + } + } + } + + return nil +} + +func configure(rules core_xds.Rules, subset core_xds.Subset, protocol core_mesh.Protocol, listener *envoy_listener.Listener, cluster *envoy_cluster.Cluster, routeActions []*envoy_route.RouteAction) error { + var conf api.Conf + if computed := rules.Compute(subset); computed != nil { + conf = computed.Conf.(api.Conf) + } else { + return nil + } + + configurer := plugin_xds.Configurer{ + Conf: conf, + Protocol: protocol, + } + + for _, chain := range listener.FilterChains { + if err := configurer.ConfigureListener(chain); err != nil { + return err + } + } + + for _, routeAction := range routeActions { + configurer.ConfigureRouteAction(routeAction) + } + + if err := configurer.ConfigureCluster(cluster); err != nil { + return err + } + return nil +} + +func inferProtocol(routing core_xds.Routing, serviceName string) core_mesh.Protocol { + var allEndpoints []core_xds.Endpoint + outboundEndpoints := core_xds.EndpointList(routing.OutboundTargets[serviceName]) + allEndpoints = append(allEndpoints, outboundEndpoints...) + externalEndpoints := routing.ExternalServiceOutboundTargets[serviceName] + allEndpoints = append(allEndpoints, externalEndpoints...) + + return envoy_common.InferServiceProtocol(allEndpoints) +} + +func toProtocol(p mesh_proto.MeshGateway_Listener_Protocol) core_mesh.Protocol { + return core_mesh.ParseProtocol(mesh_proto.MeshGateway_Listener_Protocol_name[int32(p.Number())]) +} + +func routeActionPerCluster(route *envoy_route.RouteConfiguration) map[string][]*envoy_route.RouteAction { + actions := map[string][]*envoy_route.RouteAction{} + for _, vh := range route.VirtualHosts { + for _, r := range vh.Routes { + routeAction := r.GetRoute() + if routeAction == nil { + continue + } + cluster := routeAction.GetWeightedClusters().GetClusters()[0].Name + if actions[cluster] == nil { + actions[cluster] = []*envoy_route.RouteAction{routeAction} + } else { + actions[cluster] = append(actions[cluster], routeAction) + } + } + } + return actions +} + +func createInboundClusterName(servicePort uint32, listenerPort uint32) string { + if servicePort != 0 { + return envoy_names.GetLocalClusterName(servicePort) + } else { + return envoy_names.GetLocalClusterName(listenerPort) + } +} diff --git a/pkg/plugins/policies/meshtimeout/plugin/v1alpha1/plugin_suite_test.go b/pkg/plugins/policies/meshtimeout/plugin/v1alpha1/plugin_suite_test.go new file mode 100644 index 000000000000..b0739a818163 --- /dev/null +++ b/pkg/plugins/policies/meshtimeout/plugin/v1alpha1/plugin_suite_test.go @@ -0,0 +1,11 @@ +package v1alpha1_test + +import ( + "testing" + + "github.com/kumahq/kuma/pkg/test" +) + +func TestPlugin(t *testing.T) { + test.RunSpecs(t, "MeshTimeout") +} diff --git a/pkg/plugins/policies/meshtimeout/plugin/v1alpha1/plugin_test.go b/pkg/plugins/policies/meshtimeout/plugin/v1alpha1/plugin_test.go new file mode 100644 index 000000000000..fc8e88715f2e --- /dev/null +++ b/pkg/plugins/policies/meshtimeout/plugin/v1alpha1/plugin_test.go @@ -0,0 +1,405 @@ +package v1alpha1 + +import ( + "fmt" + "path/filepath" + "time" + + envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" + envoy_resource "github.com/envoyproxy/go-control-plane/pkg/resource/v3" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + k8s "k8s.io/apimachinery/pkg/apis/meta/v1" + + mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1" + core_plugins "github.com/kumahq/kuma/pkg/core/plugins" + core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh" + core_model "github.com/kumahq/kuma/pkg/core/resources/model" + "github.com/kumahq/kuma/pkg/core/xds" + core_xds "github.com/kumahq/kuma/pkg/core/xds" + api "github.com/kumahq/kuma/pkg/plugins/policies/meshtimeout/api/v1alpha1" + gateway_plugin "github.com/kumahq/kuma/pkg/plugins/runtime/gateway" + "github.com/kumahq/kuma/pkg/plugins/runtime/k8s/controllers" + "github.com/kumahq/kuma/pkg/test/matchers" + "github.com/kumahq/kuma/pkg/test/resources/builders" + test_model "github.com/kumahq/kuma/pkg/test/resources/model" + "github.com/kumahq/kuma/pkg/test/resources/samples" + test_xds "github.com/kumahq/kuma/pkg/test/xds" + util_proto "github.com/kumahq/kuma/pkg/util/proto" + xds_context "github.com/kumahq/kuma/pkg/xds/context" + envoy_common "github.com/kumahq/kuma/pkg/xds/envoy" + "github.com/kumahq/kuma/pkg/xds/envoy/clusters" + clusters_builder "github.com/kumahq/kuma/pkg/xds/envoy/clusters" + . "github.com/kumahq/kuma/pkg/xds/envoy/listeners" + "github.com/kumahq/kuma/pkg/xds/generator" +) + +var _ = Describe("MeshTimeout", func() { + type sidecarTestCase struct { + resources []core_xds.Resource + toRules core_xds.ToRules + fromRules core_xds.FromRules + expectedListener string + expectedCluster string + } + DescribeTable("should generate proper Envoy config", func(given sidecarTestCase) { + // given + resourceSet := core_xds.NewResourceSet() + for _, res := range given.resources { + r := res + resourceSet.Add(&r) + } + + context := createSimpleMeshContextWith(xds_context.NewResources()) + + proxy := xds.Proxy{ + Dataplane: builders.Dataplane(). + WithName("backend"). + WithMesh("default"). + WithAddress("127.0.0.1"). + AddOutboundsToServices("other-service", "second-service"). + WithInboundOfTags(mesh_proto.ServiceTag, "backend", mesh_proto.ProtocolTag, "http"). + Build(), + Policies: xds.MatchedPolicies{ + Dynamic: map[core_model.ResourceType]xds.TypedMatchingPolicies{ + api.MeshTimeoutType: { + Type: api.MeshTimeoutType, + ToRules: given.toRules, + FromRules: given.fromRules, + }, + }, + }, + Routing: core_xds.Routing{ + OutboundTargets: core_xds.EndpointMap{ + "other-service": []core_xds.Endpoint{{ + Tags: map[string]string{ + "kuma.io/protocol": "http", + }, + }}, + "second-service": []core_xds.Endpoint{{ + Tags: map[string]string{ + "kuma.io/protocol": "tcp", + }, + }}, + }, + }, + } + + // when + plugin := NewPlugin().(core_plugins.PolicyPlugin) + Expect(plugin.Apply(resourceSet, context, &proxy)).To(Succeed()) + + // then + Expect(getResourceYaml(resourceSet.ListOf(envoy_resource.ListenerType))).To(matchers.MatchGoldenYAML(filepath.Join("..", "testdata", given.expectedListener))) + Expect(getResourceYaml(resourceSet.ListOf(envoy_resource.ClusterType))).To(matchers.MatchGoldenYAML(filepath.Join("..", "testdata", given.expectedCluster))) + }, + Entry("http outbound route", sidecarTestCase{ + resources: []core_xds.Resource{{ + Name: "outbound", + Origin: generator.OriginOutbound, + Resource: httpOutboundListenerWith(10001), + }, + { + Name: "outbound", + Origin: generator.OriginOutbound, + Resource: clusterWithName("other-service"), + }}, + toRules: core_xds.ToRules{ + Rules: []*core_xds.Rule{ + { + Subset: core_xds.Subset{}, + Conf: api.Conf{ + ConnectionTimeout: parseDuration("10s"), + IdleTimeout: parseDuration("1h"), + Http: &api.Http{ + RequestTimeout: parseDuration("5s"), + StreamIdleTimeout: parseDuration("1s"), + MaxStreamDuration: parseDuration("10m"), + MaxConnectionDuration: parseDuration("10m"), + }, + }, + }, + }, + }, + expectedListener: "http_outbound_listener.golden.yaml", + expectedCluster: "http_outbound_cluster.golden.yaml", + }), + Entry("tcp outbound route", sidecarTestCase{ + resources: []core_xds.Resource{{ + Name: "outbound", + Origin: generator.OriginOutbound, + Resource: NewListenerBuilder(envoy_common.APIV3). + Configure(OutboundListener("outbound:127.0.0.1:10002", "127.0.0.1", 10002, core_xds.SocketAddressProtocolTCP)). + Configure(FilterChain(NewFilterChainBuilder(envoy_common.APIV3). + Configure(TcpProxy( + "127.0.0.1:10002", + envoy_common.NewCluster( + envoy_common.WithService("backend"), + envoy_common.WithWeight(100), + ), + )), + )). + MustBuild(), + }, + { + Name: "outbound", + Origin: generator.OriginOutbound, + Resource: clusterWithName("second-service"), + }}, + toRules: core_xds.ToRules{ + Rules: []*core_xds.Rule{ + { + Subset: core_xds.Subset{core_xds.Tag{ + Key: mesh_proto.ServiceTag, + Value: "second-service", + }}, + Conf: api.Conf{ + ConnectionTimeout: parseDuration("10s"), + IdleTimeout: parseDuration("30s"), + }, + }, + }, + }, + expectedCluster: "basic_tcp_cluster.golden.yaml", + expectedListener: "basic_tcp_listener.golden.yaml", + }), + Entry("basic inbound route", sidecarTestCase{ + resources: []core_xds.Resource{{ + Name: "inbound", + Origin: generator.OriginInbound, + Resource: httpInboundListenerWith(80), + }, + { + Name: "inbound", + Origin: generator.OriginInbound, + Resource: clusterWithName(fmt.Sprintf("localhost:%d", builders.FirstInboundServicePort)), + }}, + fromRules: core_xds.FromRules{ + Rules: map[core_xds.InboundListener]core_xds.Rules{ + { + Address: "127.0.0.1", + Port: 80, + }: []*core_xds.Rule{ + { + Subset: core_xds.Subset{}, + Conf: api.Conf{ + ConnectionTimeout: parseDuration("10s"), + IdleTimeout: parseDuration("1h"), + Http: &api.Http{ + RequestTimeout: parseDuration("5s"), + StreamIdleTimeout: parseDuration("1s"), + MaxStreamDuration: parseDuration("10m"), + MaxConnectionDuration: parseDuration("10m"), + }, + }, + }, + }}, + }, + expectedCluster: "basic_inbound_cluster.golden.yaml", + expectedListener: "basic_inbound_listener.golden.yaml", + }), + Entry("outbound with defaults when http conf missing", sidecarTestCase{ + resources: []core_xds.Resource{{ + Name: "outbound", + Origin: generator.OriginOutbound, + Resource: httpOutboundListenerWith(10001), + }, + { + Name: "outbound", + Origin: generator.OriginOutbound, + Resource: clusterWithName("other-service"), + }, + }, + toRules: core_xds.ToRules{ + Rules: []*core_xds.Rule{ + { + Subset: core_xds.Subset{ + { + Key: mesh_proto.ServiceTag, + Value: "other-service", + }, + }, + Conf: api.Conf{ + ConnectionTimeout: parseDuration("10s"), + IdleTimeout: parseDuration("1h"), + }, + }, + }, + }, + expectedCluster: "outbound_with_defaults_cluster.golden.yaml", + expectedListener: "outbound_with_defaults_listener.golden.yaml", + }), + ) + + It("should generate proper Envoy config for MeshGateway Dataplanes", func() { + // given + toRules := core_xds.ToRules{ + Rules: []*core_xds.Rule{ + { + Subset: core_xds.Subset{}, + Conf: api.Conf{ + ConnectionTimeout: parseDuration("10s"), + IdleTimeout: parseDuration("1h"), + Http: &api.Http{ + RequestTimeout: parseDuration("5s"), + StreamIdleTimeout: parseDuration("1s"), + MaxStreamDuration: parseDuration("10m"), + MaxConnectionDuration: parseDuration("10m"), + }, + }, + }, + }, + } + + resources := xds_context.NewResources() + resources.MeshLocalResources[core_mesh.MeshGatewayType] = &core_mesh.MeshGatewayResourceList{ + Items: []*core_mesh.MeshGatewayResource{samples.GatewayResource()}, + } + resources.MeshLocalResources[core_mesh.MeshGatewayRouteType] = &core_mesh.MeshGatewayRouteResourceList{ + Items: []*core_mesh.MeshGatewayRouteResource{samples.BackendGatewayRoute()}, + } + + context := createSimpleMeshContextWith(resources) + proxy := xds.Proxy{ + APIVersion: "v3", + Dataplane: samples.GatewayDataplane(), + Policies: xds.MatchedPolicies{ + Dynamic: map[core_model.ResourceType]xds.TypedMatchingPolicies{ + api.MeshTimeoutType: { + Type: api.MeshTimeoutType, + ToRules: toRules, + }, + }, + }, + } + gatewayGenerator := gatewayGenerator() + generatedResources, err := gatewayGenerator.Generate(context, &proxy) + Expect(err).NotTo(HaveOccurred()) + + // when + plugin := NewPlugin().(core_plugins.PolicyPlugin) + Expect(plugin.Apply(generatedResources, context, &proxy)).To(Succeed()) + + // then + Expect(getResourceYaml(generatedResources.ListOf(envoy_resource.ListenerType))).To(matchers.MatchGoldenYAML(filepath.Join("..", "testdata", "gateway_listener.golden.yaml"))) + Expect(getResourceYaml(generatedResources.ListOf(envoy_resource.ClusterType))).To(matchers.MatchGoldenYAML(filepath.Join("..", "testdata", "gateway_cluster.golden.yaml"))) + Expect(getResourceYaml(generatedResources.ListOf(envoy_resource.RouteType))).To(matchers.MatchGoldenYAML(filepath.Join("..", "testdata", "gateway_route.golden.yaml"))) + }) +}) + +func parseDuration(duration string) *k8s.Duration { + d, _ := time.ParseDuration(duration) + return &k8s.Duration{Duration: d} +} + +func clusterWithName(name string) envoy_common.NamedResource { + return clusters.NewClusterBuilder(envoy_common.APIV3). + Configure(WithName(name)). + MustBuild() +} + +type NameConfigurer struct { + Name string +} + +func (n *NameConfigurer) Configure(c *envoy_cluster.Cluster) error { + c.Name = n.Name + return nil +} + +func WithName(name string) clusters_builder.ClusterBuilderOpt { + return clusters_builder.ClusterBuilderOptFunc(func(config *clusters_builder.ClusterBuilderConfig) { + config.AddV3(&NameConfigurer{Name: name}) + }) +} + +func getResourceYaml(list core_xds.ResourceList) []byte { + actualListener, err := util_proto.ToYAML(list[0].Resource) + Expect(err).ToNot(HaveOccurred()) + return actualListener +} + +func createSimpleMeshContextWith(resources xds_context.Resources) xds_context.Context { + return xds_context.Context{ + Mesh: xds_context.MeshContext{ + Resource: &core_mesh.MeshResource{ + Meta: &test_model.ResourceMeta{ + Name: "default", + }, + }, + Resources: resources, + EndpointMap: map[core_xds.ServiceName][]core_xds.Endpoint{ + "backend": { + { + Tags: map[string]string{ + controllers.KubeServiceTag: "some-service", + }, + }, + }, + }, + }, + ControlPlane: &xds_context.ControlPlaneContext{CLACache: &test_xds.DummyCLACache{}, Zone: "test-zone"}, + } +} + +func gatewayGenerator() gateway_plugin.Generator { + return gateway_plugin.Generator{ + FilterChainGenerators: gateway_plugin.FilterChainGenerators{ + FilterChainGenerators: map[mesh_proto.MeshGateway_Listener_Protocol]gateway_plugin.FilterChainGenerator{ + mesh_proto.MeshGateway_Listener_HTTP: &gateway_plugin.HTTPFilterChainGenerator{}, + mesh_proto.MeshGateway_Listener_HTTPS: &gateway_plugin.HTTPSFilterChainGenerator{}, + mesh_proto.MeshGateway_Listener_TCP: &gateway_plugin.TCPFilterChainGenerator{}, + }}, + ClusterGenerator: gateway_plugin.ClusterGenerator{ + Zone: "test-zone", + }, + Zone: "test-zone", + } +} + +func httpOutboundListenerWith(port uint32) envoy_common.NamedResource { + return createListener( + port, + OutboundListener(fmt.Sprintf("outbound:127.0.0.1:%d", port), "127.0.0.1", port, core_xds.SocketAddressProtocolTCP), + HttpOutboundRoute( + "backend", + envoy_common.Routes{{ + Clusters: []envoy_common.Cluster{envoy_common.NewCluster( + envoy_common.WithService("backend"), + envoy_common.WithWeight(100), + )}, + }}, + map[string]map[string]bool{ + "kuma.io/service": { + "web": true, + }, + }, + ), + "outbound", + ) +} + +func httpInboundListenerWith(port uint32) envoy_common.NamedResource { + return createListener( + port, + InboundListener(fmt.Sprintf("inbound:127.0.0.1:%d", port), "127.0.0.1", port, core_xds.SocketAddressProtocolTCP), + HttpInboundRoutes( + "backend", + envoy_common.Routes{{ + Clusters: []envoy_common.Cluster{envoy_common.NewCluster( + envoy_common.WithService("backend"), + envoy_common.WithWeight(100), + )}, + }}, + ), + "inbound") +} + +func createListener(port uint32, listener ListenerBuilderOpt, route FilterChainBuilderOpt, direction string) envoy_common.NamedResource { + return NewListenerBuilder(envoy_common.APIV3). + Configure(listener). + Configure(FilterChain(NewFilterChainBuilder(envoy_common.APIV3). + Configure(HttpConnectionManager(fmt.Sprintf("%s:127.0.0.1:%d", direction, port), false)). + Configure(route), + )).MustBuild() +} diff --git a/pkg/plugins/policies/meshtimeout/plugin/xds/configurer.go b/pkg/plugins/policies/meshtimeout/plugin/xds/configurer.go new file mode 100644 index 000000000000..85be55f44dec --- /dev/null +++ b/pkg/plugins/policies/meshtimeout/plugin/xds/configurer.go @@ -0,0 +1,118 @@ +package xds + +import ( + "time" + + envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" + envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + envoy_listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" + envoy_route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + envoy_hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + envoy_tcp "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3" + envoy_upstream_http "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/v3" + "github.com/pkg/errors" + "google.golang.org/protobuf/types/known/durationpb" + kube_meta "k8s.io/apimachinery/pkg/apis/meta/v1" + + core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh" + api "github.com/kumahq/kuma/pkg/plugins/policies/meshtimeout/api/v1alpha1" + util_proto "github.com/kumahq/kuma/pkg/util/proto" + clusters_v3 "github.com/kumahq/kuma/pkg/xds/envoy/clusters/v3" + listeners_v3 "github.com/kumahq/kuma/pkg/xds/envoy/listeners/v3" +) + +const ( + defaultConnectionTimeout = time.Second * 5 + defaultIdleTimeout = time.Hour + defaultRequestTimeout = time.Second * 15 + defaultStreamIdleTimeout = time.Minute * 30 + defaultMaxStreamDuration = 0 + defaultMaxConnectionDuration = 0 +) + +type Configurer struct { + Conf api.Conf + Protocol core_mesh.Protocol +} + +func (c *Configurer) ConfigureListener(filterChain *envoy_listener.FilterChain) error { + httpTimeouts := func(hcm *envoy_hcm.HttpConnectionManager) error { + if c.Conf.Http != nil { + hcm.StreamIdleTimeout = toProtoDurationOrDefault(c.Conf.Http.StreamIdleTimeout, defaultStreamIdleTimeout) + c.configureRequestTimeout(hcm.GetRouteConfig()) + } else { + hcm.StreamIdleTimeout = util_proto.Duration(defaultStreamIdleTimeout) + c.configureRequestTimeout(hcm.GetRouteConfig()) + } + return nil + } + tcpTimeouts := func(proxy *envoy_tcp.TcpProxy) error { + proxy.IdleTimeout = toProtoDurationOrDefault(c.Conf.IdleTimeout, defaultIdleTimeout) + return nil + } + switch c.Protocol { + case core_mesh.ProtocolHTTP, core_mesh.ProtocolHTTP2: + if err := listeners_v3.UpdateHTTPConnectionManager(filterChain, httpTimeouts); err != nil && !errors.Is(err, &listeners_v3.UnexpectedFilterConfigTypeError{}) { + return err + } + case core_mesh.ProtocolUnknown, core_mesh.ProtocolTCP, core_mesh.ProtocolKafka: + if err := listeners_v3.UpdateTCPProxy(filterChain, tcpTimeouts); err != nil && !errors.Is(err, &listeners_v3.UnexpectedFilterConfigTypeError{}) { + return err + } + } + + return nil +} + +func (c *Configurer) ConfigureCluster(cluster *envoy_cluster.Cluster) error { + cluster.ConnectTimeout = toProtoDurationOrDefault(c.Conf.ConnectionTimeout, defaultConnectionTimeout) + switch c.Protocol { + case core_mesh.ProtocolHTTP, core_mesh.ProtocolHTTP2: + err := clusters_v3.UpdateCommonHttpProtocolOptions(cluster, func(options *envoy_upstream_http.HttpProtocolOptions) { + if options.CommonHttpProtocolOptions == nil { + options.CommonHttpProtocolOptions = &envoy_core.HttpProtocolOptions{} + } + commonHttp := options.CommonHttpProtocolOptions + commonHttp.IdleTimeout = toProtoDurationOrDefault(c.Conf.IdleTimeout, defaultIdleTimeout) + if c.Conf.Http != nil { + commonHttp.MaxStreamDuration = toProtoDurationOrDefault(c.Conf.Http.MaxStreamDuration, defaultMaxStreamDuration) + commonHttp.MaxConnectionDuration = toProtoDurationOrDefault(c.Conf.Http.MaxConnectionDuration, defaultMaxConnectionDuration) + } else { + commonHttp.MaxStreamDuration = util_proto.Duration(defaultMaxStreamDuration) + commonHttp.MaxConnectionDuration = util_proto.Duration(defaultMaxConnectionDuration) + } + }) + if err != nil { + return err + } + } + return nil +} + +func (c *Configurer) ConfigureRouteAction(routeAction *envoy_route.RouteAction) { + if routeAction == nil { + return + } + if c.Conf.Http != nil { + routeAction.Timeout = toProtoDurationOrDefault(c.Conf.Http.RequestTimeout, defaultRequestTimeout) + } else { + routeAction.Timeout = util_proto.Duration(defaultRequestTimeout) + } +} + +func (c *Configurer) configureRequestTimeout(routeConfiguration *envoy_route.RouteConfiguration) { + if routeConfiguration != nil { + for _, vh := range routeConfiguration.VirtualHosts { + for _, route := range vh.Routes { + c.ConfigureRouteAction(route.GetRoute()) + } + } + } +} + +func toProtoDurationOrDefault(d *kube_meta.Duration, defaultDuration time.Duration) *durationpb.Duration { + if d == nil { + return util_proto.Duration(defaultDuration) + } + return util_proto.Duration(d.Duration) +} diff --git a/pkg/plugins/policies/xds/clusters.go b/pkg/plugins/policies/xds/clusters.go new file mode 100644 index 000000000000..24335d891ef4 --- /dev/null +++ b/pkg/plugins/policies/xds/clusters.go @@ -0,0 +1,39 @@ +package xds + +import ( + envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" + envoy_resource "github.com/envoyproxy/go-control-plane/pkg/resource/v3" + + "github.com/kumahq/kuma/pkg/core/xds" + "github.com/kumahq/kuma/pkg/plugins/runtime/gateway/metadata" + "github.com/kumahq/kuma/pkg/xds/generator" +) + +type Clusters struct { + Inbound map[string]*envoy_cluster.Cluster + Outbound map[string]*envoy_cluster.Cluster + Gateway map[string]*envoy_cluster.Cluster +} + +func GatherClusters(rs *xds.ResourceSet) Clusters { + clusters := Clusters{ + Inbound: map[string]*envoy_cluster.Cluster{}, + Outbound: map[string]*envoy_cluster.Cluster{}, + Gateway: map[string]*envoy_cluster.Cluster{}, + } + for _, res := range rs.Resources(envoy_resource.ClusterType) { + cluster := res.Resource.(*envoy_cluster.Cluster) + + switch res.Origin { + case generator.OriginOutbound: + clusters.Outbound[cluster.Name] = cluster + case generator.OriginInbound: + clusters.Inbound[cluster.Name] = cluster + case metadata.OriginGateway: + clusters.Gateway[cluster.Name] = cluster + default: + continue + } + } + return clusters +} diff --git a/pkg/plugins/policies/xds/routes.go b/pkg/plugins/policies/xds/routes.go new file mode 100644 index 000000000000..421a4f072bec --- /dev/null +++ b/pkg/plugins/policies/xds/routes.go @@ -0,0 +1,26 @@ +package xds + +import ( + envoy_route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + envoy_resource "github.com/envoyproxy/go-control-plane/pkg/resource/v3" + + core_xds "github.com/kumahq/kuma/pkg/core/xds" + "github.com/kumahq/kuma/pkg/plugins/runtime/gateway/metadata" +) + +type Routes struct { + Gateway map[string]*envoy_route.RouteConfiguration +} + +func GatherRoutes(rs *core_xds.ResourceSet) Routes { + routes := Routes{ + Gateway: map[string]*envoy_route.RouteConfiguration{}, + } + for _, res := range rs.Resources(envoy_resource.RouteType) { + if res.Origin == metadata.OriginGateway { + routeConfig := res.Resource.(*envoy_route.RouteConfiguration) + routes.Gateway[routeConfig.Name] = routeConfig + } + } + return routes +} diff --git a/pkg/plugins/runtime/gateway/cluster_generator.go b/pkg/plugins/runtime/gateway/cluster_generator.go index bdbd36217003..baaee8a5add0 100644 --- a/pkg/plugins/runtime/gateway/cluster_generator.go +++ b/pkg/plugins/runtime/gateway/cluster_generator.go @@ -37,7 +37,7 @@ func (c *ClusterGenerator) GenerateClusters(ctx context.Context, xdsCtx xds_cont // an array of endpoint and checks whether the first entry is from // an external service. Because the dataplane endpoints happen to be // generated first, the mesh service will have priority. - for _, dest := range routeDestinationsMutable(hostInfo.Entries) { + for _, dest := range RouteDestinationsMutable(hostInfo.Entries) { service := dest.Destination[mesh_proto.ServiceTag] firstEndpointExternalService := route.HasExternalServiceEndpoint(xdsCtx.Mesh.Resource, info.OutboundEndpoints, *dest) @@ -242,14 +242,14 @@ func buildClusterResource( func routeDestinations(entries []route.Entry) []route.Destination { var destinations []route.Destination - for _, dest := range routeDestinationsMutable(entries) { + for _, dest := range RouteDestinationsMutable(entries) { destinations = append(destinations, *dest) } return destinations } -func routeDestinationsMutable(entries []route.Entry) []*route.Destination { +func RouteDestinationsMutable(entries []route.Entry) []*route.Destination { var destinations []*route.Destination for _, e := range entries { diff --git a/pkg/plugins/runtime/gateway/generator.go b/pkg/plugins/runtime/gateway/generator.go index 47caebf2cb21..758c0d48e32e 100644 --- a/pkg/plugins/runtime/gateway/generator.go +++ b/pkg/plugins/runtime/gateway/generator.go @@ -88,16 +88,16 @@ type FilterChainGenerator interface { // Generator generates xDS resources for an entire Gateway. type Generator struct { - FilterChainGenerators filterChainGenerators + FilterChainGenerators FilterChainGenerators ClusterGenerator ClusterGenerator Zone string } -type filterChainGenerators struct { +type FilterChainGenerators struct { FilterChainGenerators map[mesh_proto.MeshGateway_Listener_Protocol]FilterChainGenerator } -func (g *filterChainGenerators) For(ctx xds_context.Context, info GatewayListenerInfo) FilterChainGenerator { +func (g *FilterChainGenerators) For(ctx xds_context.Context, info GatewayListenerInfo) FilterChainGenerator { gen := g.FilterChainGenerators[info.Listener.Protocol] return gen } diff --git a/pkg/plugins/runtime/gateway/match/policy.go b/pkg/plugins/runtime/gateway/match/policy.go index e6fcc090d2ba..42526a93bbd5 100644 --- a/pkg/plugins/runtime/gateway/match/policy.go +++ b/pkg/plugins/runtime/gateway/match/policy.go @@ -11,6 +11,9 @@ import ( // ToConnectionPolicies casts a ResourceList to a slice of ConnectionPolicy. func ToConnectionPolicies(policies model.ResourceList) []policy.ConnectionPolicy { + if policies == nil { + return []policy.ConnectionPolicy{} + } items := policies.GetItems() c := make([]policy.ConnectionPolicy, 0, len(items)) diff --git a/pkg/plugins/runtime/gateway/plugin.go b/pkg/plugins/runtime/gateway/plugin.go index d1e8407acd9b..259dd1f78bc9 100644 --- a/pkg/plugins/runtime/gateway/plugin.go +++ b/pkg/plugins/runtime/gateway/plugin.go @@ -69,7 +69,7 @@ func NewProxyProfile(zone string) generator_core.ResourceGenerator { generator.TransparentProxyGenerator{}, generator.DNSGenerator{}, Generator{ - FilterChainGenerators: filterChainGenerators{ + FilterChainGenerators: FilterChainGenerators{ FilterChainGenerators: map[mesh_proto.MeshGateway_Listener_Protocol]FilterChainGenerator{ mesh_proto.MeshGateway_Listener_HTTP: &HTTPFilterChainGenerator{}, mesh_proto.MeshGateway_Listener_HTTPS: &HTTPSFilterChainGenerator{}, diff --git a/pkg/test/resources/builders/dataplane_builder.go b/pkg/test/resources/builders/dataplane_builder.go index 1884aae0dedf..19c866db3ad6 100644 --- a/pkg/test/resources/builders/dataplane_builder.go +++ b/pkg/test/resources/builders/dataplane_builder.go @@ -161,6 +161,16 @@ func (d *DataplaneBuilder) WithPrometheusMetrics(config *mesh_proto.PrometheusMe return d } +func (d *DataplaneBuilder) WithBuiltInGateway(name string) *DataplaneBuilder { + d.res.Spec.Networking.Gateway = &mesh_proto.Dataplane_Networking_Gateway{ + Tags: map[string]string{ + mesh_proto.ServiceTag: name, + }, + Type: mesh_proto.Dataplane_Networking_Gateway_BUILTIN, + } + return d +} + type InboundBuilder struct { res *mesh_proto.Dataplane_Networking_Inbound } diff --git a/pkg/test/resources/builders/gateway_route_builder.go b/pkg/test/resources/builders/gateway_route_builder.go new file mode 100644 index 000000000000..ad6ec37cb91b --- /dev/null +++ b/pkg/test/resources/builders/gateway_route_builder.go @@ -0,0 +1,78 @@ +package builders + +import ( + mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1" + core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh" + core_model "github.com/kumahq/kuma/pkg/core/resources/model" + test_model "github.com/kumahq/kuma/pkg/test/resources/model" +) + +type GatewayRouteBuilder struct { + res *core_mesh.MeshGatewayRouteResource +} + +func GatewayRoute() *GatewayRouteBuilder { + return &GatewayRouteBuilder{ + res: &core_mesh.MeshGatewayRouteResource{ + Meta: &test_model.ResourceMeta{ + Mesh: core_model.DefaultMesh, + Name: "gateway-route", + }, + Spec: &mesh_proto.MeshGatewayRoute{}, + }, + } +} + +func (gr *GatewayRouteBuilder) Build() *core_mesh.MeshGatewayRouteResource { + if err := gr.res.Validate(); err != nil { + panic(err) + } + return gr.res +} + +func (gr *GatewayRouteBuilder) With(fn func(*core_mesh.MeshGatewayRouteResource)) *GatewayRouteBuilder { + fn(gr.res) + return gr +} + +func (gr *GatewayRouteBuilder) WithName(name string) *GatewayRouteBuilder { + gr.res.Meta.(*test_model.ResourceMeta).Name = name + return gr +} + +func (gr *GatewayRouteBuilder) WithGateway(gatewayName string) *GatewayRouteBuilder { + gr.res.Spec.Selectors = []*mesh_proto.Selector{{ + Match: map[string]string{ + mesh_proto.ServiceTag: gatewayName, + }}, + } + return gr +} + +func (gr *GatewayRouteBuilder) WithExactMatchHttpRoute(path string, backend string) *GatewayRouteBuilder { + gr.res.Spec.Conf = &mesh_proto.MeshGatewayRoute_Conf{ + Route: &mesh_proto.MeshGatewayRoute_Conf_Http{ + Http: &mesh_proto.MeshGatewayRoute_HttpRoute{ + Rules: []*mesh_proto.MeshGatewayRoute_HttpRoute_Rule{ + { + Matches: []*mesh_proto.MeshGatewayRoute_HttpRoute_Match{ + {Path: &mesh_proto.MeshGatewayRoute_HttpRoute_Match_Path{ + Match: mesh_proto.MeshGatewayRoute_HttpRoute_Match_Path_EXACT, + Value: path, + }}, + }, + Backends: []*mesh_proto.MeshGatewayRoute_Backend{ + { + Weight: 1, + Destination: map[string]string{ + "kuma.io/service": backend, + }, + }, + }, + }, + }, + }, + }, + } + return gr +} diff --git a/pkg/test/resources/samples/dataplane_samples.go b/pkg/test/resources/samples/dataplane_samples.go index 375da8c9de73..05bed0667163 100644 --- a/pkg/test/resources/samples/dataplane_samples.go +++ b/pkg/test/resources/samples/dataplane_samples.go @@ -26,3 +26,11 @@ func DataplaneWebBuilder() *builders.DataplaneBuilder { func DataplaneWeb() *mesh.DataplaneResource { return DataplaneWebBuilder().Build() } + +func GatewayDataplane() *mesh.DataplaneResource { + return builders.Dataplane(). + WithName("sample-gateway"). + WithAddress("192.168.0.1"). + WithBuiltInGateway("sample-gateway"). + Build() +} diff --git a/pkg/test/resources/samples/gateway_samples.go b/pkg/test/resources/samples/gateway_samples.go new file mode 100644 index 000000000000..4db0777d8217 --- /dev/null +++ b/pkg/test/resources/samples/gateway_samples.go @@ -0,0 +1,37 @@ +package samples + +import ( + mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1" + core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh" + "github.com/kumahq/kuma/pkg/test/resources/builders" + test_model "github.com/kumahq/kuma/pkg/test/resources/model" +) + +func BackendGatewayRoute() *core_mesh.MeshGatewayRouteResource { + return builders.GatewayRoute(). + WithName("sample-gateway-route"). + WithGateway("sample-gateway"). + WithExactMatchHttpRoute("/", "backend"). + Build() +} + +func GatewayResource() *core_mesh.MeshGatewayResource { + return &core_mesh.MeshGatewayResource{ + Meta: &test_model.ResourceMeta{Name: "sample-gateway", Mesh: "default"}, + Spec: &mesh_proto.MeshGateway{ + Selectors: []*mesh_proto.Selector{{ + Match: map[string]string{ + mesh_proto.ServiceTag: "sample-gateway", + }}, + }, + Conf: &mesh_proto.MeshGateway_Conf{ + Listeners: []*mesh_proto.MeshGateway_Listener{ + { + Protocol: mesh_proto.MeshGateway_Listener_HTTP, + Port: 8080, + }, + }, + }, + }, + } +} diff --git a/pkg/test/xds/cla_cache.go b/pkg/test/xds/cla_cache.go new file mode 100644 index 000000000000..c6cf9bf0b174 --- /dev/null +++ b/pkg/test/xds/cla_cache.go @@ -0,0 +1,21 @@ +package xds + +import ( + "context" + + "google.golang.org/protobuf/proto" + + core_xds "github.com/kumahq/kuma/pkg/core/xds" + envoy_common "github.com/kumahq/kuma/pkg/xds/envoy" + "github.com/kumahq/kuma/pkg/xds/envoy/endpoints/v3" +) + +type DummyCLACache struct { + OutboundTargets core_xds.EndpointMap +} + +func (d *DummyCLACache) GetCLA(ctx context.Context, meshName, meshHash string, cluster envoy_common.Cluster, apiVersion core_xds.APIVersion, endpointMap core_xds.EndpointMap) (proto.Message, error) { + return endpoints.CreateClusterLoadAssignment(cluster.Service(), d.OutboundTargets[cluster.Service()]), nil +} + +var _ envoy_common.CLACache = &DummyCLACache{} diff --git a/pkg/xds/envoy/clusters/cluster_builder.go b/pkg/xds/envoy/clusters/cluster_builder.go index c33af52a2a48..08c98c62b7e3 100644 --- a/pkg/xds/envoy/clusters/cluster_builder.go +++ b/pkg/xds/envoy/clusters/cluster_builder.go @@ -1,9 +1,8 @@ package clusters import ( - "errors" - envoy_api "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" + "github.com/pkg/errors" core_xds "github.com/kumahq/kuma/pkg/core/xds" "github.com/kumahq/kuma/pkg/xds/envoy" @@ -55,6 +54,15 @@ func (b *ClusterBuilder) Build() (envoy.NamedResource, error) { } } +func (b *ClusterBuilder) MustBuild() envoy.NamedResource { + cluster, err := b.Build() + if err != nil { + panic(errors.Wrap(err, "failed to build Envoy Cluster").Error()) + } + + return cluster +} + // ClusterBuilderConfig holds configuration of a ClusterBuilder. type ClusterBuilderConfig struct { // A series of ClusterConfigurers to apply to Envoy cluster. diff --git a/pkg/xds/generator/outbound_proxy_generator_test.go b/pkg/xds/generator/outbound_proxy_generator_test.go index 6cc10768aacd..d776b59c9086 100644 --- a/pkg/xds/generator/outbound_proxy_generator_test.go +++ b/pkg/xds/generator/outbound_proxy_generator_test.go @@ -15,6 +15,7 @@ import ( . "github.com/kumahq/kuma/pkg/test/matchers" test_model "github.com/kumahq/kuma/pkg/test/resources/model" "github.com/kumahq/kuma/pkg/test/xds" + test_xds "github.com/kumahq/kuma/pkg/test/xds" util_proto "github.com/kumahq/kuma/pkg/util/proto" "github.com/kumahq/kuma/pkg/xds/cache/cla" xds_context "github.com/kumahq/kuma/pkg/xds/context" @@ -815,7 +816,7 @@ var _ = Describe("OutboundProxyGenerator", func() { } // when - plainCtx.ControlPlane.CLACache = &dummyCLACache{outboundTargets: outboundTargets} + plainCtx.ControlPlane.CLACache = &test_xds.DummyCLACache{OutboundTargets: outboundTargets} rs, err := gen.Generate(plainCtx, proxy) // then diff --git a/pkg/xds/generator/proxy_template_profile_source_test.go b/pkg/xds/generator/proxy_template_profile_source_test.go index a17f39615511..3c857fc1d203 100644 --- a/pkg/xds/generator/proxy_template_profile_source_test.go +++ b/pkg/xds/generator/proxy_template_profile_source_test.go @@ -1,13 +1,11 @@ package generator_test import ( - "context" "path/filepath" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "google.golang.org/protobuf/proto" mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1" core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh" @@ -15,24 +13,14 @@ import ( . "github.com/kumahq/kuma/pkg/test/matchers" test_model "github.com/kumahq/kuma/pkg/test/resources/model" "github.com/kumahq/kuma/pkg/test/xds" + test_xds "github.com/kumahq/kuma/pkg/test/xds" "github.com/kumahq/kuma/pkg/tls" util_proto "github.com/kumahq/kuma/pkg/util/proto" xds_context "github.com/kumahq/kuma/pkg/xds/context" envoy_common "github.com/kumahq/kuma/pkg/xds/envoy" - "github.com/kumahq/kuma/pkg/xds/envoy/endpoints/v3" "github.com/kumahq/kuma/pkg/xds/generator" ) -type dummyCLACache struct { - outboundTargets core_xds.EndpointMap -} - -func (d *dummyCLACache) GetCLA(ctx context.Context, meshName, meshHash string, cluster envoy_common.Cluster, apiVersion core_xds.APIVersion, endpointMap core_xds.EndpointMap) (proto.Message, error) { - return endpoints.CreateClusterLoadAssignment(cluster.Service(), d.outboundTargets[cluster.Service()]), nil -} - -var _ envoy_common.CLACache = &dummyCLACache{} - var _ = Describe("ProxyTemplateProfileSource", func() { type testCase struct { @@ -70,7 +58,7 @@ var _ = Describe("ProxyTemplateProfileSource", func() { } ctx := xds_context.Context{ ControlPlane: &xds_context.ControlPlaneContext{ - CLACache: &dummyCLACache{outboundTargets: outboundTargets}, + CLACache: &test_xds.DummyCLACache{OutboundTargets: outboundTargets}, Secrets: &xds.TestSecrets{}, }, Mesh: xds_context.MeshContext{ diff --git a/test/e2e_env/kubernetes/kubernetes_suite_test.go b/test/e2e_env/kubernetes/kubernetes_suite_test.go index 38010cde9f0d..6f219e702494 100644 --- a/test/e2e_env/kubernetes/kubernetes_suite_test.go +++ b/test/e2e_env/kubernetes/kubernetes_suite_test.go @@ -22,6 +22,7 @@ import ( "github.com/kumahq/kuma/test/e2e_env/kubernetes/k8s_api_bypass" "github.com/kumahq/kuma/test/e2e_env/kubernetes/kic" "github.com/kumahq/kuma/test/e2e_env/kubernetes/membership" + "github.com/kumahq/kuma/test/e2e_env/kubernetes/meshtimeout" "github.com/kumahq/kuma/test/e2e_env/kubernetes/meshtrafficpermission" "github.com/kumahq/kuma/test/e2e_env/kubernetes/observability" "github.com/kumahq/kuma/test/e2e_env/kubernetes/reachableservices" @@ -118,3 +119,4 @@ var _ = Describe("External Services", externalservices.ExternalServices, Ordered var _ = Describe("Virtual Outbound", virtualoutbound.VirtualOutbound, Ordered) var _ = Describe("Kong Ingress Controller", Label("arm-not-supported"), kic.KICKubernetes, Ordered) var _ = Describe("MeshTrafficPermission API", meshtrafficpermission.API, Ordered) +var _ = Describe("MeshTimeout API", meshtimeout.MeshTimeout, Ordered) diff --git a/test/e2e_env/kubernetes/meshtimeout/meshtimeout.go b/test/e2e_env/kubernetes/meshtimeout/meshtimeout.go new file mode 100644 index 000000000000..d4e2e90a42f0 --- /dev/null +++ b/test/e2e_env/kubernetes/meshtimeout/meshtimeout.go @@ -0,0 +1,107 @@ +package meshtimeout + +import ( + "fmt" + "time" + + "github.com/gruntwork-io/terratest/modules/k8s" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/kumahq/kuma/test/e2e_env/kubernetes/env" + . "github.com/kumahq/kuma/test/framework" + "github.com/kumahq/kuma/test/framework/deployments/testserver" +) + +func MeshTimeout() { + namespace := "meshtimeout-namespace" + mesh := "meshtimeout" + var clientPodName string + + BeforeAll(func() { + err := NewClusterSetup(). + Install(MeshKubernetes(mesh)). + Install(NamespaceWithSidecarInjection(namespace)). + Install(DemoClientK8s(mesh, namespace)). + Install(testserver.Install(testserver.WithMesh(mesh), testserver.WithNamespace(namespace))). + Setup(env.Cluster) + Expect(err).ToNot(HaveOccurred()) + + clientPodName, err = PodNameOfApp(env.Cluster, "demo-client", namespace) + Expect(err).ToNot(HaveOccurred()) + }) + + E2EAfterEach(func() { + Expect( + k8s.RunKubectlE(env.Cluster.GetTesting(), env.Cluster.GetKubectlOptions(), "delete", "meshtimeouts", "-A", "--all"), + ).To(Succeed()) + }) + + E2EAfterAll(func() { + Expect(env.Cluster.TriggerDeleteNamespace(namespace)).To(Succeed()) + Expect(env.Cluster.DeleteMesh(mesh)).To(Succeed()) + }) + + DescribeTable("should add timeouts for outbound connections", func(timeoutConfig string) { + // given no MeshTimeout + mts, err := env.Cluster.GetKumactlOptions().KumactlList("meshtimeouts", mesh) + Expect(err).ToNot(HaveOccurred()) + Expect(mts).To(HaveLen(0)) + Eventually(func(g Gomega) { + start := time.Now() + _, sterr, err := env.Cluster.Exec(namespace, clientPodName, "demo-client", "curl", "-v", "-H", "x-set-response-delay-ms: 5000", fmt.Sprintf("test-server_%s_svc_80.mesh", namespace)) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(sterr).To(ContainSubstring("HTTP/1.1 200 OK")) + g.Expect(time.Since(start)).To(BeNumerically(">", time.Second*5)) + }).WithTimeout(30 * time.Second).Should(Succeed()) + + // when + Expect(YamlK8s(timeoutConfig)(env.Cluster)).To(Succeed()) + + // then + Eventually(func(g Gomega) { + stdout, _, err := env.Cluster.Exec(namespace, clientPodName, "demo-client", "curl", "-v", "-H", "x-set-response-delay-ms: 5000", fmt.Sprintf("test-server_%s_svc_80.mesh", namespace)) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(stdout).To(ContainSubstring("upstream request timeout")) + }).WithTimeout(30 * time.Second).Should(Succeed()) + }, + Entry("outbound timeout", fmt.Sprintf(` +apiVersion: kuma.io/v1alpha1 +kind: MeshTimeout +metadata: + name: mt1 + namespace: %s + labels: + kuma.io/mesh: %s +spec: + targetRef: + kind: Mesh + to: + - targetRef: + kind: Mesh + default: + idleTimeout: 20s + http: + requestTimeout: 2s + maxStreamDuration: 20s`, Config.KumaNamespace, mesh)), + Entry("inbound timeout", fmt.Sprintf(` +apiVersion: kuma.io/v1alpha1 +kind: MeshTimeout +metadata: + name: mt1 + namespace: %s + labels: + kuma.io/mesh: %s +spec: + targetRef: + kind: Mesh + from: + - targetRef: + kind: Mesh + default: + idleTimeout: 20s + http: + requestTimeout: 2s + maxStreamDuration: 20s`, Config.KumaNamespace, mesh)), + ) +} diff --git a/test/e2e_env/universal/timeout/meshtimeout.go b/test/e2e_env/universal/timeout/meshtimeout.go new file mode 100644 index 000000000000..7bb0b79d1fce --- /dev/null +++ b/test/e2e_env/universal/timeout/meshtimeout.go @@ -0,0 +1,89 @@ +package timeout + +import ( + "fmt" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/kumahq/kuma/test/e2e_env/universal/env" + . "github.com/kumahq/kuma/test/framework" +) + +func PluginTest() { + meshName := "meshtimeout" + + BeforeAll(func() { + err := NewClusterSetup(). + Install(MeshUniversal(meshName)). + Install(DemoClientUniversal("demo-client", meshName, + WithTransparentProxy(true)), + ). + Install(TestServerUniversal("test-server", meshName, + WithArgs([]string{"echo", "--instance", "universal-1"})), + ). + Setup(env.Cluster) + Expect(err).ToNot(HaveOccurred()) + }) + E2EAfterAll(func() { + Expect(env.Cluster.DeleteMeshApps(meshName)).To(Succeed()) + Expect(env.Cluster.DeleteMesh(meshName)).To(Succeed()) + }) + E2EAfterEach(func() { + Expect(env.Cluster.GetKumactlOptions().KumactlDelete("meshtimeout", "default", meshName)).To(Succeed()) + }) + + DescribeTable("should reset the connection by timeout", func(timeoutConfig string) { + By("check requests take over 5s") + Eventually(func(g Gomega) { + start := time.Now() + stdout, _, err := env.Cluster.Exec("", "", "demo-client", + "curl", "-v", "-H", "\"x-set-response-delay-ms: 5000\"", "--fail", "test-server.mesh") + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(stdout).To(ContainSubstring("HTTP/1.1 200 OK")) + g.Expect(time.Since(start)).To(BeNumerically(">", time.Second*5)) + }).Should(Succeed()) + + By("apply a new policy") + Expect(env.Cluster.Install(YamlUniversal(timeoutConfig))).To(Succeed()) + + By("eventually requests timeout consistently") + Eventually(func(g Gomega) { + stdout, _, err := env.Cluster.Exec("", "", "demo-client", + "curl", "-v", "-H", "\"x-set-response-delay-ms: 5000\"", "test-server.mesh") + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(stdout).To(ContainSubstring("upstream request timeout")) + }).Should(Succeed()) + }, + Entry("outbound timeout", fmt.Sprintf(` +type: MeshTimeout +name: default +mesh: %s +spec: + targetRef: + kind: Mesh + to: + - targetRef: + kind: MeshService + name: test-server + default: + connectionTimeout: 20s + http: + requestTimeout: 2s`, meshName)), + Entry("inbound timeout", fmt.Sprintf(` +type: MeshTimeout +name: default +mesh: %s +spec: + targetRef: + kind: Mesh + from: + - targetRef: + kind: Mesh + default: + connectionTimeout: 20s + http: + requestTimeout: 1s`, meshName)), + ) +} diff --git a/test/e2e_env/universal/universal_suite_test.go b/test/e2e_env/universal/universal_suite_test.go index c204d69916eb..a845b33045bd 100644 --- a/test/e2e_env/universal/universal_suite_test.go +++ b/test/e2e_env/universal/universal_suite_test.go @@ -116,3 +116,4 @@ var _ = Describe("Virtual Outbound", virtualoutbound.VirtualOutbound, Ordered) var _ = Describe("Transparent Proxy", transparentproxy.TransparentProxy, Ordered) var _ = Describe("Mesh Traffic Permission", meshtrafficpermission.MeshTrafficPermissionUniversal, Ordered) var _ = Describe("GRPC", grpc.GRPC, Ordered) +var _ = Describe("MeshTimeout", timeout.PluginTest, Ordered) diff --git a/test/server/cmd/echo.go b/test/server/cmd/echo.go index 211695af8031..fc4293b2de91 100644 --- a/test/server/cmd/echo.go +++ b/test/server/cmd/echo.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "strconv" + "time" "github.com/spf13/cobra" @@ -30,6 +31,7 @@ func newEchoHTTPCmd() *cobra.Command { RunE: func(cmd *cobra.Command, _ []string) error { http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { headers := request.Header + handleDelay(headers) headers.Add("host", request.Host) resp := &types.EchoResponse{ Instance: args.instance, @@ -99,3 +101,12 @@ func newEchoHTTPCmd() *cobra.Command { cmd.PersistentFlags().BoolVar(&args.probes, "probes", false, "generate readiness and liveness endpoints") return cmd } + +func handleDelay(headers http.Header) { + delayHeader := headers.Get("x-set-response-delay-ms") + delay, err := strconv.Atoi(delayHeader) + if err != nil { + return + } + time.Sleep(time.Duration(delay) * time.Millisecond) +}