/* * Copyright 2023 gRPC 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. * */ import { ClusterLoadAssignment } from "../src/generated/envoy/config/endpoint/v3/ClusterLoadAssignment"; import { Cluster } from "../src/generated/envoy/config/cluster/v3/Cluster"; import { Backend } from "./backend"; import { Locality } from "../src/generated/envoy/config/core/v3/Locality"; import { RouteConfiguration } from "../src/generated/envoy/config/route/v3/RouteConfiguration"; import { Route } from "../src/generated/envoy/config/route/v3/Route"; import { Listener } from "../src/generated/envoy/config/listener/v3/Listener"; import { HttpConnectionManager } from "../src/generated/envoy/extensions/filters/network/http_connection_manager/v3/HttpConnectionManager"; import { AnyExtension } from "@grpc/proto-loader"; import { CLUSTER_CONFIG_TYPE_URL, HTTP_CONNECTION_MANGER_TYPE_URL, UPSTREAM_TLS_CONTEXT_TYPE_URL } from "../src/resources"; import { LocalityLbEndpoints } from "../src/generated/envoy/config/endpoint/v3/LocalityLbEndpoints"; import { LbEndpoint } from "../src/generated/envoy/config/endpoint/v3/LbEndpoint"; import { ClusterConfig } from "../src/generated/envoy/extensions/clusters/aggregate/v3/ClusterConfig"; import { Any } from "../src/generated/google/protobuf/Any"; import { ControlPlaneServer } from "./xds-server"; import { UpstreamTlsContext } from "../src/generated/envoy/extensions/transport_sockets/tls/v3/UpstreamTlsContext"; import { HttpFilter } from "../src/generated/envoy/extensions/filters/network/http_connection_manager/v3/HttpFilter"; interface Endpoint { locality: Locality; backends: Backend[]; weight?: number; priority?: number; } function getLbEndpoint(backend: Backend): LbEndpoint { return { health_status: "HEALTHY", endpoint: { address: { socket_address: { address: '::1', port_value: backend.getPort() } } } }; } function getLocalityLbEndpoints(endpoint: Endpoint): LocalityLbEndpoints { return { lb_endpoints: endpoint.backends.map(getLbEndpoint), locality: endpoint.locality, load_balancing_weight: {value: endpoint.weight ?? 1}, priority: endpoint.priority ?? 0 } } export interface FakeCluster { getClusterConfig(): Cluster; getAllClusterConfigs(): Cluster[]; getName(): string; startAllBackends(controlPlaneServer: ControlPlaneServer): Promise; haveAllBackendsReceivedTraffic(): boolean; waitForAllBackendsToReceiveTraffic(): Promise; } export class FakeEdsCluster implements FakeCluster { constructor( private clusterName: string, private endpointName: string, private endpoints: Endpoint[], private loadBalancingPolicyOverride?: Any | 'RING_HASH' | undefined, private upstreamTlsContext?: UpstreamTlsContext ) {} getEndpointConfig(): ClusterLoadAssignment { return { cluster_name: this.endpointName, endpoints: this.endpoints.map(getLocalityLbEndpoints) }; } getClusterConfig(): Cluster { const result: Cluster = { name: this.clusterName, type: 'EDS', eds_cluster_config: {eds_config: {ads: {}}, service_name: this.endpointName}, lrs_server: {self: {}}, circuit_breakers: { thresholds: [ { priority: 'DEFAULT', max_requests: {value: 1000} } ] } }; if (this.loadBalancingPolicyOverride === 'RING_HASH') { result.lb_policy = 'RING_HASH'; } else if (this.loadBalancingPolicyOverride) { result.load_balancing_policy = { policies: [ { typed_extension_config: { 'name': 'test', typed_config: this.loadBalancingPolicyOverride } } ] } } else { result.lb_policy = 'ROUND_ROBIN'; } if (this.upstreamTlsContext) { result.transport_socket = { typed_config: { '@type': UPSTREAM_TLS_CONTEXT_TYPE_URL, ...this.upstreamTlsContext } } } return result; } getAllClusterConfigs(): Cluster[] { return [this.getClusterConfig()]; } getName() { return this.clusterName; } startAllBackends(controlPlaneServer: ControlPlaneServer): Promise { return Promise.all(this.endpoints.map(endpoint => Promise.all(endpoint.backends.map(backend => backend.startAsync(controlPlaneServer))))); } haveAllBackendsReceivedTraffic(): boolean { for (const endpoint of this.endpoints) { for (const backend of endpoint.backends) { if (backend.getCallCount() < 1) { return false; } } } return true; } waitForAllBackendsToReceiveTraffic(): Promise { for (const endpoint of this.endpoints) { for (const backend of endpoint.backends) { backend.resetCallCount(); } } return new Promise((resolve, reject) => { let finishedPromise = false; for (const endpoint of this.endpoints) { for (const backend of endpoint.backends) { backend.onCall(() => { if (finishedPromise) { return; } if (this.haveAllBackendsReceivedTraffic()) { finishedPromise = true; resolve(); } }); } } }); } } export class FakeDnsCluster implements FakeCluster { constructor(private name: string, private backend: Backend) {} getClusterConfig(): Cluster { return { name: this.name, type: 'LOGICAL_DNS', lb_policy: 'ROUND_ROBIN', load_assignment: { endpoints: [{ lb_endpoints: [{ endpoint: { address: { socket_address: { address: 'localhost', port_value: this.backend.getPort() } } } }] }] }, lrs_server: {self: {}} }; } getAllClusterConfigs(): Cluster[] { return [this.getClusterConfig()]; } getName(): string { return this.name; } startAllBackends(controlPlaneServer: ControlPlaneServer): Promise { return this.backend.startAsync(controlPlaneServer); } haveAllBackendsReceivedTraffic(): boolean { return this.backend.getCallCount() > 0; } waitForAllBackendsToReceiveTraffic(): Promise { return new Promise((resolve, reject) => { this.backend.onCall(resolve); }); } } export class FakeAggregateCluster implements FakeCluster { constructor(private name: string, private children: FakeCluster[]) {} getClusterConfig(): Cluster { const clusterConfig: ClusterConfig & AnyExtension = { '@type': CLUSTER_CONFIG_TYPE_URL, clusters: this.children.map(child => child.getName()) }; return { name: this.name, lb_policy: 'ROUND_ROBIN', cluster_type: { typed_config: clusterConfig } } } getAllClusterConfigs(): Cluster[] { const allConfigs = [this.getClusterConfig()]; for (const child of this.children) { allConfigs.push(...child.getAllClusterConfigs()); } return allConfigs; } getName(): string { return this.name; } startAllBackends(controlPlaneServer: ControlPlaneServer): Promise { return Promise.all(this.children.map(child => child.startAllBackends(controlPlaneServer))); } haveAllBackendsReceivedTraffic(): boolean { for (const child of this.children) { if (!child.haveAllBackendsReceivedTraffic()) { return false; } } return true; } waitForAllBackendsToReceiveTraffic(): Promise { return Promise.all(this.children.map(child => child.waitForAllBackendsToReceiveTraffic())).then(() => {}); } } interface FakeRoute { cluster?: FakeCluster; weightedClusters?: [{cluster: FakeCluster, weight: number}]; } function createRouteConfig(route: FakeRoute): Route { if (route.cluster) { return { match: { prefix: '' }, route: { cluster: route.cluster.getName(), // Default to consistent hash hash_policy: [{ filter_state: { key: 'io.grpc.channel_id' } }] }, }; } else { return { match: { prefix: '' }, route: { weighted_clusters: { clusters: route.weightedClusters!.map(clusterWeight => ({ name: clusterWeight.cluster.getName(), weight: {value: clusterWeight.weight} })) }, // Default to consistent hash hash_policy: [{ filter_state: { key: 'io.grpc.channel_id' } }] } } } } export class FakeRouteGroup { constructor(private listenerName: string, private routeName: string, private routes: FakeRoute[]) {} getRouteConfiguration(): RouteConfiguration { return { name: this.routeName, virtual_hosts: [{ domains: ['*'], routes: this.routes.map(createRouteConfig) }] }; } getListener(): Listener { const httpConnectionManager: HttpConnectionManager & AnyExtension = { '@type': HTTP_CONNECTION_MANGER_TYPE_URL, rds: { route_config_name: this.routeName, config_source: {ads: {}} } } return { name: this.listenerName, api_listener: { api_listener: httpConnectionManager } }; } startAllBackends(controlPlaneServer: ControlPlaneServer): Promise { return Promise.all(this.routes.map(route => { if (route.cluster) { return route.cluster.startAllBackends(controlPlaneServer); } else if (route.weightedClusters) { return Promise.all(route.weightedClusters.map(clusterWeight => clusterWeight.cluster.startAllBackends(controlPlaneServer))); } else { return Promise.resolve(); } })); } haveAllBackendsReceivedTraffic(): boolean { for (const route of this.routes) { if (route.cluster) { return route.cluster.haveAllBackendsReceivedTraffic(); } else if (route.weightedClusters) { for (const weightedCluster of route.weightedClusters) { if (!weightedCluster.cluster.haveAllBackendsReceivedTraffic()) { return false; } } } } return true; } waitForAllBackendsToReceiveTraffic(): Promise { return Promise.all(this.routes.map(route => { if (route.cluster) { return route.cluster.waitForAllBackendsToReceiveTraffic(); } else if (route.weightedClusters) { return Promise.all(route.weightedClusters.map(clusterWeight => clusterWeight.cluster.waitForAllBackendsToReceiveTraffic())).then(() => {}); } else { return Promise.resolve(); } })); } } const DEFAULT_BASE_SERVER_LISTENER: Listener = { default_filter_chain: { filter_chain_match: { source_type: 'SAME_IP_OR_LOOPBACK' } } }; const DEFAULT_BASE_SERVER_ROUTE_CONFIG: RouteConfiguration = { virtual_hosts: [{ domains: ['*'], routes: [{ match: { prefix: '' }, action: 'non_forwarding_action', non_forwarding_action: {} }] }] }; export class FakeServerRoute { private listener: Listener; private routeConfiguration: RouteConfiguration; constructor(port: number, routeName: string, baseListener?: Listener | undefined, baseRouteConfiguration?: RouteConfiguration | undefined, httpFilters?: HttpFilter[]) { this.listener = baseListener ?? {...DEFAULT_BASE_SERVER_LISTENER}; this.listener.name = `[::1]:${port}`; this.listener.address = { socket_address: { address: '::1', port_value: port } } const httpConnectionManager: HttpConnectionManager & AnyExtension = { '@type': HTTP_CONNECTION_MANGER_TYPE_URL, rds: { route_config_name: routeName, config_source: {ads: {}} }, http_filters: httpFilters ?? [] }; const filterList = [{ typed_config: httpConnectionManager }]; if (this.listener.default_filter_chain) { this.listener.default_filter_chain.filters = filterList; } for (const filterChain of this.listener.filter_chains ?? []) { filterChain.filters = filterList; } this.routeConfiguration = baseRouteConfiguration ?? {...DEFAULT_BASE_SERVER_ROUTE_CONFIG}; this.routeConfiguration.name = routeName; } getRouteConfiguration(): RouteConfiguration { return this.routeConfiguration; } getListener(): Listener { return this.listener; } }