From d28b9e8c378d4f81911056d1a9d27e36adc94f32 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 7 Aug 2023 17:23:57 -0700 Subject: [PATCH 001/109] grpc-js: Return LB policy configs from resolvers in JSON form --- packages/grpc-js/src/experimental.ts | 9 +- packages/grpc-js/src/index.ts | 2 + .../src/load-balancer-child-handler.ts | 10 +-- .../src/load-balancer-outlier-detection.ts | 60 +++++++------- .../grpc-js/src/load-balancer-pick-first.ts | 6 +- .../grpc-js/src/load-balancer-round-robin.ts | 6 +- packages/grpc-js/src/load-balancer.ts | 83 ++++++++++--------- .../grpc-js/src/resolving-load-balancer.ts | 8 +- packages/grpc-js/src/service-config.ts | 25 +++++- packages/grpc-js/test/test-deadline.ts | 4 +- 10 files changed, 119 insertions(+), 94 deletions(-) diff --git a/packages/grpc-js/src/experimental.ts b/packages/grpc-js/src/experimental.ts index 9e4bbf45b..e4bd164ec 100644 --- a/packages/grpc-js/src/experimental.ts +++ b/packages/grpc-js/src/experimental.ts @@ -8,16 +8,15 @@ export { } from './resolver'; export { GrpcUri, uriToString } from './uri-parser'; export { Duration, durationToMs } from './duration'; -export { ServiceConfig, MethodConfig, RetryPolicy } from './service-config'; export { BackoffTimeout } from './backoff-timeout'; export { LoadBalancer, - LoadBalancingConfig, + TypedLoadBalancingConfig, ChannelControlHelper, createChildChannelControlHelper, registerLoadBalancerType, - getFirstUsableConfig, - validateLoadBalancingConfig, + selectLbConfigFromList, + parseLoadBalancingConfig } from './load-balancer'; export { SubchannelAddress, @@ -42,7 +41,7 @@ export { ConnectivityStateListener, } from './subchannel-interface'; export { - OutlierDetectionLoadBalancingConfig, + OutlierDetectionRawConfig, SuccessRateEjectionConfig, FailurePercentageEjectionConfig, } from './load-balancer-outlier-detection'; diff --git a/packages/grpc-js/src/index.ts b/packages/grpc-js/src/index.ts index adacae08f..d44a2dc6e 100644 --- a/packages/grpc-js/src/index.ts +++ b/packages/grpc-js/src/index.ts @@ -261,6 +261,8 @@ export { getChannelzServiceDefinition, getChannelzHandlers } from './channelz'; export { addAdminServicesToServer } from './admin'; +export { ServiceConfig, LoadBalancingConfig, MethodConfig, RetryPolicy } from './service-config'; + import * as experimental from './experimental'; export { experimental }; diff --git a/packages/grpc-js/src/load-balancer-child-handler.ts b/packages/grpc-js/src/load-balancer-child-handler.ts index a4dc90c4f..b23f19263 100644 --- a/packages/grpc-js/src/load-balancer-child-handler.ts +++ b/packages/grpc-js/src/load-balancer-child-handler.ts @@ -18,7 +18,7 @@ import { LoadBalancer, ChannelControlHelper, - LoadBalancingConfig, + TypedLoadBalancingConfig, createLoadBalancer, } from './load-balancer'; import { SubchannelAddress } from './subchannel-address'; @@ -33,7 +33,7 @@ const TYPE_NAME = 'child_load_balancer_helper'; export class ChildLoadBalancerHandler implements LoadBalancer { private currentChild: LoadBalancer | null = null; private pendingChild: LoadBalancer | null = null; - private latestConfig: LoadBalancingConfig | null = null; + private latestConfig: TypedLoadBalancingConfig | null = null; private ChildPolicyHelper = class { private child: LoadBalancer | null = null; @@ -87,8 +87,8 @@ export class ChildLoadBalancerHandler implements LoadBalancer { constructor(private readonly channelControlHelper: ChannelControlHelper) {} protected configUpdateRequiresNewPolicyInstance( - oldConfig: LoadBalancingConfig, - newConfig: LoadBalancingConfig + oldConfig: TypedLoadBalancingConfig, + newConfig: TypedLoadBalancingConfig ): boolean { return oldConfig.getLoadBalancerName() !== newConfig.getLoadBalancerName(); } @@ -101,7 +101,7 @@ export class ChildLoadBalancerHandler implements LoadBalancer { */ updateAddressList( addressList: SubchannelAddress[], - lbConfig: LoadBalancingConfig, + lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown } ): void { let childToUpdate: LoadBalancer; diff --git a/packages/grpc-js/src/load-balancer-outlier-detection.ts b/packages/grpc-js/src/load-balancer-outlier-detection.ts index 4abbd0843..00d0e0a53 100644 --- a/packages/grpc-js/src/load-balancer-outlier-detection.ts +++ b/packages/grpc-js/src/load-balancer-outlier-detection.ts @@ -18,17 +18,16 @@ import { ChannelOptions } from './channel-options'; import { ConnectivityState } from './connectivity-state'; import { LogVerbosity, Status } from './constants'; -import { durationToMs, isDuration, msToDuration } from './duration'; +import { Duration, durationToMs, isDuration, msToDuration } from './duration'; import { ChannelControlHelper, createChildChannelControlHelper, registerLoadBalancerType, } from './experimental'; import { - getFirstUsableConfig, + selectLbConfigFromList, LoadBalancer, - LoadBalancingConfig, - validateLoadBalancingConfig, + TypedLoadBalancingConfig, } from './load-balancer'; import { ChildLoadBalancerHandler } from './load-balancer-child-handler'; import { PickArgs, Picker, PickResult, PickResultType } from './picker'; @@ -42,6 +41,7 @@ import { SubchannelInterface, } from './subchannel-interface'; import * as logging from './logging'; +import { LoadBalancingConfig } from './service-config'; const TRACER_NAME = 'outlier_detection'; @@ -68,6 +68,16 @@ export interface FailurePercentageEjectionConfig { readonly request_volume: number; } +export interface OutlierDetectionRawConfig { + interval?: Duration; + base_ejection_time?: Duration; + max_ejection_time?: Duration; + max_ejection_percent?: number; + success_rate_ejection?: Partial; + failure_percentage_ejection?: Partial; + child_policy: LoadBalancingConfig[]; +} + const defaultSuccessRateEjectionConfig: SuccessRateEjectionConfig = { stdev_factor: 1900, enforcement_percentage: 100, @@ -147,7 +157,7 @@ function validatePercentage(obj: any, fieldName: string, objectName?: string) { } export class OutlierDetectionLoadBalancingConfig - implements LoadBalancingConfig + implements TypedLoadBalancingConfig { private readonly intervalMs: number; private readonly baseEjectionTimeMs: number; @@ -163,11 +173,10 @@ export class OutlierDetectionLoadBalancingConfig maxEjectionPercent: number | null, successRateEjection: Partial | null, failurePercentageEjection: Partial | null, - private readonly childPolicy: LoadBalancingConfig[] + private readonly childPolicy: TypedLoadBalancingConfig ) { if ( - childPolicy.length > 0 && - childPolicy[0].getLoadBalancerName() === 'pick_first' + childPolicy.getLoadBalancerName() === 'pick_first' ) { throw new Error( 'outlier_detection LB policy cannot have a pick_first child policy' @@ -198,7 +207,7 @@ export class OutlierDetectionLoadBalancingConfig max_ejection_percent: this.maxEjectionPercent, success_rate_ejection: this.successRateEjection, failure_percentage_ejection: this.failurePercentageEjection, - child_policy: this.childPolicy.map(policy => policy.toJsonObject()), + child_policy: [this.childPolicy.toJsonObject()] }; } @@ -220,24 +229,10 @@ export class OutlierDetectionLoadBalancingConfig getFailurePercentageEjectionConfig(): FailurePercentageEjectionConfig | null { return this.failurePercentageEjection; } - getChildPolicy(): LoadBalancingConfig[] { + getChildPolicy(): TypedLoadBalancingConfig { return this.childPolicy; } - copyWithChildPolicy( - childPolicy: LoadBalancingConfig[] - ): OutlierDetectionLoadBalancingConfig { - return new OutlierDetectionLoadBalancingConfig( - this.intervalMs, - this.baseEjectionTimeMs, - this.maxEjectionTimeMs, - this.maxEjectionPercent, - this.successRateEjection, - this.failurePercentageEjection, - childPolicy - ); - } - static createFromJson(obj: any): OutlierDetectionLoadBalancingConfig { validatePositiveDuration(obj, 'interval'); validatePositiveDuration(obj, 'base_ejection_time'); @@ -303,6 +298,14 @@ export class OutlierDetectionLoadBalancingConfig ); } + if (!('child_policy' in obj) || !Array.isArray(obj.child_policy)) { + throw new Error('outlier detection config child_policy must be an array'); + } + const childPolicy = selectLbConfigFromList(obj.child_policy); + if (!childPolicy) { + throw new Error('outlier detection config child_policy: no valid recognized policy found'); + } + return new OutlierDetectionLoadBalancingConfig( obj.interval ? durationToMs(obj.interval) : null, obj.base_ejection_time ? durationToMs(obj.base_ejection_time) : null, @@ -310,7 +313,7 @@ export class OutlierDetectionLoadBalancingConfig obj.max_ejection_percent ?? null, obj.success_rate_ejection, obj.failure_percentage_ejection, - obj.child_policy.map(validateLoadBalancingConfig) + childPolicy ); } } @@ -794,7 +797,7 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { updateAddressList( addressList: SubchannelAddress[], - lbConfig: LoadBalancingConfig, + lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown } ): void { if (!(lbConfig instanceof OutlierDetectionLoadBalancingConfig)) { @@ -821,10 +824,7 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { this.addressMap.delete(key); } } - const childPolicy: LoadBalancingConfig = getFirstUsableConfig( - lbConfig.getChildPolicy(), - true - ); + const childPolicy = lbConfig.getChildPolicy(); this.childBalancer.updateAddressList(addressList, childPolicy, attributes); if ( diff --git a/packages/grpc-js/src/load-balancer-pick-first.ts b/packages/grpc-js/src/load-balancer-pick-first.ts index 08971980b..8635482ce 100644 --- a/packages/grpc-js/src/load-balancer-pick-first.ts +++ b/packages/grpc-js/src/load-balancer-pick-first.ts @@ -18,7 +18,7 @@ import { LoadBalancer, ChannelControlHelper, - LoadBalancingConfig, + TypedLoadBalancingConfig, registerDefaultLoadBalancerType, registerLoadBalancerType, } from './load-balancer'; @@ -53,7 +53,7 @@ const TYPE_NAME = 'pick_first'; */ const CONNECTION_DELAY_INTERVAL_MS = 250; -export class PickFirstLoadBalancingConfig implements LoadBalancingConfig { +export class PickFirstLoadBalancingConfig implements TypedLoadBalancingConfig { constructor(private readonly shuffleAddressList: boolean) {} getLoadBalancerName(): string { @@ -374,7 +374,7 @@ export class PickFirstLoadBalancer implements LoadBalancer { updateAddressList( addressList: SubchannelAddress[], - lbConfig: LoadBalancingConfig + lbConfig: TypedLoadBalancingConfig ): void { if (!(lbConfig instanceof PickFirstLoadBalancingConfig)) { return; diff --git a/packages/grpc-js/src/load-balancer-round-robin.ts b/packages/grpc-js/src/load-balancer-round-robin.ts index f389fefc0..a611cfd64 100644 --- a/packages/grpc-js/src/load-balancer-round-robin.ts +++ b/packages/grpc-js/src/load-balancer-round-robin.ts @@ -18,7 +18,7 @@ import { LoadBalancer, ChannelControlHelper, - LoadBalancingConfig, + TypedLoadBalancingConfig, registerLoadBalancerType, } from './load-balancer'; import { ConnectivityState } from './connectivity-state'; @@ -49,7 +49,7 @@ function trace(text: string): void { const TYPE_NAME = 'round_robin'; -class RoundRobinLoadBalancingConfig implements LoadBalancingConfig { +class RoundRobinLoadBalancingConfig implements TypedLoadBalancingConfig { getLoadBalancerName(): string { return TYPE_NAME; } @@ -192,7 +192,7 @@ export class RoundRobinLoadBalancer implements LoadBalancer { updateAddressList( addressList: SubchannelAddress[], - lbConfig: LoadBalancingConfig + lbConfig: TypedLoadBalancingConfig ): void { this.resetSubchannelList(); trace( diff --git a/packages/grpc-js/src/load-balancer.ts b/packages/grpc-js/src/load-balancer.ts index f18638788..d5d69543f 100644 --- a/packages/grpc-js/src/load-balancer.ts +++ b/packages/grpc-js/src/load-balancer.ts @@ -21,6 +21,9 @@ import { ConnectivityState } from './connectivity-state'; import { Picker } from './picker'; import { ChannelRef, SubchannelRef } from './channelz'; import { SubchannelInterface } from './subchannel-interface'; +import { LoadBalancingConfig } from './service-config'; +import { log } from './logging'; +import { LogVerbosity } from './constants'; /** * A collection of functions associated with a channel that a load balancer @@ -98,7 +101,7 @@ export interface LoadBalancer { */ updateAddressList( addressList: SubchannelAddress[], - lbConfig: LoadBalancingConfig, + lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown } ): void; /** @@ -128,22 +131,22 @@ export interface LoadBalancerConstructor { new (channelControlHelper: ChannelControlHelper): LoadBalancer; } -export interface LoadBalancingConfig { +export interface TypedLoadBalancingConfig { getLoadBalancerName(): string; toJsonObject(): object; } -export interface LoadBalancingConfigConstructor { +export interface TypedLoadBalancingConfigConstructor { // eslint-disable-next-line @typescript-eslint/no-explicit-any - new (...args: any): LoadBalancingConfig; + new (...args: any): TypedLoadBalancingConfig; // eslint-disable-next-line @typescript-eslint/no-explicit-any - createFromJson(obj: any): LoadBalancingConfig; + createFromJson(obj: any): TypedLoadBalancingConfig; } const registeredLoadBalancerTypes: { [name: string]: { LoadBalancer: LoadBalancerConstructor; - LoadBalancingConfig: LoadBalancingConfigConstructor; + LoadBalancingConfig: TypedLoadBalancingConfigConstructor; }; } = {}; @@ -152,7 +155,7 @@ let defaultLoadBalancerType: string | null = null; export function registerLoadBalancerType( typeName: string, loadBalancerType: LoadBalancerConstructor, - loadBalancingConfigType: LoadBalancingConfigConstructor + loadBalancingConfigType: TypedLoadBalancingConfigConstructor ) { registeredLoadBalancerTypes[typeName] = { LoadBalancer: loadBalancerType, @@ -165,7 +168,7 @@ export function registerDefaultLoadBalancerType(typeName: string) { } export function createLoadBalancer( - config: LoadBalancingConfig, + config: TypedLoadBalancingConfig, channelControlHelper: ChannelControlHelper ): LoadBalancer | null { const typeName = config.getLoadBalancerName(); @@ -182,17 +185,44 @@ export function isLoadBalancerNameRegistered(typeName: string): boolean { return typeName in registeredLoadBalancerTypes; } -export function getFirstUsableConfig( - configs: LoadBalancingConfig[], - fallbackTodefault?: true -): LoadBalancingConfig; -export function getFirstUsableConfig( +export function parseLoadBalancingConfig(rawConfig: LoadBalancingConfig): TypedLoadBalancingConfig { + const keys = Object.keys(rawConfig); + if (keys.length !== 1) { + throw new Error( + 'Provided load balancing config has multiple conflicting entries' + ); + } + const typeName = keys[0]; + if (typeName in registeredLoadBalancerTypes) { + try { + return registeredLoadBalancerTypes[ + typeName + ].LoadBalancingConfig.createFromJson(rawConfig[typeName]); + } catch (e) { + throw new Error(`${typeName}: ${(e as Error).message}`); + } + } else { + throw new Error(`Unrecognized load balancing config name ${typeName}`); + } +} + +export function getDefaultConfig() { + if (!defaultLoadBalancerType) { + throw new Error('No default load balancer type registered'); + } + return new registeredLoadBalancerTypes[defaultLoadBalancerType]!.LoadBalancingConfig(); +} + +export function selectLbConfigFromList( configs: LoadBalancingConfig[], fallbackTodefault = false -): LoadBalancingConfig | null { +): TypedLoadBalancingConfig | null { for (const config of configs) { - if (config.getLoadBalancerName() in registeredLoadBalancerTypes) { - return config; + try { + return parseLoadBalancingConfig(config); + } catch (e) { + log(LogVerbosity.DEBUG, 'Config parsing failed with error', (e as Error).message); + continue; } } if (fallbackTodefault) { @@ -207,24 +237,3 @@ export function getFirstUsableConfig( return null; } } - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function validateLoadBalancingConfig(obj: any): LoadBalancingConfig { - if (!(obj !== null && typeof obj === 'object')) { - throw new Error('Load balancing config must be an object'); - } - const keys = Object.keys(obj); - if (keys.length !== 1) { - throw new Error( - 'Provided load balancing config has multiple conflicting entries' - ); - } - const typeName = keys[0]; - if (typeName in registeredLoadBalancerTypes) { - return registeredLoadBalancerTypes[ - typeName - ].LoadBalancingConfig.createFromJson(obj[typeName]); - } else { - throw new Error(`Unrecognized load balancing config name ${typeName}`); - } -} diff --git a/packages/grpc-js/src/resolving-load-balancer.ts b/packages/grpc-js/src/resolving-load-balancer.ts index d49609ff2..e2b2c1fa5 100644 --- a/packages/grpc-js/src/resolving-load-balancer.ts +++ b/packages/grpc-js/src/resolving-load-balancer.ts @@ -18,8 +18,8 @@ import { ChannelControlHelper, LoadBalancer, - LoadBalancingConfig, - getFirstUsableConfig, + TypedLoadBalancingConfig, + selectLbConfigFromList, } from './load-balancer'; import { ServiceConfig, validateServiceConfig } from './service-config'; import { ConnectivityState } from './connectivity-state'; @@ -211,7 +211,7 @@ export class ResolvingLoadBalancer implements LoadBalancer { } const workingConfigList = workingServiceConfig?.loadBalancingConfig ?? []; - const loadBalancingConfig = getFirstUsableConfig( + const loadBalancingConfig = selectLbConfigFromList( workingConfigList, true ); @@ -308,7 +308,7 @@ export class ResolvingLoadBalancer implements LoadBalancer { updateAddressList( addressList: SubchannelAddress[], - lbConfig: LoadBalancingConfig | null + lbConfig: TypedLoadBalancingConfig | null ): never { throw new Error('updateAddressList not supported on ResolvingLoadBalancer'); } diff --git a/packages/grpc-js/src/service-config.ts b/packages/grpc-js/src/service-config.ts index 91bee52c2..168f28c78 100644 --- a/packages/grpc-js/src/service-config.ts +++ b/packages/grpc-js/src/service-config.ts @@ -29,10 +29,6 @@ import * as os from 'os'; import { Status } from './constants'; import { Duration } from './duration'; -import { - LoadBalancingConfig, - validateLoadBalancingConfig, -} from './load-balancer'; export interface MethodConfigName { service: string; @@ -68,6 +64,10 @@ export interface RetryThrottling { tokenRatio: number; } +export interface LoadBalancingConfig { + [key: string]: object; +} + export interface ServiceConfig { loadBalancingPolicy?: string; loadBalancingConfig: LoadBalancingConfig[]; @@ -338,6 +338,22 @@ export function validateRetryThrottling(obj: any): RetryThrottling { }; } +function validateLoadBalancingConfig(obj: any): LoadBalancingConfig { + if (!(typeof obj === 'object' && obj !== null)) { + throw new Error(`Invalid loadBalancingConfig: unexpected type ${typeof obj}`); + } + const keys = Object.keys(obj); + if (keys.length > 1) { + throw new Error(`Invalid loadBalancingConfig: unexpected multiple keys ${keys}`); + } + if (keys.length === 0) { + throw new Error('Invalid loadBalancingConfig: load balancing policy name required'); + } + return { + [keys[0]]: obj[keys[0]] + }; +} + export function validateServiceConfig(obj: any): ServiceConfig { const result: ServiceConfig = { loadBalancingConfig: [], @@ -353,6 +369,7 @@ export function validateServiceConfig(obj: any): ServiceConfig { if ('loadBalancingConfig' in obj) { if (Array.isArray(obj.loadBalancingConfig)) { for (const config of obj.loadBalancingConfig) { + result.loadBalancingConfig.push(validateLoadBalancingConfig(config)); } } else { diff --git a/packages/grpc-js/test/test-deadline.ts b/packages/grpc-js/test/test-deadline.ts index 9de3687c4..24aebd4d7 100644 --- a/packages/grpc-js/test/test-deadline.ts +++ b/packages/grpc-js/test/test-deadline.ts @@ -18,12 +18,10 @@ import * as assert from 'assert'; import * as grpc from '../src'; -import { experimental } from '../src'; import { ServiceClient, ServiceClientConstructor } from '../src/make-client'; import { loadProtoFile } from './common'; -import ServiceConfig = experimental.ServiceConfig; -const TIMEOUT_SERVICE_CONFIG: ServiceConfig = { +const TIMEOUT_SERVICE_CONFIG: grpc.ServiceConfig = { loadBalancingConfig: [], methodConfig: [ { From 08bcbfc677612e21c9af2bd0c6ea0f5f75517688 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 7 Aug 2023 17:25:39 -0700 Subject: [PATCH 002/109] grpc-js-xds: Adjust LB policy config handling for grpc-js changes --- packages/grpc-js-xds/src/duration.ts | 33 +++++ packages/grpc-js-xds/src/load-balancer-cds.ts | 83 +++++------ packages/grpc-js-xds/src/load-balancer-lrs.ts | 25 ++-- .../grpc-js-xds/src/load-balancer-priority.ts | 79 +++++++---- .../src/load-balancer-weighted-target.ts | 48 +++++-- .../src/load-balancer-xds-cluster-impl.ts | 21 +-- .../src/load-balancer-xds-cluster-manager.ts | 45 +++--- .../src/load-balancer-xds-cluster-resolver.ts | 131 +++++++++++------- packages/grpc-js-xds/src/resolver-xds.ts | 42 ++---- packages/grpc-js-xds/src/route-action.ts | 5 +- .../cluster-resource-type.ts | 46 ++---- 11 files changed, 302 insertions(+), 256 deletions(-) create mode 100644 packages/grpc-js-xds/src/duration.ts diff --git a/packages/grpc-js-xds/src/duration.ts b/packages/grpc-js-xds/src/duration.ts new file mode 100644 index 000000000..07f33651f --- /dev/null +++ b/packages/grpc-js-xds/src/duration.ts @@ -0,0 +1,33 @@ +/* + * 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 { experimental } from '@grpc/grpc-js'; +import { Duration__Output } from './generated/google/protobuf/Duration'; +import Duration = experimental.Duration; + +/** + * Convert a Duration protobuf message object to a Duration object as used in + * the ServiceConfig definition. The difference is that the protobuf message + * defines seconds as a long, which is represented as a string in JavaScript, + * and the one used in the service config defines it as a number. + * @param duration + */ +export function protoDurationToDuration(duration: Duration__Output): Duration { + return { + seconds: Number.parseInt(duration.seconds), + nanos: duration.nanos + }; +} diff --git a/packages/grpc-js-xds/src/load-balancer-cds.ts b/packages/grpc-js-xds/src/load-balancer-cds.ts index 6f791299c..3ebcbbdf6 100644 --- a/packages/grpc-js-xds/src/load-balancer-cds.ts +++ b/packages/grpc-js-xds/src/load-balancer-cds.ts @@ -15,7 +15,7 @@ * */ -import { connectivityState, status, Metadata, logVerbosity, experimental } from '@grpc/grpc-js'; +import { connectivityState, status, Metadata, logVerbosity, experimental, LoadBalancingConfig } from '@grpc/grpc-js'; import { getSingletonXdsClient, Watcher, XdsClient } from './xds-client'; import { Cluster__Output } from './generated/envoy/config/cluster/v3/Cluster'; import SubchannelAddress = experimental.SubchannelAddress; @@ -24,17 +24,18 @@ import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; import LoadBalancer = experimental.LoadBalancer; import ChannelControlHelper = experimental.ChannelControlHelper; import registerLoadBalancerType = experimental.registerLoadBalancerType; -import LoadBalancingConfig = experimental.LoadBalancingConfig; -import OutlierDetectionLoadBalancingConfig = experimental.OutlierDetectionLoadBalancingConfig; +import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; import SuccessRateEjectionConfig = experimental.SuccessRateEjectionConfig; import FailurePercentageEjectionConfig = experimental.FailurePercentageEjectionConfig; import QueuePicker = experimental.QueuePicker; +import OutlierDetectionRawConfig = experimental.OutlierDetectionRawConfig; +import parseLoadBalancingConfig = experimental.parseLoadBalancingConfig; import { OutlierDetection__Output } from './generated/envoy/config/cluster/v3/OutlierDetection'; import { Duration__Output } from './generated/google/protobuf/Duration'; import { EXPERIMENTAL_OUTLIER_DETECTION } from './environment'; -import { DiscoveryMechanism, XdsClusterResolverChildPolicyHandler, XdsClusterResolverLoadBalancingConfig } from './load-balancer-xds-cluster-resolver'; +import { DiscoveryMechanism, XdsClusterResolverChildPolicyHandler } from './load-balancer-xds-cluster-resolver'; import { CLUSTER_CONFIG_TYPE_URL, decodeSingleResource } from './resources'; -import { CdsUpdate, ClusterResourceType, OutlierDetectionUpdate } from './xds-resource-type/cluster-resource-type'; +import { CdsUpdate, ClusterResourceType } from './xds-resource-type/cluster-resource-type'; const TRACER_NAME = 'cds_balancer'; @@ -44,7 +45,7 @@ function trace(text: string): void { const TYPE_NAME = 'cds'; -export class CdsLoadBalancingConfig implements LoadBalancingConfig { +class CdsLoadBalancingConfig implements TypedLoadBalancingConfig { getLoadBalancerName(): string { return TYPE_NAME; } @@ -72,29 +73,6 @@ export class CdsLoadBalancingConfig implements LoadBalancingConfig { } } -function durationToMs(duration: Duration__Output): number { - return (Number(duration.seconds) * 1_000 + duration.nanos / 1_000_000) | 0; -} - -function translateOutlierDetectionConfig(outlierDetection: OutlierDetectionUpdate | undefined): OutlierDetectionLoadBalancingConfig | undefined { - if (!EXPERIMENTAL_OUTLIER_DETECTION) { - return undefined; - } - if (!outlierDetection) { - /* No-op outlier detection config, with all fields unset. */ - return new OutlierDetectionLoadBalancingConfig(null, null, null, null, null, null, []); - } - return new OutlierDetectionLoadBalancingConfig( - outlierDetection.intervalMs, - outlierDetection.baseEjectionTimeMs, - outlierDetection.maxEjectionTimeMs, - outlierDetection.maxEjectionPercent, - outlierDetection.successRateConfig, - outlierDetection.failurePercentageConfig, - [] - ); -} - interface ClusterEntry { watcher: Watcher; latestUpdate?: CdsUpdate; @@ -133,7 +111,7 @@ function generateDiscoverymechanismForCdsUpdate(config: CdsUpdate): DiscoveryMec type: config.type, eds_service_name: config.edsServiceName, dns_hostname: config.dnsHostname, - outlier_detection: translateOutlierDetectionConfig(config.outlierDetectionUpdate) + outlier_detection: config.outlierDetectionUpdate }; } @@ -141,8 +119,8 @@ const RECURSION_DEPTH_LIMIT = 15; /** * Prerequisite: isClusterTreeFullyUpdated(tree, root) - * @param tree - * @param root + * @param tree + * @param root */ function getDiscoveryMechanismList(tree: ClusterTree, root: string): DiscoveryMechanism[] { const visited = new Set(); @@ -189,6 +167,11 @@ export class CdsLoadBalancer implements LoadBalancer { this.childBalancer = new XdsClusterResolverChildPolicyHandler(channelControlHelper); } + private reportError(errorMessage: string) { + trace('CDS cluster reporting error ' + errorMessage); + this.channelControlHelper.updateState(connectivityState.TRANSIENT_FAILURE, new UnavailablePicker({code: status.UNAVAILABLE, details: errorMessage, metadata: new Metadata()})); + } + private addCluster(cluster: string) { if (cluster in this.clusterTree) { return; @@ -208,19 +191,28 @@ export class CdsLoadBalancer implements LoadBalancer { try { discoveryMechanismList = getDiscoveryMechanismList(this.clusterTree, this.latestConfig!.getCluster()); } catch (e) { - this.channelControlHelper.updateState(connectivityState.TRANSIENT_FAILURE, new UnavailablePicker({code: status.UNAVAILABLE, details: e.message, metadata: new Metadata()})); + this.reportError((e as Error).message); + return; + } + const clusterResolverConfig: LoadBalancingConfig = { + xds_cluster_resolver: { + discovery_mechanisms: discoveryMechanismList, + locality_picking_policy: [], + endpoint_picking_policy: [] + } + }; + let parsedClusterResolverConfig: TypedLoadBalancingConfig; + try { + parsedClusterResolverConfig = parseLoadBalancingConfig(clusterResolverConfig); + } catch (e) { + this.reportError(`CDS cluster ${this.latestConfig?.getCluster()} child config parsing failed with error ${(e as Error).message}`); return; } - const clusterResolverConfig = new XdsClusterResolverLoadBalancingConfig( - discoveryMechanismList, - [], - [] - ); trace('Child update config: ' + JSON.stringify(clusterResolverConfig)); this.updatedChild = true; this.childBalancer.updateAddressList( [], - clusterResolverConfig, + parsedClusterResolverConfig, this.latestAttributes ); } @@ -231,20 +223,13 @@ export class CdsLoadBalancer implements LoadBalancer { this.clusterTree[cluster].latestUpdate = undefined; this.clusterTree[cluster].children = []; } - this.channelControlHelper.updateState(connectivityState.TRANSIENT_FAILURE, new UnavailablePicker({code: status.UNAVAILABLE, details: `CDS resource ${cluster} does not exist`, metadata: new Metadata()})); + this.reportError(`CDS resource ${cluster} does not exist`); this.childBalancer.destroy(); }, onError: (statusObj) => { if (!this.updatedChild) { trace('Transitioning to transient failure due to onError update for cluster' + cluster); - this.channelControlHelper.updateState( - connectivityState.TRANSIENT_FAILURE, - new UnavailablePicker({ - code: status.UNAVAILABLE, - details: `xDS request failed with error ${statusObj.details}`, - metadata: new Metadata(), - }) - ); + this.reportError(`xDS request failed with error ${statusObj.details}`); } } }); @@ -275,7 +260,7 @@ export class CdsLoadBalancer implements LoadBalancer { updateAddressList( addressList: SubchannelAddress[], - lbConfig: LoadBalancingConfig, + lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown } ): void { if (!(lbConfig instanceof CdsLoadBalancingConfig)) { diff --git a/packages/grpc-js-xds/src/load-balancer-lrs.ts b/packages/grpc-js-xds/src/load-balancer-lrs.ts index a2deb72c3..145b4deda 100644 --- a/packages/grpc-js-xds/src/load-balancer-lrs.ts +++ b/packages/grpc-js-xds/src/load-balancer-lrs.ts @@ -22,9 +22,7 @@ import { XdsClusterLocalityStats, XdsClient, getSingletonXdsClient } from './xds import LoadBalancer = experimental.LoadBalancer; import ChannelControlHelper = experimental.ChannelControlHelper; import registerLoadBalancerType = experimental.registerLoadBalancerType; -import getFirstUsableConfig = experimental.getFirstUsableConfig; import SubchannelAddress = experimental.SubchannelAddress; -import LoadBalancingConfig = experimental.LoadBalancingConfig; import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; import Picker = experimental.Picker; import PickArgs = experimental.PickArgs; @@ -34,11 +32,12 @@ import Filter = experimental.Filter; import BaseFilter = experimental.BaseFilter; import FilterFactory = experimental.FilterFactory; import Call = experimental.CallStream; -import validateLoadBalancingConfig = experimental.validateLoadBalancingConfig +import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; +import selectLbConfigFromList = experimental.selectLbConfigFromList; const TYPE_NAME = 'lrs'; -export class LrsLoadBalancingConfig implements LoadBalancingConfig { +class LrsLoadBalancingConfig implements TypedLoadBalancingConfig { getLoadBalancerName(): string { return TYPE_NAME; } @@ -49,12 +48,12 @@ export class LrsLoadBalancingConfig implements LoadBalancingConfig { eds_service_name: this.edsServiceName, lrs_load_reporting_server_name: this.lrsLoadReportingServer, locality: this.locality, - child_policy: this.childPolicy.map(policy => policy.toJsonObject()) + child_policy: [this.childPolicy.toJsonObject()] } } } - constructor(private clusterName: string, private edsServiceName: string, private lrsLoadReportingServer: XdsServerConfig, private locality: Locality__Output, private childPolicy: LoadBalancingConfig[]) {} + constructor(private clusterName: string, private edsServiceName: string, private lrsLoadReportingServer: XdsServerConfig, private locality: Locality__Output, private childPolicy: TypedLoadBalancingConfig) {} getClusterName() { return this.clusterName; @@ -98,11 +97,15 @@ export class LrsLoadBalancingConfig implements LoadBalancingConfig { if (!('child_policy' in obj && Array.isArray(obj.child_policy))) { throw new Error('lrs config must have a child_policy array'); } + const childConfig = selectLbConfigFromList(obj.config); + if (!childConfig) { + throw new Error('lrs config child_policy parsing failed'); + } return new LrsLoadBalancingConfig(obj.cluster_name, obj.eds_service_name, validateXdsServerConfig(obj.lrs_load_reporting_server), { region: obj.locality.region ?? '', zone: obj.locality.zone ?? '', sub_zone: obj.locality.sub_zone ?? '' - }, obj.child_policy.map(validateLoadBalancingConfig)); + }, childConfig); } } @@ -161,7 +164,7 @@ export class LrsLoadBalancer implements LoadBalancer { updateAddressList( addressList: SubchannelAddress[], - lbConfig: LoadBalancingConfig, + lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown } ): void { if (!(lbConfig instanceof LrsLoadBalancingConfig)) { @@ -173,11 +176,7 @@ export class LrsLoadBalancer implements LoadBalancer { lbConfig.getEdsServiceName(), lbConfig.getLocality() ); - const childPolicy: LoadBalancingConfig = getFirstUsableConfig( - lbConfig.getChildPolicy(), - true - ); - this.childBalancer.updateAddressList(addressList, childPolicy, attributes); + this.childBalancer.updateAddressList(addressList, lbConfig.getChildPolicy(), attributes); } exitIdle(): void { this.childBalancer.exitIdle(); diff --git a/packages/grpc-js-xds/src/load-balancer-priority.ts b/packages/grpc-js-xds/src/load-balancer-priority.ts index a9d03d0a6..4d26a0f41 100644 --- a/packages/grpc-js-xds/src/load-balancer-priority.ts +++ b/packages/grpc-js-xds/src/load-balancer-priority.ts @@ -15,19 +15,18 @@ * */ -import { connectivityState as ConnectivityState, status as Status, Metadata, logVerbosity as LogVerbosity, experimental, ChannelOptions } from '@grpc/grpc-js'; -import validateLoadBalancingConfig = experimental.validateLoadBalancingConfig; +import { connectivityState as ConnectivityState, status as Status, Metadata, logVerbosity as LogVerbosity, experimental, LoadBalancingConfig } from '@grpc/grpc-js'; import LoadBalancer = experimental.LoadBalancer; import ChannelControlHelper = experimental.ChannelControlHelper; -import getFirstUsableConfig = experimental.getFirstUsableConfig; import registerLoadBalancerType = experimental.registerLoadBalancerType; import SubchannelAddress = experimental.SubchannelAddress; import subchannelAddressToString = experimental.subchannelAddressToString; -import LoadBalancingConfig = experimental.LoadBalancingConfig; +import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; import Picker = experimental.Picker; import QueuePicker = experimental.QueuePicker; import UnavailablePicker = experimental.UnavailablePicker; import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; +import selectLbConfigFromList = experimental.selectLbConfigFromList; const TRACER_NAME = 'priority'; @@ -50,12 +49,31 @@ export function isLocalitySubchannelAddress( return Array.isArray((address as LocalitySubchannelAddress).localityPath); } -export interface PriorityChild { +/** + * Type of the config for an individual child in the JSON representation of + * a priority LB policy config. + */ +export interface PriorityChildRaw { config: LoadBalancingConfig[]; ignore_reresolution_requests: boolean; } -export class PriorityLoadBalancingConfig implements LoadBalancingConfig { +/** + * The JSON representation of the config for the priority LB policy. The + * LoadBalancingConfig for a priority policy should have the form + * { priority: PriorityRawConfig } + */ +export interface PriorityRawConfig { + children: {[name: string]: PriorityChildRaw}; + priorities: string[]; +} + +interface PriorityChild { + config: TypedLoadBalancingConfig; + ignore_reresolution_requests: boolean; +} + +class PriorityLoadBalancingConfig implements TypedLoadBalancingConfig { getLoadBalancerName(): string { return TYPE_NAME; } @@ -63,7 +81,7 @@ export class PriorityLoadBalancingConfig implements LoadBalancingConfig { const childrenField: {[key: string]: object} = {} for (const [childName, childValue] of this.children.entries()) { childrenField[childName] = { - config: childValue.config.map(value => value.toJsonObject()) + config: [childValue.config.toJsonObject()] }; } return { @@ -93,7 +111,7 @@ export class PriorityLoadBalancingConfig implements LoadBalancingConfig { throw new Error('Priority config must have a priorities list'); } const childrenMap: Map = new Map(); - for (const childName of obj.children) { + for (const childName of Object.keys(obj.children)) { const childObj = obj.children[childName] if (!('config' in childObj && Array.isArray(childObj.config))) { throw new Error(`Priority child ${childName} must have a config list`); @@ -101,8 +119,12 @@ export class PriorityLoadBalancingConfig implements LoadBalancingConfig { if (!('ignore_reresolution_requests' in childObj && typeof childObj.ignore_reresolution_requests === 'boolean')) { throw new Error(`Priority child ${childName} must have a boolean field ignore_reresolution_requests`); } + const childConfig = selectLbConfigFromList(childObj.config); + if (!childConfig) { + throw new Error(`Priority child ${childName} config parsing failed`); + } childrenMap.set(childName, { - config: childObj.config.map(validateLoadBalancingConfig), + config: childConfig, ignore_reresolution_requests: childObj.ignore_reresolution_requests }); } @@ -113,7 +135,7 @@ export class PriorityLoadBalancingConfig implements LoadBalancingConfig { interface PriorityChildBalancer { updateAddressList( addressList: SubchannelAddress[], - lbConfig: LoadBalancingConfig, + lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown } ): void; exitIdle(): void; @@ -129,7 +151,7 @@ interface PriorityChildBalancer { interface UpdateArgs { subchannelAddress: SubchannelAddress[]; - lbConfig: LoadBalancingConfig; + lbConfig: TypedLoadBalancingConfig; ignoreReresolutionRequests: boolean; } @@ -193,7 +215,7 @@ export class PriorityLoadBalancer implements LoadBalancer { updateAddressList( addressList: SubchannelAddress[], - lbConfig: LoadBalancingConfig, + lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown } ): void { this.childBalancer.updateAddressList(addressList, lbConfig, attributes); @@ -387,7 +409,7 @@ export class PriorityLoadBalancer implements LoadBalancer { updateAddressList( addressList: SubchannelAddress[], - lbConfig: LoadBalancingConfig, + lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown } ): void { if (!(lbConfig instanceof PriorityLoadBalancingConfig)) { @@ -431,23 +453,20 @@ export class PriorityLoadBalancer implements LoadBalancer { /* Pair up the new child configs with the corresponding address lists, and * update all existing children with their new configs */ for (const [childName, childConfig] of lbConfig.getChildren()) { - const chosenChildConfig = getFirstUsableConfig(childConfig.config); - if (chosenChildConfig !== null) { - const childAddresses = childAddressMap.get(childName) ?? []; - trace('Assigning child ' + childName + ' address list ' + childAddresses.map(address => '(' + subchannelAddressToString(address) + ' path=' + address.localityPath + ')')) - this.latestUpdates.set(childName, { - subchannelAddress: childAddresses, - lbConfig: chosenChildConfig, - ignoreReresolutionRequests: childConfig.ignore_reresolution_requests - }); - const existingChild = this.children.get(childName); - if (existingChild !== undefined) { - existingChild.updateAddressList( - childAddresses, - chosenChildConfig, - attributes - ); - } + const childAddresses = childAddressMap.get(childName) ?? []; + trace('Assigning child ' + childName + ' address list ' + childAddresses.map(address => '(' + subchannelAddressToString(address) + ' path=' + address.localityPath + ')')) + this.latestUpdates.set(childName, { + subchannelAddress: childAddresses, + lbConfig: childConfig.config, + ignoreReresolutionRequests: childConfig.ignore_reresolution_requests + }); + const existingChild = this.children.get(childName); + if (existingChild !== undefined) { + existingChild.updateAddressList( + childAddresses, + childConfig.config, + attributes + ); } } // Deactivate all children that are no longer in the priority list diff --git a/packages/grpc-js-xds/src/load-balancer-weighted-target.ts b/packages/grpc-js-xds/src/load-balancer-weighted-target.ts index 7cd92d98b..231f3b179 100644 --- a/packages/grpc-js-xds/src/load-balancer-weighted-target.ts +++ b/packages/grpc-js-xds/src/load-balancer-weighted-target.ts @@ -15,12 +15,11 @@ * */ -import { connectivityState as ConnectivityState, status as Status, Metadata, logVerbosity, experimental } from "@grpc/grpc-js"; +import { connectivityState as ConnectivityState, status as Status, Metadata, logVerbosity, experimental, LoadBalancingConfig } from "@grpc/grpc-js"; import { isLocalitySubchannelAddress, LocalitySubchannelAddress } from "./load-balancer-priority"; -import LoadBalancingConfig = experimental.LoadBalancingConfig; +import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; import LoadBalancer = experimental.LoadBalancer; import ChannelControlHelper = experimental.ChannelControlHelper; -import getFirstUsableConfig = experimental.getFirstUsableConfig; import registerLoadBalancerType = experimental.registerLoadBalancerType; import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; import Picker = experimental.Picker; @@ -30,7 +29,7 @@ import QueuePicker = experimental.QueuePicker; import UnavailablePicker = experimental.UnavailablePicker; import SubchannelAddress = experimental.SubchannelAddress; import subchannelAddressToString = experimental.subchannelAddressToString; -import validateLoadBalancingConfig = experimental.validateLoadBalancingConfig; +import selectLbConfigFromList = experimental.selectLbConfigFromList; const TRACER_NAME = 'weighted_target'; @@ -42,12 +41,30 @@ const TYPE_NAME = 'weighted_target'; const DEFAULT_RETENTION_INTERVAL_MS = 15 * 60 * 1000; - export interface WeightedTarget { +/** + * Type of the config for an individual child in the JSON representation of + * a weighted target LB policy config. + */ +export interface WeightedTargetRaw { weight: number; child_policy: LoadBalancingConfig[]; } -export class WeightedTargetLoadBalancingConfig implements LoadBalancingConfig { +/** + * The JSON representation of the config for the weighted target LB policy. The + * LoadBalancingConfig for a weighted target policy should have the form + * { weighted_target: WeightedTargetRawConfig } + */ +export interface WeightedTargetRawConfig { + targets: {[name: string]: WeightedTargetRaw }; +} + +interface WeightedTarget { + weight: number; + child_policy: TypedLoadBalancingConfig; +} + +class WeightedTargetLoadBalancingConfig implements TypedLoadBalancingConfig { getLoadBalancerName(): string { return TYPE_NAME; } @@ -64,7 +81,7 @@ export class WeightedTargetLoadBalancingConfig implements LoadBalancingConfig { for (const [targetName, targetValue] of this.targets.entries()) { targetsField[targetName] = { weight: targetValue.weight, - child_policy: targetValue.child_policy.map(policy => policy.toJsonObject()) + child_policy: [targetValue.child_policy.toJsonObject()] }; } return { @@ -79,7 +96,7 @@ export class WeightedTargetLoadBalancingConfig implements LoadBalancingConfig { if (!('targets' in obj && obj.targets !== null && typeof obj.targets === 'object')) { throw new Error('Weighted target config must have a targets map'); } - for (const key of obj.targets) { + for (const key of Object.keys(obj.targets)) { const targetObj = obj.targets[key]; if (!('weight' in targetObj && typeof targetObj.weight === 'number')) { throw new Error(`Weighted target ${key} must have a numeric weight`); @@ -87,9 +104,13 @@ export class WeightedTargetLoadBalancingConfig implements LoadBalancingConfig { if (!('child_policy' in targetObj && Array.isArray(targetObj.child_policy))) { throw new Error(`Weighted target ${key} must have a child_policy array`); } + const childConfig = selectLbConfigFromList(targetObj.child_policy); + if (!childConfig) { + throw new Error(`Weighted target ${key} config parsing failed`); + } const validatedTarget: WeightedTarget = { weight: targetObj.weight, - child_policy: targetObj.child_policy.map(validateLoadBalancingConfig) + child_policy: childConfig } targetsMap.set(key, validatedTarget); } @@ -171,10 +192,7 @@ export class WeightedTargetLoadBalancer implements LoadBalancer { updateAddressList(addressList: SubchannelAddress[], lbConfig: WeightedTarget, attributes: { [key: string]: unknown; }): void { this.weight = lbConfig.weight; - const childConfig = getFirstUsableConfig(lbConfig.child_policy); - if (childConfig !== null) { - this.childBalancer.updateAddressList(addressList, childConfig, attributes); - } + this.childBalancer.updateAddressList(addressList, lbConfig.child_policy, attributes); } exitIdle(): void { this.childBalancer.exitIdle(); @@ -301,7 +319,7 @@ export class WeightedTargetLoadBalancer implements LoadBalancer { this.channelControlHelper.updateState(connectivityState, picker); } - updateAddressList(addressList: SubchannelAddress[], lbConfig: LoadBalancingConfig, attributes: { [key: string]: unknown; }): void { + updateAddressList(addressList: SubchannelAddress[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { if (!(lbConfig instanceof WeightedTargetLoadBalancingConfig)) { // Reject a config of the wrong type trace('Discarding address list update with unrecognized config ' + JSON.stringify(lbConfig.toJsonObject(), undefined, 2)); @@ -384,4 +402,4 @@ export class WeightedTargetLoadBalancer implements LoadBalancer { export function setup() { registerLoadBalancerType(TYPE_NAME, WeightedTargetLoadBalancer, WeightedTargetLoadBalancingConfig); -} \ No newline at end of file +} diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts index edfd52016..e5db45afd 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts @@ -19,8 +19,6 @@ import { experimental, logVerbosity, status as Status, Metadata, connectivitySta import { validateXdsServerConfig, XdsServerConfig } from "./xds-bootstrap"; import { getSingletonXdsClient, XdsClient, XdsClusterDropStats } from "./xds-client"; -import LoadBalancingConfig = experimental.LoadBalancingConfig; -import validateLoadBalancingConfig = experimental.validateLoadBalancingConfig; import LoadBalancer = experimental.LoadBalancer; import registerLoadBalancerType = experimental.registerLoadBalancerType; import SubchannelAddress = experimental.SubchannelAddress; @@ -31,7 +29,8 @@ import PickResultType = experimental.PickResultType; import ChannelControlHelper = experimental.ChannelControlHelper; import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; import createChildChannelControlHelper = experimental.createChildChannelControlHelper; -import getFirstUsableConfig = experimental.getFirstUsableConfig; +import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; +import selectLbConfigFromList = experimental.selectLbConfigFromList; const TRACER_NAME = 'xds_cluster_impl'; @@ -58,7 +57,7 @@ function validateDropCategory(obj: any): DropCategory { return obj; } -export class XdsClusterImplLoadBalancingConfig implements LoadBalancingConfig { +class XdsClusterImplLoadBalancingConfig implements TypedLoadBalancingConfig { private maxConcurrentRequests: number; getLoadBalancerName(): string { return TYPE_NAME; @@ -67,7 +66,7 @@ export class XdsClusterImplLoadBalancingConfig implements LoadBalancingConfig { const jsonObj: {[key: string]: any} = { cluster: this.cluster, drop_categories: this.dropCategories, - child_policy: this.childPolicy.map(policy => policy.toJsonObject()), + child_policy: [this.childPolicy.toJsonObject()], max_concurrent_requests: this.maxConcurrentRequests }; if (this.edsServiceName !== undefined) { @@ -81,7 +80,7 @@ export class XdsClusterImplLoadBalancingConfig implements LoadBalancingConfig { }; } - constructor(private cluster: string, private dropCategories: DropCategory[], private childPolicy: LoadBalancingConfig[], private edsServiceName?: string, private lrsLoadReportingServer?: XdsServerConfig, maxConcurrentRequests?: number) { + constructor(private cluster: string, private dropCategories: DropCategory[], private childPolicy: TypedLoadBalancingConfig, private edsServiceName?: string, private lrsLoadReportingServer?: XdsServerConfig, maxConcurrentRequests?: number) { this.maxConcurrentRequests = maxConcurrentRequests ?? DEFAULT_MAX_CONCURRENT_REQUESTS; } @@ -125,7 +124,11 @@ export class XdsClusterImplLoadBalancingConfig implements LoadBalancingConfig { if (!('child_policy' in obj && Array.isArray(obj.child_policy))) { throw new Error('xds_cluster_impl config must have an array field child_policy'); } - return new XdsClusterImplLoadBalancingConfig(obj.cluster, obj.drop_categories.map(validateDropCategory), obj.child_policy.map(validateLoadBalancingConfig), obj.eds_service_name, obj.lrs_load_reporting_server ? validateXdsServerConfig(obj.lrs_load_reporting_server) : undefined, obj.max_concurrent_requests); + const childConfig = selectLbConfigFromList(obj.child_policy); + if (!childConfig) { + throw new Error('xds_cluster_impl config child_policy parsing failed'); + } + return new XdsClusterImplLoadBalancingConfig(obj.cluster, obj.drop_categories.map(validateDropCategory), childConfig, obj.eds_service_name, obj.lrs_load_reporting_server ? validateXdsServerConfig(obj.lrs_load_reporting_server) : undefined, obj.max_concurrent_requests); } } @@ -234,7 +237,7 @@ class XdsClusterImplBalancer implements LoadBalancer { } })); } - updateAddressList(addressList: SubchannelAddress[], lbConfig: LoadBalancingConfig, attributes: { [key: string]: unknown; }): void { + updateAddressList(addressList: SubchannelAddress[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { if (!(lbConfig instanceof XdsClusterImplLoadBalancingConfig)) { trace('Discarding address list update with unrecognized config ' + JSON.stringify(lbConfig.toJsonObject(), undefined, 2)); return; @@ -251,7 +254,7 @@ class XdsClusterImplBalancer implements LoadBalancer { ); } - this.childBalancer.updateAddressList(addressList, getFirstUsableConfig(lbConfig.getChildPolicy(), true), attributes); + this.childBalancer.updateAddressList(addressList, lbConfig.getChildPolicy(), attributes); } exitIdle(): void { this.childBalancer.exitIdle(); diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-manager.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-manager.ts index bfdb4dccc..ce3207dfd 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-manager.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-manager.ts @@ -17,8 +17,7 @@ import { connectivityState as ConnectivityState, status as Status, experimental, logVerbosity, Metadata, status } from "@grpc/grpc-js/"; -import LoadBalancingConfig = experimental.LoadBalancingConfig; -import validateLoadBalancingConfig = experimental.validateLoadBalancingConfig; +import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; import LoadBalancer = experimental.LoadBalancer; import Picker = experimental.Picker; import PickResult = experimental.PickResult; @@ -28,8 +27,8 @@ import UnavailablePicker = experimental.UnavailablePicker; import QueuePicker = experimental.QueuePicker; import SubchannelAddress = experimental.SubchannelAddress; import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; -import getFirstUsableConfig = experimental.getFirstUsableConfig; import ChannelControlHelper = experimental.ChannelControlHelper; +import selectLbConfigFromList = experimental.selectLbConfigFromList; import registerLoadBalancerType = experimental.registerLoadBalancerType; const TRACER_NAME = 'xds_cluster_manager'; @@ -40,16 +39,12 @@ function trace(text: string): void { const TYPE_NAME = 'xds_cluster_manager'; -interface ClusterManagerChild { - child_policy: LoadBalancingConfig[]; -} - -export class XdsClusterManagerLoadBalancingConfig implements LoadBalancingConfig { +class XdsClusterManagerLoadBalancingConfig implements TypedLoadBalancingConfig { getLoadBalancerName(): string { return TYPE_NAME; } - constructor(private children: Map) {} + constructor(private children: Map) {} getChildren() { return this.children; @@ -57,9 +52,9 @@ export class XdsClusterManagerLoadBalancingConfig implements LoadBalancingConfig toJsonObject(): object { const childrenField: {[key: string]: object} = {}; - for (const [childName, childValue] of this.children.entries()) { + for (const [childName, childPolicy] of this.children.entries()) { childrenField[childName] = { - child_policy: childValue.child_policy.map(policy => policy.toJsonObject()) + child_policy: [childPolicy.toJsonObject()] }; } return { @@ -70,19 +65,20 @@ export class XdsClusterManagerLoadBalancingConfig implements LoadBalancingConfig } static createFromJson(obj: any): XdsClusterManagerLoadBalancingConfig { - const childrenMap: Map = new Map(); + const childrenMap: Map = new Map(); if (!('children' in obj && obj.children !== null && typeof obj.children === 'object')) { throw new Error('xds_cluster_manager config must have a children map'); } - for (const key of obj.children) { + for (const key of Object.keys(obj.children)) { const childObj = obj.children[key]; if (!('child_policy' in childObj && Array.isArray(childObj.child_policy))) { throw new Error(`xds_cluster_manager child ${key} must have a child_policy array`); } - const validatedChild = { - child_policy: childObj.child_policy.map(validateLoadBalancingConfig) - }; - childrenMap.set(key, validatedChild); + const childPolicy = selectLbConfigFromList(childObj.child_policy); + if (childPolicy === null) { + throw new Error(`xds_cluster_mananger child ${key} has no recognized sucessfully parsed child_policy`); + } + childrenMap.set(key, childPolicy); } return new XdsClusterManagerLoadBalancingConfig(childrenMap); } @@ -115,7 +111,7 @@ class XdsClusterManagerPicker implements Picker { } interface XdsClusterManagerChild { - updateAddressList(addressList: SubchannelAddress[], lbConfig: ClusterManagerChild, attributes: { [key: string]: unknown; }): void; + updateAddressList(addressList: SubchannelAddress[], childConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void; exitIdle(): void; resetBackoff(): void; destroy(): void; @@ -146,11 +142,8 @@ class XdsClusterManager implements LoadBalancer { this.picker = picker; this.parent.maybeUpdateState(); } - updateAddressList(addressList: SubchannelAddress[], lbConfig: ClusterManagerChild, attributes: { [key: string]: unknown; }): void { - const childConfig = getFirstUsableConfig(lbConfig.child_policy); - if (childConfig !== null) { - this.childBalancer.updateAddressList(addressList, childConfig, attributes); - } + updateAddressList(addressList: SubchannelAddress[], childConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { + this.childBalancer.updateAddressList(addressList, childConfig, attributes); } exitIdle(): void { this.childBalancer.exitIdle(); @@ -241,8 +234,8 @@ class XdsClusterManager implements LoadBalancer { ); this.channelControlHelper.updateState(connectivityState, picker); } - - updateAddressList(addressList: SubchannelAddress[], lbConfig: LoadBalancingConfig, attributes: { [key: string]: unknown; }): void { + + updateAddressList(addressList: SubchannelAddress[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { if (!(lbConfig instanceof XdsClusterManagerLoadBalancingConfig)) { // Reject a config of the wrong type trace('Discarding address list update with unrecognized config ' + JSON.stringify(lbConfig.toJsonObject(), undefined, 2)); @@ -296,4 +289,4 @@ class XdsClusterManager implements LoadBalancer { export function setup() { registerLoadBalancerType(TYPE_NAME, XdsClusterManager, XdsClusterManagerLoadBalancingConfig); -} \ No newline at end of file +} diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts index 40f67c476..f3c64aecb 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts @@ -15,29 +15,30 @@ * */ -import { experimental, logVerbosity } from "@grpc/grpc-js"; +import { LoadBalancingConfig, Metadata, connectivityState, experimental, logVerbosity, status } from "@grpc/grpc-js"; import { registerLoadBalancerType } from "@grpc/grpc-js/build/src/load-balancer"; import { EXPERIMENTAL_OUTLIER_DETECTION } from "./environment"; import { Locality__Output } from "./generated/envoy/config/core/v3/Locality"; import { ClusterLoadAssignment__Output } from "./generated/envoy/config/endpoint/v3/ClusterLoadAssignment"; -import { LrsLoadBalancingConfig } from "./load-balancer-lrs"; -import { LocalitySubchannelAddress, PriorityChild, PriorityLoadBalancingConfig } from "./load-balancer-priority"; -import { WeightedTarget, WeightedTargetLoadBalancingConfig } from "./load-balancer-weighted-target"; +import { LocalitySubchannelAddress, PriorityChildRaw } from "./load-balancer-priority"; import { getSingletonXdsClient, Watcher, XdsClient } from "./xds-client"; -import { DropCategory, XdsClusterImplLoadBalancingConfig } from "./load-balancer-xds-cluster-impl"; +import { DropCategory } from "./load-balancer-xds-cluster-impl"; -import LoadBalancingConfig = experimental.LoadBalancingConfig; -import validateLoadBalancingConfig = experimental.validateLoadBalancingConfig; +import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; import LoadBalancer = experimental.LoadBalancer; import Resolver = experimental.Resolver; import SubchannelAddress = experimental.SubchannelAddress; import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; import createResolver = experimental.createResolver; import ChannelControlHelper = experimental.ChannelControlHelper; -import OutlierDetectionLoadBalancingConfig = experimental.OutlierDetectionLoadBalancingConfig; +import OutlierDetectionRawConfig = experimental.OutlierDetectionRawConfig; import subchannelAddressToString = experimental.subchannelAddressToString; +import selectLbConfigFromList = experimental.selectLbConfigFromList; +import parseLoadBalancingConfig = experimental.parseLoadBalancingConfig; +import UnavailablePicker = experimental.UnavailablePicker; import { serverConfigEqual, validateXdsServerConfig, XdsServerConfig } from "./xds-bootstrap"; import { EndpointResourceType } from "./xds-resource-type/endpoint-resource-type"; +import { WeightedTargetRaw } from "./load-balancer-weighted-target"; const TRACER_NAME = 'xds_cluster_resolver'; @@ -52,7 +53,7 @@ export interface DiscoveryMechanism { type: 'EDS' | 'LOGICAL_DNS'; eds_service_name?: string; dns_hostname?: string; - outlier_detection?: OutlierDetectionLoadBalancingConfig; + outlier_detection?: OutlierDetectionRawConfig; } function validateDiscoveryMechanism(obj: any): DiscoveryMechanism { @@ -62,41 +63,34 @@ function validateDiscoveryMechanism(obj: any): DiscoveryMechanism { if (!('type' in obj && (obj.type === 'EDS' || obj.type === 'LOGICAL_DNS'))) { throw new Error('discovery_mechanisms entry must have a field "type" with the value "EDS" or "LOGICAL_DNS"'); } - if ('max_concurrent_requests' in obj && typeof obj.max_concurrent_requests !== "number") { + if ('max_concurrent_requests' in obj && obj.max_concurrent_requests !== undefined && typeof obj.max_concurrent_requests !== "number") { throw new Error('discovery_mechanisms entry max_concurrent_requests field must be a number if provided'); } - if ('eds_service_name' in obj && typeof obj.eds_service_name !== 'string') { + if ('eds_service_name' in obj && obj.eds_service_name !== undefined && typeof obj.eds_service_name !== 'string') { throw new Error('discovery_mechanisms entry eds_service_name field must be a string if provided'); } - if ('dns_hostname' in obj && typeof obj.dns_hostname !== 'string') { + if ('dns_hostname' in obj && obj.dns_hostname !== undefined && typeof obj.dns_hostname !== 'string') { throw new Error('discovery_mechanisms entry dns_hostname field must be a string if provided'); } - if (EXPERIMENTAL_OUTLIER_DETECTION) { - const outlierDetectionConfig = validateLoadBalancingConfig(obj.outlier_detection); - if (!(outlierDetectionConfig instanceof OutlierDetectionLoadBalancingConfig)) { - throw new Error('eds config outlier_detection must be a valid outlier detection config if provided'); - } - return {...obj, lrs_load_reporting_server: validateXdsServerConfig(obj.lrs_load_reporting_server), outlier_detection: outlierDetectionConfig}; - } - return obj; + return {...obj, lrs_load_reporting_server: obj.lrs_load_reporting_server ? validateXdsServerConfig(obj.lrs_load_reporting_server) : undefined}; } const TYPE_NAME = 'xds_cluster_resolver'; -export class XdsClusterResolverLoadBalancingConfig implements LoadBalancingConfig { +class XdsClusterResolverLoadBalancingConfig implements TypedLoadBalancingConfig { getLoadBalancerName(): string { return TYPE_NAME; } toJsonObject(): object { return { [TYPE_NAME]: { - discovery_mechanisms: this.discoveryMechanisms.map(mechanism => ({...mechanism, outlier_detection: mechanism.outlier_detection?.toJsonObject()})), - locality_picking_policy: this.localityPickingPolicy.map(policy => policy.toJsonObject()), - endpoint_picking_policy: this.endpointPickingPolicy.map(policy => policy.toJsonObject()) + discovery_mechanisms: this.discoveryMechanisms, + locality_picking_policy: this.localityPickingPolicy, + endpoint_picking_policy: this.endpointPickingPolicy } } } - + constructor(private discoveryMechanisms: DiscoveryMechanism[], private localityPickingPolicy: LoadBalancingConfig[], private endpointPickingPolicy: LoadBalancingConfig[]) {} getDiscoveryMechanisms() { @@ -123,8 +117,8 @@ export class XdsClusterResolverLoadBalancingConfig implements LoadBalancingConfi } return new XdsClusterResolverLoadBalancingConfig( obj.discovery_mechanisms.map(validateDiscoveryMechanism), - obj.locality_picking_policy.map(validateLoadBalancingConfig), - obj.endpoint_picking_policy.map(validateLoadBalancingConfig) + obj.locality_picking_policy, + obj.endpoint_picking_policy ); } } @@ -264,16 +258,15 @@ export class XdsClusterResolver implements LoadBalancer { } } const fullPriorityList: string[] = []; - const priorityChildren = new Map(); + const priorityChildren: {[name: string]: PriorityChildRaw} = {}; const addressList: LocalitySubchannelAddress[] = []; for (const entry of this.discoveryMechanismList) { const newPriorityNames: string[] = []; const newLocalityPriorities = new Map(); - const defaultEndpointPickingPolicy = entry.discoveryMechanism.type === 'EDS' ? validateLoadBalancingConfig({ round_robin: {} }) : validateLoadBalancingConfig({ pick_first: {} }); - const endpointPickingPolicy: LoadBalancingConfig[] = [ - ...this.latestConfig.getEndpointPickingPolicy(), - defaultEndpointPickingPolicy - ]; + const configEndpointPickingPolicy = this.latestConfig.getEndpointPickingPolicy(); + const defaultEndpointPickingPolicy: LoadBalancingConfig = entry.discoveryMechanism.type === 'EDS' ? { round_robin: {} } : { pick_first: {} }; + const endpointPickingPolicy: LoadBalancingConfig[] = configEndpointPickingPolicy.length > 0 ? configEndpointPickingPolicy : [defaultEndpointPickingPolicy]; + for (const [priority, priorityEntry] of entry.latestUpdate!.entries()) { /** * Highest (smallest number) priority value that any of the localities in @@ -308,18 +301,25 @@ export class XdsClusterResolver implements LoadBalancer { } newPriorityNames[priority] = newPriorityName; - const childTargets = new Map(); + const childTargets: {[locality: string]: WeightedTargetRaw} = {}; for (const localityObj of priorityEntry.localities) { let childPolicy: LoadBalancingConfig[]; if (entry.discoveryMechanism.lrs_load_reporting_server !== undefined) { - childPolicy = [new LrsLoadBalancingConfig(entry.discoveryMechanism.cluster, entry.discoveryMechanism.eds_service_name ?? '', entry.discoveryMechanism.lrs_load_reporting_server, localityObj.locality, endpointPickingPolicy)]; + childPolicy = [{ + lrs: { + cluster_name: entry.discoveryMechanism.cluster, + eds_service_name: entry.discoveryMechanism.eds_service_name ?? '', + locality: {...localityObj.locality}, + lrs_load_reporting_server: {...entry.discoveryMechanism.lrs_load_reporting_server} + } + }]; } else { childPolicy = endpointPickingPolicy; } - childTargets.set(localityToName(localityObj.locality), { + childTargets[localityToName(localityObj.locality)] = { weight: localityObj.weight, child_policy: childPolicy, - }); + }; for (const address of localityObj.addresses) { addressList.push({ localityPath: [ @@ -331,34 +331,65 @@ export class XdsClusterResolver implements LoadBalancer { } newLocalityPriorities.set(localityToName(localityObj.locality), priority); } - const weightedTargetConfig = new WeightedTargetLoadBalancingConfig(childTargets); - const xdsClusterImplConfig = new XdsClusterImplLoadBalancingConfig(entry.discoveryMechanism.cluster, priorityEntry.dropCategories, [weightedTargetConfig], entry.discoveryMechanism.eds_service_name, entry.discoveryMechanism.lrs_load_reporting_server, entry.discoveryMechanism.max_concurrent_requests); - let outlierDetectionConfig: OutlierDetectionLoadBalancingConfig | undefined; + const xdsClusterImplConfig = { + xds_cluster_impl: { + cluster: entry.discoveryMechanism.cluster, + drop_categories: priorityEntry.dropCategories, + max_concurrent_requests: entry.discoveryMechanism.max_concurrent_requests, + eds_service_name: entry.discoveryMechanism.eds_service_name, + lrs_load_reporting_server: entry.discoveryMechanism.lrs_load_reporting_server, + child_policy: [{ + weighted_target: { + targets: childTargets + } + }] + } + } + let priorityChildConfig: LoadBalancingConfig; if (EXPERIMENTAL_OUTLIER_DETECTION) { - outlierDetectionConfig = entry.discoveryMechanism.outlier_detection?.copyWithChildPolicy([xdsClusterImplConfig]); + priorityChildConfig = { + outlier_detection: { + ...entry.discoveryMechanism.outlier_detection, + child_policy: [xdsClusterImplConfig] + } + } + } else { + priorityChildConfig = xdsClusterImplConfig; } - const priorityChildConfig = outlierDetectionConfig ?? xdsClusterImplConfig; - - priorityChildren.set(newPriorityName, { + + priorityChildren[newPriorityName] = { config: [priorityChildConfig], ignore_reresolution_requests: entry.discoveryMechanism.type === 'EDS' - }); + }; } entry.localityPriorities = newLocalityPriorities; entry.priorityNames = newPriorityNames; fullPriorityList.push(...newPriorityNames); } - const childConfig: PriorityLoadBalancingConfig = new PriorityLoadBalancingConfig(priorityChildren, fullPriorityList); + const childConfig = { + priority: { + children: priorityChildren, + priorities: fullPriorityList + } + } + let typedChildConfig: TypedLoadBalancingConfig; + try { + typedChildConfig = parseLoadBalancingConfig(childConfig); + } catch (e) { + trace('LB policy config parsing failed with error ' + (e as Error).message); + this.channelControlHelper.updateState(connectivityState.TRANSIENT_FAILURE, new UnavailablePicker({code: status.UNAVAILABLE, details: `LB policy config parsing failed with error ${(e as Error).message}`, metadata: new Metadata()})); + return; + } trace('Child update addresses: ' + addressList.map(address => '(' + subchannelAddressToString(address) + ' path=' + address.localityPath + ')')); - trace('Child update priority config: ' + JSON.stringify(childConfig.toJsonObject(), undefined, 2)); + trace('Child update priority config: ' + JSON.stringify(childConfig, undefined, 2)); this.childBalancer.updateAddressList( addressList, - childConfig, + typedChildConfig, this.latestAttributes ); } - updateAddressList(addressList: SubchannelAddress[], lbConfig: LoadBalancingConfig, attributes: { [key: string]: unknown; }): void { + updateAddressList(addressList: SubchannelAddress[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { if (!(lbConfig instanceof XdsClusterResolverLoadBalancingConfig)) { trace('Discarding address list update with unrecognized config ' + JSON.stringify(lbConfig, undefined, 2)); return; @@ -459,7 +490,7 @@ function maybeServerConfigEqual(config1: XdsServerConfig | undefined, config2: X } export class XdsClusterResolverChildPolicyHandler extends ChildLoadBalancerHandler { - protected configUpdateRequiresNewPolicyInstance(oldConfig: LoadBalancingConfig, newConfig: LoadBalancingConfig): boolean { + protected configUpdateRequiresNewPolicyInstance(oldConfig: TypedLoadBalancingConfig, newConfig: TypedLoadBalancingConfig): boolean { if (!(oldConfig instanceof XdsClusterResolverLoadBalancingConfig && newConfig instanceof XdsClusterResolverLoadBalancingConfig)) { return super.configUpdateRequiresNewPolicyInstance(oldConfig, newConfig); } diff --git a/packages/grpc-js-xds/src/resolver-xds.ts b/packages/grpc-js-xds/src/resolver-xds.ts index a934e7285..cd830d521 100644 --- a/packages/grpc-js-xds/src/resolver-xds.ts +++ b/packages/grpc-js-xds/src/resolver-xds.ts @@ -19,23 +19,19 @@ import * as protoLoader from '@grpc/proto-loader'; import { RE2 } from 're2-wasm'; import { getSingletonXdsClient, Watcher, XdsClient } from './xds-client'; -import { StatusObject, status, logVerbosity, Metadata, experimental, ChannelOptions } from '@grpc/grpc-js'; +import { StatusObject, status, logVerbosity, Metadata, experimental, ChannelOptions, ServiceConfig, LoadBalancingConfig, RetryPolicy } from '@grpc/grpc-js'; import Resolver = experimental.Resolver; import GrpcUri = experimental.GrpcUri; import ResolverListener = experimental.ResolverListener; import uriToString = experimental.uriToString; -import ServiceConfig = experimental.ServiceConfig; import registerResolver = experimental.registerResolver; import { Listener__Output } from './generated/envoy/config/listener/v3/Listener'; import { RouteConfiguration__Output } from './generated/envoy/config/route/v3/RouteConfiguration'; import { HttpConnectionManager__Output } from './generated/envoy/extensions/filters/network/http_connection_manager/v3/HttpConnectionManager'; -import { CdsLoadBalancingConfig } from './load-balancer-cds'; import { VirtualHost__Output } from './generated/envoy/config/route/v3/VirtualHost'; import { RouteMatch__Output } from './generated/envoy/config/route/v3/RouteMatch'; import { HeaderMatcher__Output } from './generated/envoy/config/route/v3/HeaderMatcher'; import ConfigSelector = experimental.ConfigSelector; -import LoadBalancingConfig = experimental.LoadBalancingConfig; -import { XdsClusterManagerLoadBalancingConfig } from './load-balancer-xds-cluster-manager'; import { ContainsValueMatcher, ExactValueMatcher, FullMatcher, HeaderMatcher, Matcher, PathExactValueMatcher, PathPrefixValueMatcher, PathSafeRegexValueMatcher, PrefixValueMatcher, PresentValueMatcher, RangeValueMatcher, RejectValueMatcher, SafeRegexValueMatcher, SuffixValueMatcher, ValueMatcher } from './matcher'; import { envoyFractionToFraction, Fraction } from "./fraction"; import { RouteAction, SingleClusterRouteAction, WeightedCluster, WeightedClusterRouteAction } from './route-action'; @@ -46,10 +42,10 @@ import { createHttpFilter, HttpFilterConfig, parseOverrideFilterConfig, parseTop import { EXPERIMENTAL_FAULT_INJECTION, EXPERIMENTAL_FEDERATION, EXPERIMENTAL_RETRY } from './environment'; import Filter = experimental.Filter; import FilterFactory = experimental.FilterFactory; -import RetryPolicy = experimental.RetryPolicy; import { BootstrapInfo, loadBootstrapInfo, validateBootstrapConfig } from './xds-bootstrap'; import { ListenerResourceType } from './xds-resource-type/listener-resource-type'; import { RouteConfigurationResourceType } from './xds-resource-type/route-config-resource-type'; +import { protoDurationToDuration } from './duration'; const TRACER_NAME = 'xds_resolver'; @@ -208,20 +204,6 @@ function getPredicateForMatcher(routeMatch: RouteMatch__Output): Matcher { return new FullMatcher(pathMatcher, headerMatchers, runtimeFraction); } -/** - * Convert a Duration protobuf message object to a Duration object as used in - * the ServiceConfig definition. The difference is that the protobuf message - * defines seconds as a long, which is represented as a string in JavaScript, - * and the one used in the service config defines it as a number. - * @param duration - */ -function protoDurationToDuration(duration: Duration__Output): Duration { - return { - seconds: Number.parseInt(duration.seconds), - nanos: duration.nanos - } -} - function protoDurationToSecondsString(duration: Duration__Output): string { return `${duration.seconds + duration.nanos / 1_000_000_000}s`; } @@ -235,7 +217,7 @@ function getDefaultRetryMaxInterval(baseInterval: string): string { /** * Encode a text string as a valid path of a URI, as specified in RFC-3986 section 3.3 * @param uriPath A value representing an unencoded URI path - * @returns + * @returns */ function encodeURIPath(uriPath: string): string { return uriPath.replace(/[^A-Za-z0-9._~!$&^()*+,;=/-]/g, substring => encodeURIComponent(substring)); @@ -447,7 +429,7 @@ class XdsResolver implements Resolver { } } } - let retryPolicy: RetryPolicy | undefined = undefined; + let retryPolicy: RetryPolicy | undefined = undefined; if (EXPERIMENTAL_RETRY) { const retryConfig = route.route!.retry_policy ?? virtualHost.retry_policy; if (retryConfig) { @@ -458,10 +440,10 @@ class XdsResolver implements Resolver { } } if (retryableStatusCodes.length > 0) { - const baseInterval = retryConfig.retry_back_off?.base_interval ? - protoDurationToSecondsString(retryConfig.retry_back_off.base_interval) : + const baseInterval = retryConfig.retry_back_off?.base_interval ? + protoDurationToSecondsString(retryConfig.retry_back_off.base_interval) : DEFAULT_RETRY_BASE_INTERVAL; - const maxInterval = retryConfig.retry_back_off?.max_interval ? + const maxInterval = retryConfig.retry_back_off?.max_interval ? protoDurationToSecondsString(retryConfig.retry_back_off.max_interval) : getDefaultRetryMaxInterval(baseInterval); retryPolicy = { @@ -602,11 +584,11 @@ class XdsResolver implements Resolver { trace(matcher.toString()); trace('=> ' + action.toString()); } - const clusterConfigMap = new Map(); + const clusterConfigMap: {[key: string]: {child_policy: LoadBalancingConfig[]}} = {}; for (const clusterName of this.clusterRefcounts.keys()) { - clusterConfigMap.set(clusterName, {child_policy: [new CdsLoadBalancingConfig(clusterName)]}); + clusterConfigMap[clusterName] = {child_policy: [{cds: {cluster: clusterName}}]}; } - const lbPolicyConfig = new XdsClusterManagerLoadBalancingConfig(clusterConfigMap); + const lbPolicyConfig = {xds_cluster_manager: {children: clusterConfigMap}}; const serviceConfig: ServiceConfig = { methodConfig: [], loadBalancingConfig: [lbPolicyConfig] @@ -634,7 +616,7 @@ class XdsResolver implements Resolver { this.isLdsWatcherActive = true; } catch (e) { - this.reportResolutionError(e.message); + this.reportResolutionError((e as Error).message); } } } @@ -647,7 +629,7 @@ class XdsResolver implements Resolver { try { this.bootstrapInfo = loadBootstrapInfo(); } catch (e) { - this.reportResolutionError(e.message); + this.reportResolutionError((e as Error).message); } this.startResolution(); } diff --git a/packages/grpc-js-xds/src/route-action.ts b/packages/grpc-js-xds/src/route-action.ts index 5ae5885af..83530f1b4 100644 --- a/packages/grpc-js-xds/src/route-action.ts +++ b/packages/grpc-js-xds/src/route-action.ts @@ -14,11 +14,10 @@ * limitations under the License. */ -import { experimental } from '@grpc/grpc-js'; +import { MethodConfig, experimental } from '@grpc/grpc-js'; import Duration = experimental.Duration; import Filter = experimental.Filter; import FilterFactory = experimental.FilterFactory; -import MethodConfig = experimental.MethodConfig; export interface ClusterResult { name: string; @@ -101,4 +100,4 @@ export class WeightedClusterRouteAction implements RouteAction { const clusterListString = this.clusters.map(({name, weight}) => '(' + name + ':' + weight + ')').join(', ') return 'WeightedCluster(' + clusterListString + ', ' + JSON.stringify(this.methodConfig) + ')'; } -} \ No newline at end of file +} diff --git a/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts index f431bf238..e617bef94 100644 --- a/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts +++ b/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts @@ -29,15 +29,7 @@ import { Any__Output } from "../generated/google/protobuf/Any"; import SuccessRateEjectionConfig = experimental.SuccessRateEjectionConfig; import FailurePercentageEjectionConfig = experimental.FailurePercentageEjectionConfig; import { Watcher, XdsClient } from "../xds-client"; - -export interface OutlierDetectionUpdate { - intervalMs: number | null; - baseEjectionTimeMs: number | null; - maxEjectionTimeMs: number | null; - maxEjectionPercent: number | null; - successRateConfig: Partial | null; - failurePercentageConfig: Partial | null; -} +import { protoDurationToDuration } from "../duration"; export interface CdsUpdate { type: 'AGGREGATE' | 'EDS' | 'LOGICAL_DNS'; @@ -47,29 +39,20 @@ export interface CdsUpdate { maxConcurrentRequests?: number; edsServiceName?: string; dnsHostname?: string; - outlierDetectionUpdate?: OutlierDetectionUpdate; + outlierDetectionUpdate?: experimental.OutlierDetectionRawConfig; } -function durationToMs(duration: Duration__Output): number { - return (Number(duration.seconds) * 1_000 + duration.nanos / 1_000_000) | 0; -} - -function convertOutlierDetectionUpdate(outlierDetection: OutlierDetection__Output | null): OutlierDetectionUpdate | undefined { +function convertOutlierDetectionUpdate(outlierDetection: OutlierDetection__Output | null): experimental.OutlierDetectionRawConfig | undefined { if (!EXPERIMENTAL_OUTLIER_DETECTION) { return undefined; } if (!outlierDetection) { /* No-op outlier detection config, with all fields unset. */ return { - intervalMs: null, - baseEjectionTimeMs: null, - maxEjectionTimeMs: null, - maxEjectionPercent: null, - successRateConfig: null, - failurePercentageConfig: null + child_policy: [] }; } - let successRateConfig: Partial | null = null; + let successRateConfig: Partial | undefined = undefined; /* Success rate ejection is enabled by default, so we only disable it if * enforcing_success_rate is set and it has the value 0 */ if (!outlierDetection.enforcing_success_rate || outlierDetection.enforcing_success_rate.value > 0) { @@ -80,7 +63,7 @@ function convertOutlierDetectionUpdate(outlierDetection: OutlierDetection__Outpu stdev_factor: outlierDetection.success_rate_stdev_factor?.value }; } - let failurePercentageConfig: Partial | null = null; + let failurePercentageConfig: Partial | undefined = undefined; /* Failure percentage ejection is disabled by default, so we only enable it * if enforcing_failure_percentage is set and it has a value greater than 0 */ if (outlierDetection.enforcing_failure_percentage && outlierDetection.enforcing_failure_percentage.value > 0) { @@ -92,19 +75,20 @@ function convertOutlierDetectionUpdate(outlierDetection: OutlierDetection__Outpu } } return { - intervalMs: outlierDetection.interval ? durationToMs(outlierDetection.interval) : null, - baseEjectionTimeMs: outlierDetection.base_ejection_time ? durationToMs(outlierDetection.base_ejection_time) : null, - maxEjectionTimeMs: outlierDetection.max_ejection_time ? durationToMs(outlierDetection.max_ejection_time) : null, - maxEjectionPercent : outlierDetection.max_ejection_percent?.value ?? null, - successRateConfig: successRateConfig, - failurePercentageConfig: failurePercentageConfig + interval: outlierDetection.interval ? protoDurationToDuration(outlierDetection.interval) : undefined, + base_ejection_time: outlierDetection.base_ejection_time ? protoDurationToDuration(outlierDetection.base_ejection_time) : undefined, + max_ejection_time: outlierDetection.max_ejection_time ? protoDurationToDuration(outlierDetection.max_ejection_time) : undefined, + max_ejection_percent: outlierDetection.max_ejection_percent?.value, + success_rate_ejection: successRateConfig, + failure_percentage_ejection: failurePercentageConfig, + child_policy: [] }; } export class ClusterResourceType extends XdsResourceType { private static singleton: ClusterResourceType = new ClusterResourceType(); - + private constructor() { super(); } @@ -124,7 +108,7 @@ export class ClusterResourceType extends XdsResourceType { /* The maximum values here come from the official Protobuf documentation: * https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Duration */ - return Number(duration.seconds) >= 0 && + return Number(duration.seconds) >= 0 && Number(duration.seconds) <= 315_576_000_000 && duration.nanos >= 0 && duration.nanos <= 999_999_999; From 8f9bd7a9ee600ec0ac014ec21a782e2a44dd54ff Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 9 Aug 2023 10:45:07 -0700 Subject: [PATCH 003/109] grpc-js-xds: Fix handling of LRS server configs --- packages/grpc-js-xds/src/xds-bootstrap.ts | 26 ++++++++-------- packages/grpc-js-xds/src/xds-client.ts | 16 +++++----- packages/grpc-js-xds/test/test-bootstrap.ts | 33 +++++++++++++++++++++ 3 files changed, 54 insertions(+), 21 deletions(-) create mode 100644 packages/grpc-js-xds/test/test-bootstrap.ts diff --git a/packages/grpc-js-xds/src/xds-bootstrap.ts b/packages/grpc-js-xds/src/xds-bootstrap.ts index 327ee1b06..f5df10dfa 100644 --- a/packages/grpc-js-xds/src/xds-bootstrap.ts +++ b/packages/grpc-js-xds/src/xds-bootstrap.ts @@ -41,9 +41,9 @@ export interface ChannelCredsConfig { } export interface XdsServerConfig { - serverUri: string; - channelCreds: ChannelCredsConfig[]; - serverFeatures: string[]; + server_uri: string; + channel_creds: ChannelCredsConfig[]; + server_features: string[]; } export interface Authority { @@ -61,19 +61,19 @@ export interface BootstrapInfo { const KNOWN_SERVER_FEATURES = ['ignore_resource_deletion']; export function serverConfigEqual(config1: XdsServerConfig, config2: XdsServerConfig): boolean { - if (config1.serverUri !== config2.serverUri) { + if (config1.server_uri !== config2.server_uri) { return false; } for (const feature of KNOWN_SERVER_FEATURES) { - if ((feature in config1.serverFeatures) !== (feature in config2.serverFeatures)) { + if ((feature in config1.server_features) !== (feature in config2.server_features)) { return false; } } - if (config1.channelCreds.length !== config2.channelCreds.length) { + if (config1.channel_creds.length !== config2.channel_creds.length) { return false; } - for (const [index, creds1] of config1.channelCreds.entries()) { - const creds2 = config2.channelCreds[index]; + for (const [index, creds1] of config1.channel_creds.entries()) { + const creds2 = config2.channel_creds[index]; if (creds1.type !== creds2.type) { return false; } @@ -93,7 +93,7 @@ function validateChannelCredsConfig(obj: any): ChannelCredsConfig { `xds_servers.channel_creds.type field: expected string, got ${typeof obj.type}` ); } - if ('config' in obj) { + if ('config' in obj && obj.config !== undefined) { if (typeof obj.config !== 'object' || obj.config === null) { throw new Error( 'xds_servers.channel_creds config field must be an object if provided' @@ -152,9 +152,9 @@ export function validateXdsServerConfig(obj: any): XdsServerConfig { } } return { - serverUri: obj.server_uri, - channelCreds: obj.channel_creds.map(validateChannelCredsConfig), - serverFeatures: obj.server_features ?? [] + server_uri: obj.server_uri, + channel_creds: obj.channel_creds.map(validateChannelCredsConfig), + server_features: obj.server_features ?? [] }; } @@ -387,7 +387,7 @@ export function loadBootstrapInfo(): BootstrapInfo { return loadedBootstrapInfo; } - + throw new Error( 'The GRPC_XDS_BOOTSTRAP or GRPC_XDS_BOOTSTRAP_CONFIG environment variables need to be set to the path to the bootstrap file to use xDS' ); diff --git a/packages/grpc-js-xds/src/xds-client.ts b/packages/grpc-js-xds/src/xds-client.ts index 2ae9618b3..464e26596 100644 --- a/packages/grpc-js-xds/src/xds-client.ts +++ b/packages/grpc-js-xds/src/xds-client.ts @@ -602,9 +602,9 @@ class ClusterLoadReportMap { * Get the indicated map entry if it exists, or create a new one if it does * not. Increments the refcount of that entry, so a call to this method * should correspond to a later call to unref - * @param clusterName - * @param edsServiceName - * @returns + * @param clusterName + * @param edsServiceName + * @returns */ getOrCreate(clusterName: string, edsServiceName: string): ClusterLoadReport { for (const statsObj of this.statsMap) { @@ -833,12 +833,12 @@ class XdsSingleServerClient { this.maybeStartLrsStream(); }); this.lrsBackoff.unref(); - this.ignoreResourceDeletion = xdsServerConfig.serverFeatures.includes('ignore_resource_deletion'); + this.ignoreResourceDeletion = xdsServerConfig.server_features.includes('ignore_resource_deletion'); const channelArgs = { // 5 minutes 'grpc.keepalive_time_ms': 5 * 60 * 1000 } - const credentialsConfigs = xdsServerConfig.channelCreds; + const credentialsConfigs = xdsServerConfig.channel_creds; let channelCreds: ChannelCredentials | null = null; for (const config of credentialsConfigs) { if (config.type === 'google_default') { @@ -849,8 +849,8 @@ class XdsSingleServerClient { break; } } - const serverUri = this.xdsServerConfig.serverUri - this.trace('Starting xDS client connected to server URI ' + this.xdsServerConfig.serverUri); + const serverUri = this.xdsServerConfig.server_uri + this.trace('Starting xDS client connected to server URI ' + this.xdsServerConfig.server_uri); /* Bootstrap validation rules guarantee that a matching channel credentials * config exists in the list. */ const channel = new Channel(serverUri, channelCreds!, channelArgs); @@ -949,7 +949,7 @@ class XdsSingleServerClient { } trace(text: string) { - trace(this.xdsServerConfig.serverUri + ' ' + text); + trace(this.xdsServerConfig.server_uri + ' ' + text); } subscribe(type: XdsResourceType, name: XdsResourceName) { diff --git a/packages/grpc-js-xds/test/test-bootstrap.ts b/packages/grpc-js-xds/test/test-bootstrap.ts new file mode 100644 index 000000000..f7874d2a1 --- /dev/null +++ b/packages/grpc-js-xds/test/test-bootstrap.ts @@ -0,0 +1,33 @@ +/* + * 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 * as assert from 'assert'; +import { validateXdsServerConfig } from "../src/xds-bootstrap"; + +describe('bootstrap', () => { + /* validateXdsServerConfig is used when creating the cds config, and then + * the resulting value is validated again when creating the + * xds_cluster_resolver config. */ + it('validateXdsServerConfig should be idempotent', () => { + const config = { + server_uri: 'localhost', + channel_creds: [{type: 'google_default'}], + server_features: ['test_feature'] + }; + assert.deepStrictEqual(validateXdsServerConfig(validateXdsServerConfig(config)), validateXdsServerConfig(config)); + }); +}); From 11e19fb4502de9fe9481b807969abea2a586f0f2 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 9 Aug 2023 11:02:33 -0700 Subject: [PATCH 004/109] Enable LRS in local tests and fix LRS config generation bugs --- packages/grpc-js-xds/src/load-balancer-lrs.ts | 2 +- packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts | 3 ++- packages/grpc-js-xds/test/framework.ts | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/grpc-js-xds/src/load-balancer-lrs.ts b/packages/grpc-js-xds/src/load-balancer-lrs.ts index 145b4deda..a0e568eaf 100644 --- a/packages/grpc-js-xds/src/load-balancer-lrs.ts +++ b/packages/grpc-js-xds/src/load-balancer-lrs.ts @@ -97,7 +97,7 @@ class LrsLoadBalancingConfig implements TypedLoadBalancingConfig { if (!('child_policy' in obj && Array.isArray(obj.child_policy))) { throw new Error('lrs config must have a child_policy array'); } - const childConfig = selectLbConfigFromList(obj.config); + const childConfig = selectLbConfigFromList(obj.child_policy); if (!childConfig) { throw new Error('lrs config child_policy parsing failed'); } diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts index f3c64aecb..dd9b80107 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts @@ -310,7 +310,8 @@ export class XdsClusterResolver implements LoadBalancer { cluster_name: entry.discoveryMechanism.cluster, eds_service_name: entry.discoveryMechanism.eds_service_name ?? '', locality: {...localityObj.locality}, - lrs_load_reporting_server: {...entry.discoveryMechanism.lrs_load_reporting_server} + lrs_load_reporting_server: {...entry.discoveryMechanism.lrs_load_reporting_server}, + child_policy: endpointPickingPolicy } }]; } else { diff --git a/packages/grpc-js-xds/test/framework.ts b/packages/grpc-js-xds/test/framework.ts index 4bb9fa142..8fd2b34e0 100644 --- a/packages/grpc-js-xds/test/framework.ts +++ b/packages/grpc-js-xds/test/framework.ts @@ -83,7 +83,8 @@ export class FakeEdsCluster implements FakeCluster { name: this.clusterName, type: 'EDS', eds_cluster_config: {eds_config: {ads: {}}, service_name: this.endpointName}, - lb_policy: 'ROUND_ROBIN' + lb_policy: 'ROUND_ROBIN', + lrs_server: {self: {}} } } From 7ae331bd930af590c2f77156b2fecda0d56c4616 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 9 Aug 2023 11:07:34 -0700 Subject: [PATCH 005/109] Also enable LRS for LOGICAL_DNS test cluster resources --- packages/grpc-js-xds/test/framework.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/test/framework.ts b/packages/grpc-js-xds/test/framework.ts index 8fd2b34e0..250811206 100644 --- a/packages/grpc-js-xds/test/framework.ts +++ b/packages/grpc-js-xds/test/framework.ts @@ -157,7 +157,8 @@ export class FakeDnsCluster implements FakeCluster { } }] }] - } + }, + lrs_server: {self: {}} }; } getAllClusterConfigs(): Cluster[] { From 4f8db6907ef2f4c98bdad2cc60fb0ca873bfd144 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 10 Aug 2023 09:40:37 -0700 Subject: [PATCH 006/109] grpc-js-xds: Fix a typo in xds_cluster_impl parsing code --- .../grpc-js-xds/src/load-balancer-xds-cluster-impl.ts | 2 +- packages/grpc-js-xds/test/framework.ts | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts index e5db45afd..9a9304518 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts @@ -115,7 +115,7 @@ class XdsClusterImplLoadBalancingConfig implements TypedLoadBalancingConfig { if ('eds_service_name' in obj && !(obj.eds_service_name === undefined || typeof obj.eds_service_name === 'string')) { throw new Error('xds_cluster_impl config eds_service_name field must be a string if provided'); } - if ('max_concurrent_requests' in obj && (!obj.max_concurrent_requests === undefined || typeof obj.max_concurrent_requests === 'number')) { + if ('max_concurrent_requests' in obj && !(obj.max_concurrent_requests === undefined || typeof obj.max_concurrent_requests === 'number')) { throw new Error('xds_cluster_impl config max_concurrent_requests must be a number if provided'); } if (!('drop_categories' in obj && Array.isArray(obj.drop_categories))) { diff --git a/packages/grpc-js-xds/test/framework.ts b/packages/grpc-js-xds/test/framework.ts index 250811206..f5e7bc1de 100644 --- a/packages/grpc-js-xds/test/framework.ts +++ b/packages/grpc-js-xds/test/framework.ts @@ -84,7 +84,15 @@ export class FakeEdsCluster implements FakeCluster { type: 'EDS', eds_cluster_config: {eds_config: {ads: {}}, service_name: this.endpointName}, lb_policy: 'ROUND_ROBIN', - lrs_server: {self: {}} + lrs_server: {self: {}}, + circuit_breakers: { + thresholds: [ + { + priority: 'DEFAULT', + max_requests: {value: 1000} + } + ] + } } } From b2ad73a0f3244e80da7871a54809941d1b0dbf24 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 10 Aug 2023 13:54:43 -0700 Subject: [PATCH 007/109] grpc-js-xds: Add config parsing tests --- packages/grpc-js-xds/src/load-balancer-cds.ts | 7 +- packages/grpc-js-xds/src/load-balancer-lrs.ts | 5 +- .../grpc-js-xds/src/load-balancer-priority.ts | 3 +- packages/grpc-js-xds/src/xds-bootstrap.ts | 3 + .../grpc-js-xds/test/test-confg-parsing.ts | 370 ++++++++++++++++++ 5 files changed, 382 insertions(+), 6 deletions(-) create mode 100644 packages/grpc-js-xds/test/test-confg-parsing.ts diff --git a/packages/grpc-js-xds/src/load-balancer-cds.ts b/packages/grpc-js-xds/src/load-balancer-cds.ts index 3ebcbbdf6..44de10ac4 100644 --- a/packages/grpc-js-xds/src/load-balancer-cds.ts +++ b/packages/grpc-js-xds/src/load-balancer-cds.ts @@ -65,11 +65,10 @@ class CdsLoadBalancingConfig implements TypedLoadBalancingConfig { } static createFromJson(obj: any): CdsLoadBalancingConfig { - if ('cluster' in obj) { - return new CdsLoadBalancingConfig(obj.cluster); - } else { - throw new Error('Missing "cluster" in cds load balancing config'); + if (!('cluster' in obj && typeof obj.cluster === 'string')) { + throw new Error('cds config must have a string field cluster'); } + return new CdsLoadBalancingConfig(obj.cluster); } } diff --git a/packages/grpc-js-xds/src/load-balancer-lrs.ts b/packages/grpc-js-xds/src/load-balancer-lrs.ts index a0e568eaf..40c653a03 100644 --- a/packages/grpc-js-xds/src/load-balancer-lrs.ts +++ b/packages/grpc-js-xds/src/load-balancer-lrs.ts @@ -46,7 +46,7 @@ class LrsLoadBalancingConfig implements TypedLoadBalancingConfig { [TYPE_NAME]: { cluster_name: this.clusterName, eds_service_name: this.edsServiceName, - lrs_load_reporting_server_name: this.lrsLoadReportingServer, + lrs_load_reporting_server: this.lrsLoadReportingServer, locality: this.locality, child_policy: [this.childPolicy.toJsonObject()] } @@ -97,6 +97,9 @@ class LrsLoadBalancingConfig implements TypedLoadBalancingConfig { if (!('child_policy' in obj && Array.isArray(obj.child_policy))) { throw new Error('lrs config must have a child_policy array'); } + if (!('lrs_load_reporting_server' in obj && obj.lrs_load_reporting_server !== null && typeof obj.lrs_load_reporting_server === 'object')) { + throw new Error('lrs config must have an object field lrs_load_reporting_server'); + } const childConfig = selectLbConfigFromList(obj.child_policy); if (!childConfig) { throw new Error('lrs config child_policy parsing failed'); diff --git a/packages/grpc-js-xds/src/load-balancer-priority.ts b/packages/grpc-js-xds/src/load-balancer-priority.ts index 4d26a0f41..4372b0eac 100644 --- a/packages/grpc-js-xds/src/load-balancer-priority.ts +++ b/packages/grpc-js-xds/src/load-balancer-priority.ts @@ -81,7 +81,8 @@ class PriorityLoadBalancingConfig implements TypedLoadBalancingConfig { const childrenField: {[key: string]: object} = {} for (const [childName, childValue] of this.children.entries()) { childrenField[childName] = { - config: [childValue.config.toJsonObject()] + config: [childValue.config.toJsonObject()], + ignore_reresolution_requests: childValue.ignore_reresolution_requests }; } return { diff --git a/packages/grpc-js-xds/src/xds-bootstrap.ts b/packages/grpc-js-xds/src/xds-bootstrap.ts index f5df10dfa..fccd3edcf 100644 --- a/packages/grpc-js-xds/src/xds-bootstrap.ts +++ b/packages/grpc-js-xds/src/xds-bootstrap.ts @@ -112,6 +112,9 @@ const SUPPORTED_CHANNEL_CREDS_TYPES = [ ]; export function validateXdsServerConfig(obj: any): XdsServerConfig { + if (!(typeof obj === 'object' && obj !== null)) { + throw new Error('xDS server config must be an object'); + } if (!('server_uri' in obj)) { throw new Error('server_uri field missing in xds_servers element'); } diff --git a/packages/grpc-js-xds/test/test-confg-parsing.ts b/packages/grpc-js-xds/test/test-confg-parsing.ts new file mode 100644 index 000000000..ba91bb2e1 --- /dev/null +++ b/packages/grpc-js-xds/test/test-confg-parsing.ts @@ -0,0 +1,370 @@ +/* + * 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 { experimental, LoadBalancingConfig } from "@grpc/grpc-js"; +import { register } from "../src"; +import assert = require("assert"); +import parseLoadbalancingConfig = experimental.parseLoadBalancingConfig; + +register(); + +/** + * Describes a test case for config parsing. input is passed to + * parseLoadBalancingConfig. If error is set, the expectation is that that + * operation throws an error with a matching message. Otherwise, toJsonObject + * is called on the result, and it is expected to match output, or input if + * output is unset. + */ +interface TestCase { + name: string; + input: object, + output?: object; + error?: RegExp; +} + +/* The main purpose of these tests is to verify that configs that are expected + * to be valid parse successfully, and configs that are expected to be invalid + * throw errors. The specific output of this parsing is a lower priority + * concern. + * Note: some tests have an expected output that is different from the output, + * but all non-error tests additionally verify that parsing the output again + * produces the same output. */ +const allTestCases: {[lbPolicyName: string]: TestCase[]} = { + cds: [ + { + name: 'populated cluster field', + input: { + cluster: 'abc' + } + }, + { + name: 'empty', + input: {}, + error: /cluster/ + }, + { + name: 'non-string cluster', + input: { + cluster: 123 + }, + error: /string.*cluster/ + } + ], + xds_cluster_resolver: [ + { + name: 'empty fields', + input: { + discovery_mechanisms: [], + locality_picking_policy: [], + endpoint_picking_policy: [] + } + }, + { + name: 'missing discovery_mechanisms', + input: { + locality_picking_policy: [], + endpoint_picking_policy: [] + }, + error: /discovery_mechanisms/ + }, + { + name: 'missing locality_picking_policy', + input: { + discovery_mechanisms: [], + endpoint_picking_policy: [] + }, + error: /locality_picking_policy/ + }, + { + name: 'missing endpoint_picking_policy', + input: { + discovery_mechanisms: [], + locality_picking_policy: [] + }, + error: /endpoint_picking_policy/ + }, + { + name: 'discovery_mechanism: EDS', + input: { + discovery_mechanisms: [{ + cluster: 'abc', + type: 'EDS' + }], + locality_picking_policy: [], + endpoint_picking_policy: [] + }, + output: { + discovery_mechanisms: [{ + cluster: 'abc', + type: 'EDS', + lrs_load_reporting_server: undefined + }], + locality_picking_policy: [], + endpoint_picking_policy: [] + } + }, + { + name: 'discovery_mechanism: LOGICAL_DNS', + input: { + discovery_mechanisms: [{ + cluster: 'abc', + type: 'LOGICAL_DNS' + }], + locality_picking_policy: [], + endpoint_picking_policy: [] + }, + output: { + discovery_mechanisms: [{ + cluster: 'abc', + type: 'LOGICAL_DNS', + lrs_load_reporting_server: undefined + }], + locality_picking_policy: [], + endpoint_picking_policy: [] + } + }, + { + name: 'discovery_mechanism: undefined optional fields', + input: { + discovery_mechanisms: [{ + cluster: 'abc', + type: 'EDS', + max_concurrent_requests: undefined, + eds_service_name: undefined, + dns_hostname: undefined, + lrs_load_reporting_server: undefined + }], + locality_picking_policy: [], + endpoint_picking_policy: [] + } + }, + { + name: 'discovery_mechanism: populated optional fields', + input: { + discovery_mechanisms: [{ + cluster: 'abc', + type: 'EDS', + max_concurrent_requests: 100, + eds_service_name: 'def', + dns_hostname: 'localhost', + lrs_load_reporting_server: { + server_uri: 'localhost:12345', + channel_creds: [{ + type: 'google_default', + config: {} + }], + server_features: ['test'] + } + }], + locality_picking_policy: [], + endpoint_picking_policy: [] + } + } + ], + xds_cluster_impl: [ + { + name: 'only required fields', + input: { + cluster: 'abc', + drop_categories: [], + child_policy: [{round_robin: {}}] + }, + output: { + cluster: 'abc', + drop_categories: [], + child_policy: [{round_robin: {}}], + max_concurrent_requests: 1024 + } + }, + { + name: 'undefined optional fields', + input: { + cluster: 'abc', + drop_categories: [], + child_policy: [{round_robin: {}}], + eds_service_name: undefined, + max_concurrent_requests: undefined + }, + output: { + cluster: 'abc', + drop_categories: [], + child_policy: [{round_robin: {}}], + max_concurrent_requests: 1024 + } + }, + { + name: 'populated optional fields', + input: { + cluster: 'abc', + drop_categories: [{ + category: 'test', + requests_per_million: 100 + }], + child_policy: [{round_robin: {}}], + eds_service_name: 'def', + max_concurrent_requests: 123 + }, + } + ], + lrs: [ + { + name: 'only required fields', + input: { + cluster_name: 'abc', + eds_service_name: 'def', + locality: {}, + child_policy: [{round_robin: {}}], + lrs_load_reporting_server: { + server_uri: 'localhost:12345', + channel_creds: [{ + type: 'google_default', + config: {} + }], + server_features: ['test'] + } + }, + output: { + cluster_name: 'abc', + eds_service_name: 'def', + locality: { + region: '', + zone: '', + sub_zone: '' + }, + child_policy: [{round_robin: {}}], + lrs_load_reporting_server: { + server_uri: 'localhost:12345', + channel_creds: [{ + type: 'google_default', + config: {} + }], + server_features: ['test'] + } + } + }, + { + name: 'populated optional fields', + input: { + cluster_name: 'abc', + eds_service_name: 'def', + locality: { + region: 'a', + zone: 'b', + sub_zone: 'c' + }, + child_policy: [{round_robin: {}}], + lrs_load_reporting_server: { + server_uri: 'localhost:12345', + channel_creds: [{ + type: 'google_default', + config: {} + }], + server_features: ['test'] + } + } + } + ], + priority: [ + { + name: 'empty fields', + input: { + children: {}, + priorities: [] + } + }, + { + name: 'populated fields', + input: { + children: { + child0: { + config: [{round_robin: {}}], + ignore_reresolution_requests: true + }, + child1: { + config: [{round_robin: {}}], + ignore_reresolution_requests: false + } + }, + priorities: ['child0', 'child1'] + } + } + ], + weighted_target: [ + { + name: 'empty targets field', + input: { + targets: {} + } + }, + { + name: 'populated targets field', + input: { + targets: { + target0: { + weight: 1, + child_policy: [{round_robin: {}}] + }, + target1: { + weight: 2, + child_policy: [{round_robin: {}}] + } + } + } + } + ], + xds_cluster_manager: [ + { + name: 'empty children field', + input: { + children: {} + } + }, + { + name: 'populated children field', + input: { + children: { + child0: { + child_policy: [{round_robin: {}}] + } + } + } + } + ] +} + +describe('Load balancing policy config parsing', () => { + for (const [lbPolicyName, testCases] of Object.entries(allTestCases)) { + describe(lbPolicyName, () => { + for (const testCase of testCases) { + it(testCase.name, () => { + const lbConfigInput = {[lbPolicyName]: testCase.input}; + if (testCase.error) { + assert.throws(() => { + parseLoadbalancingConfig(lbConfigInput); + }, testCase.error); + } else { + const expectedOutput = testCase.output ?? testCase.input; + const parsedJson = parseLoadbalancingConfig(lbConfigInput).toJsonObject(); + assert.deepStrictEqual(parsedJson, {[lbPolicyName]: expectedOutput}); + // Test idempotency + assert.deepStrictEqual(parseLoadbalancingConfig(parsedJson).toJsonObject(), parsedJson); + } + }); + } + }); + } +}); From d7c27fb3aa329fc924d7ae35a4334849898ff18e Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 11 Aug 2023 11:09:55 -0700 Subject: [PATCH 008/109] grpc-js: Add config parsing tests and fix outlier detection config parsing --- .../src/load-balancer-outlier-detection.ts | 26 ++- packages/grpc-js/test/test-confg-parsing.ts | 212 ++++++++++++++++++ 2 files changed, 226 insertions(+), 12 deletions(-) create mode 100644 packages/grpc-js/test/test-confg-parsing.ts diff --git a/packages/grpc-js/src/load-balancer-outlier-detection.ts b/packages/grpc-js/src/load-balancer-outlier-detection.ts index 00d0e0a53..be62b4c36 100644 --- a/packages/grpc-js/src/load-balancer-outlier-detection.ts +++ b/packages/grpc-js/src/load-balancer-outlier-detection.ts @@ -107,7 +107,7 @@ function validateFieldType( expectedType: TypeofValues, objectName?: string ) { - if (fieldName in obj && typeof obj[fieldName] !== expectedType) { + if (fieldName in obj && obj[fieldName] !== undefined && typeof obj[fieldName] !== expectedType) { const fullFieldName = objectName ? `${objectName}.${fieldName}` : fieldName; throw new Error( `outlier detection config ${fullFieldName} parse error: expected ${expectedType}, got ${typeof obj[ @@ -123,7 +123,7 @@ function validatePositiveDuration( objectName?: string ) { const fullFieldName = objectName ? `${objectName}.${fieldName}` : fieldName; - if (fieldName in obj) { + if (fieldName in obj && obj[fieldName] !== undefined) { if (!isDuration(obj[fieldName])) { throw new Error( `outlier detection config ${fullFieldName} parse error: expected Duration, got ${typeof obj[ @@ -149,7 +149,7 @@ function validatePositiveDuration( function validatePercentage(obj: any, fieldName: string, objectName?: string) { const fullFieldName = objectName ? `${objectName}.${fieldName}` : fieldName; validateFieldType(obj, fieldName, 'number', objectName); - if (fieldName in obj && !(obj[fieldName] >= 0 && obj[fieldName] <= 100)) { + if (fieldName in obj && obj[fieldName] !== undefined && !(obj[fieldName] >= 0 && obj[fieldName] <= 100)) { throw new Error( `outlier detection config ${fullFieldName} parse error: value out of range for percentage (0-100)` ); @@ -201,13 +201,15 @@ export class OutlierDetectionLoadBalancingConfig } toJsonObject(): object { return { - interval: msToDuration(this.intervalMs), - base_ejection_time: msToDuration(this.baseEjectionTimeMs), - max_ejection_time: msToDuration(this.maxEjectionTimeMs), - max_ejection_percent: this.maxEjectionPercent, - success_rate_ejection: this.successRateEjection, - failure_percentage_ejection: this.failurePercentageEjection, - child_policy: [this.childPolicy.toJsonObject()] + outlier_detection: { + interval: msToDuration(this.intervalMs), + base_ejection_time: msToDuration(this.baseEjectionTimeMs), + max_ejection_time: msToDuration(this.maxEjectionTimeMs), + max_ejection_percent: this.maxEjectionPercent, + success_rate_ejection: this.successRateEjection ?? undefined, + failure_percentage_ejection: this.failurePercentageEjection ?? undefined, + child_policy: [this.childPolicy.toJsonObject()] + } }; } @@ -238,7 +240,7 @@ export class OutlierDetectionLoadBalancingConfig validatePositiveDuration(obj, 'base_ejection_time'); validatePositiveDuration(obj, 'max_ejection_time'); validatePercentage(obj, 'max_ejection_percent'); - if ('success_rate_ejection' in obj) { + if ('success_rate_ejection' in obj && obj.success_rate_ejection !== undefined) { if (typeof obj.success_rate_ejection !== 'object') { throw new Error( 'outlier detection config success_rate_ejection must be an object' @@ -268,7 +270,7 @@ export class OutlierDetectionLoadBalancingConfig 'success_rate_ejection' ); } - if ('failure_percentage_ejection' in obj) { + if ('failure_percentage_ejection' in obj && obj.failure_percentage_ejection !== undefined) { if (typeof obj.failure_percentage_ejection !== 'object') { throw new Error( 'outlier detection config failure_percentage_ejection must be an object' diff --git a/packages/grpc-js/test/test-confg-parsing.ts b/packages/grpc-js/test/test-confg-parsing.ts new file mode 100644 index 000000000..569b83f5e --- /dev/null +++ b/packages/grpc-js/test/test-confg-parsing.ts @@ -0,0 +1,212 @@ +/* + * 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 { experimental } from '../src'; +import * as assert from 'assert'; +import parseLoadBalancingConfig = experimental.parseLoadBalancingConfig; + +/** + * Describes a test case for config parsing. input is passed to + * parseLoadBalancingConfig. If error is set, the expectation is that that + * operation throws an error with a matching message. Otherwise, toJsonObject + * is called on the result, and it is expected to match output, or input if + * output is unset. + */ +interface TestCase { + name: string; + input: object, + output?: object; + error?: RegExp; +} + +/* The main purpose of these tests is to verify that configs that are expected + * to be valid parse successfully, and configs that are expected to be invalid + * throw errors. The specific output of this parsing is a lower priority + * concern. + * Note: some tests have an expected output that is different from the output, + * but all non-error tests additionally verify that parsing the output again + * produces the same output. */ +const allTestCases: {[lbPolicyName: string]: TestCase[]} = { + pick_first: [ + { + name: 'no fields set', + input: {}, + output: { + shuffleAddressList: false + } + }, + { + name: 'shuffleAddressList set', + input: { + shuffleAddressList: true + } + } + ], + round_robin: [ + { + name: 'no fields set', + input: {} + } + ], + outlier_detection: [ + { + name: 'only required fields set', + input: { + child_policy: [{round_robin: {}}] + }, + output: { + interval: { + seconds: 10, + nanos: 0 + }, + base_ejection_time: { + seconds: 30, + nanos: 0 + }, + max_ejection_time: { + seconds: 300, + nanos: 0 + }, + max_ejection_percent: 10, + success_rate_ejection: undefined, + failure_percentage_ejection: undefined, + child_policy: [{round_robin: {}}] + } + }, + { + name: 'all optional fields undefined', + input: { + interval: undefined, + base_ejection_time: undefined, + max_ejection_time: undefined, + max_ejection_percent: undefined, + success_rate_ejection: undefined, + failure_percentage_ejection: undefined, + child_policy: [{round_robin: {}}] + }, + output: { + interval: { + seconds: 10, + nanos: 0 + }, + base_ejection_time: { + seconds: 30, + nanos: 0 + }, + max_ejection_time: { + seconds: 300, + nanos: 0 + }, + max_ejection_percent: 10, + success_rate_ejection: undefined, + failure_percentage_ejection: undefined, + child_policy: [{round_robin: {}}] + } + }, + { + name: 'empty ejection configs', + input: { + success_rate_ejection: {}, + failure_percentage_ejection: {}, + child_policy: [{round_robin: {}}] + }, + output: { + interval: { + seconds: 10, + nanos: 0 + }, + base_ejection_time: { + seconds: 30, + nanos: 0 + }, + max_ejection_time: { + seconds: 300, + nanos: 0 + }, + max_ejection_percent: 10, + success_rate_ejection: { + stdev_factor: 1900, + enforcement_percentage: 100, + minimum_hosts: 5, + request_volume: 100 + }, + failure_percentage_ejection: { + threshold: 85, + enforcement_percentage: 100, + minimum_hosts: 5, + request_volume: 50, + }, + child_policy: [{round_robin: {}}] + } + }, + { + name: 'all fields populated', + input: { + interval: { + seconds: 20, + nanos: 0 + }, + base_ejection_time: { + seconds: 40, + nanos: 0 + }, + max_ejection_time: { + seconds: 400, + nanos: 0 + }, + max_ejection_percent: 20, + success_rate_ejection: { + stdev_factor: 1800, + enforcement_percentage: 90, + minimum_hosts: 4, + request_volume: 200 + }, + failure_percentage_ejection: { + threshold: 95, + enforcement_percentage: 90, + minimum_hosts: 4, + request_volume: 60, + }, + child_policy: [{round_robin: {}}] + + } + } + ] +} + +describe('Load balancing policy config parsing', () => { + for (const [lbPolicyName, testCases] of Object.entries(allTestCases)) { + describe(lbPolicyName, () => { + for (const testCase of testCases) { + it(testCase.name, () => { + const lbConfigInput = {[lbPolicyName]: testCase.input}; + if (testCase.error) { + assert.throws(() => { + parseLoadBalancingConfig(lbConfigInput); + }, testCase.error); + } else { + const expectedOutput = testCase.output ?? testCase.input; + const parsedJson = parseLoadBalancingConfig(lbConfigInput).toJsonObject(); + assert.deepStrictEqual(parsedJson, {[lbPolicyName]: expectedOutput}); + // Test idempotency + assert.deepStrictEqual(parseLoadBalancingConfig(parsedJson).toJsonObject(), parsedJson); + } + }); + } + }); + } +}); From ea5c18d2329d5c3c4cd9eda699cadb4a0dd412d2 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 14 Aug 2023 10:15:46 -0700 Subject: [PATCH 009/109] grpc-js: Switch Timer type to Timeout --- packages/grpc-js/src/backoff-timeout.ts | 2 +- packages/grpc-js/src/internal-channel.ts | 4 ++-- packages/grpc-js/src/load-balancer-outlier-detection.ts | 2 +- packages/grpc-js/src/resolver-dns.ts | 2 +- packages/grpc-js/src/resolving-call.ts | 2 +- packages/grpc-js/src/retrying-call.ts | 2 +- packages/grpc-js/src/server-call.ts | 2 +- packages/grpc-js/src/server.ts | 6 +++--- packages/grpc-js/src/subchannel-pool.ts | 2 +- packages/grpc-js/src/transport.ts | 4 ++-- 10 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/grpc-js/src/backoff-timeout.ts b/packages/grpc-js/src/backoff-timeout.ts index f523e259a..3ffd26064 100644 --- a/packages/grpc-js/src/backoff-timeout.ts +++ b/packages/grpc-js/src/backoff-timeout.ts @@ -63,7 +63,7 @@ export class BackoffTimeout { * to an object representing a timer that has ended, but it can still be * interacted with without error. */ - private timerId: NodeJS.Timer; + private timerId: NodeJS.Timeout; /** * Indicates whether the timer is currently running. */ diff --git a/packages/grpc-js/src/internal-channel.ts b/packages/grpc-js/src/internal-channel.ts index 88dd34741..2817201e2 100644 --- a/packages/grpc-js/src/internal-channel.ts +++ b/packages/grpc-js/src/internal-channel.ts @@ -166,7 +166,7 @@ export class InternalChannel { * the invariant is that callRefTimer is reffed if and only if pickQueue * is non-empty. */ - private readonly callRefTimer: NodeJS.Timer; + private readonly callRefTimer: NodeJS.Timeout; private configSelector: ConfigSelector | null = null; /** * This is the error from the name resolver if it failed most recently. It @@ -182,7 +182,7 @@ export class InternalChannel { new Set(); private callCount = 0; - private idleTimer: NodeJS.Timer | null = null; + private idleTimer: NodeJS.Timeout | null = null; private readonly idleTimeoutMs: number; // Channelz info diff --git a/packages/grpc-js/src/load-balancer-outlier-detection.ts b/packages/grpc-js/src/load-balancer-outlier-detection.ts index be62b4c36..63a1d8f1d 100644 --- a/packages/grpc-js/src/load-balancer-outlier-detection.ts +++ b/packages/grpc-js/src/load-balancer-outlier-detection.ts @@ -507,7 +507,7 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { private childBalancer: ChildLoadBalancerHandler; private addressMap: Map = new Map(); private latestConfig: OutlierDetectionLoadBalancingConfig | null = null; - private ejectionTimer: NodeJS.Timer; + private ejectionTimer: NodeJS.Timeout; private timerStartTime: Date | null = null; constructor(channelControlHelper: ChannelControlHelper) { diff --git a/packages/grpc-js/src/resolver-dns.ts b/packages/grpc-js/src/resolver-dns.ts index b55278525..c40cb8ec5 100644 --- a/packages/grpc-js/src/resolver-dns.ts +++ b/packages/grpc-js/src/resolver-dns.ts @@ -96,7 +96,7 @@ class DnsResolver implements Resolver { private defaultResolutionError: StatusObject; private backoff: BackoffTimeout; private continueResolving = false; - private nextResolutionTimer: NodeJS.Timer; + private nextResolutionTimer: NodeJS.Timeout; private isNextResolutionTimerRunning = false; private isServiceConfigEnabled = true; constructor( diff --git a/packages/grpc-js/src/resolving-call.ts b/packages/grpc-js/src/resolving-call.ts index 8aa717c06..723533dba 100644 --- a/packages/grpc-js/src/resolving-call.ts +++ b/packages/grpc-js/src/resolving-call.ts @@ -53,7 +53,7 @@ export class ResolvingCall implements Call { private deadline: Deadline; private host: string; private statusWatchers: ((status: StatusObject) => void)[] = []; - private deadlineTimer: NodeJS.Timer = setTimeout(() => {}, 0); + private deadlineTimer: NodeJS.Timeout = setTimeout(() => {}, 0); private filterStack: FilterStack | null = null; constructor( diff --git a/packages/grpc-js/src/retrying-call.ts b/packages/grpc-js/src/retrying-call.ts index c329161c3..e6e1cbb44 100644 --- a/packages/grpc-js/src/retrying-call.ts +++ b/packages/grpc-js/src/retrying-call.ts @@ -194,7 +194,7 @@ export class RetryingCall implements Call { * Number of attempts so far */ private attempts = 0; - private hedgingTimer: NodeJS.Timer | null = null; + private hedgingTimer: NodeJS.Timeout | null = null; private committedCallIndex: number | null = null; private initialRetryBackoffSec = 0; private nextRetryBackoffSec = 0; diff --git a/packages/grpc-js/src/server-call.ts b/packages/grpc-js/src/server-call.ts index b1898fd26..95f928350 100644 --- a/packages/grpc-js/src/server-call.ts +++ b/packages/grpc-js/src/server-call.ts @@ -408,7 +408,7 @@ export class Http2ServerCallStream< ResponseType > extends EventEmitter { cancelled = false; - deadlineTimer: NodeJS.Timer | null = null; + deadlineTimer: NodeJS.Timeout | null = null; private statusSent = false; private deadline: Deadline = Infinity; private wantTrailers = false; diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index f5e79a339..c9308ca62 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -1079,8 +1079,8 @@ export class Server { ); this.sessionChildrenTracker.refChild(channelzRef); } - let connectionAgeTimer: NodeJS.Timer | null = null; - let connectionAgeGraceTimer: NodeJS.Timer | null = null; + let connectionAgeTimer: NodeJS.Timeout | null = null; + let connectionAgeGraceTimer: NodeJS.Timeout | null = null; let sessionClosedByServer = false; if (this.maxConnectionAgeMs !== UNLIMITED_CONNECTION_AGE_MS) { // Apply a random jitter within a +/-10% range @@ -1115,7 +1115,7 @@ export class Server { } }, this.maxConnectionAgeMs + jitter).unref?.(); } - const keeapliveTimeTimer: NodeJS.Timer | null = setInterval(() => { + const keeapliveTimeTimer: NodeJS.Timeout | null = setInterval(() => { const timeoutTImer = setTimeout(() => { sessionClosedByServer = true; if (this.channelzEnabled) { diff --git a/packages/grpc-js/src/subchannel-pool.ts b/packages/grpc-js/src/subchannel-pool.ts index 0cbc028ed..a5dec729d 100644 --- a/packages/grpc-js/src/subchannel-pool.ts +++ b/packages/grpc-js/src/subchannel-pool.ts @@ -45,7 +45,7 @@ export class SubchannelPool { /** * A timer of a task performing a periodic subchannel cleanup. */ - private cleanupTimer: NodeJS.Timer | null = null; + private cleanupTimer: NodeJS.Timeout | null = null; /** * A pool of subchannels use for making connections. Subchannels with the diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index 37854e68a..18d83cbfe 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -108,7 +108,7 @@ class Http2Transport implements Transport { /** * Timer reference for timeout that indicates when to send the next ping */ - private keepaliveTimerId: NodeJS.Timer | null = null; + private keepaliveTimerId: NodeJS.Timeout | null = null; /** * Indicates that the keepalive timer ran out while there were no active * calls, and a ping should be sent the next time a call starts. @@ -117,7 +117,7 @@ class Http2Transport implements Transport { /** * Timer reference tracking when the most recent ping will be considered lost */ - private keepaliveTimeoutId: NodeJS.Timer | null = null; + private keepaliveTimeoutId: NodeJS.Timeout | null = null; /** * Indicates whether keepalive pings should be sent without any active calls */ From eb6f1338ab852e716f45099a199ba9ddd28c092f Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 14 Aug 2023 09:29:25 -0700 Subject: [PATCH 010/109] grpc-js-xds: Implement custom LB policies --- packages/grpc-js-xds/src/environment.ts | 1 + packages/grpc-js-xds/src/index.ts | 6 +- .../grpc-js-xds/src/lb-policy-registry.ts | 62 ++++++++++ .../src/lb-policy-registry/round-robin.ts | 36 ++++++ .../src/lb-policy-registry/typed-struct.ts | 110 +++++++++++++++++ packages/grpc-js-xds/src/load-balancer-cds.ts | 4 +- .../grpc-js-xds/src/load-balancer-priority.ts | 3 + .../src/load-balancer-xds-cluster-impl.ts | 54 +++++++-- .../src/load-balancer-xds-cluster-resolver.ts | 60 +++------- .../src/load-balancer-xds-wrr-locality.ts | 112 ++++++++++++++++++ .../cluster-resource-type.ts | 42 +++++-- 11 files changed, 421 insertions(+), 69 deletions(-) create mode 100644 packages/grpc-js-xds/src/lb-policy-registry.ts create mode 100644 packages/grpc-js-xds/src/lb-policy-registry/round-robin.ts create mode 100644 packages/grpc-js-xds/src/lb-policy-registry/typed-struct.ts create mode 100644 packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts diff --git a/packages/grpc-js-xds/src/environment.ts b/packages/grpc-js-xds/src/environment.ts index 32c9f28ba..530bb256c 100644 --- a/packages/grpc-js-xds/src/environment.ts +++ b/packages/grpc-js-xds/src/environment.ts @@ -19,3 +19,4 @@ export const EXPERIMENTAL_FAULT_INJECTION = (process.env.GRPC_XDS_EXPERIMENTAL_F export const EXPERIMENTAL_OUTLIER_DETECTION = (process.env.GRPC_EXPERIMENTAL_ENABLE_OUTLIER_DETECTION ?? 'true') === 'true'; export const EXPERIMENTAL_RETRY = (process.env.GRPC_XDS_EXPERIMENTAL_ENABLE_RETRY ?? 'true') === 'true'; export const EXPERIMENTAL_FEDERATION = (process.env.GRPC_EXPERIMENTAL_XDS_FEDERATION ?? 'false') === 'true'; +export const EXPERIMENTAL_CUSTOM_LB_CONFIG = (process.env.GRPC_EXPERIMENTAL_XDS_CUSTOM_LB_CONFIG ?? 'false') === 'true'; diff --git a/packages/grpc-js-xds/src/index.ts b/packages/grpc-js-xds/src/index.ts index 51926ac47..319719bde 100644 --- a/packages/grpc-js-xds/src/index.ts +++ b/packages/grpc-js-xds/src/index.ts @@ -19,10 +19,10 @@ import * as resolver_xds from './resolver-xds'; import * as load_balancer_cds from './load-balancer-cds'; import * as xds_cluster_resolver from './load-balancer-xds-cluster-resolver'; import * as xds_cluster_impl from './load-balancer-xds-cluster-impl'; -import * as load_balancer_lrs from './load-balancer-lrs'; import * as load_balancer_priority from './load-balancer-priority'; import * as load_balancer_weighted_target from './load-balancer-weighted-target'; import * as load_balancer_xds_cluster_manager from './load-balancer-xds-cluster-manager'; +import * as xds_wrr_locality from './load-balancer-xds-wrr-locality'; import * as router_filter from './http-filter/router-filter'; import * as fault_injection_filter from './http-filter/fault-injection-filter'; import * as csds from './csds'; @@ -35,11 +35,11 @@ export function register() { load_balancer_cds.setup(); xds_cluster_resolver.setup(); xds_cluster_impl.setup(); - load_balancer_lrs.setup(); load_balancer_priority.setup(); load_balancer_weighted_target.setup(); load_balancer_xds_cluster_manager.setup(); + xds_wrr_locality.setup(); router_filter.setup(); fault_injection_filter.setup(); csds.setup(); -} \ No newline at end of file +} diff --git a/packages/grpc-js-xds/src/lb-policy-registry.ts b/packages/grpc-js-xds/src/lb-policy-registry.ts new file mode 100644 index 000000000..18d86655e --- /dev/null +++ b/packages/grpc-js-xds/src/lb-policy-registry.ts @@ -0,0 +1,62 @@ +/* + * 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 { LoadBalancingConfig, experimental, logVerbosity } from "@grpc/grpc-js"; +import { LoadBalancingPolicy__Output } from "./generated/envoy/config/cluster/v3/LoadBalancingPolicy"; +import { TypedExtensionConfig__Output } from "./generated/envoy/config/core/v3/TypedExtensionConfig"; + +const TRACER_NAME = 'lb_policy_registry'; +function trace(text: string) { + experimental.trace(logVerbosity.DEBUG, TRACER_NAME, text); +} + +const MAX_RECURSION_DEPTH = 16; + +interface ProtoLbPolicyConverter { + (protoPolicy: TypedExtensionConfig__Output, selectChildPolicy: (childPolicy: LoadBalancingPolicy__Output) => LoadBalancingConfig): LoadBalancingConfig +} + +interface RegisteredLbPolicy { + convertToLoadBalancingPolicy: ProtoLbPolicyConverter; +} + +const registry: {[typeUrl: string]: RegisteredLbPolicy} = {} + +export function registerLbPolicy(typeUrl: string, converter: ProtoLbPolicyConverter) { + registry[typeUrl] = {convertToLoadBalancingPolicy: converter}; +} + +export function convertToLoadBalancingConfig(protoPolicy: LoadBalancingPolicy__Output, recursionDepth = 0): LoadBalancingConfig { + if (recursionDepth > MAX_RECURSION_DEPTH) { + throw new Error(`convertToLoadBalancingConfig: Max recursion depth ${MAX_RECURSION_DEPTH} reached`); + } + for (const policyCandidate of protoPolicy.policies) { + const extensionConfig = policyCandidate.typed_extension_config; + if (!extensionConfig?.typed_config) { + continue; + } + const typeUrl = extensionConfig.typed_config.type_url; + if (typeUrl in registry) { + try { + return registry[typeUrl].convertToLoadBalancingPolicy(extensionConfig, childPolicy => convertToLoadBalancingConfig(childPolicy, recursionDepth + 1)); + } catch (e) { + throw new Error(`Error parsing ${typeUrl} LoadBalancingPolicy: ${(e as Error).message}`); + } + } + } + throw new Error('No registered LB policy found in list'); +} diff --git a/packages/grpc-js-xds/src/lb-policy-registry/round-robin.ts b/packages/grpc-js-xds/src/lb-policy-registry/round-robin.ts new file mode 100644 index 000000000..9585bfde2 --- /dev/null +++ b/packages/grpc-js-xds/src/lb-policy-registry/round-robin.ts @@ -0,0 +1,36 @@ +/* + * 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 { LoadBalancingConfig } from "@grpc/grpc-js"; +import { LoadBalancingPolicy__Output } from "../generated/envoy/config/cluster/v3/LoadBalancingPolicy"; +import { TypedExtensionConfig__Output } from "../generated/envoy/config/core/v3/TypedExtensionConfig"; +import { registerLbPolicy } from "../lb-policy-registry"; + +const ROUND_ROBIN_TYPE_URL = 'envoy.extensions.load_balancing_policies.round_robin.v3.RoundRobin'; + +function convertToLoadBalancingPolicy(protoPolicy: TypedExtensionConfig__Output, selectChildPolicy: (childPolicy: LoadBalancingPolicy__Output) => LoadBalancingConfig): LoadBalancingConfig { + if (protoPolicy.typed_config?.type_url !== ROUND_ROBIN_TYPE_URL) { + throw new Error(`Round robin LB policy parsing error: unexpected type URL ${protoPolicy.typed_config?.type_url}`); + } + return { + round_robin: {} + }; +} + +export function setup() { + registerLbPolicy(ROUND_ROBIN_TYPE_URL, convertToLoadBalancingPolicy); +} diff --git a/packages/grpc-js-xds/src/lb-policy-registry/typed-struct.ts b/packages/grpc-js-xds/src/lb-policy-registry/typed-struct.ts new file mode 100644 index 000000000..70a6c02b3 --- /dev/null +++ b/packages/grpc-js-xds/src/lb-policy-registry/typed-struct.ts @@ -0,0 +1,110 @@ +/* + * 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 { LoadBalancingConfig } from "@grpc/grpc-js"; +import { LoadBalancingPolicy__Output } from "../generated/envoy/config/cluster/v3/LoadBalancingPolicy"; +import { TypedExtensionConfig__Output } from "../generated/envoy/config/core/v3/TypedExtensionConfig"; +import { registerLbPolicy } from "../lb-policy-registry"; +import { loadProtosWithOptionsSync } from "@grpc/proto-loader/build/src/util"; +import { Any__Output } from "../generated/google/protobuf/Any"; +import { Struct__Output } from "../generated/google/protobuf/Struct"; +import { Value__Output } from "../generated/google/protobuf/Value"; +import { TypedStruct__Output } from "../generated/xds/type/v3/TypedStruct"; + +const XDS_TYPED_STRUCT_TYPE_URL = 'xds.type.v3.TypedStruct'; +const UDPA_TYPED_STRUCT_TYPE_URL = 'udpa.type.v1.TypedStruct'; + +const resourceRoot = loadProtosWithOptionsSync([ + 'xds/type/v3/typed_struct.proto', + 'udpa/type/v1/typed_struct.proto'], { + keepCase: true, + includeDirs: [ + // Paths are relative to src/build + __dirname + '/../../deps/xds/', + ], + } +); + +const toObjectOptions = { + longs: String, + enums: String, + defaults: true, + oneofs: true +} + +/* xds.type.v3.TypedStruct and udpa.type.v1.TypedStruct have identical interfaces */ +function decodeTypedStruct(message: Any__Output): TypedStruct__Output { + const name = message.type_url.substring(message.type_url.lastIndexOf('/') + 1); + const type = resourceRoot.lookup(name); + if (type) { + const decodedMessage = (type as any).decode(message.value); + return decodedMessage.$type.toObject(decodedMessage, toObjectOptions) as TypedStruct__Output; + } else { + throw new Error(`TypedStruct parsing error: unexpected type URL ${message.type_url}`); + } +} + +type FlatValue = boolean | null | number | string | FlatValue[] | FlatStruct; +interface FlatStruct { + [key: string]: FlatValue; +} + +function flattenValue(value: Value__Output): FlatValue { + switch (value.kind) { + case 'boolValue': + return value.boolValue!; + case 'listValue': + return value.listValue!.values.map(flattenValue); + case 'nullValue': + return null; + case 'numberValue': + return value.numberValue!; + case 'stringValue': + return value.stringValue!; + case 'structValue': + return flattenStruct(value.structValue!); + default: + throw new Error(`Struct parsing error: unexpected value kind ${value.kind}`); + } +} + +function flattenStruct(struct: Struct__Output): FlatStruct { + const result: FlatStruct = {}; + for (const [key, value] of Object.entries(struct.fields)) { + result[key] = flattenValue(value); + } + return result; +} + +function convertToLoadBalancingPolicy(protoPolicy: TypedExtensionConfig__Output, selectChildPolicy: (childPolicy: LoadBalancingPolicy__Output) => LoadBalancingConfig): LoadBalancingConfig { + if (protoPolicy.typed_config?.type_url !== XDS_TYPED_STRUCT_TYPE_URL && protoPolicy.typed_config?.type_url !== UDPA_TYPED_STRUCT_TYPE_URL) { + throw new Error(`Typed struct LB policy parsing error: unexpected type URL ${protoPolicy.typed_config?.type_url}`); + } + const typedStruct = decodeTypedStruct(protoPolicy.typed_config); + if (!typedStruct.value) { + throw new Error(`Typed struct LB parsing error: unexpected value ${typedStruct.value}`); + } + const policyName = typedStruct.type_url.substring(typedStruct.type_url.lastIndexOf('/') + 1); + return { + [policyName]: flattenStruct(typedStruct.value) + }; +} + +export function setup() { + registerLbPolicy(XDS_TYPED_STRUCT_TYPE_URL, convertToLoadBalancingPolicy); + registerLbPolicy(UDPA_TYPED_STRUCT_TYPE_URL, convertToLoadBalancingPolicy); +} diff --git a/packages/grpc-js-xds/src/load-balancer-cds.ts b/packages/grpc-js-xds/src/load-balancer-cds.ts index 44de10ac4..647ab2b97 100644 --- a/packages/grpc-js-xds/src/load-balancer-cds.ts +++ b/packages/grpc-js-xds/src/load-balancer-cds.ts @@ -193,11 +193,11 @@ export class CdsLoadBalancer implements LoadBalancer { this.reportError((e as Error).message); return; } + const rootClusterUpdate = this.clusterTree[this.latestConfig!.getCluster()].latestUpdate!; const clusterResolverConfig: LoadBalancingConfig = { xds_cluster_resolver: { discovery_mechanisms: discoveryMechanismList, - locality_picking_policy: [], - endpoint_picking_policy: [] + xds_lb_policy: rootClusterUpdate.lbPolicyConfig } }; let parsedClusterResolverConfig: TypedLoadBalancingConfig; diff --git a/packages/grpc-js-xds/src/load-balancer-priority.ts b/packages/grpc-js-xds/src/load-balancer-priority.ts index 4372b0eac..4a3e41a11 100644 --- a/packages/grpc-js-xds/src/load-balancer-priority.ts +++ b/packages/grpc-js-xds/src/load-balancer-priority.ts @@ -27,6 +27,7 @@ import QueuePicker = experimental.QueuePicker; import UnavailablePicker = experimental.UnavailablePicker; import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; import selectLbConfigFromList = experimental.selectLbConfigFromList; +import { Locality__Output } from './generated/envoy/config/core/v3/Locality'; const TRACER_NAME = 'priority'; @@ -41,6 +42,8 @@ const DEFAULT_RETENTION_INTERVAL_MS = 15 * 60 * 1000; export type LocalitySubchannelAddress = SubchannelAddress & { localityPath: string[]; + locality: Locality__Output; + weight: number; }; export function isLocalitySubchannelAddress( diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts index 9a9304518..eb3df8c38 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts @@ -17,7 +17,8 @@ import { experimental, logVerbosity, status as Status, Metadata, connectivityState } from "@grpc/grpc-js"; import { validateXdsServerConfig, XdsServerConfig } from "./xds-bootstrap"; -import { getSingletonXdsClient, XdsClient, XdsClusterDropStats } from "./xds-client"; +import { getSingletonXdsClient, XdsClient, XdsClusterDropStats, XdsClusterLocalityStats } from "./xds-client"; +import { LocalitySubchannelAddress } from "./load-balancer-priority"; import LoadBalancer = experimental.LoadBalancer; import registerLoadBalancerType = experimental.registerLoadBalancerType; @@ -31,6 +32,8 @@ import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; import createChildChannelControlHelper = experimental.createChildChannelControlHelper; import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; import selectLbConfigFromList = experimental.selectLbConfigFromList; +import SubchannelInterface = experimental.SubchannelInterface; +import BaseSubchannelWrapper = experimental.BaseSubchannelWrapper; const TRACER_NAME = 'xds_cluster_impl'; @@ -80,7 +83,7 @@ class XdsClusterImplLoadBalancingConfig implements TypedLoadBalancingConfig { }; } - constructor(private cluster: string, private dropCategories: DropCategory[], private childPolicy: TypedLoadBalancingConfig, private edsServiceName?: string, private lrsLoadReportingServer?: XdsServerConfig, maxConcurrentRequests?: number) { + constructor(private cluster: string, private dropCategories: DropCategory[], private childPolicy: TypedLoadBalancingConfig, private edsServiceName: string, private lrsLoadReportingServer: XdsServerConfig, maxConcurrentRequests?: number) { this.maxConcurrentRequests = maxConcurrentRequests ?? DEFAULT_MAX_CONCURRENT_REQUESTS; } @@ -112,8 +115,8 @@ class XdsClusterImplLoadBalancingConfig implements TypedLoadBalancingConfig { if (!('cluster' in obj && typeof obj.cluster === 'string')) { throw new Error('xds_cluster_impl config must have a string field cluster'); } - if ('eds_service_name' in obj && !(obj.eds_service_name === undefined || typeof obj.eds_service_name === 'string')) { - throw new Error('xds_cluster_impl config eds_service_name field must be a string if provided'); + if (!('eds_service_name' in obj && typeof obj.eds_service_name === 'string')) { + throw new Error('xds_cluster_impl config must have a string field eds_service_name'); } if ('max_concurrent_requests' in obj && !(obj.max_concurrent_requests === undefined || typeof obj.max_concurrent_requests === 'number')) { throw new Error('xds_cluster_impl config max_concurrent_requests must be a number if provided'); @@ -128,7 +131,7 @@ class XdsClusterImplLoadBalancingConfig implements TypedLoadBalancingConfig { if (!childConfig) { throw new Error('xds_cluster_impl config child_policy parsing failed'); } - return new XdsClusterImplLoadBalancingConfig(obj.cluster, obj.drop_categories.map(validateDropCategory), childConfig, obj.eds_service_name, obj.lrs_load_reporting_server ? validateXdsServerConfig(obj.lrs_load_reporting_server) : undefined, obj.max_concurrent_requests); + return new XdsClusterImplLoadBalancingConfig(obj.cluster, obj.drop_categories.map(validateDropCategory), childConfig, obj.eds_service_name, validateXdsServerConfig(obj.lrs_load_reporting_server), obj.max_concurrent_requests); } } @@ -156,7 +159,25 @@ class CallCounterMap { const callCounterMap = new CallCounterMap(); -class DropPicker implements Picker { +class LocalitySubchannelWrapper extends BaseSubchannelWrapper implements SubchannelInterface { + constructor(child: SubchannelInterface, private statsObject: XdsClusterLocalityStats) { + super(child); + } + + getStatsObject() { + return this.statsObject; + } + + getWrappedSubchannel(): SubchannelInterface { + return this.child; + } +} + +/** + * This picker is responsible for implementing the drop configuration, and for + * recording drop stats and per-locality stats. + */ +class XdsClusterImplPicker implements Picker { constructor(private originalPicker: Picker, private callCounterMapKey: string, private maxConcurrentRequests: number, private dropCategories: DropCategory[], private clusterDropStats: XdsClusterDropStats | null) {} private checkForMaxConcurrentRequestsDrop(): boolean { @@ -186,16 +207,19 @@ class DropPicker implements Picker { } if (details === null) { const originalPick = this.originalPicker.pick(pickArgs); + const pickSubchannel = originalPick.subchannel ? (originalPick.subchannel as LocalitySubchannelWrapper) : null; return { pickResultType: originalPick.pickResultType, status: originalPick.status, - subchannel: originalPick.subchannel, + subchannel: pickSubchannel?.getWrappedSubchannel() ?? null, onCallStarted: () => { originalPick.onCallStarted?.(); + pickSubchannel?.getStatsObject().addCallStarted(); callCounterMap.startCall(this.callCounterMapKey); }, onCallEnded: status => { originalPick.onCallEnded?.(status); + pickSubchannel?.getStatsObject().addCallFinished(status !== Status.OK) callCounterMap.endCall(this.callCounterMapKey); } }; @@ -227,11 +251,25 @@ class XdsClusterImplBalancer implements LoadBalancer { constructor(private readonly channelControlHelper: ChannelControlHelper) { this.childBalancer = new ChildLoadBalancerHandler(createChildChannelControlHelper(channelControlHelper, { + createSubchannel: (subchannelAddress, subchannelArgs) => { + if (!this.xdsClient || !this.latestConfig) { + throw new Error('xds_cluster_impl: invalid state: createSubchannel called with xdsClient or latestConfig not populated'); + } + const locality = (subchannelAddress as LocalitySubchannelAddress).locality ?? ''; + const wrapperChild = channelControlHelper.createSubchannel(subchannelAddress, subchannelArgs); + const statsObj = this.xdsClient.addClusterLocalityStats( + this.latestConfig.getLrsLoadReportingServer(), + this.latestConfig.getCluster(), + this.latestConfig.getEdsServiceName(), + locality + ); + return new LocalitySubchannelWrapper(wrapperChild, statsObj); + }, updateState: (connectivityState, originalPicker) => { if (this.latestConfig === null) { channelControlHelper.updateState(connectivityState, originalPicker); } else { - const picker = new DropPicker(originalPicker, getCallCounterMapKey(this.latestConfig.getCluster(), this.latestConfig.getEdsServiceName()), this.latestConfig.getMaxConcurrentRequests(), this.latestConfig.getDropCategories(), this.clusterDropStats); + const picker = new XdsClusterImplPicker(originalPicker, getCallCounterMapKey(this.latestConfig.getCluster(), this.latestConfig.getEdsServiceName()), this.latestConfig.getMaxConcurrentRequests(), this.latestConfig.getDropCategories(), this.clusterDropStats); channelControlHelper.updateState(connectivityState, picker); } } diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts index dd9b80107..6c11c52bf 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts @@ -38,7 +38,6 @@ import parseLoadBalancingConfig = experimental.parseLoadBalancingConfig; import UnavailablePicker = experimental.UnavailablePicker; import { serverConfigEqual, validateXdsServerConfig, XdsServerConfig } from "./xds-bootstrap"; import { EndpointResourceType } from "./xds-resource-type/endpoint-resource-type"; -import { WeightedTargetRaw } from "./load-balancer-weighted-target"; const TRACER_NAME = 'xds_cluster_resolver'; @@ -85,40 +84,31 @@ class XdsClusterResolverLoadBalancingConfig implements TypedLoadBalancingConfig return { [TYPE_NAME]: { discovery_mechanisms: this.discoveryMechanisms, - locality_picking_policy: this.localityPickingPolicy, - endpoint_picking_policy: this.endpointPickingPolicy + xds_lb_policy: this.xdsLbPolicy } } } - constructor(private discoveryMechanisms: DiscoveryMechanism[], private localityPickingPolicy: LoadBalancingConfig[], private endpointPickingPolicy: LoadBalancingConfig[]) {} + constructor(private discoveryMechanisms: DiscoveryMechanism[], private xdsLbPolicy: LoadBalancingConfig[]) {} getDiscoveryMechanisms() { return this.discoveryMechanisms; } - getLocalityPickingPolicy() { - return this.localityPickingPolicy; - } - - getEndpointPickingPolicy() { - return this.endpointPickingPolicy; + getXdsLbPolicy() { + return this.xdsLbPolicy; } static createFromJson(obj: any): XdsClusterResolverLoadBalancingConfig { if (!('discovery_mechanisms' in obj && Array.isArray(obj.discovery_mechanisms))) { throw new Error('xds_cluster_resolver config must have a discovery_mechanisms array'); } - if (!('locality_picking_policy' in obj && Array.isArray(obj.locality_picking_policy))) { - throw new Error('xds_cluster_resolver config must have a locality_picking_policy array'); - } - if (!('endpoint_picking_policy' in obj && Array.isArray(obj.endpoint_picking_policy))) { - throw new Error('xds_cluster_resolver config must have a endpoint_picking_policy array'); + if (!('xds_lb_policy' in obj && Array.isArray(obj.xds_lb_policy))) { + throw new Error('xds_cluster_resolver config must have a xds_lb_policy array'); } return new XdsClusterResolverLoadBalancingConfig( obj.discovery_mechanisms.map(validateDiscoveryMechanism), - obj.locality_picking_policy, - obj.endpoint_picking_policy + obj.xds_lb_policy ); } } @@ -223,7 +213,7 @@ function getDnsPriorities(addresses: SubchannelAddress[]): PriorityEntry[] { }]; } -function localityToName(locality: Locality__Output) { +export function localityToName(locality: Locality__Output) { return `{region=${locality.region},zone=${locality.zone},sub_zone=${locality.sub_zone}}`; } @@ -260,12 +250,11 @@ export class XdsClusterResolver implements LoadBalancer { const fullPriorityList: string[] = []; const priorityChildren: {[name: string]: PriorityChildRaw} = {}; const addressList: LocalitySubchannelAddress[] = []; + const edsChildPolicy = this.latestConfig.getXdsLbPolicy(); for (const entry of this.discoveryMechanismList) { const newPriorityNames: string[] = []; const newLocalityPriorities = new Map(); - const configEndpointPickingPolicy = this.latestConfig.getEndpointPickingPolicy(); - const defaultEndpointPickingPolicy: LoadBalancingConfig = entry.discoveryMechanism.type === 'EDS' ? { round_robin: {} } : { pick_first: {} }; - const endpointPickingPolicy: LoadBalancingConfig[] = configEndpointPickingPolicy.length > 0 ? configEndpointPickingPolicy : [defaultEndpointPickingPolicy]; + const xdsClusterImplChildPolicy: LoadBalancingConfig[] = entry.discoveryMechanism.type === 'EDS' ? edsChildPolicy : [{ pick_first: {} }]; for (const [priority, priorityEntry] of entry.latestUpdate!.entries()) { /** @@ -301,32 +290,15 @@ export class XdsClusterResolver implements LoadBalancer { } newPriorityNames[priority] = newPriorityName; - const childTargets: {[locality: string]: WeightedTargetRaw} = {}; for (const localityObj of priorityEntry.localities) { - let childPolicy: LoadBalancingConfig[]; - if (entry.discoveryMechanism.lrs_load_reporting_server !== undefined) { - childPolicy = [{ - lrs: { - cluster_name: entry.discoveryMechanism.cluster, - eds_service_name: entry.discoveryMechanism.eds_service_name ?? '', - locality: {...localityObj.locality}, - lrs_load_reporting_server: {...entry.discoveryMechanism.lrs_load_reporting_server}, - child_policy: endpointPickingPolicy - } - }]; - } else { - childPolicy = endpointPickingPolicy; - } - childTargets[localityToName(localityObj.locality)] = { - weight: localityObj.weight, - child_policy: childPolicy, - }; for (const address of localityObj.addresses) { addressList.push({ localityPath: [ newPriorityName, localityToName(localityObj.locality), ], + locality: localityObj.locality, + weight: localityObj.weight, ...address, }); } @@ -337,13 +309,9 @@ export class XdsClusterResolver implements LoadBalancer { cluster: entry.discoveryMechanism.cluster, drop_categories: priorityEntry.dropCategories, max_concurrent_requests: entry.discoveryMechanism.max_concurrent_requests, - eds_service_name: entry.discoveryMechanism.eds_service_name, + eds_service_name: entry.discoveryMechanism.eds_service_name ?? '', lrs_load_reporting_server: entry.discoveryMechanism.lrs_load_reporting_server, - child_policy: [{ - weighted_target: { - targets: childTargets - } - }] + child_policy: xdsClusterImplChildPolicy } } let priorityChildConfig: LoadBalancingConfig; diff --git a/packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts b/packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts new file mode 100644 index 000000000..757f9b93b --- /dev/null +++ b/packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts @@ -0,0 +1,112 @@ +/* + * 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 { LoadBalancingConfig, experimental, logVerbosity } from "@grpc/grpc-js"; +import { WeightedTargetRaw } from "./load-balancer-weighted-target"; +import { isLocalitySubchannelAddress } from "./load-balancer-priority"; +import { localityToName } from "./load-balancer-xds-cluster-resolver"; +import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; +import LoadBalancer = experimental.LoadBalancer; +import ChannelControlHelper = experimental.ChannelControlHelper; +import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; +import SubchannelAddress = experimental.SubchannelAddress; +import parseLoadBalancingConfig = experimental.parseLoadBalancingConfig; +import registerLoadBalancerType = experimental.registerLoadBalancerType; + +const TRACER_NAME = 'xds_wrr_locality'; + +function trace(text: string): void { + experimental.trace(logVerbosity.DEBUG, TRACER_NAME, text); +} + +const TYPE_NAME = 'xds_wrr_locality'; + +class XdsWrrLocalityLoadBalancingConfig implements TypedLoadBalancingConfig { + getLoadBalancerName(): string { + return TYPE_NAME; + } + toJsonObject(): object { + return { + [TYPE_NAME]: { + child_policy: this.childPolicy + } + } + } + + constructor(private childPolicy: LoadBalancingConfig[]) {} + + getChildPolicy() { + return this.childPolicy; + } + + static createFromJson(obj: any): XdsWrrLocalityLoadBalancingConfig { + if (!('child_policy' in obj && Array.isArray(obj.child_policy))) { + throw new Error('xds_wrr_locality config must have a child_policy array'); + } + return new XdsWrrLocalityLoadBalancingConfig( + obj.child_policy + ); + } +} + +class XdsWrrLocalityLoadBalancer implements LoadBalancer { + private childBalancer: ChildLoadBalancerHandler; + constructor(private readonly channelControlHelper: ChannelControlHelper) { + this.childBalancer = new ChildLoadBalancerHandler(channelControlHelper); + } + updateAddressList(addressList: SubchannelAddress[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { + if (!(lbConfig instanceof XdsWrrLocalityLoadBalancingConfig)) { + trace('Discarding address list update with unrecognized config ' + JSON.stringify(lbConfig, undefined, 2)); + return; + } + const targets: {[localityName: string]: WeightedTargetRaw} = {}; + for (const address of addressList) { + if (!isLocalitySubchannelAddress(address)) { + return; + } + const localityName = localityToName(address.locality); + if (!(localityName in targets)) { + targets[localityName] = { + child_policy: lbConfig.getChildPolicy(), + weight: address.weight + }; + } + } + const childConfig = { + weighted_target: { + targets: targets + } + }; + this.childBalancer.updateAddressList(addressList, parseLoadBalancingConfig(childConfig), attributes); + } + exitIdle(): void { + this.childBalancer.exitIdle(); + } + resetBackoff(): void { + this.childBalancer.resetBackoff(); + } + destroy(): void { + this.childBalancer.destroy(); + } + getTypeName(): string { + return TYPE_NAME; + } +} + +export function setup() { + registerLoadBalancerType(TYPE_NAME, XdsWrrLocalityLoadBalancer, XdsWrrLocalityLoadBalancingConfig); +} diff --git a/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts index e617bef94..726a5d2d2 100644 --- a/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts +++ b/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts @@ -17,19 +17,20 @@ import { CDS_TYPE_URL, CLUSTER_CONFIG_TYPE_URL, decodeSingleResource } from "../resources"; import { XdsDecodeContext, XdsDecodeResult, XdsResourceType } from "./xds-resource-type"; -import { experimental } from "@grpc/grpc-js"; +import { LoadBalancingConfig, experimental } from "@grpc/grpc-js"; import { XdsServerConfig } from "../xds-bootstrap"; import { Duration__Output } from "../generated/google/protobuf/Duration"; import { OutlierDetection__Output } from "../generated/envoy/config/cluster/v3/OutlierDetection"; -import { EXPERIMENTAL_OUTLIER_DETECTION } from "../environment"; +import { EXPERIMENTAL_CUSTOM_LB_CONFIG, EXPERIMENTAL_OUTLIER_DETECTION } from "../environment"; import { Cluster__Output } from "../generated/envoy/config/cluster/v3/Cluster"; import { UInt32Value__Output } from "../generated/google/protobuf/UInt32Value"; import { Any__Output } from "../generated/google/protobuf/Any"; - -import SuccessRateEjectionConfig = experimental.SuccessRateEjectionConfig; -import FailurePercentageEjectionConfig = experimental.FailurePercentageEjectionConfig; import { Watcher, XdsClient } from "../xds-client"; import { protoDurationToDuration } from "../duration"; +import { convertToLoadBalancingConfig } from "../lb-policy-registry"; +import SuccessRateEjectionConfig = experimental.SuccessRateEjectionConfig; +import FailurePercentageEjectionConfig = experimental.FailurePercentageEjectionConfig; +import parseLoadBalancingConfig = experimental.parseLoadBalancingConfig; export interface CdsUpdate { type: 'AGGREGATE' | 'EDS' | 'LOGICAL_DNS'; @@ -39,6 +40,7 @@ export interface CdsUpdate { maxConcurrentRequests?: number; edsServiceName?: string; dnsHostname?: string; + lbPolicyConfig: LoadBalancingConfig[]; outlierDetectionUpdate?: experimental.OutlierDetectionRawConfig; } @@ -85,7 +87,6 @@ function convertOutlierDetectionUpdate(outlierDetection: OutlierDetection__Outpu }; } - export class ClusterResourceType extends XdsResourceType { private static singleton: ClusterResourceType = new ClusterResourceType(); @@ -122,7 +123,25 @@ export class ClusterResourceType extends XdsResourceType { } private validateResource(context: XdsDecodeContext, message: Cluster__Output): CdsUpdate | null { - if (message.lb_policy !== 'ROUND_ROBIN') { + let lbPolicyConfig: LoadBalancingConfig; + if (EXPERIMENTAL_CUSTOM_LB_CONFIG && message.load_balancing_policy) { + try { + lbPolicyConfig = convertToLoadBalancingConfig(message.load_balancing_policy); + } catch (e) { + return null; + } + try { + parseLoadBalancingConfig(lbPolicyConfig); + } catch (e) { + return null; + } + } else if (message.lb_policy === 'ROUND_ROBIN') { + lbPolicyConfig = { + xds_wrr_locality: { + child_policy: [{round_robin: {}}] + } + }; + } else { return null; } if (message.lrs_server) { @@ -167,7 +186,8 @@ export class ClusterResourceType extends XdsResourceType { type: 'AGGREGATE', name: message.name, aggregateChildren: clusterConfig.clusters, - outlierDetectionUpdate: convertOutlierDetectionUpdate(null) + outlierDetectionUpdate: convertOutlierDetectionUpdate(null), + lbPolicyConfig: [lbPolicyConfig] }; } else { let maxConcurrentRequests: number | undefined = undefined; @@ -190,7 +210,8 @@ export class ClusterResourceType extends XdsResourceType { maxConcurrentRequests: maxConcurrentRequests, edsServiceName: message.eds_cluster_config.service_name === '' ? undefined : message.eds_cluster_config.service_name, lrsLoadReportingServer: message.lrs_server ? context.server : undefined, - outlierDetectionUpdate: convertOutlierDetectionUpdate(message.outlier_detection) + outlierDetectionUpdate: convertOutlierDetectionUpdate(message.outlier_detection), + lbPolicyConfig: [lbPolicyConfig] } } else if (message.type === 'LOGICAL_DNS') { if (!message.load_assignment) { @@ -219,7 +240,8 @@ export class ClusterResourceType extends XdsResourceType { maxConcurrentRequests: maxConcurrentRequests, dnsHostname: `${socketAddress.address}:${socketAddress.port_value}`, lrsLoadReportingServer: message.lrs_server ? context.server : undefined, - outlierDetectionUpdate: convertOutlierDetectionUpdate(message.outlier_detection) + outlierDetectionUpdate: convertOutlierDetectionUpdate(message.outlier_detection), + lbPolicyConfig: [lbPolicyConfig] }; } } From 13a6e6d273d1fe57bb4e5142431fee46d294779a Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 16 Aug 2023 10:24:47 -0700 Subject: [PATCH 011/109] grpc-js-xds: Update envoy-api dependency and code generation --- packages/grpc-js-xds/deps/envoy-api | 2 +- packages/grpc-js-xds/package.json | 2 +- packages/grpc-js-xds/src/generated/ads.ts | 7 +- packages/grpc-js-xds/src/generated/cluster.ts | 15 +- packages/grpc-js-xds/src/generated/csds.ts | 191 +------ .../grpc-js-xds/src/generated/endpoint.ts | 6 + .../envoy/admin/v3/ClientResourceStatus.ts | 2 +- .../envoy/admin/v3/ClustersConfigDump.ts | 6 +- .../envoy/admin/v3/EcdsConfigDump.ts | 100 ++++ .../envoy/admin/v3/EndpointsConfigDump.ts | 6 +- .../envoy/admin/v3/ListenersConfigDump.ts | 6 +- .../envoy/admin/v3/RoutesConfigDump.ts | 6 +- .../envoy/admin/v3/ScopedRoutesConfigDump.ts | 6 +- .../envoy/admin/v3/UpdateFailureState.ts | 2 +- .../envoy/config/accesslog/v3/AccessLog.ts | 8 +- .../config/accesslog/v3/AccessLogFilter.ts | 19 +- .../config/accesslog/v3/DurationFilter.ts | 10 +- .../config/accesslog/v3/LogTypeFilter.ts | 33 ++ .../config/accesslog/v3/RuntimeFilter.ts | 12 +- .../config/cluster/v3/CircuitBreakers.ts | 32 ++ .../envoy/config/cluster/v3/Cluster.ts | 401 +++++++++------ .../config/cluster/v3/ClusterCollection.ts | 4 +- .../envoy/config/cluster/v3/Filter.ts | 12 +- .../config/cluster/v3/LoadBalancingPolicy.ts | 6 + .../config/cluster/v3/OutlierDetection.ts | 30 +- .../cluster/v3/UpstreamConnectionOptions.ts | 12 + .../generated/envoy/config/core/v3/Address.ts | 6 +- .../core/v3/AlternateProtocolsCacheOptions.ts | 80 +++ .../envoy/config/core/v3/ApiConfigSource.ts | 27 +- .../envoy/config/core/v3/BindConfig.ts | 51 +- .../envoy/config/core/v3/ConfigSource.ts | 49 +- .../envoy/config/core/v3/DataSource.ts | 16 +- .../config/core/v3/DnsResolutionConfig.ts | 6 - .../config/core/v3/DnsResolverOptions.ts | 6 - .../config/core/v3/EnvoyInternalAddress.ts | 28 +- .../envoy/config/core/v3/Extension.ts | 12 +- .../config/core/v3/ExtensionConfigSource.ts | 6 +- .../config/core/v3/ExtraSourceAddress.ts | 38 ++ .../envoy/config/core/v3/GrpcService.ts | 23 +- .../envoy/config/core/v3/HeaderValue.ts | 4 +- .../envoy/config/core/v3/HeaderValueOption.ts | 36 +- .../envoy/config/core/v3/HealthCheck.ts | 129 ++++- .../envoy/config/core/v3/HealthStatus.ts | 6 +- .../envoy/config/core/v3/HealthStatusSet.ts | 17 + .../config/core/v3/Http1ProtocolOptions.ts | 94 +++- .../config/core/v3/Http2ProtocolOptions.ts | 72 ++- .../config/core/v3/HttpProtocolOptions.ts | 20 +- .../generated/envoy/config/core/v3/HttpUri.ts | 4 +- .../envoy/config/core/v3/KeepaliveSettings.ts | 12 +- .../envoy/config/core/v3/Metadata.ts | 24 +- .../generated/envoy/config/core/v3/Node.ts | 8 +- .../envoy/config/core/v3/PathConfigSource.ts | 85 ++++ .../config/core/v3/ProxyProtocolConfig.ts | 11 + .../core/v3/ProxyProtocolPassThroughTLVs.ts | 43 ++ .../config/core/v3/QuicKeepAliveSettings.ts | 53 ++ .../config/core/v3/QuicProtocolOptions.ts | 35 +- .../core/v3/RuntimeFractionalPercent.ts | 4 +- .../envoy/config/core/v3/SocketAddress.ts | 12 +- .../envoy/config/core/v3/SocketOption.ts | 40 ++ .../config/core/v3/SocketOptionsOverride.ts | 11 + .../core/v3/SubstitutionFormatString.ts | 12 +- .../config/core/v3/TypedExtensionConfig.ts | 10 +- .../core/v3/UpstreamHttpProtocolOptions.ts | 14 +- .../envoy/config/endpoint/v3/ClusterStats.ts | 12 +- .../envoy/config/endpoint/v3/Endpoint.ts | 26 + .../envoy/config/endpoint/v3/LbEndpoint.ts | 20 +- .../config/endpoint/v3/LocalityLbEndpoints.ts | 18 +- .../config/listener/v3/AdditionalAddress.ts | 38 ++ .../config/listener/v3/ApiListenerManager.ts | 18 + .../envoy/config/listener/v3/Filter.ts | 6 +- .../envoy/config/listener/v3/FilterChain.ts | 34 +- .../config/listener/v3/FilterChainMatch.ts | 2 + .../envoy/config/listener/v3/Listener.ts | 191 +++++-- .../config/listener/v3/ListenerCollection.ts | 4 +- .../config/listener/v3/ListenerFilter.ts | 29 +- .../config/listener/v3/ListenerManager.ts | 18 + .../config/listener/v3/QuicProtocolOptions.ts | 52 +- .../config/listener/v3/UdpListenerConfig.ts | 31 +- .../listener/v3/ValidationListenerManager.ts | 18 + .../config/route/v3/ClusterSpecifierPlugin.ts | 18 +- .../envoy/config/route/v3/CorsPolicy.ts | 54 +- .../config/route/v3/DirectResponseAction.ts | 4 +- .../envoy/config/route/v3/FilterConfig.ts | 36 +- .../envoy/config/route/v3/HeaderMatcher.ts | 106 +++- .../config/route/v3/QueryParameterMatcher.ts | 4 +- .../envoy/config/route/v3/RateLimit.ts | 197 +++++++- .../envoy/config/route/v3/RedirectAction.ts | 16 +- .../envoy/config/route/v3/RetryPolicy.ts | 16 +- .../generated/envoy/config/route/v3/Route.ts | 68 ++- .../envoy/config/route/v3/RouteAction.ts | 298 ++++++++--- .../config/route/v3/RouteConfiguration.ts | 100 +++- .../envoy/config/route/v3/RouteList.ts | 25 + .../envoy/config/route/v3/RouteMatch.ts | 65 ++- .../route/v3/ScopedRouteConfiguration.ts | 27 +- .../envoy/config/route/v3/Tracing.ts | 4 +- .../envoy/config/route/v3/VirtualCluster.ts | 4 +- .../envoy/config/route/v3/VirtualHost.ts | 90 +++- .../envoy/config/route/v3/WeightedCluster.ts | 106 ++-- .../data/accesslog/v3/AccessLogCommon.ts | 421 ++++++++++++++++ .../envoy/data/accesslog/v3/AccessLogType.ts | 15 + .../data/accesslog/v3/ConnectionProperties.ts | 31 ++ .../data/accesslog/v3/HTTPAccessLogEntry.ts | 50 ++ .../accesslog/v3/HTTPRequestProperties.ts | 163 ++++++ .../accesslog/v3/HTTPResponseProperties.ts | 92 ++++ .../envoy/data/accesslog/v3/ResponseFlags.ts | 255 ++++++++++ .../data/accesslog/v3/TCPAccessLogEntry.ts | 26 + .../envoy/data/accesslog/v3/TLSProperties.ts | 131 +++++ .../filters/http/fault/v3/HTTPFault.ts | 41 +- .../v3/HttpConnectionManager.ts | 465 ++++++++++++++++-- .../http_connection_manager/v3/HttpFilter.ts | 10 +- .../v3/ResponseMapper.ts | 12 +- .../wrr_locality/v3/WrrLocality.ts | 25 + .../v3/AggregatedDiscoveryService.ts | 4 +- .../discovery/v3/DeltaDiscoveryRequest.ts | 53 +- .../discovery/v3/DeltaDiscoveryResponse.ts | 17 +- .../service/discovery/v3/DiscoveryRequest.ts | 29 +- .../v3/DynamicParameterConstraints.ts | 119 +++++ .../envoy/service/discovery/v3/Resource.ts | 32 +- .../service/discovery/v3/ResourceLocator.ts | 34 ++ .../service/discovery/v3/ResourceName.ts | 33 ++ .../load_stats/v3/LoadStatsResponse.ts | 20 +- .../envoy/type/http/v3/PathTransformation.ts | 16 +- .../envoy/type/matcher/v3/RegexMatcher.ts | 40 +- .../envoy/type/matcher/v3/StringMatcher.ts | 20 +- .../envoy/type/matcher/v3/StructMatcher.ts | 4 +- .../envoy/type/metadata/v3/MetadataKey.ts | 4 +- packages/grpc-js-xds/src/generated/fault.ts | 26 +- .../google/protobuf/MethodOptions.ts | 3 - .../src/generated/http_connection_manager.ts | 33 ++ .../grpc-js-xds/src/generated/listener.ts | 38 ++ packages/grpc-js-xds/src/generated/lrs.ts | 2 + packages/grpc-js-xds/src/generated/route.ts | 16 + .../grpc-js-xds/src/generated/wrr_locality.ts | 231 +++++++++ .../xds/core/v3/TypedExtensionConfig.ts | 43 ++ .../xds/type/matcher/v3/ListStringMatcher.ts | 17 + .../generated/xds/type/matcher/v3/Matcher.ts | 307 ++++++++++++ .../xds/type/matcher/v3/RegexMatcher.ts | 80 +++ .../xds/type/matcher/v3/StringMatcher.ts | 109 ++++ packages/grpc-js-xds/src/load-balancer-lrs.ts | 200 -------- 139 files changed, 5715 insertions(+), 1347 deletions(-) create mode 100644 packages/grpc-js-xds/src/generated/envoy/admin/v3/EcdsConfigDump.ts create mode 100644 packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/LogTypeFilter.ts create mode 100644 packages/grpc-js-xds/src/generated/envoy/config/core/v3/ExtraSourceAddress.ts create mode 100644 packages/grpc-js-xds/src/generated/envoy/config/core/v3/HealthStatusSet.ts create mode 100644 packages/grpc-js-xds/src/generated/envoy/config/core/v3/PathConfigSource.ts create mode 100644 packages/grpc-js-xds/src/generated/envoy/config/core/v3/ProxyProtocolPassThroughTLVs.ts create mode 100644 packages/grpc-js-xds/src/generated/envoy/config/core/v3/QuicKeepAliveSettings.ts create mode 100644 packages/grpc-js-xds/src/generated/envoy/config/core/v3/SocketOptionsOverride.ts create mode 100644 packages/grpc-js-xds/src/generated/envoy/config/listener/v3/AdditionalAddress.ts create mode 100644 packages/grpc-js-xds/src/generated/envoy/config/listener/v3/ApiListenerManager.ts create mode 100644 packages/grpc-js-xds/src/generated/envoy/config/listener/v3/ListenerManager.ts create mode 100644 packages/grpc-js-xds/src/generated/envoy/config/listener/v3/ValidationListenerManager.ts create mode 100644 packages/grpc-js-xds/src/generated/envoy/config/route/v3/RouteList.ts create mode 100644 packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/AccessLogCommon.ts create mode 100644 packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/AccessLogType.ts create mode 100644 packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/ConnectionProperties.ts create mode 100644 packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/HTTPAccessLogEntry.ts create mode 100644 packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/HTTPRequestProperties.ts create mode 100644 packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/HTTPResponseProperties.ts create mode 100644 packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/ResponseFlags.ts create mode 100644 packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/TCPAccessLogEntry.ts create mode 100644 packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/TLSProperties.ts create mode 100644 packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/wrr_locality/v3/WrrLocality.ts create mode 100644 packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/DynamicParameterConstraints.ts create mode 100644 packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/ResourceLocator.ts create mode 100644 packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/ResourceName.ts create mode 100644 packages/grpc-js-xds/src/generated/wrr_locality.ts create mode 100644 packages/grpc-js-xds/src/generated/xds/core/v3/TypedExtensionConfig.ts create mode 100644 packages/grpc-js-xds/src/generated/xds/type/matcher/v3/ListStringMatcher.ts create mode 100644 packages/grpc-js-xds/src/generated/xds/type/matcher/v3/Matcher.ts create mode 100644 packages/grpc-js-xds/src/generated/xds/type/matcher/v3/RegexMatcher.ts create mode 100644 packages/grpc-js-xds/src/generated/xds/type/matcher/v3/StringMatcher.ts delete mode 100644 packages/grpc-js-xds/src/load-balancer-lrs.ts diff --git a/packages/grpc-js-xds/deps/envoy-api b/packages/grpc-js-xds/deps/envoy-api index 20b1b5fce..e53e7bbd0 160000 --- a/packages/grpc-js-xds/deps/envoy-api +++ b/packages/grpc-js-xds/deps/envoy-api @@ -1 +1 @@ -Subproject commit 20b1b5fcee88a20a08b71051a961181839ec7268 +Subproject commit e53e7bbd012f81965f2e79848ad9a58ceb67201f diff --git a/packages/grpc-js-xds/package.json b/packages/grpc-js-xds/package.json index 7fd7e700d..3c61b7383 100644 --- a/packages/grpc-js-xds/package.json +++ b/packages/grpc-js-xds/package.json @@ -12,7 +12,7 @@ "prepare": "npm run compile", "pretest": "npm run compile", "posttest": "npm run check", - "generate-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --includeDirs deps/envoy-api/ deps/xds/ deps/googleapis/ deps/protoc-gen-validate/ -O src/generated/ --grpcLib @grpc/grpc-js envoy/service/discovery/v3/ads.proto envoy/service/load_stats/v3/lrs.proto envoy/config/listener/v3/listener.proto envoy/config/route/v3/route.proto envoy/config/cluster/v3/cluster.proto envoy/config/endpoint/v3/endpoint.proto envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto udpa/type/v1/typed_struct.proto xds/type/v3/typed_struct.proto envoy/extensions/filters/http/fault/v3/fault.proto envoy/service/status/v3/csds.proto", + "generate-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --includeDirs deps/envoy-api/ deps/xds/ deps/googleapis/ deps/protoc-gen-validate/ -O src/generated/ --grpcLib @grpc/grpc-js envoy/service/discovery/v3/ads.proto envoy/service/load_stats/v3/lrs.proto envoy/config/listener/v3/listener.proto envoy/config/route/v3/route.proto envoy/config/cluster/v3/cluster.proto envoy/config/endpoint/v3/endpoint.proto envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto udpa/type/v1/typed_struct.proto xds/type/v3/typed_struct.proto envoy/extensions/filters/http/fault/v3/fault.proto envoy/service/status/v3/csds.proto envoy/extensions/load_balancing_policies/wrr_locality/v3/wrr_locality.proto", "generate-interop-types": "proto-loader-gen-types --keep-case --longs String --enums String --defaults --oneofs --json --includeComments --includeDirs proto/ -O interop/generated --grpcLib @grpc/grpc-js grpc/testing/test.proto", "generate-test-types": "proto-loader-gen-types --keep-case --longs String --enums String --defaults --oneofs --json --includeComments --includeDirs proto/ -O test/generated --grpcLib @grpc/grpc-js grpc/testing/echo.proto" }, diff --git a/packages/grpc-js-xds/src/generated/ads.ts b/packages/grpc-js-xds/src/generated/ads.ts index 228f6f1d4..d7483075c 100644 --- a/packages/grpc-js-xds/src/generated/ads.ts +++ b/packages/grpc-js-xds/src/generated/ads.ts @@ -24,6 +24,7 @@ export interface ProtoGrpcType { DataSource: MessageTypeDefinition EnvoyInternalAddress: MessageTypeDefinition Extension: MessageTypeDefinition + ExtraSourceAddress: MessageTypeDefinition HeaderMap: MessageTypeDefinition HeaderValue: MessageTypeDefinition HeaderValueOption: MessageTypeDefinition @@ -44,6 +45,7 @@ export interface ProtoGrpcType { RuntimeUInt32: MessageTypeDefinition SocketAddress: MessageTypeDefinition SocketOption: MessageTypeDefinition + SocketOptionsOverride: MessageTypeDefinition TcpKeepalive: MessageTypeDefinition TrafficDirection: EnumTypeDefinition TransportSocket: MessageTypeDefinition @@ -56,7 +58,7 @@ export interface ProtoGrpcType { v3: { AdsDummy: MessageTypeDefinition /** - * See https://github.com/lyft/envoy-api#apis for a description of the role of + * See https://github.com/envoyproxy/envoy-api#apis for a description of the role of * ADS and how it is intended to be used by a management server. ADS requests * have the same structure as their singleton xDS counterparts, but can * multiplex many resource types on a single stream. The type_url in the @@ -68,7 +70,10 @@ export interface ProtoGrpcType { DeltaDiscoveryResponse: MessageTypeDefinition DiscoveryRequest: MessageTypeDefinition DiscoveryResponse: MessageTypeDefinition + DynamicParameterConstraints: MessageTypeDefinition Resource: MessageTypeDefinition + ResourceLocator: MessageTypeDefinition + ResourceName: MessageTypeDefinition } } } diff --git a/packages/grpc-js-xds/src/generated/cluster.ts b/packages/grpc-js-xds/src/generated/cluster.ts index 681bc5a2d..1aa37589b 100644 --- a/packages/grpc-js-xds/src/generated/cluster.ts +++ b/packages/grpc-js-xds/src/generated/cluster.ts @@ -20,7 +20,6 @@ export interface ProtoGrpcType { LoadBalancingPolicy: MessageTypeDefinition OutlierDetection: MessageTypeDefinition TrackClusterStats: MessageTypeDefinition - UpstreamBindConfig: MessageTypeDefinition UpstreamConnectionOptions: MessageTypeDefinition } } @@ -45,6 +44,7 @@ export interface ProtoGrpcType { EventServiceConfig: MessageTypeDefinition Extension: MessageTypeDefinition ExtensionConfigSource: MessageTypeDefinition + ExtraSourceAddress: MessageTypeDefinition GrpcProtocolOptions: MessageTypeDefinition GrpcService: MessageTypeDefinition HeaderMap: MessageTypeDefinition @@ -52,6 +52,7 @@ export interface ProtoGrpcType { HeaderValueOption: MessageTypeDefinition HealthCheck: MessageTypeDefinition HealthStatus: EnumTypeDefinition + HealthStatusSet: MessageTypeDefinition Http1ProtocolOptions: MessageTypeDefinition Http2ProtocolOptions: MessageTypeDefinition Http3ProtocolOptions: MessageTypeDefinition @@ -61,8 +62,10 @@ export interface ProtoGrpcType { Locality: MessageTypeDefinition Metadata: MessageTypeDefinition Node: MessageTypeDefinition + PathConfigSource: MessageTypeDefinition Pipe: MessageTypeDefinition QueryParameter: MessageTypeDefinition + QuicKeepAliveSettings: MessageTypeDefinition QuicProtocolOptions: MessageTypeDefinition RateLimitSettings: MessageTypeDefinition RemoteDataSource: MessageTypeDefinition @@ -78,6 +81,7 @@ export interface ProtoGrpcType { SelfConfigSource: MessageTypeDefinition SocketAddress: MessageTypeDefinition SocketOption: MessageTypeDefinition + SocketOptionsOverride: MessageTypeDefinition TcpKeepalive: MessageTypeDefinition TcpProtocolOptions: MessageTypeDefinition TrafficDirection: EnumTypeDefinition @@ -97,15 +101,6 @@ export interface ProtoGrpcType { } } } - extensions: { - clusters: { - aggregate: { - v3: { - ClusterConfig: MessageTypeDefinition - } - } - } - } type: { matcher: { v3: { diff --git a/packages/grpc-js-xds/src/generated/csds.ts b/packages/grpc-js-xds/src/generated/csds.ts index 58e903bc9..e09151f50 100644 --- a/packages/grpc-js-xds/src/generated/csds.ts +++ b/packages/grpc-js-xds/src/generated/csds.ts @@ -11,109 +11,41 @@ export interface ProtoGrpcType { envoy: { admin: { v3: { - BootstrapConfigDump: MessageTypeDefinition ClientResourceStatus: EnumTypeDefinition ClustersConfigDump: MessageTypeDefinition - ConfigDump: MessageTypeDefinition + EcdsConfigDump: MessageTypeDefinition EndpointsConfigDump: MessageTypeDefinition ListenersConfigDump: MessageTypeDefinition RoutesConfigDump: MessageTypeDefinition ScopedRoutesConfigDump: MessageTypeDefinition - SecretsConfigDump: MessageTypeDefinition UpdateFailureState: MessageTypeDefinition } } annotations: { } config: { - accesslog: { - v3: { - AccessLog: MessageTypeDefinition - AccessLogFilter: MessageTypeDefinition - AndFilter: MessageTypeDefinition - ComparisonFilter: MessageTypeDefinition - DurationFilter: MessageTypeDefinition - ExtensionFilter: MessageTypeDefinition - GrpcStatusFilter: MessageTypeDefinition - HeaderFilter: MessageTypeDefinition - MetadataFilter: MessageTypeDefinition - NotHealthCheckFilter: MessageTypeDefinition - OrFilter: MessageTypeDefinition - ResponseFlagFilter: MessageTypeDefinition - RuntimeFilter: MessageTypeDefinition - StatusCodeFilter: MessageTypeDefinition - TraceableFilter: MessageTypeDefinition - } - } - bootstrap: { - v3: { - Admin: MessageTypeDefinition - Bootstrap: MessageTypeDefinition - ClusterManager: MessageTypeDefinition - CustomInlineHeader: MessageTypeDefinition - FatalAction: MessageTypeDefinition - LayeredRuntime: MessageTypeDefinition - Runtime: MessageTypeDefinition - RuntimeLayer: MessageTypeDefinition - Watchdog: MessageTypeDefinition - Watchdogs: MessageTypeDefinition - } - } - cluster: { - v3: { - CircuitBreakers: MessageTypeDefinition - Cluster: MessageTypeDefinition - ClusterCollection: MessageTypeDefinition - Filter: MessageTypeDefinition - LoadBalancingPolicy: MessageTypeDefinition - OutlierDetection: MessageTypeDefinition - TrackClusterStats: MessageTypeDefinition - UpstreamBindConfig: MessageTypeDefinition - UpstreamConnectionOptions: MessageTypeDefinition - } - } core: { v3: { Address: MessageTypeDefinition - AggregatedConfigSource: MessageTypeDefinition - AlternateProtocolsCacheOptions: MessageTypeDefinition - ApiConfigSource: MessageTypeDefinition - ApiVersion: EnumTypeDefinition AsyncDataSource: MessageTypeDefinition BackoffStrategy: MessageTypeDefinition BindConfig: MessageTypeDefinition BuildVersion: MessageTypeDefinition CidrRange: MessageTypeDefinition - ConfigSource: MessageTypeDefinition ControlPlane: MessageTypeDefinition DataSource: MessageTypeDefinition - DnsResolutionConfig: MessageTypeDefinition - DnsResolverOptions: MessageTypeDefinition EnvoyInternalAddress: MessageTypeDefinition - EventServiceConfig: MessageTypeDefinition Extension: MessageTypeDefinition - ExtensionConfigSource: MessageTypeDefinition - GrpcProtocolOptions: MessageTypeDefinition - GrpcService: MessageTypeDefinition + ExtraSourceAddress: MessageTypeDefinition HeaderMap: MessageTypeDefinition HeaderValue: MessageTypeDefinition HeaderValueOption: MessageTypeDefinition - HealthCheck: MessageTypeDefinition - HealthStatus: EnumTypeDefinition - Http1ProtocolOptions: MessageTypeDefinition - Http2ProtocolOptions: MessageTypeDefinition - Http3ProtocolOptions: MessageTypeDefinition - HttpProtocolOptions: MessageTypeDefinition HttpUri: MessageTypeDefinition - KeepaliveSettings: MessageTypeDefinition Locality: MessageTypeDefinition Metadata: MessageTypeDefinition Node: MessageTypeDefinition Pipe: MessageTypeDefinition - ProxyProtocolConfig: MessageTypeDefinition QueryParameter: MessageTypeDefinition - QuicProtocolOptions: MessageTypeDefinition - RateLimitSettings: MessageTypeDefinition RemoteDataSource: MessageTypeDefinition RequestMethod: EnumTypeDefinition RetryPolicy: MessageTypeDefinition @@ -123,114 +55,15 @@ export interface ProtoGrpcType { RuntimeFractionalPercent: MessageTypeDefinition RuntimePercent: MessageTypeDefinition RuntimeUInt32: MessageTypeDefinition - SchemeHeaderTransformation: MessageTypeDefinition - SelfConfigSource: MessageTypeDefinition SocketAddress: MessageTypeDefinition SocketOption: MessageTypeDefinition + SocketOptionsOverride: MessageTypeDefinition TcpKeepalive: MessageTypeDefinition - TcpProtocolOptions: MessageTypeDefinition TrafficDirection: EnumTypeDefinition TransportSocket: MessageTypeDefinition - TypedExtensionConfig: MessageTypeDefinition - UdpSocketConfig: MessageTypeDefinition - UpstreamHttpProtocolOptions: MessageTypeDefinition WatchedDirectory: MessageTypeDefinition } } - endpoint: { - v3: { - ClusterLoadAssignment: MessageTypeDefinition - Endpoint: MessageTypeDefinition - LbEndpoint: MessageTypeDefinition - LedsClusterLocalityConfig: MessageTypeDefinition - LocalityLbEndpoints: MessageTypeDefinition - } - } - listener: { - v3: { - ActiveRawUdpListenerConfig: MessageTypeDefinition - ApiListener: MessageTypeDefinition - Filter: MessageTypeDefinition - FilterChain: MessageTypeDefinition - FilterChainMatch: MessageTypeDefinition - Listener: MessageTypeDefinition - ListenerCollection: MessageTypeDefinition - ListenerFilter: MessageTypeDefinition - ListenerFilterChainMatchPredicate: MessageTypeDefinition - QuicProtocolOptions: MessageTypeDefinition - UdpListenerConfig: MessageTypeDefinition - } - } - metrics: { - v3: { - DogStatsdSink: MessageTypeDefinition - HistogramBucketSettings: MessageTypeDefinition - HystrixSink: MessageTypeDefinition - StatsConfig: MessageTypeDefinition - StatsMatcher: MessageTypeDefinition - StatsSink: MessageTypeDefinition - StatsdSink: MessageTypeDefinition - TagSpecifier: MessageTypeDefinition - } - } - overload: { - v3: { - BufferFactoryConfig: MessageTypeDefinition - OverloadAction: MessageTypeDefinition - OverloadManager: MessageTypeDefinition - ResourceMonitor: MessageTypeDefinition - ScaleTimersOverloadActionConfig: MessageTypeDefinition - ScaledTrigger: MessageTypeDefinition - ThresholdTrigger: MessageTypeDefinition - Trigger: MessageTypeDefinition - } - } - route: { - v3: { - CorsPolicy: MessageTypeDefinition - Decorator: MessageTypeDefinition - DirectResponseAction: MessageTypeDefinition - FilterAction: MessageTypeDefinition - FilterConfig: MessageTypeDefinition - HeaderMatcher: MessageTypeDefinition - HedgePolicy: MessageTypeDefinition - InternalRedirectPolicy: MessageTypeDefinition - NonForwardingAction: MessageTypeDefinition - QueryParameterMatcher: MessageTypeDefinition - RateLimit: MessageTypeDefinition - RedirectAction: MessageTypeDefinition - RetryPolicy: MessageTypeDefinition - Route: MessageTypeDefinition - RouteAction: MessageTypeDefinition - RouteMatch: MessageTypeDefinition - Tracing: MessageTypeDefinition - VirtualCluster: MessageTypeDefinition - VirtualHost: MessageTypeDefinition - WeightedCluster: MessageTypeDefinition - } - } - trace: { - v3: { - Tracing: MessageTypeDefinition - } - } - } - extensions: { - transport_sockets: { - tls: { - v3: { - CertificateProviderPluginInstance: MessageTypeDefinition - CertificateValidationContext: MessageTypeDefinition - GenericSecret: MessageTypeDefinition - PrivateKeyProvider: MessageTypeDefinition - SdsSecretConfig: MessageTypeDefinition - Secret: MessageTypeDefinition - TlsCertificate: MessageTypeDefinition - TlsParameters: MessageTypeDefinition - TlsSessionTicketKeys: MessageTypeDefinition - } - } - } } service: { status: { @@ -256,7 +89,6 @@ export interface ProtoGrpcType { DoubleMatcher: MessageTypeDefinition ListMatcher: MessageTypeDefinition ListStringMatcher: MessageTypeDefinition - MetadataMatcher: MessageTypeDefinition NodeMatcher: MessageTypeDefinition RegexMatchAndSubstitute: MessageTypeDefinition RegexMatcher: MessageTypeDefinition @@ -265,19 +97,7 @@ export interface ProtoGrpcType { ValueMatcher: MessageTypeDefinition } } - metadata: { - v3: { - MetadataKey: MessageTypeDefinition - MetadataKind: MessageTypeDefinition - } - } - tracing: { - v3: { - CustomTag: MessageTypeDefinition - } - } v3: { - CodecClientType: EnumTypeDefinition DoubleRange: MessageTypeDefinition FractionalPercent: MessageTypeDefinition Int32Range: MessageTypeDefinition @@ -300,7 +120,6 @@ export interface ProtoGrpcType { DescriptorProto: MessageTypeDefinition DoubleValue: MessageTypeDefinition Duration: MessageTypeDefinition - Empty: MessageTypeDefinition EnumDescriptorProto: MessageTypeDefinition EnumOptions: MessageTypeDefinition EnumValueDescriptorProto: MessageTypeDefinition @@ -336,7 +155,6 @@ export interface ProtoGrpcType { udpa: { annotations: { FieldMigrateAnnotation: MessageTypeDefinition - FieldSecurityAnnotation: MessageTypeDefinition FileMigrateAnnotation: MessageTypeDefinition MigrateAnnotation: MessageTypeDefinition PackageVersionStatus: EnumTypeDefinition @@ -382,10 +200,7 @@ export interface ProtoGrpcType { } core: { v3: { - Authority: MessageTypeDefinition - CollectionEntry: MessageTypeDefinition ContextParams: MessageTypeDefinition - ResourceLocator: MessageTypeDefinition } } } diff --git a/packages/grpc-js-xds/src/generated/endpoint.ts b/packages/grpc-js-xds/src/generated/endpoint.ts index 9a87bc9a5..4fcf914e3 100644 --- a/packages/grpc-js-xds/src/generated/endpoint.ts +++ b/packages/grpc-js-xds/src/generated/endpoint.ts @@ -28,16 +28,20 @@ export interface ProtoGrpcType { EnvoyInternalAddress: MessageTypeDefinition EventServiceConfig: MessageTypeDefinition Extension: MessageTypeDefinition + ExtensionConfigSource: MessageTypeDefinition + ExtraSourceAddress: MessageTypeDefinition GrpcService: MessageTypeDefinition HeaderMap: MessageTypeDefinition HeaderValue: MessageTypeDefinition HeaderValueOption: MessageTypeDefinition HealthCheck: MessageTypeDefinition HealthStatus: EnumTypeDefinition + HealthStatusSet: MessageTypeDefinition HttpUri: MessageTypeDefinition Locality: MessageTypeDefinition Metadata: MessageTypeDefinition Node: MessageTypeDefinition + PathConfigSource: MessageTypeDefinition Pipe: MessageTypeDefinition QueryParameter: MessageTypeDefinition RateLimitSettings: MessageTypeDefinition @@ -53,9 +57,11 @@ export interface ProtoGrpcType { SelfConfigSource: MessageTypeDefinition SocketAddress: MessageTypeDefinition SocketOption: MessageTypeDefinition + SocketOptionsOverride: MessageTypeDefinition TcpKeepalive: MessageTypeDefinition TrafficDirection: EnumTypeDefinition TransportSocket: MessageTypeDefinition + TypedExtensionConfig: MessageTypeDefinition WatchedDirectory: MessageTypeDefinition } } diff --git a/packages/grpc-js-xds/src/generated/envoy/admin/v3/ClientResourceStatus.ts b/packages/grpc-js-xds/src/generated/envoy/admin/v3/ClientResourceStatus.ts index 31c3a813a..8488bbdd7 100644 --- a/packages/grpc-js-xds/src/generated/envoy/admin/v3/ClientResourceStatus.ts +++ b/packages/grpc-js-xds/src/generated/envoy/admin/v3/ClientResourceStatus.ts @@ -1,4 +1,4 @@ -// Original file: deps/envoy-api/envoy/admin/v3/config_dump.proto +// Original file: deps/envoy-api/envoy/admin/v3/config_dump_shared.proto /** * Resource status from the view of a xDS client, which tells the synchronization diff --git a/packages/grpc-js-xds/src/generated/envoy/admin/v3/ClustersConfigDump.ts b/packages/grpc-js-xds/src/generated/envoy/admin/v3/ClustersConfigDump.ts index ab7c528bf..aabcd212a 100644 --- a/packages/grpc-js-xds/src/generated/envoy/admin/v3/ClustersConfigDump.ts +++ b/packages/grpc-js-xds/src/generated/envoy/admin/v3/ClustersConfigDump.ts @@ -1,4 +1,4 @@ -// Original file: deps/envoy-api/envoy/admin/v3/config_dump.proto +// Original file: deps/envoy-api/envoy/admin/v3/config_dump_shared.proto import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../google/protobuf/Any'; import type { Timestamp as _google_protobuf_Timestamp, Timestamp__Output as _google_protobuf_Timestamp__Output } from '../../../google/protobuf/Timestamp'; @@ -27,7 +27,7 @@ export interface _envoy_admin_v3_ClustersConfigDump_DynamicCluster { 'last_updated'?: (_google_protobuf_Timestamp | null); /** * Set if the last update failed, cleared after the next successful update. - * The *error_state* field contains the rejected version of this particular + * The ``error_state`` field contains the rejected version of this particular * resource along with the reason and timestamp. For successfully updated or * acknowledged resource, this field should be empty. * [#not-implemented-hide:] @@ -62,7 +62,7 @@ export interface _envoy_admin_v3_ClustersConfigDump_DynamicCluster__Output { 'last_updated': (_google_protobuf_Timestamp__Output | null); /** * Set if the last update failed, cleared after the next successful update. - * The *error_state* field contains the rejected version of this particular + * The ``error_state`` field contains the rejected version of this particular * resource along with the reason and timestamp. For successfully updated or * acknowledged resource, this field should be empty. * [#not-implemented-hide:] diff --git a/packages/grpc-js-xds/src/generated/envoy/admin/v3/EcdsConfigDump.ts b/packages/grpc-js-xds/src/generated/envoy/admin/v3/EcdsConfigDump.ts new file mode 100644 index 000000000..e63307cb5 --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/admin/v3/EcdsConfigDump.ts @@ -0,0 +1,100 @@ +// Original file: deps/envoy-api/envoy/admin/v3/config_dump_shared.proto + +import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../google/protobuf/Any'; +import type { Timestamp as _google_protobuf_Timestamp, Timestamp__Output as _google_protobuf_Timestamp__Output } from '../../../google/protobuf/Timestamp'; +import type { UpdateFailureState as _envoy_admin_v3_UpdateFailureState, UpdateFailureState__Output as _envoy_admin_v3_UpdateFailureState__Output } from '../../../envoy/admin/v3/UpdateFailureState'; +import type { ClientResourceStatus as _envoy_admin_v3_ClientResourceStatus } from '../../../envoy/admin/v3/ClientResourceStatus'; + +/** + * [#next-free-field: 6] + */ +export interface _envoy_admin_v3_EcdsConfigDump_EcdsFilterConfig { + /** + * This is the per-resource version information. This version is currently + * taken from the :ref:`version_info + * ` + * field at the time that the ECDS filter was loaded. + */ + 'version_info'?: (string); + /** + * The ECDS filter config. + */ + 'ecds_filter'?: (_google_protobuf_Any | null); + /** + * The timestamp when the ECDS filter was last updated. + */ + 'last_updated'?: (_google_protobuf_Timestamp | null); + /** + * Set if the last update failed, cleared after the next successful update. + * The ``error_state`` field contains the rejected version of this + * particular resource along with the reason and timestamp. For successfully + * updated or acknowledged resource, this field should be empty. + * [#not-implemented-hide:] + */ + 'error_state'?: (_envoy_admin_v3_UpdateFailureState | null); + /** + * The client status of this resource. + * [#not-implemented-hide:] + */ + 'client_status'?: (_envoy_admin_v3_ClientResourceStatus | keyof typeof _envoy_admin_v3_ClientResourceStatus); +} + +/** + * [#next-free-field: 6] + */ +export interface _envoy_admin_v3_EcdsConfigDump_EcdsFilterConfig__Output { + /** + * This is the per-resource version information. This version is currently + * taken from the :ref:`version_info + * ` + * field at the time that the ECDS filter was loaded. + */ + 'version_info': (string); + /** + * The ECDS filter config. + */ + 'ecds_filter': (_google_protobuf_Any__Output | null); + /** + * The timestamp when the ECDS filter was last updated. + */ + 'last_updated': (_google_protobuf_Timestamp__Output | null); + /** + * Set if the last update failed, cleared after the next successful update. + * The ``error_state`` field contains the rejected version of this + * particular resource along with the reason and timestamp. For successfully + * updated or acknowledged resource, this field should be empty. + * [#not-implemented-hide:] + */ + 'error_state': (_envoy_admin_v3_UpdateFailureState__Output | null); + /** + * The client status of this resource. + * [#not-implemented-hide:] + */ + 'client_status': (keyof typeof _envoy_admin_v3_ClientResourceStatus); +} + +/** + * Envoy's ECDS service fills this message with all currently extension + * configuration. Extension configuration information can be used to recreate + * an Envoy ECDS listener and HTTP filters as static filters or by returning + * them in ECDS response. + */ +export interface EcdsConfigDump { + /** + * The ECDS filter configs. + */ + 'ecds_filters'?: (_envoy_admin_v3_EcdsConfigDump_EcdsFilterConfig)[]; +} + +/** + * Envoy's ECDS service fills this message with all currently extension + * configuration. Extension configuration information can be used to recreate + * an Envoy ECDS listener and HTTP filters as static filters or by returning + * them in ECDS response. + */ +export interface EcdsConfigDump__Output { + /** + * The ECDS filter configs. + */ + 'ecds_filters': (_envoy_admin_v3_EcdsConfigDump_EcdsFilterConfig__Output)[]; +} diff --git a/packages/grpc-js-xds/src/generated/envoy/admin/v3/EndpointsConfigDump.ts b/packages/grpc-js-xds/src/generated/envoy/admin/v3/EndpointsConfigDump.ts index d68b27e7c..ab5485dbe 100644 --- a/packages/grpc-js-xds/src/generated/envoy/admin/v3/EndpointsConfigDump.ts +++ b/packages/grpc-js-xds/src/generated/envoy/admin/v3/EndpointsConfigDump.ts @@ -1,4 +1,4 @@ -// Original file: deps/envoy-api/envoy/admin/v3/config_dump.proto +// Original file: deps/envoy-api/envoy/admin/v3/config_dump_shared.proto import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../google/protobuf/Any'; import type { Timestamp as _google_protobuf_Timestamp, Timestamp__Output as _google_protobuf_Timestamp__Output } from '../../../google/protobuf/Timestamp'; @@ -25,7 +25,7 @@ export interface _envoy_admin_v3_EndpointsConfigDump_DynamicEndpointConfig { 'last_updated'?: (_google_protobuf_Timestamp | null); /** * Set if the last update failed, cleared after the next successful update. - * The *error_state* field contains the rejected version of this particular + * The ``error_state`` field contains the rejected version of this particular * resource along with the reason and timestamp. For successfully updated or * acknowledged resource, this field should be empty. * [#not-implemented-hide:] @@ -58,7 +58,7 @@ export interface _envoy_admin_v3_EndpointsConfigDump_DynamicEndpointConfig__Outp 'last_updated': (_google_protobuf_Timestamp__Output | null); /** * Set if the last update failed, cleared after the next successful update. - * The *error_state* field contains the rejected version of this particular + * The ``error_state`` field contains the rejected version of this particular * resource along with the reason and timestamp. For successfully updated or * acknowledged resource, this field should be empty. * [#not-implemented-hide:] diff --git a/packages/grpc-js-xds/src/generated/envoy/admin/v3/ListenersConfigDump.ts b/packages/grpc-js-xds/src/generated/envoy/admin/v3/ListenersConfigDump.ts index 745abedae..946e37953 100644 --- a/packages/grpc-js-xds/src/generated/envoy/admin/v3/ListenersConfigDump.ts +++ b/packages/grpc-js-xds/src/generated/envoy/admin/v3/ListenersConfigDump.ts @@ -1,4 +1,4 @@ -// Original file: deps/envoy-api/envoy/admin/v3/config_dump.proto +// Original file: deps/envoy-api/envoy/admin/v3/config_dump_shared.proto import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../google/protobuf/Any'; import type { Timestamp as _google_protobuf_Timestamp, Timestamp__Output as _google_protobuf_Timestamp__Output } from '../../../google/protobuf/Timestamp'; @@ -35,7 +35,7 @@ export interface _envoy_admin_v3_ListenersConfigDump_DynamicListener { 'draining_state'?: (_envoy_admin_v3_ListenersConfigDump_DynamicListenerState | null); /** * Set if the last update failed, cleared after the next successful update. - * The *error_state* field contains the rejected version of this particular + * The ``error_state`` field contains the rejected version of this particular * resource along with the reason and timestamp. For successfully updated or * acknowledged resource, this field should be empty. */ @@ -77,7 +77,7 @@ export interface _envoy_admin_v3_ListenersConfigDump_DynamicListener__Output { 'draining_state': (_envoy_admin_v3_ListenersConfigDump_DynamicListenerState__Output | null); /** * Set if the last update failed, cleared after the next successful update. - * The *error_state* field contains the rejected version of this particular + * The ``error_state`` field contains the rejected version of this particular * resource along with the reason and timestamp. For successfully updated or * acknowledged resource, this field should be empty. */ diff --git a/packages/grpc-js-xds/src/generated/envoy/admin/v3/RoutesConfigDump.ts b/packages/grpc-js-xds/src/generated/envoy/admin/v3/RoutesConfigDump.ts index 2a62e9b74..7b9bb29d0 100644 --- a/packages/grpc-js-xds/src/generated/envoy/admin/v3/RoutesConfigDump.ts +++ b/packages/grpc-js-xds/src/generated/envoy/admin/v3/RoutesConfigDump.ts @@ -1,4 +1,4 @@ -// Original file: deps/envoy-api/envoy/admin/v3/config_dump.proto +// Original file: deps/envoy-api/envoy/admin/v3/config_dump_shared.proto import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../google/protobuf/Any'; import type { Timestamp as _google_protobuf_Timestamp, Timestamp__Output as _google_protobuf_Timestamp__Output } from '../../../google/protobuf/Timestamp'; @@ -25,7 +25,7 @@ export interface _envoy_admin_v3_RoutesConfigDump_DynamicRouteConfig { 'last_updated'?: (_google_protobuf_Timestamp | null); /** * Set if the last update failed, cleared after the next successful update. - * The *error_state* field contains the rejected version of this particular + * The ``error_state`` field contains the rejected version of this particular * resource along with the reason and timestamp. For successfully updated or * acknowledged resource, this field should be empty. * [#not-implemented-hide:] @@ -58,7 +58,7 @@ export interface _envoy_admin_v3_RoutesConfigDump_DynamicRouteConfig__Output { 'last_updated': (_google_protobuf_Timestamp__Output | null); /** * Set if the last update failed, cleared after the next successful update. - * The *error_state* field contains the rejected version of this particular + * The ``error_state`` field contains the rejected version of this particular * resource along with the reason and timestamp. For successfully updated or * acknowledged resource, this field should be empty. * [#not-implemented-hide:] diff --git a/packages/grpc-js-xds/src/generated/envoy/admin/v3/ScopedRoutesConfigDump.ts b/packages/grpc-js-xds/src/generated/envoy/admin/v3/ScopedRoutesConfigDump.ts index f271635bc..c0723ce69 100644 --- a/packages/grpc-js-xds/src/generated/envoy/admin/v3/ScopedRoutesConfigDump.ts +++ b/packages/grpc-js-xds/src/generated/envoy/admin/v3/ScopedRoutesConfigDump.ts @@ -1,4 +1,4 @@ -// Original file: deps/envoy-api/envoy/admin/v3/config_dump.proto +// Original file: deps/envoy-api/envoy/admin/v3/config_dump_shared.proto import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../google/protobuf/Any'; import type { Timestamp as _google_protobuf_Timestamp, Timestamp__Output as _google_protobuf_Timestamp__Output } from '../../../google/protobuf/Timestamp'; @@ -29,7 +29,7 @@ export interface _envoy_admin_v3_ScopedRoutesConfigDump_DynamicScopedRouteConfig 'last_updated'?: (_google_protobuf_Timestamp | null); /** * Set if the last update failed, cleared after the next successful update. - * The *error_state* field contains the rejected version of this particular + * The ``error_state`` field contains the rejected version of this particular * resource along with the reason and timestamp. For successfully updated or * acknowledged resource, this field should be empty. * [#not-implemented-hide:] @@ -66,7 +66,7 @@ export interface _envoy_admin_v3_ScopedRoutesConfigDump_DynamicScopedRouteConfig 'last_updated': (_google_protobuf_Timestamp__Output | null); /** * Set if the last update failed, cleared after the next successful update. - * The *error_state* field contains the rejected version of this particular + * The ``error_state`` field contains the rejected version of this particular * resource along with the reason and timestamp. For successfully updated or * acknowledged resource, this field should be empty. * [#not-implemented-hide:] diff --git a/packages/grpc-js-xds/src/generated/envoy/admin/v3/UpdateFailureState.ts b/packages/grpc-js-xds/src/generated/envoy/admin/v3/UpdateFailureState.ts index b98e8cd4d..100c65a1b 100644 --- a/packages/grpc-js-xds/src/generated/envoy/admin/v3/UpdateFailureState.ts +++ b/packages/grpc-js-xds/src/generated/envoy/admin/v3/UpdateFailureState.ts @@ -1,4 +1,4 @@ -// Original file: deps/envoy-api/envoy/admin/v3/config_dump.proto +// Original file: deps/envoy-api/envoy/admin/v3/config_dump_shared.proto import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../google/protobuf/Any'; import type { Timestamp as _google_protobuf_Timestamp, Timestamp__Output as _google_protobuf_Timestamp__Output } from '../../../google/protobuf/Timestamp'; diff --git a/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/AccessLog.ts b/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/AccessLog.ts index 367d8f302..73a031fdd 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/AccessLog.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/AccessLog.ts @@ -5,9 +5,7 @@ import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__ export interface AccessLog { /** - * The name of the access log extension to instantiate. - * The name must match one of the compiled in loggers. - * See the :ref:`extensions listed in typed_config below ` for the default list of available loggers. + * The name of the access log extension configuration. */ 'name'?: (string); /** @@ -24,9 +22,7 @@ export interface AccessLog { export interface AccessLog__Output { /** - * The name of the access log extension to instantiate. - * The name must match one of the compiled in loggers. - * See the :ref:`extensions listed in typed_config below ` for the default list of available loggers. + * The name of the access log extension configuration. */ 'name': (string); /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/AccessLogFilter.ts b/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/AccessLogFilter.ts index 85a952c9c..09563cb7a 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/AccessLogFilter.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/AccessLogFilter.ts @@ -12,9 +12,10 @@ import type { ResponseFlagFilter as _envoy_config_accesslog_v3_ResponseFlagFilte import type { GrpcStatusFilter as _envoy_config_accesslog_v3_GrpcStatusFilter, GrpcStatusFilter__Output as _envoy_config_accesslog_v3_GrpcStatusFilter__Output } from '../../../../envoy/config/accesslog/v3/GrpcStatusFilter'; import type { ExtensionFilter as _envoy_config_accesslog_v3_ExtensionFilter, ExtensionFilter__Output as _envoy_config_accesslog_v3_ExtensionFilter__Output } from '../../../../envoy/config/accesslog/v3/ExtensionFilter'; import type { MetadataFilter as _envoy_config_accesslog_v3_MetadataFilter, MetadataFilter__Output as _envoy_config_accesslog_v3_MetadataFilter__Output } from '../../../../envoy/config/accesslog/v3/MetadataFilter'; +import type { LogTypeFilter as _envoy_config_accesslog_v3_LogTypeFilter, LogTypeFilter__Output as _envoy_config_accesslog_v3_LogTypeFilter__Output } from '../../../../envoy/config/accesslog/v3/LogTypeFilter'; /** - * [#next-free-field: 13] + * [#next-free-field: 14] */ export interface AccessLogFilter { /** @@ -59,17 +60,22 @@ export interface AccessLogFilter { 'grpc_status_filter'?: (_envoy_config_accesslog_v3_GrpcStatusFilter | null); /** * Extension filter. + * [#extension-category: envoy.access_loggers.extension_filters] */ 'extension_filter'?: (_envoy_config_accesslog_v3_ExtensionFilter | null); /** * Metadata Filter */ 'metadata_filter'?: (_envoy_config_accesslog_v3_MetadataFilter | null); - 'filter_specifier'?: "status_code_filter"|"duration_filter"|"not_health_check_filter"|"traceable_filter"|"runtime_filter"|"and_filter"|"or_filter"|"header_filter"|"response_flag_filter"|"grpc_status_filter"|"extension_filter"|"metadata_filter"; + /** + * Log Type Filter + */ + 'log_type_filter'?: (_envoy_config_accesslog_v3_LogTypeFilter | null); + 'filter_specifier'?: "status_code_filter"|"duration_filter"|"not_health_check_filter"|"traceable_filter"|"runtime_filter"|"and_filter"|"or_filter"|"header_filter"|"response_flag_filter"|"grpc_status_filter"|"extension_filter"|"metadata_filter"|"log_type_filter"; } /** - * [#next-free-field: 13] + * [#next-free-field: 14] */ export interface AccessLogFilter__Output { /** @@ -114,11 +120,16 @@ export interface AccessLogFilter__Output { 'grpc_status_filter'?: (_envoy_config_accesslog_v3_GrpcStatusFilter__Output | null); /** * Extension filter. + * [#extension-category: envoy.access_loggers.extension_filters] */ 'extension_filter'?: (_envoy_config_accesslog_v3_ExtensionFilter__Output | null); /** * Metadata Filter */ 'metadata_filter'?: (_envoy_config_accesslog_v3_MetadataFilter__Output | null); - 'filter_specifier': "status_code_filter"|"duration_filter"|"not_health_check_filter"|"traceable_filter"|"runtime_filter"|"and_filter"|"or_filter"|"header_filter"|"response_flag_filter"|"grpc_status_filter"|"extension_filter"|"metadata_filter"; + /** + * Log Type Filter + */ + 'log_type_filter'?: (_envoy_config_accesslog_v3_LogTypeFilter__Output | null); + 'filter_specifier': "status_code_filter"|"duration_filter"|"not_health_check_filter"|"traceable_filter"|"runtime_filter"|"and_filter"|"or_filter"|"header_filter"|"response_flag_filter"|"grpc_status_filter"|"extension_filter"|"metadata_filter"|"log_type_filter"; } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/DurationFilter.ts b/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/DurationFilter.ts index ee61e55fa..024936704 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/DurationFilter.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/DurationFilter.ts @@ -3,7 +3,10 @@ import type { ComparisonFilter as _envoy_config_accesslog_v3_ComparisonFilter, ComparisonFilter__Output as _envoy_config_accesslog_v3_ComparisonFilter__Output } from '../../../../envoy/config/accesslog/v3/ComparisonFilter'; /** - * Filters on total request duration in milliseconds. + * Filters based on the duration of the request or stream, in milliseconds. + * For end of stream access logs, the total duration of the stream will be used. + * For :ref:`periodic access logs`, + * the duration of the stream at the time of log recording will be used. */ export interface DurationFilter { /** @@ -13,7 +16,10 @@ export interface DurationFilter { } /** - * Filters on total request duration in milliseconds. + * Filters based on the duration of the request or stream, in milliseconds. + * For end of stream access logs, the total duration of the stream will be used. + * For :ref:`periodic access logs`, + * the duration of the stream at the time of log recording will be used. */ export interface DurationFilter__Output { /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/LogTypeFilter.ts b/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/LogTypeFilter.ts new file mode 100644 index 000000000..8d51cd33f --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/LogTypeFilter.ts @@ -0,0 +1,33 @@ +// Original file: deps/envoy-api/envoy/config/accesslog/v3/accesslog.proto + +import type { AccessLogType as _envoy_data_accesslog_v3_AccessLogType } from '../../../../envoy/data/accesslog/v3/AccessLogType'; + +/** + * Filters based on access log type. + */ +export interface LogTypeFilter { + /** + * Logs only records which their type is one of the types defined in this field. + */ + 'types'?: (_envoy_data_accesslog_v3_AccessLogType | keyof typeof _envoy_data_accesslog_v3_AccessLogType)[]; + /** + * If this field is set to true, the filter will instead block all records + * with a access log type in types field, and allow all other records. + */ + 'exclude'?: (boolean); +} + +/** + * Filters based on access log type. + */ +export interface LogTypeFilter__Output { + /** + * Logs only records which their type is one of the types defined in this field. + */ + 'types': (keyof typeof _envoy_data_accesslog_v3_AccessLogType)[]; + /** + * If this field is set to true, the filter will instead block all records + * with a access log type in types field, and allow all other records. + */ + 'exclude': (boolean); +} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/RuntimeFilter.ts b/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/RuntimeFilter.ts index 83b075388..b1c940088 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/RuntimeFilter.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/RuntimeFilter.ts @@ -8,7 +8,7 @@ import type { FractionalPercent as _envoy_type_v3_FractionalPercent, FractionalP export interface RuntimeFilter { /** * Runtime key to get an optional overridden numerator for use in the - * *percent_sampled* field. If found in runtime, this value will replace the + * ``percent_sampled`` field. If found in runtime, this value will replace the * default numerator. */ 'runtime_key'?: (string); @@ -24,9 +24,9 @@ export interface RuntimeFilter { * is present, the filter will consistently sample across multiple hosts based * on the runtime key value and the value extracted from * :ref:`x-request-id`. If it is - * missing, or *use_independent_randomness* is set to true, the filter will + * missing, or ``use_independent_randomness`` is set to true, the filter will * randomly sample based on the runtime key value alone. - * *use_independent_randomness* can be used for logging kill switches within + * ``use_independent_randomness`` can be used for logging kill switches within * complex nested :ref:`AndFilter * ` and :ref:`OrFilter * ` blocks that are easier to @@ -43,7 +43,7 @@ export interface RuntimeFilter { export interface RuntimeFilter__Output { /** * Runtime key to get an optional overridden numerator for use in the - * *percent_sampled* field. If found in runtime, this value will replace the + * ``percent_sampled`` field. If found in runtime, this value will replace the * default numerator. */ 'runtime_key': (string); @@ -59,9 +59,9 @@ export interface RuntimeFilter__Output { * is present, the filter will consistently sample across multiple hosts based * on the runtime key value and the value extracted from * :ref:`x-request-id`. If it is - * missing, or *use_independent_randomness* is set to true, the filter will + * missing, or ``use_independent_randomness`` is set to true, the filter will * randomly sample based on the runtime key value alone. - * *use_independent_randomness* can be used for logging kill switches within + * ``use_independent_randomness`` can be used for logging kill switches within * complex nested :ref:`AndFilter * ` and :ref:`OrFilter * ` blocks that are easier to diff --git a/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/CircuitBreakers.ts b/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/CircuitBreakers.ts index 12731b056..4a8a4be36 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/CircuitBreakers.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/CircuitBreakers.ts @@ -59,11 +59,13 @@ export interface _envoy_config_cluster_v3_CircuitBreakers_Thresholds { /** * The maximum number of pending requests that Envoy will allow to the * upstream cluster. If not specified, the default is 1024. + * This limit is applied as a connection limit for non-HTTP traffic. */ 'max_pending_requests'?: (_google_protobuf_UInt32Value | null); /** * The maximum number of parallel requests that Envoy will make to the * upstream cluster. If not specified, the default is 1024. + * This limit does not apply to non-HTTP traffic. */ 'max_requests'?: (_google_protobuf_UInt32Value | null); /** @@ -121,11 +123,13 @@ export interface _envoy_config_cluster_v3_CircuitBreakers_Thresholds__Output { /** * The maximum number of pending requests that Envoy will allow to the * upstream cluster. If not specified, the default is 1024. + * This limit is applied as a connection limit for non-HTTP traffic. */ 'max_pending_requests': (_google_protobuf_UInt32Value__Output | null); /** * The maximum number of parallel requests that Envoy will make to the * upstream cluster. If not specified, the default is 1024. + * This limit does not apply to non-HTTP traffic. */ 'max_requests': (_google_protobuf_UInt32Value__Output | null); /** @@ -177,6 +181,20 @@ export interface CircuitBreakers { * are used. */ 'thresholds'?: (_envoy_config_cluster_v3_CircuitBreakers_Thresholds)[]; + /** + * Optional per-host limits which apply to each individual host in a cluster. + * + * .. note:: + * currently only the :ref:`max_connections + * ` field is supported for per-host limits. + * + * If multiple per-host :ref:`Thresholds` + * are defined with the same :ref:`RoutingPriority`, + * the first one in the list is used. If no per-host Thresholds are defined for a given + * :ref:`RoutingPriority`, + * the cluster will not have per-host limits. + */ + 'per_host_thresholds'?: (_envoy_config_cluster_v3_CircuitBreakers_Thresholds)[]; } /** @@ -192,4 +210,18 @@ export interface CircuitBreakers__Output { * are used. */ 'thresholds': (_envoy_config_cluster_v3_CircuitBreakers_Thresholds__Output)[]; + /** + * Optional per-host limits which apply to each individual host in a cluster. + * + * .. note:: + * currently only the :ref:`max_connections + * ` field is supported for per-host limits. + * + * If multiple per-host :ref:`Thresholds` + * are defined with the same :ref:`RoutingPriority`, + * the first one in the list is used. If no per-host Thresholds are defined for a given + * :ref:`RoutingPriority`, + * the cluster will not have per-host limits. + */ + 'per_host_thresholds': (_envoy_config_cluster_v3_CircuitBreakers_Thresholds__Output)[]; } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/Cluster.ts b/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/Cluster.ts index 12ec7b633..be30d8212 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/Cluster.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/Cluster.ts @@ -25,8 +25,9 @@ import type { DnsResolutionConfig as _envoy_config_core_v3_DnsResolutionConfig, import type { BoolValue as _google_protobuf_BoolValue, BoolValue__Output as _google_protobuf_BoolValue__Output } from '../../../../google/protobuf/BoolValue'; import type { Struct as _google_protobuf_Struct, Struct__Output as _google_protobuf_Struct__Output } from '../../../../google/protobuf/Struct'; import type { RuntimeDouble as _envoy_config_core_v3_RuntimeDouble, RuntimeDouble__Output as _envoy_config_core_v3_RuntimeDouble__Output } from '../../../../envoy/config/core/v3/RuntimeDouble'; -import type { UInt64Value as _google_protobuf_UInt64Value, UInt64Value__Output as _google_protobuf_UInt64Value__Output } from '../../../../google/protobuf/UInt64Value'; import type { Percent as _envoy_type_v3_Percent, Percent__Output as _envoy_type_v3_Percent__Output } from '../../../../envoy/type/v3/Percent'; +import type { UInt64Value as _google_protobuf_UInt64Value, UInt64Value__Output as _google_protobuf_UInt64Value__Output } from '../../../../google/protobuf/UInt64Value'; +import type { HealthStatusSet as _envoy_config_core_v3_HealthStatusSet, HealthStatusSet__Output as _envoy_config_core_v3_HealthStatusSet__Output } from '../../../../envoy/config/core/v3/HealthStatusSet'; import type { DoubleValue as _google_protobuf_DoubleValue, DoubleValue__Output as _google_protobuf_DoubleValue__Output } from '../../../../google/protobuf/DoubleValue'; import type { Long } from '@grpc/proto-loader'; @@ -47,7 +48,7 @@ export enum _envoy_config_cluster_v3_Cluster_ClusterProtocolSelection { /** * Common configuration for all load balancer implementations. - * [#next-free-field: 8] + * [#next-free-field: 9] */ export interface _envoy_config_cluster_v3_Cluster_CommonLbConfig { /** @@ -85,7 +86,7 @@ export interface _envoy_config_cluster_v3_Cluster_CommonLbConfig { */ 'ignore_new_hosts_until_first_hc'?: (boolean); /** - * If set to `true`, the cluster manager will drain all existing + * If set to ``true``, the cluster manager will drain all existing * connections to upstream hosts whenever hosts are added or removed from the cluster. */ 'close_connections_on_host_set_change'?: (boolean); @@ -93,12 +94,21 @@ export interface _envoy_config_cluster_v3_Cluster_CommonLbConfig { * Common Configuration for all consistent hashing load balancers (MaglevLb, RingHashLb, etc.) */ 'consistent_hashing_lb_config'?: (_envoy_config_cluster_v3_Cluster_CommonLbConfig_ConsistentHashingLbConfig | null); + /** + * This controls what hosts are considered valid when using + * :ref:`host overrides `, which is used by some + * filters to modify the load balancing decision. + * + * If this is unset then [UNKNOWN, HEALTHY, DEGRADED] will be applied by default. If this is + * set with an empty set of statuses then host overrides will be ignored by the load balancing. + */ + 'override_host_status'?: (_envoy_config_core_v3_HealthStatusSet | null); 'locality_config_specifier'?: "zone_aware_lb_config"|"locality_weighted_lb_config"; } /** * Common configuration for all load balancer implementations. - * [#next-free-field: 8] + * [#next-free-field: 9] */ export interface _envoy_config_cluster_v3_Cluster_CommonLbConfig__Output { /** @@ -136,7 +146,7 @@ export interface _envoy_config_cluster_v3_Cluster_CommonLbConfig__Output { */ 'ignore_new_hosts_until_first_hc': (boolean); /** - * If set to `true`, the cluster manager will drain all existing + * If set to ``true``, the cluster manager will drain all existing * connections to upstream hosts whenever hosts are added or removed from the cluster. */ 'close_connections_on_host_set_change': (boolean); @@ -144,6 +154,15 @@ export interface _envoy_config_cluster_v3_Cluster_CommonLbConfig__Output { * Common Configuration for all consistent hashing load balancers (MaglevLb, RingHashLb, etc.) */ 'consistent_hashing_lb_config': (_envoy_config_cluster_v3_Cluster_CommonLbConfig_ConsistentHashingLbConfig__Output | null); + /** + * This controls what hosts are considered valid when using + * :ref:`host overrides `, which is used by some + * filters to modify the load balancing decision. + * + * If this is unset then [UNKNOWN, HEALTHY, DEGRADED] will be applied by default. If this is + * set with an empty set of statuses then host overrides will be ignored by the load balancing. + */ + 'override_host_status': (_envoy_config_core_v3_HealthStatusSet__Output | null); 'locality_config_specifier': "zone_aware_lb_config"|"locality_weighted_lb_config"; } @@ -152,7 +171,7 @@ export interface _envoy_config_cluster_v3_Cluster_CommonLbConfig__Output { */ export interface _envoy_config_cluster_v3_Cluster_CommonLbConfig_ConsistentHashingLbConfig { /** - * If set to `true`, the cluster will use hostname instead of the resolved + * If set to ``true``, the cluster will use hostname instead of the resolved * address as the key to consistently hash to an upstream host. Only valid for StrictDNS clusters with hostnames which resolve to a single IP address. */ 'use_hostname_for_hashing'?: (boolean); @@ -165,7 +184,7 @@ export interface _envoy_config_cluster_v3_Cluster_CommonLbConfig_ConsistentHashi * Applies to both Ring Hash and Maglev load balancers. * * This is implemented based on the method described in the paper https://arxiv.org/abs/1608.01350. For the specified - * `hash_balance_factor`, requests to any upstream host are capped at `hash_balance_factor/100` times the average number of requests + * ``hash_balance_factor``, requests to any upstream host are capped at ``hash_balance_factor/100`` times the average number of requests * across the cluster. When a request arrives for an upstream host that is currently serving at its max capacity, linear probing * is used to identify an eligible host. Further, the linear probe is implemented using a random jump in hosts ring/table to identify * the eligible host (this technique is as described in the paper https://arxiv.org/abs/1908.08762 - the random jump avoids the @@ -173,7 +192,7 @@ export interface _envoy_config_cluster_v3_Cluster_CommonLbConfig_ConsistentHashi * * If weights are specified on the hosts, they are respected. * - * This is an O(N) algorithm, unlike other load balancers. Using a lower `hash_balance_factor` results in more hosts + * This is an O(N) algorithm, unlike other load balancers. Using a lower ``hash_balance_factor`` results in more hosts * being probed, so use a higher value if you require better performance. */ 'hash_balance_factor'?: (_google_protobuf_UInt32Value | null); @@ -184,7 +203,7 @@ export interface _envoy_config_cluster_v3_Cluster_CommonLbConfig_ConsistentHashi */ export interface _envoy_config_cluster_v3_Cluster_CommonLbConfig_ConsistentHashingLbConfig__Output { /** - * If set to `true`, the cluster will use hostname instead of the resolved + * If set to ``true``, the cluster will use hostname instead of the resolved * address as the key to consistently hash to an upstream host. Only valid for StrictDNS clusters with hostnames which resolve to a single IP address. */ 'use_hostname_for_hashing': (boolean); @@ -197,7 +216,7 @@ export interface _envoy_config_cluster_v3_Cluster_CommonLbConfig_ConsistentHashi * Applies to both Ring Hash and Maglev load balancers. * * This is implemented based on the method described in the paper https://arxiv.org/abs/1608.01350. For the specified - * `hash_balance_factor`, requests to any upstream host are capped at `hash_balance_factor/100` times the average number of requests + * ``hash_balance_factor``, requests to any upstream host are capped at ``hash_balance_factor/100`` times the average number of requests * across the cluster. When a request arrives for an upstream host that is currently serving at its max capacity, linear probing * is used to identify an eligible host. Further, the linear probe is implemented using a random jump in hosts ring/table to identify * the eligible host (this technique is as described in the paper https://arxiv.org/abs/1908.08762 - the random jump avoids the @@ -205,7 +224,7 @@ export interface _envoy_config_cluster_v3_Cluster_CommonLbConfig_ConsistentHashi * * If weights are specified on the hosts, they are respected. * - * This is an O(N) algorithm, unlike other load balancers. Using a lower `hash_balance_factor` results in more hosts + * This is an O(N) algorithm, unlike other load balancers. Using a lower ``hash_balance_factor`` results in more hosts * being probed, so use a higher value if you require better performance. */ 'hash_balance_factor': (_google_protobuf_UInt32Value__Output | null); @@ -294,6 +313,10 @@ export enum _envoy_config_cluster_v3_Cluster_DiscoveryType { * If V4_PREFERRED is specified, the DNS resolver will first perform a lookup for addresses in the * IPv4 family and fallback to a lookup for addresses in the IPv6 family. i.e., the callback * target will only get v6 addresses if there were NO v4 addresses to return. + * If ALL is specified, the DNS resolver will perform a lookup for both IPv4 and IPv6 families, + * and return all resolved addresses. When this is used, Happy Eyeballs will be enabled for + * upstream connections. Refer to :ref:`Happy Eyeballs Support ` + * for more information. * For cluster types other than * :ref:`STRICT_DNS` and * :ref:`LOGICAL_DNS`, @@ -306,6 +329,7 @@ export enum _envoy_config_cluster_v3_Cluster_DnsLookupFamily { V4_ONLY = 1, V6_ONLY = 2, V4_PREFERRED = 3, + ALL = 4, } /** @@ -403,9 +427,9 @@ export enum _envoy_config_cluster_v3_Cluster_LbPolicy { /** * Use the new :ref:`load_balancing_policy * ` field to determine the LB policy. - * [#next-major-version: In the v3 API, we should consider deprecating the lb_policy field - * and instead using the new load_balancing_policy field as the one and only mechanism for - * configuring this.] + * This has been deprecated in favor of using the :ref:`load_balancing_policy + * ` field without + * setting any value in :ref:`lb_policy`. */ LOAD_BALANCING_POLICY_CONFIG = 7, } @@ -413,7 +437,7 @@ export enum _envoy_config_cluster_v3_Cluster_LbPolicy { /** * Optionally divide the endpoints in this cluster into subsets defined by * endpoint metadata and selected by route and weighted cluster metadata. - * [#next-free-field: 8] + * [#next-free-field: 9] */ export interface _envoy_config_cluster_v3_Cluster_LbSubsetConfig { /** @@ -427,7 +451,7 @@ export interface _envoy_config_cluster_v3_Cluster_LbSubsetConfig { * fallback_policy is * :ref:`DEFAULT_SUBSET`. * Each field in default_subset is - * compared to the matching LbEndpoint.Metadata under the *envoy.lb* + * compared to the matching LbEndpoint.Metadata under the ``envoy.lb`` * namespace. It is valid for no hosts to match, in which case the behavior * is the same as a fallback_policy of * :ref:`NO_FALLBACK`. @@ -435,7 +459,7 @@ export interface _envoy_config_cluster_v3_Cluster_LbSubsetConfig { 'default_subset'?: (_google_protobuf_Struct | null); /** * For each entry, LbEndpoint.Metadata's - * *envoy.lb* namespace is traversed and a subset is created for each unique + * ``envoy.lb`` namespace is traversed and a subset is created for each unique * combination of key and value. For example: * * .. code-block:: json @@ -485,12 +509,22 @@ export interface _envoy_config_cluster_v3_Cluster_LbSubsetConfig { * and any of the elements in the list matches the criteria. */ 'list_as_any'?: (boolean); + /** + * Fallback mechanism that allows to try different route metadata until a host is found. + * If load balancing process, including all its mechanisms (like + * :ref:`fallback_policy`) + * fails to select a host, this policy decides if and how the process is repeated using another metadata. + * + * The value defaults to + * :ref:`METADATA_NO_FALLBACK`. + */ + 'metadata_fallback_policy'?: (_envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetMetadataFallbackPolicy | keyof typeof _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetMetadataFallbackPolicy); } /** * Optionally divide the endpoints in this cluster into subsets defined by * endpoint metadata and selected by route and weighted cluster metadata. - * [#next-free-field: 8] + * [#next-free-field: 9] */ export interface _envoy_config_cluster_v3_Cluster_LbSubsetConfig__Output { /** @@ -504,7 +538,7 @@ export interface _envoy_config_cluster_v3_Cluster_LbSubsetConfig__Output { * fallback_policy is * :ref:`DEFAULT_SUBSET`. * Each field in default_subset is - * compared to the matching LbEndpoint.Metadata under the *envoy.lb* + * compared to the matching LbEndpoint.Metadata under the ``envoy.lb`` * namespace. It is valid for no hosts to match, in which case the behavior * is the same as a fallback_policy of * :ref:`NO_FALLBACK`. @@ -512,7 +546,7 @@ export interface _envoy_config_cluster_v3_Cluster_LbSubsetConfig__Output { 'default_subset': (_google_protobuf_Struct__Output | null); /** * For each entry, LbEndpoint.Metadata's - * *envoy.lb* namespace is traversed and a subset is created for each unique + * ``envoy.lb`` namespace is traversed and a subset is created for each unique * combination of key and value. For example: * * .. code-block:: json @@ -562,6 +596,16 @@ export interface _envoy_config_cluster_v3_Cluster_LbSubsetConfig__Output { * and any of the elements in the list matches the criteria. */ 'list_as_any': (boolean); + /** + * Fallback mechanism that allows to try different route metadata until a host is found. + * If load balancing process, including all its mechanisms (like + * :ref:`fallback_policy`) + * fails to select a host, this policy decides if and how the process is repeated using another metadata. + * + * The value defaults to + * :ref:`METADATA_NO_FALLBACK`. + */ + 'metadata_fallback_policy': (keyof typeof _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetMetadataFallbackPolicy); } // Original file: deps/envoy-api/envoy/config/cluster/v3/cluster.proto @@ -579,6 +623,57 @@ export enum _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetFallbackPoli DEFAULT_SUBSET = 2, } +// Original file: deps/envoy-api/envoy/config/cluster/v3/cluster.proto + +export enum _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetMetadataFallbackPolicy { + /** + * No fallback. Route metadata will be used as-is. + */ + METADATA_NO_FALLBACK = 0, + /** + * A special metadata key ``fallback_list`` will be used to provide variants of metadata to try. + * Value of ``fallback_list`` key has to be a list. Every list element has to be a struct - it will + * be merged with route metadata, overriding keys that appear in both places. + * ``fallback_list`` entries will be used in order until a host is found. + * + * ``fallback_list`` key itself is removed from metadata before subset load balancing is performed. + * + * Example: + * + * for metadata: + * + * .. code-block:: yaml + * + * version: 1.0 + * fallback_list: + * - version: 2.0 + * hardware: c64 + * - hardware: c32 + * - version: 3.0 + * + * at first, metadata: + * + * .. code-block:: json + * + * {"version": "2.0", "hardware": "c64"} + * + * will be used for load balancing. If no host is found, metadata: + * + * .. code-block:: json + * + * {"version": "1.0", "hardware": "c32"} + * + * is next to try. If it still results in no host, finally metadata: + * + * .. code-block:: json + * + * {"version": "3.0"} + * + * is used. + */ + FALLBACK_LIST = 1, +} + /** * Specifications for subsets. */ @@ -591,12 +686,9 @@ export interface _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetSelecto * Selects a mode of operation in which each subset has only one host. This mode uses the same rules for * choosing a host, but updating hosts is faster, especially for large numbers of hosts. * - * If a match is found to a host, that host will be used regardless of priority levels, unless the host is unhealthy. - * - * Currently, this mode is only supported if `subset_selectors` has only one entry, and `keys` contains - * only one entry. + * If a match is found to a host, that host will be used regardless of priority levels. * - * When this mode is enabled, configurations that contain more than one host with the same metadata value for the single key in `keys` + * When this mode is enabled, configurations that contain more than one host with the same metadata value for the single key in ``keys`` * will use only one of the hosts with the given key; no requests will be routed to the others. The cluster gauge * :ref:`lb_subsets_single_host_per_subset_duplicate` indicates how many duplicates are * present in the current configuration. @@ -616,7 +708,7 @@ export interface _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetSelecto * For any other fallback policy the parameter is not used and should not be set. * Only values also present in * :ref:`keys` are allowed, but - * `fallback_keys_subset` cannot be equal to `keys`. + * ``fallback_keys_subset`` cannot be equal to ``keys``. */ 'fallback_keys_subset'?: (string)[]; } @@ -633,12 +725,9 @@ export interface _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetSelecto * Selects a mode of operation in which each subset has only one host. This mode uses the same rules for * choosing a host, but updating hosts is faster, especially for large numbers of hosts. * - * If a match is found to a host, that host will be used regardless of priority levels, unless the host is unhealthy. + * If a match is found to a host, that host will be used regardless of priority levels. * - * Currently, this mode is only supported if `subset_selectors` has only one entry, and `keys` contains - * only one entry. - * - * When this mode is enabled, configurations that contain more than one host with the same metadata value for the single key in `keys` + * When this mode is enabled, configurations that contain more than one host with the same metadata value for the single key in ``keys`` * will use only one of the hosts with the given key; no requests will be routed to the others. The cluster gauge * :ref:`lb_subsets_single_host_per_subset_duplicate` indicates how many duplicates are * present in the current configuration. @@ -658,7 +747,7 @@ export interface _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetSelecto * For any other fallback policy the parameter is not used and should not be set. * Only values also present in * :ref:`keys` are allowed, but - * `fallback_keys_subset` cannot be equal to `keys`. + * ``fallback_keys_subset`` cannot be equal to ``keys``. */ 'fallback_keys_subset': (string)[]; } @@ -710,18 +799,18 @@ export interface _envoy_config_cluster_v3_Cluster_LeastRequestLbConfig { * The following formula is used to calculate the dynamic weights when hosts have different load * balancing weights: * - * `weight = load_balancing_weight / (active_requests + 1)^active_request_bias` + * ``weight = load_balancing_weight / (active_requests + 1)^active_request_bias`` * * The larger the active request bias is, the more aggressively active requests will lower the * effective weight when all host weights are not equal. * - * `active_request_bias` must be greater than or equal to 0.0. + * ``active_request_bias`` must be greater than or equal to 0.0. * - * When `active_request_bias == 0.0` the Least Request Load Balancer doesn't consider the number + * When ``active_request_bias == 0.0`` the Least Request Load Balancer doesn't consider the number * of active requests at the time it picks a host and behaves like the Round Robin Load * Balancer. * - * When `active_request_bias > 0.0` the Least Request Load Balancer scales the load balancing + * When ``active_request_bias > 0.0`` the Least Request Load Balancer scales the load balancing * weight by the number of active requests at the time it does a pick. * * The value is cached for performance reasons and refreshed whenever one of the Load Balancer's @@ -752,18 +841,18 @@ export interface _envoy_config_cluster_v3_Cluster_LeastRequestLbConfig__Output { * The following formula is used to calculate the dynamic weights when hosts have different load * balancing weights: * - * `weight = load_balancing_weight / (active_requests + 1)^active_request_bias` + * ``weight = load_balancing_weight / (active_requests + 1)^active_request_bias`` * * The larger the active request bias is, the more aggressively active requests will lower the * effective weight when all host weights are not equal. * - * `active_request_bias` must be greater than or equal to 0.0. + * ``active_request_bias`` must be greater than or equal to 0.0. * - * When `active_request_bias == 0.0` the Least Request Load Balancer doesn't consider the number + * When ``active_request_bias == 0.0`` the Least Request Load Balancer doesn't consider the number * of active requests at the time it picks a host and behaves like the Round Robin Load * Balancer. * - * When `active_request_bias > 0.0` the Least Request Load Balancer scales the load balancing + * When ``active_request_bias > 0.0`` the Least Request Load Balancer scales the load balancing * weight by the number of active requests at the time it does a pick. * * The value is cached for performance reasons and refreshed whenever one of the Load Balancer's @@ -801,8 +890,8 @@ export interface _envoy_config_cluster_v3_Cluster_CommonLbConfig_LocalityWeighte */ export interface _envoy_config_cluster_v3_Cluster_MaglevLbConfig { /** - * The table size for Maglev hashing. The Maglev aims for ‘minimal disruption’ rather than an absolute guarantee. - * Minimal disruption means that when the set of upstreams changes, a connection will likely be sent to the same + * The table size for Maglev hashing. Maglev aims for "minimal disruption" rather than an absolute guarantee. + * Minimal disruption means that when the set of upstream hosts change, a connection will likely be sent to the same * upstream as it was before. Increasing the table size reduces the amount of disruption. * The table size must be prime number limited to 5000011. If it is not specified, the default is 65537. */ @@ -815,8 +904,8 @@ export interface _envoy_config_cluster_v3_Cluster_MaglevLbConfig { */ export interface _envoy_config_cluster_v3_Cluster_MaglevLbConfig__Output { /** - * The table size for Maglev hashing. The Maglev aims for ‘minimal disruption’ rather than an absolute guarantee. - * Minimal disruption means that when the set of upstreams changes, a connection will likely be sent to the same + * The table size for Maglev hashing. Maglev aims for "minimal disruption" rather than an absolute guarantee. + * Minimal disruption means that when the set of upstream hosts change, a connection will likely be sent to the same * upstream as it was before. Increasing the table size reduces the amount of disruption. * The table size must be prime number limited to 5000011. If it is not specified, the default is 65537. */ @@ -827,12 +916,12 @@ export interface _envoy_config_cluster_v3_Cluster_MaglevLbConfig__Output { * Specific configuration for the * :ref:`Original Destination ` * load balancing policy. + * [#extension: envoy.clusters.original_dst] */ export interface _envoy_config_cluster_v3_Cluster_OriginalDstLbConfig { /** - * When true, :ref:`x-envoy-original-dst-host - * ` can be used to override destination - * address. + * When true, a HTTP header can be used to override the original dst address. The default header is + * :ref:`x-envoy-original-dst-host `. * * .. attention:: * @@ -845,18 +934,28 @@ export interface _envoy_config_cluster_v3_Cluster_OriginalDstLbConfig { * If the header appears multiple times only the first value is used. */ 'use_http_header'?: (boolean); + /** + * The http header to override destination address if :ref:`use_http_header `. + * is set to true. If the value is empty, :ref:`x-envoy-original-dst-host ` will be used. + */ + 'http_header_name'?: (string); + /** + * The port to override for the original dst address. This port + * will take precedence over filter state and header override ports + */ + 'upstream_port_override'?: (_google_protobuf_UInt32Value | null); } /** * Specific configuration for the * :ref:`Original Destination ` * load balancing policy. + * [#extension: envoy.clusters.original_dst] */ export interface _envoy_config_cluster_v3_Cluster_OriginalDstLbConfig__Output { /** - * When true, :ref:`x-envoy-original-dst-host - * ` can be used to override destination - * address. + * When true, a HTTP header can be used to override the original dst address. The default header is + * :ref:`x-envoy-original-dst-host `. * * .. attention:: * @@ -869,6 +968,16 @@ export interface _envoy_config_cluster_v3_Cluster_OriginalDstLbConfig__Output { * If the header appears multiple times only the first value is used. */ 'use_http_header': (boolean); + /** + * The http header to override destination address if :ref:`use_http_header `. + * is set to true. If the value is empty, :ref:`x-envoy-original-dst-host ` will be used. + */ + 'http_header_name': (string); + /** + * The port to override for the original dst address. This port + * will take precedence over filter state and header override ports + */ + 'upstream_port_override': (_google_protobuf_UInt32Value__Output | null); } export interface _envoy_config_cluster_v3_Cluster_PreconnectPolicy { @@ -900,10 +1009,10 @@ export interface _envoy_config_cluster_v3_Cluster_PreconnectPolicy { */ 'per_upstream_preconnect_ratio'?: (_google_protobuf_DoubleValue | null); /** - * Indicates how many many streams (rounded up) can be anticipated across a cluster for each + * Indicates how many streams (rounded up) can be anticipated across a cluster for each * stream, useful for low QPS services. This is currently supported for a subset of * deterministic non-hash-based load-balancing algorithms (weighted round robin, random). - * Unlike *per_upstream_preconnect_ratio* this preconnects across the upstream instances in a + * Unlike ``per_upstream_preconnect_ratio`` this preconnects across the upstream instances in a * cluster, doing best effort predictions of what upstream would be picked next and * pre-establishing a connection. * @@ -955,10 +1064,10 @@ export interface _envoy_config_cluster_v3_Cluster_PreconnectPolicy__Output { */ 'per_upstream_preconnect_ratio': (_google_protobuf_DoubleValue__Output | null); /** - * Indicates how many many streams (rounded up) can be anticipated across a cluster for each + * Indicates how many streams (rounded up) can be anticipated across a cluster for each * stream, useful for low QPS services. This is currently supported for a subset of * deterministic non-hash-based load-balancing algorithms (weighted round robin, random). - * Unlike *per_upstream_preconnect_ratio* this preconnects across the upstream instances in a + * Unlike ``per_upstream_preconnect_ratio`` this preconnects across the upstream instances in a * cluster, doing best effort predictions of what upstream would be picked next and * pre-establishing a connection. * @@ -1103,13 +1212,19 @@ export interface _envoy_config_cluster_v3_Cluster_SlowStartConfig { * By tuning the parameter, is possible to achieve polynomial or exponential shape of ramp-up curve. * * During slow start window, effective weight of an endpoint would be scaled with time factor and aggression: - * `new_weight = weight * time_factor ^ (1 / aggression)`, - * where `time_factor=(time_since_start_seconds / slow_start_time_seconds)`. + * ``new_weight = weight * max(min_weight_percent, time_factor ^ (1 / aggression))``, + * where ``time_factor=(time_since_start_seconds / slow_start_time_seconds)``. * * As time progresses, more and more traffic would be sent to endpoint, which is in slow start window. * Once host exits slow start, time_factor and aggression no longer affect its weight. */ 'aggression'?: (_envoy_config_core_v3_RuntimeDouble | null); + /** + * Configures the minimum percentage of origin weight that avoids too small new weight, + * which may cause endpoints in slow start mode receive no traffic in slow start window. + * If not specified, the default is 10%. + */ + 'min_weight_percent'?: (_envoy_type_v3_Percent | null); } /** @@ -1130,13 +1245,19 @@ export interface _envoy_config_cluster_v3_Cluster_SlowStartConfig__Output { * By tuning the parameter, is possible to achieve polynomial or exponential shape of ramp-up curve. * * During slow start window, effective weight of an endpoint would be scaled with time factor and aggression: - * `new_weight = weight * time_factor ^ (1 / aggression)`, - * where `time_factor=(time_since_start_seconds / slow_start_time_seconds)`. + * ``new_weight = weight * max(min_weight_percent, time_factor ^ (1 / aggression))``, + * where ``time_factor=(time_since_start_seconds / slow_start_time_seconds)``. * * As time progresses, more and more traffic would be sent to endpoint, which is in slow start window. * Once host exits slow start, time_factor and aggression no longer affect its weight. */ 'aggression': (_envoy_config_core_v3_RuntimeDouble__Output | null); + /** + * Configures the minimum percentage of origin weight that avoids too small new weight, + * which may cause endpoints in slow start mode receive no traffic in slow start window. + * If not specified, the default is 10%. + */ + 'min_weight_percent': (_envoy_type_v3_Percent__Output | null); } /** @@ -1152,7 +1273,7 @@ export interface _envoy_config_cluster_v3_Cluster_TransportSocketMatch { * Optional endpoint metadata match criteria. * The connection to the endpoint with metadata matching what is set in this field * will use the transport socket configuration specified here. - * The endpoint's metadata entry in *envoy.transport_socket_match* is used to match + * The endpoint's metadata entry in ``envoy.transport_socket_match`` is used to match * against the values specified in this field. */ 'match'?: (_google_protobuf_Struct | null); @@ -1176,7 +1297,7 @@ export interface _envoy_config_cluster_v3_Cluster_TransportSocketMatch__Output { * Optional endpoint metadata match criteria. * The connection to the endpoint with metadata matching what is set in this field * will use the transport socket configuration specified here. - * The endpoint's metadata entry in *envoy.transport_socket_match* is used to match + * The endpoint's metadata entry in ``envoy.transport_socket_match`` is used to match * against the values specified in this field. */ 'match': (_google_protobuf_Struct__Output | null); @@ -1319,7 +1440,7 @@ export interface Cluster { * set so that Envoy will assume that the upstream supports HTTP/2 when * making new HTTP connection pool connections. Currently, Envoy only * supports prior knowledge for upstream connections. Even if TLS is used - * with ALPN, `http2_protocol_options` must be specified. As an aside this allows HTTP/2 + * with ALPN, ``http2_protocol_options`` must be specified. As an aside this allows HTTP/2 * connections to happen over plain text. * This has been deprecated in favor of http2_protocol_options fields in the * :ref:`http_protocol_options ` @@ -1359,10 +1480,7 @@ export interface Cluster { * :ref:`STRICT_DNS` * and :ref:`LOGICAL_DNS` * this setting is ignored. - * Setting this value causes failure if the - * ``envoy.restart_features.use_apple_api_for_dns_lookups`` runtime value is true during - * server startup. Apple's API only allows overriding DNS resolvers via system settings. - * This field is deprecated in favor of *dns_resolution_config* + * This field is deprecated in favor of ``dns_resolution_config`` * which aggregates all of the DNS resolver configuration in a single message. */ 'dns_resolvers'?: (_envoy_config_core_v3_Address)[]; @@ -1404,8 +1522,8 @@ export interface Cluster { 'ring_hash_lb_config'?: (_envoy_config_cluster_v3_Cluster_RingHashLbConfig | null); /** * Optional custom transport socket implementation to use for upstream connections. - * To setup TLS, set a transport socket with name `envoy.transport_sockets.tls` and - * :ref:`UpstreamTlsContexts ` in the `typed_config`. + * To setup TLS, set a transport socket with name ``envoy.transport_sockets.tls`` and + * :ref:`UpstreamTlsContexts ` in the ``typed_config``. * If no transport socket configuration is specified, new connections * will be set up with plaintext. */ @@ -1415,7 +1533,7 @@ export interface Cluster { * cluster. It can be used for stats, logging, and varying filter behavior. * Fields should use reverse DNS notation to denote which entity within Envoy * will need the information. For instance, if the metadata is intended for - * the Router filter, the filter name should be specified as *envoy.filters.http.router*. + * the Router filter, the filter name should be specified as ``envoy.filters.http.router``. */ 'metadata'?: (_envoy_config_core_v3_Metadata | null); /** @@ -1436,11 +1554,9 @@ export interface Cluster { * emitting stats for the cluster and access logging the cluster name. This will appear as * additional information in configuration dumps of a cluster's current status as * :ref:`observability_name ` - * and as an additional tag "upstream_cluster.name" while tracing. Note: access logging using - * this field is presently enabled with runtime feature - * `envoy.reloadable_features.use_observable_cluster_name`. Any ``:`` in the name will be - * converted to ``_`` when emitting statistics. This should not be confused with :ref:`Router - * Filter Header `. + * and as an additional tag "upstream_cluster.name" while tracing. Note: Any ``:`` in the name + * will be converted to ``_`` when emitting statistics. This should not be confused with + * :ref:`Router Filter Header `. */ 'alt_stat_name'?: (string); /** @@ -1487,7 +1603,7 @@ export interface Cluster { * :ref:`STATIC`, * :ref:`STRICT_DNS` * or :ref:`LOGICAL_DNS` clusters. - * This field supersedes the *hosts* field in the v2 API. + * This field supersedes the ``hosts`` field in the v2 API. * * .. attention:: * @@ -1528,9 +1644,8 @@ export interface Cluster { */ 'filters'?: (_envoy_config_cluster_v3_Filter)[]; /** - * New mechanism for LB policy configuration. Used only if the - * :ref:`lb_policy` field has the value - * :ref:`LOAD_BALANCING_POLICY_CONFIG`. + * If this field is set and is supported by the client, it will supersede the value of + * :ref:`lb_policy`. */ 'load_balancing_policy'?: (_envoy_config_cluster_v3_LoadBalancingPolicy | null); /** @@ -1552,7 +1667,7 @@ export interface Cluster { 'lrs_server'?: (_envoy_config_core_v3_ConfigSource | null); /** * Configuration to use different transport sockets for different endpoints. - * The entry of *envoy.transport_socket_match* in the + * The entry of ``envoy.transport_socket_match`` in the * :ref:`LbEndpoint.Metadata ` * is used to match against the transport sockets as they appear in the list. The first * :ref:`match ` is used. @@ -1572,16 +1687,16 @@ export interface Cluster { * transport_socket: * name: envoy.transport_sockets.raw_buffer * - * Connections to the endpoints whose metadata value under *envoy.transport_socket_match* + * Connections to the endpoints whose metadata value under ``envoy.transport_socket_match`` * having "acceptMTLS"/"true" key/value pair use the "enableMTLS" socket configuration. * * If a :ref:`socket match ` with empty match * criteria is provided, that always match any endpoint. For example, the "defaultToPlaintext" * socket match in case above. * - * If an endpoint metadata's value under *envoy.transport_socket_match* does not match any - * *TransportSocketMatch*, socket configuration fallbacks to use the *tls_context* or - * *transport_socket* specified in this cluster. + * If an endpoint metadata's value under ``envoy.transport_socket_match`` does not match any + * ``TransportSocketMatch``, socket configuration fallbacks to use the ``tls_context`` or + * ``transport_socket`` specified in this cluster. * * This field allows gradual and flexible transport socket configuration changes. * @@ -1592,8 +1707,8 @@ export interface Cluster { * * Then the xDS server can configure the CDS to a client, Envoy A, to send mutual TLS * traffic for endpoints with "acceptMTLS": "true", by adding a corresponding - * *TransportSocketMatch* in this field. Other client Envoys receive CDS without - * *transport_socket_match* set, and still send plain text traffic to the same cluster. + * ``TransportSocketMatch`` in this field. Other client Envoys receive CDS without + * ``transport_socket_match`` set, and still send plain text traffic to the same cluster. * * This field can be used to specify custom transport socket configurations for health * checks by adding matching key/value pairs in a health check's @@ -1615,10 +1730,7 @@ export interface Cluster { 'dns_failure_refresh_rate'?: (_envoy_config_cluster_v3_Cluster_RefreshRate | null); /** * Always use TCP queries instead of UDP queries for DNS lookups. - * Setting this value causes failure if the - * ``envoy.restart_features.use_apple_api_for_dns_lookups`` runtime value is true during - * server startup. Apple' API only uses UDP for DNS resolution. - * This field is deprecated in favor of *dns_resolution_config* + * This field is deprecated in favor of ``dns_resolution_config`` * which aggregates all of the DNS resolver configuration in a single message. */ 'use_tcp_for_dns_lookups'?: (boolean); @@ -1644,7 +1756,7 @@ export interface Cluster { * * .. attention:: * - * This field has been deprecated in favor of `timeout_budgets`, part of + * This field has been deprecated in favor of ``timeout_budgets``, part of * :ref:`track_cluster_stats `. */ 'track_timeout_budgets'?: (boolean); @@ -1655,7 +1767,7 @@ export interface Cluster { * TCP upstreams. * * For HTTP traffic, Envoy will generally take downstream HTTP and send it upstream as upstream - * HTTP, using the http connection pool and the codec from `http2_protocol_options` + * HTTP, using the http connection pool and the codec from ``http2_protocol_options`` * * For routes where CONNECT termination is configured, Envoy will take downstream CONNECT * requests and forward the CONNECT payload upstream over raw TCP using the tcp connection pool. @@ -1678,7 +1790,7 @@ export interface Cluster { */ 'preconnect_policy'?: (_envoy_config_cluster_v3_Cluster_PreconnectPolicy | null); /** - * If `connection_pool_per_downstream_connection` is true, the cluster will use a separate + * If ``connection_pool_per_downstream_connection`` is true, the cluster will use a separate * connection pool for every downstream connection */ 'connection_pool_per_downstream_connection'?: (boolean); @@ -1688,15 +1800,15 @@ export interface Cluster { 'maglev_lb_config'?: (_envoy_config_cluster_v3_Cluster_MaglevLbConfig | null); /** * DNS resolution configuration which includes the underlying dns resolver addresses and options. - * *dns_resolution_config* will be deprecated once - * :ref:'typed_dns_resolver_config ' - * is fully supported. + * This field is deprecated in favor of + * :ref:`typed_dns_resolver_config `. */ 'dns_resolution_config'?: (_envoy_config_core_v3_DnsResolutionConfig | null); /** * Optional configuration for having cluster readiness block on warm-up. Currently, only applicable for * :ref:`STRICT_DNS`, - * or :ref:`LOGICAL_DNS`. + * or :ref:`LOGICAL_DNS`, + * or :ref:`Redis Cluster`. * If true, cluster readiness blocks on warm-up. If false, the cluster will complete * initialization whether or not warm-up has completed. Defaults to true. */ @@ -1704,16 +1816,15 @@ export interface Cluster { /** * DNS resolver type configuration extension. This extension can be used to configure c-ares, apple, * or any other DNS resolver types and the related parameters. - * For example, an object of :ref:`DnsResolutionConfig ` - * can be packed into this *typed_dns_resolver_config*. This configuration will replace the - * :ref:'dns_resolution_config ' - * configuration eventually. - * TODO(yanjunxiang): Investigate the deprecation plan for *dns_resolution_config*. - * During the transition period when both *dns_resolution_config* and *typed_dns_resolver_config* exists, - * this configuration is optional. - * When *typed_dns_resolver_config* is in place, Envoy will use it and ignore *dns_resolution_config*. - * When *typed_dns_resolver_config* is missing, the default behavior is in place. - * [#not-implemented-hide:] + * For example, an object of + * :ref:`CaresDnsResolverConfig ` + * can be packed into this ``typed_dns_resolver_config``. This configuration replaces the + * :ref:`dns_resolution_config ` + * configuration. + * During the transition period when both ``dns_resolution_config`` and ``typed_dns_resolver_config`` exists, + * when ``typed_dns_resolver_config`` is in place, Envoy will use it and ignore ``dns_resolution_config``. + * When ``typed_dns_resolver_config`` is missing, the default behavior is in place. + * [#extension-category: envoy.network.dns_resolver] */ 'typed_dns_resolver_config'?: (_envoy_config_core_v3_TypedExtensionConfig | null); /** @@ -1808,7 +1919,7 @@ export interface Cluster__Output { * set so that Envoy will assume that the upstream supports HTTP/2 when * making new HTTP connection pool connections. Currently, Envoy only * supports prior knowledge for upstream connections. Even if TLS is used - * with ALPN, `http2_protocol_options` must be specified. As an aside this allows HTTP/2 + * with ALPN, ``http2_protocol_options`` must be specified. As an aside this allows HTTP/2 * connections to happen over plain text. * This has been deprecated in favor of http2_protocol_options fields in the * :ref:`http_protocol_options ` @@ -1848,10 +1959,7 @@ export interface Cluster__Output { * :ref:`STRICT_DNS` * and :ref:`LOGICAL_DNS` * this setting is ignored. - * Setting this value causes failure if the - * ``envoy.restart_features.use_apple_api_for_dns_lookups`` runtime value is true during - * server startup. Apple's API only allows overriding DNS resolvers via system settings. - * This field is deprecated in favor of *dns_resolution_config* + * This field is deprecated in favor of ``dns_resolution_config`` * which aggregates all of the DNS resolver configuration in a single message. */ 'dns_resolvers': (_envoy_config_core_v3_Address__Output)[]; @@ -1893,8 +2001,8 @@ export interface Cluster__Output { 'ring_hash_lb_config'?: (_envoy_config_cluster_v3_Cluster_RingHashLbConfig__Output | null); /** * Optional custom transport socket implementation to use for upstream connections. - * To setup TLS, set a transport socket with name `envoy.transport_sockets.tls` and - * :ref:`UpstreamTlsContexts ` in the `typed_config`. + * To setup TLS, set a transport socket with name ``envoy.transport_sockets.tls`` and + * :ref:`UpstreamTlsContexts ` in the ``typed_config``. * If no transport socket configuration is specified, new connections * will be set up with plaintext. */ @@ -1904,7 +2012,7 @@ export interface Cluster__Output { * cluster. It can be used for stats, logging, and varying filter behavior. * Fields should use reverse DNS notation to denote which entity within Envoy * will need the information. For instance, if the metadata is intended for - * the Router filter, the filter name should be specified as *envoy.filters.http.router*. + * the Router filter, the filter name should be specified as ``envoy.filters.http.router``. */ 'metadata': (_envoy_config_core_v3_Metadata__Output | null); /** @@ -1925,11 +2033,9 @@ export interface Cluster__Output { * emitting stats for the cluster and access logging the cluster name. This will appear as * additional information in configuration dumps of a cluster's current status as * :ref:`observability_name ` - * and as an additional tag "upstream_cluster.name" while tracing. Note: access logging using - * this field is presently enabled with runtime feature - * `envoy.reloadable_features.use_observable_cluster_name`. Any ``:`` in the name will be - * converted to ``_`` when emitting statistics. This should not be confused with :ref:`Router - * Filter Header `. + * and as an additional tag "upstream_cluster.name" while tracing. Note: Any ``:`` in the name + * will be converted to ``_`` when emitting statistics. This should not be confused with + * :ref:`Router Filter Header `. */ 'alt_stat_name': (string); /** @@ -1976,7 +2082,7 @@ export interface Cluster__Output { * :ref:`STATIC`, * :ref:`STRICT_DNS` * or :ref:`LOGICAL_DNS` clusters. - * This field supersedes the *hosts* field in the v2 API. + * This field supersedes the ``hosts`` field in the v2 API. * * .. attention:: * @@ -2017,9 +2123,8 @@ export interface Cluster__Output { */ 'filters': (_envoy_config_cluster_v3_Filter__Output)[]; /** - * New mechanism for LB policy configuration. Used only if the - * :ref:`lb_policy` field has the value - * :ref:`LOAD_BALANCING_POLICY_CONFIG`. + * If this field is set and is supported by the client, it will supersede the value of + * :ref:`lb_policy`. */ 'load_balancing_policy': (_envoy_config_cluster_v3_LoadBalancingPolicy__Output | null); /** @@ -2041,7 +2146,7 @@ export interface Cluster__Output { 'lrs_server': (_envoy_config_core_v3_ConfigSource__Output | null); /** * Configuration to use different transport sockets for different endpoints. - * The entry of *envoy.transport_socket_match* in the + * The entry of ``envoy.transport_socket_match`` in the * :ref:`LbEndpoint.Metadata ` * is used to match against the transport sockets as they appear in the list. The first * :ref:`match ` is used. @@ -2061,16 +2166,16 @@ export interface Cluster__Output { * transport_socket: * name: envoy.transport_sockets.raw_buffer * - * Connections to the endpoints whose metadata value under *envoy.transport_socket_match* + * Connections to the endpoints whose metadata value under ``envoy.transport_socket_match`` * having "acceptMTLS"/"true" key/value pair use the "enableMTLS" socket configuration. * * If a :ref:`socket match ` with empty match * criteria is provided, that always match any endpoint. For example, the "defaultToPlaintext" * socket match in case above. * - * If an endpoint metadata's value under *envoy.transport_socket_match* does not match any - * *TransportSocketMatch*, socket configuration fallbacks to use the *tls_context* or - * *transport_socket* specified in this cluster. + * If an endpoint metadata's value under ``envoy.transport_socket_match`` does not match any + * ``TransportSocketMatch``, socket configuration fallbacks to use the ``tls_context`` or + * ``transport_socket`` specified in this cluster. * * This field allows gradual and flexible transport socket configuration changes. * @@ -2081,8 +2186,8 @@ export interface Cluster__Output { * * Then the xDS server can configure the CDS to a client, Envoy A, to send mutual TLS * traffic for endpoints with "acceptMTLS": "true", by adding a corresponding - * *TransportSocketMatch* in this field. Other client Envoys receive CDS without - * *transport_socket_match* set, and still send plain text traffic to the same cluster. + * ``TransportSocketMatch`` in this field. Other client Envoys receive CDS without + * ``transport_socket_match`` set, and still send plain text traffic to the same cluster. * * This field can be used to specify custom transport socket configurations for health * checks by adding matching key/value pairs in a health check's @@ -2104,10 +2209,7 @@ export interface Cluster__Output { 'dns_failure_refresh_rate': (_envoy_config_cluster_v3_Cluster_RefreshRate__Output | null); /** * Always use TCP queries instead of UDP queries for DNS lookups. - * Setting this value causes failure if the - * ``envoy.restart_features.use_apple_api_for_dns_lookups`` runtime value is true during - * server startup. Apple' API only uses UDP for DNS resolution. - * This field is deprecated in favor of *dns_resolution_config* + * This field is deprecated in favor of ``dns_resolution_config`` * which aggregates all of the DNS resolver configuration in a single message. */ 'use_tcp_for_dns_lookups': (boolean); @@ -2133,7 +2235,7 @@ export interface Cluster__Output { * * .. attention:: * - * This field has been deprecated in favor of `timeout_budgets`, part of + * This field has been deprecated in favor of ``timeout_budgets``, part of * :ref:`track_cluster_stats `. */ 'track_timeout_budgets': (boolean); @@ -2144,7 +2246,7 @@ export interface Cluster__Output { * TCP upstreams. * * For HTTP traffic, Envoy will generally take downstream HTTP and send it upstream as upstream - * HTTP, using the http connection pool and the codec from `http2_protocol_options` + * HTTP, using the http connection pool and the codec from ``http2_protocol_options`` * * For routes where CONNECT termination is configured, Envoy will take downstream CONNECT * requests and forward the CONNECT payload upstream over raw TCP using the tcp connection pool. @@ -2167,7 +2269,7 @@ export interface Cluster__Output { */ 'preconnect_policy': (_envoy_config_cluster_v3_Cluster_PreconnectPolicy__Output | null); /** - * If `connection_pool_per_downstream_connection` is true, the cluster will use a separate + * If ``connection_pool_per_downstream_connection`` is true, the cluster will use a separate * connection pool for every downstream connection */ 'connection_pool_per_downstream_connection': (boolean); @@ -2177,15 +2279,15 @@ export interface Cluster__Output { 'maglev_lb_config'?: (_envoy_config_cluster_v3_Cluster_MaglevLbConfig__Output | null); /** * DNS resolution configuration which includes the underlying dns resolver addresses and options. - * *dns_resolution_config* will be deprecated once - * :ref:'typed_dns_resolver_config ' - * is fully supported. + * This field is deprecated in favor of + * :ref:`typed_dns_resolver_config `. */ 'dns_resolution_config': (_envoy_config_core_v3_DnsResolutionConfig__Output | null); /** * Optional configuration for having cluster readiness block on warm-up. Currently, only applicable for * :ref:`STRICT_DNS`, - * or :ref:`LOGICAL_DNS`. + * or :ref:`LOGICAL_DNS`, + * or :ref:`Redis Cluster`. * If true, cluster readiness blocks on warm-up. If false, the cluster will complete * initialization whether or not warm-up has completed. Defaults to true. */ @@ -2193,16 +2295,15 @@ export interface Cluster__Output { /** * DNS resolver type configuration extension. This extension can be used to configure c-ares, apple, * or any other DNS resolver types and the related parameters. - * For example, an object of :ref:`DnsResolutionConfig ` - * can be packed into this *typed_dns_resolver_config*. This configuration will replace the - * :ref:'dns_resolution_config ' - * configuration eventually. - * TODO(yanjunxiang): Investigate the deprecation plan for *dns_resolution_config*. - * During the transition period when both *dns_resolution_config* and *typed_dns_resolver_config* exists, - * this configuration is optional. - * When *typed_dns_resolver_config* is in place, Envoy will use it and ignore *dns_resolution_config*. - * When *typed_dns_resolver_config* is missing, the default behavior is in place. - * [#not-implemented-hide:] + * For example, an object of + * :ref:`CaresDnsResolverConfig ` + * can be packed into this ``typed_dns_resolver_config``. This configuration replaces the + * :ref:`dns_resolution_config ` + * configuration. + * During the transition period when both ``dns_resolution_config`` and ``typed_dns_resolver_config`` exists, + * when ``typed_dns_resolver_config`` is in place, Envoy will use it and ignore ``dns_resolution_config``. + * When ``typed_dns_resolver_config`` is missing, the default behavior is in place. + * [#extension-category: envoy.network.dns_resolver] */ 'typed_dns_resolver_config': (_envoy_config_core_v3_TypedExtensionConfig__Output | null); /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/ClusterCollection.ts b/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/ClusterCollection.ts index 8b1394d4b..a028c8491 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/ClusterCollection.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/ClusterCollection.ts @@ -3,7 +3,7 @@ import type { CollectionEntry as _xds_core_v3_CollectionEntry, CollectionEntry__Output as _xds_core_v3_CollectionEntry__Output } from '../../../../xds/core/v3/CollectionEntry'; /** - * Cluster list collections. Entries are *Cluster* resources or references. + * Cluster list collections. Entries are ``Cluster`` resources or references. * [#not-implemented-hide:] */ export interface ClusterCollection { @@ -11,7 +11,7 @@ export interface ClusterCollection { } /** - * Cluster list collections. Entries are *Cluster* resources or references. + * Cluster list collections. Entries are ``Cluster`` resources or references. * [#not-implemented-hide:] */ export interface ClusterCollection__Output { diff --git a/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/Filter.ts b/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/Filter.ts index 9d9031b68..cdcfeae89 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/Filter.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/Filter.ts @@ -4,28 +4,28 @@ import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__ export interface Filter { /** - * The name of the filter to instantiate. The name must match a - * supported upstream filter. Note that Envoy's :ref:`downstream network - * filters ` are not valid upstream filters. + * The name of the filter configuration. */ 'name'?: (string); /** * Filter specific configuration which depends on the filter being * instantiated. See the supported filters for further documentation. + * Note that Envoy's :ref:`downstream network + * filters ` are not valid upstream filters. */ 'typed_config'?: (_google_protobuf_Any | null); } export interface Filter__Output { /** - * The name of the filter to instantiate. The name must match a - * supported upstream filter. Note that Envoy's :ref:`downstream network - * filters ` are not valid upstream filters. + * The name of the filter configuration. */ 'name': (string); /** * Filter specific configuration which depends on the filter being * instantiated. See the supported filters for further documentation. + * Note that Envoy's :ref:`downstream network + * filters ` are not valid upstream filters. */ 'typed_config': (_google_protobuf_Any__Output | null); } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/LoadBalancingPolicy.ts b/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/LoadBalancingPolicy.ts index 78d4a6bfd..4e4efbe94 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/LoadBalancingPolicy.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/LoadBalancingPolicy.ts @@ -3,10 +3,16 @@ import type { TypedExtensionConfig as _envoy_config_core_v3_TypedExtensionConfig, TypedExtensionConfig__Output as _envoy_config_core_v3_TypedExtensionConfig__Output } from '../../../../envoy/config/core/v3/TypedExtensionConfig'; export interface _envoy_config_cluster_v3_LoadBalancingPolicy_Policy { + /** + * [#extension-category: envoy.load_balancing_policies] + */ 'typed_extension_config'?: (_envoy_config_core_v3_TypedExtensionConfig | null); } export interface _envoy_config_cluster_v3_LoadBalancingPolicy_Policy__Output { + /** + * [#extension-category: envoy.load_balancing_policies] + */ 'typed_extension_config': (_envoy_config_core_v3_TypedExtensionConfig__Output | null); } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/OutlierDetection.ts b/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/OutlierDetection.ts index 789004ad4..47dfc1877 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/OutlierDetection.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/OutlierDetection.ts @@ -6,13 +6,13 @@ import type { Duration as _google_protobuf_Duration, Duration__Output as _google /** * See the :ref:`architecture overview ` for * more information on outlier detection. - * [#next-free-field: 22] + * [#next-free-field: 23] */ export interface OutlierDetection { /** - * The number of consecutive 5xx responses or local origin errors that are mapped - * to 5xx error codes before a consecutive 5xx ejection - * occurs. Defaults to 5. + * The number of consecutive server-side error responses (for HTTP traffic, + * 5xx responses; for TCP traffic, connection failures; for Redis, failure to + * respond PONG; etc.) before a consecutive 5xx ejection occurs. Defaults to 5. */ 'consecutive_5xx'?: (_google_protobuf_UInt32Value | null); /** @@ -156,18 +156,25 @@ export interface OutlierDetection { * :ref:`base_ejection_time` value is applied, whatever is larger. */ 'max_ejection_time'?: (_google_protobuf_Duration | null); + /** + * The maximum amount of jitter to add to the ejection time, in order to prevent + * a 'thundering herd' effect where all proxies try to reconnect to host at the same time. + * See :ref:`max_ejection_time_jitter` + * Defaults to 0s. + */ + 'max_ejection_time_jitter'?: (_google_protobuf_Duration | null); } /** * See the :ref:`architecture overview ` for * more information on outlier detection. - * [#next-free-field: 22] + * [#next-free-field: 23] */ export interface OutlierDetection__Output { /** - * The number of consecutive 5xx responses or local origin errors that are mapped - * to 5xx error codes before a consecutive 5xx ejection - * occurs. Defaults to 5. + * The number of consecutive server-side error responses (for HTTP traffic, + * 5xx responses; for TCP traffic, connection failures; for Redis, failure to + * respond PONG; etc.) before a consecutive 5xx ejection occurs. Defaults to 5. */ 'consecutive_5xx': (_google_protobuf_UInt32Value__Output | null); /** @@ -311,4 +318,11 @@ export interface OutlierDetection__Output { * :ref:`base_ejection_time` value is applied, whatever is larger. */ 'max_ejection_time': (_google_protobuf_Duration__Output | null); + /** + * The maximum amount of jitter to add to the ejection time, in order to prevent + * a 'thundering herd' effect where all proxies try to reconnect to host at the same time. + * See :ref:`max_ejection_time_jitter` + * Defaults to 0s. + */ + 'max_ejection_time_jitter': (_google_protobuf_Duration__Output | null); } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/UpstreamConnectionOptions.ts b/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/UpstreamConnectionOptions.ts index 483d1072d..cda367641 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/UpstreamConnectionOptions.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/UpstreamConnectionOptions.ts @@ -7,6 +7,12 @@ export interface UpstreamConnectionOptions { * If set then set SO_KEEPALIVE on the socket to enable TCP Keepalives. */ 'tcp_keepalive'?: (_envoy_config_core_v3_TcpKeepalive | null); + /** + * If enabled, associates the interface name of the local address with the upstream connection. + * This can be used by extensions during processing of requests. The association mechanism is + * implementation specific. Defaults to false due to performance concerns. + */ + 'set_local_interface_name_on_upstream_connections'?: (boolean); } export interface UpstreamConnectionOptions__Output { @@ -14,4 +20,10 @@ export interface UpstreamConnectionOptions__Output { * If set then set SO_KEEPALIVE on the socket to enable TCP Keepalives. */ 'tcp_keepalive': (_envoy_config_core_v3_TcpKeepalive__Output | null); + /** + * If enabled, associates the interface name of the local address with the upstream connection. + * This can be used by extensions during processing of requests. The association mechanism is + * implementation specific. Defaults to false due to performance concerns. + */ + 'set_local_interface_name_on_upstream_connections': (boolean); } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Address.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Address.ts index 32c483344..5e29cdbf4 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Address.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Address.ts @@ -13,7 +13,8 @@ export interface Address { 'socket_address'?: (_envoy_config_core_v3_SocketAddress | null); 'pipe'?: (_envoy_config_core_v3_Pipe | null); /** - * [#not-implemented-hide:] + * Specifies a user-space address handled by :ref:`internal listeners + * `. */ 'envoy_internal_address'?: (_envoy_config_core_v3_EnvoyInternalAddress | null); 'address'?: "socket_address"|"pipe"|"envoy_internal_address"; @@ -28,7 +29,8 @@ export interface Address__Output { 'socket_address'?: (_envoy_config_core_v3_SocketAddress__Output | null); 'pipe'?: (_envoy_config_core_v3_Pipe__Output | null); /** - * [#not-implemented-hide:] + * Specifies a user-space address handled by :ref:`internal listeners + * `. */ 'envoy_internal_address'?: (_envoy_config_core_v3_EnvoyInternalAddress__Output | null); 'address': "socket_address"|"pipe"|"envoy_internal_address"; diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/AlternateProtocolsCacheOptions.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/AlternateProtocolsCacheOptions.ts index 6fb167174..ed3027a0c 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/AlternateProtocolsCacheOptions.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/AlternateProtocolsCacheOptions.ts @@ -3,11 +3,56 @@ import type { UInt32Value as _google_protobuf_UInt32Value, UInt32Value__Output as _google_protobuf_UInt32Value__Output } from '../../../../google/protobuf/UInt32Value'; import type { TypedExtensionConfig as _envoy_config_core_v3_TypedExtensionConfig, TypedExtensionConfig__Output as _envoy_config_core_v3_TypedExtensionConfig__Output } from '../../../../envoy/config/core/v3/TypedExtensionConfig'; +/** + * Allows pre-populating the cache with HTTP/3 alternate protocols entries with a 7 day lifetime. + * This will cause Envoy to attempt HTTP/3 to those upstreams, even if the upstreams have not + * advertised HTTP/3 support. These entries will be overwritten by alt-svc + * response headers or cached values. + * As with regular cached entries, if the origin response would result in clearing an existing + * alternate protocol cache entry, pre-populated entries will also be cleared. + * Adding a cache entry with hostname=foo.com port=123 is the equivalent of getting + * response headers + * alt-svc: h3=:"123"; ma=86400" in a response to a request to foo.com:123 + */ +export interface _envoy_config_core_v3_AlternateProtocolsCacheOptions_AlternateProtocolsCacheEntry { + /** + * The host name for the alternate protocol entry. + */ + 'hostname'?: (string); + /** + * The port for the alternate protocol entry. + */ + 'port'?: (number); +} + +/** + * Allows pre-populating the cache with HTTP/3 alternate protocols entries with a 7 day lifetime. + * This will cause Envoy to attempt HTTP/3 to those upstreams, even if the upstreams have not + * advertised HTTP/3 support. These entries will be overwritten by alt-svc + * response headers or cached values. + * As with regular cached entries, if the origin response would result in clearing an existing + * alternate protocol cache entry, pre-populated entries will also be cleared. + * Adding a cache entry with hostname=foo.com port=123 is the equivalent of getting + * response headers + * alt-svc: h3=:"123"; ma=86400" in a response to a request to foo.com:123 + */ +export interface _envoy_config_core_v3_AlternateProtocolsCacheOptions_AlternateProtocolsCacheEntry__Output { + /** + * The host name for the alternate protocol entry. + */ + 'hostname': (string); + /** + * The port for the alternate protocol entry. + */ + 'port': (number); +} + /** * Configures the alternate protocols cache which tracks alternate protocols that can be used to * make an HTTP connection to an origin server. See https://tools.ietf.org/html/rfc7838 for * HTTP Alternative Services and https://datatracker.ietf.org/doc/html/draft-ietf-dnsop-svcb-https-04 * for the "HTTPS" DNS resource record. + * [#next-free-field: 6] */ export interface AlternateProtocolsCacheOptions { /** @@ -33,8 +78,25 @@ export interface AlternateProtocolsCacheOptions { * :ref:`key value store ` to flush * alternate protocols entries to disk. * This function is currently only supported if concurrency is 1 + * Cached entries will take precedence over pre-populated entries below. */ 'key_value_store_config'?: (_envoy_config_core_v3_TypedExtensionConfig | null); + /** + * Allows pre-populating the cache with entries, as described above. + */ + 'prepopulated_entries'?: (_envoy_config_core_v3_AlternateProtocolsCacheOptions_AlternateProtocolsCacheEntry)[]; + /** + * Optional list of hostnames suffixes for which Alt-Svc entries can be shared. For example, if + * this list contained the value ``.c.example.com``, then an Alt-Svc entry for ``foo.c.example.com`` + * could be shared with ``bar.c.example.com`` but would not be shared with ``baz.example.com``. On + * the other hand, if the list contained the value ``.example.com`` then all three hosts could share + * Alt-Svc entries. Each entry must start with ``.``. If a hostname matches multiple suffixes, the + * first listed suffix will be used. + * + * Since lookup in this list is O(n), it is recommended that the number of suffixes be limited. + * [#not-implemented-hide:] + */ + 'canonical_suffixes'?: (string)[]; } /** @@ -42,6 +104,7 @@ export interface AlternateProtocolsCacheOptions { * make an HTTP connection to an origin server. See https://tools.ietf.org/html/rfc7838 for * HTTP Alternative Services and https://datatracker.ietf.org/doc/html/draft-ietf-dnsop-svcb-https-04 * for the "HTTPS" DNS resource record. + * [#next-free-field: 6] */ export interface AlternateProtocolsCacheOptions__Output { /** @@ -67,6 +130,23 @@ export interface AlternateProtocolsCacheOptions__Output { * :ref:`key value store ` to flush * alternate protocols entries to disk. * This function is currently only supported if concurrency is 1 + * Cached entries will take precedence over pre-populated entries below. */ 'key_value_store_config': (_envoy_config_core_v3_TypedExtensionConfig__Output | null); + /** + * Allows pre-populating the cache with entries, as described above. + */ + 'prepopulated_entries': (_envoy_config_core_v3_AlternateProtocolsCacheOptions_AlternateProtocolsCacheEntry__Output)[]; + /** + * Optional list of hostnames suffixes for which Alt-Svc entries can be shared. For example, if + * this list contained the value ``.c.example.com``, then an Alt-Svc entry for ``foo.c.example.com`` + * could be shared with ``bar.c.example.com`` but would not be shared with ``baz.example.com``. On + * the other hand, if the list contained the value ``.example.com`` then all three hosts could share + * Alt-Svc entries. Each entry must start with ``.``. If a hostname matches multiple suffixes, the + * first listed suffix will be used. + * + * Since lookup in this list is O(n), it is recommended that the number of suffixes be limited. + * [#not-implemented-hide:] + */ + 'canonical_suffixes': (string)[]; } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ApiConfigSource.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ApiConfigSource.ts index b303a08d8..691ab93ba 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ApiConfigSource.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ApiConfigSource.ts @@ -4,6 +4,7 @@ import type { Duration as _google_protobuf_Duration, Duration__Output as _google import type { GrpcService as _envoy_config_core_v3_GrpcService, GrpcService__Output as _envoy_config_core_v3_GrpcService__Output } from '../../../../envoy/config/core/v3/GrpcService'; import type { RateLimitSettings as _envoy_config_core_v3_RateLimitSettings, RateLimitSettings__Output as _envoy_config_core_v3_RateLimitSettings__Output } from '../../../../envoy/config/core/v3/RateLimitSettings'; import type { ApiVersion as _envoy_config_core_v3_ApiVersion } from '../../../../envoy/config/core/v3/ApiVersion'; +import type { TypedExtensionConfig as _envoy_config_core_v3_TypedExtensionConfig, TypedExtensionConfig__Output as _envoy_config_core_v3_TypedExtensionConfig__Output } from '../../../../envoy/config/core/v3/TypedExtensionConfig'; // Original file: deps/envoy-api/envoy/config/core/v3/config_source.proto @@ -49,7 +50,7 @@ export enum _envoy_config_core_v3_ApiConfigSource_ApiType { /** * API configuration source. This identifies the API type and cluster that Envoy * will use to fetch an xDS API. - * [#next-free-field: 9] + * [#next-free-field: 10] */ export interface ApiConfigSource { /** @@ -94,12 +95,23 @@ export interface ApiConfigSource { * endpoint and version of [Delta]DiscoveryRequest/Response used on the wire. */ 'transport_api_version'?: (_envoy_config_core_v3_ApiVersion | keyof typeof _envoy_config_core_v3_ApiVersion); + /** + * A list of config validators that will be executed when a new update is + * received from the ApiConfigSource. Note that each validator handles a + * specific xDS service type, and only the validators corresponding to the + * type url (in ``:ref: DiscoveryResponse`` or ``:ref: DeltaDiscoveryResponse``) + * will be invoked. + * If the validator returns false or throws an exception, the config will be rejected by + * the client, and a NACK will be sent. + * [#extension-category: envoy.config.validators] + */ + 'config_validators'?: (_envoy_config_core_v3_TypedExtensionConfig)[]; } /** * API configuration source. This identifies the API type and cluster that Envoy * will use to fetch an xDS API. - * [#next-free-field: 9] + * [#next-free-field: 10] */ export interface ApiConfigSource__Output { /** @@ -144,4 +156,15 @@ export interface ApiConfigSource__Output { * endpoint and version of [Delta]DiscoveryRequest/Response used on the wire. */ 'transport_api_version': (keyof typeof _envoy_config_core_v3_ApiVersion); + /** + * A list of config validators that will be executed when a new update is + * received from the ApiConfigSource. Note that each validator handles a + * specific xDS service type, and only the validators corresponding to the + * type url (in ``:ref: DiscoveryResponse`` or ``:ref: DeltaDiscoveryResponse``) + * will be invoked. + * If the validator returns false or throws an exception, the config will be rejected by + * the client, and a NACK will be sent. + * [#extension-category: envoy.config.validators] + */ + 'config_validators': (_envoy_config_core_v3_TypedExtensionConfig__Output)[]; } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/BindConfig.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/BindConfig.ts index d3b2cc584..733c609b1 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/BindConfig.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/BindConfig.ts @@ -3,18 +3,22 @@ import type { SocketAddress as _envoy_config_core_v3_SocketAddress, SocketAddress__Output as _envoy_config_core_v3_SocketAddress__Output } from '../../../../envoy/config/core/v3/SocketAddress'; import type { BoolValue as _google_protobuf_BoolValue, BoolValue__Output as _google_protobuf_BoolValue__Output } from '../../../../google/protobuf/BoolValue'; import type { SocketOption as _envoy_config_core_v3_SocketOption, SocketOption__Output as _envoy_config_core_v3_SocketOption__Output } from '../../../../envoy/config/core/v3/SocketOption'; +import type { ExtraSourceAddress as _envoy_config_core_v3_ExtraSourceAddress, ExtraSourceAddress__Output as _envoy_config_core_v3_ExtraSourceAddress__Output } from '../../../../envoy/config/core/v3/ExtraSourceAddress'; +/** + * [#next-free-field: 6] + */ export interface BindConfig { /** * The address to bind to when creating a socket. */ 'source_address'?: (_envoy_config_core_v3_SocketAddress | null); /** - * Whether to set the *IP_FREEBIND* option when creating the socket. When this + * Whether to set the ``IP_FREEBIND`` option when creating the socket. When this * flag is set to true, allows the :ref:`source_address - * ` to be an IP address + * ` to be an IP address * that is not configured on the system running Envoy. When this flag is set - * to false, the option *IP_FREEBIND* is disabled on the socket. When this + * to false, the option ``IP_FREEBIND`` is disabled on the socket. When this * flag is not set (default), the socket is not modified, i.e. the option is * neither enabled nor disabled. */ @@ -24,19 +28,38 @@ export interface BindConfig { * precompiled binaries. */ 'socket_options'?: (_envoy_config_core_v3_SocketOption)[]; + /** + * Deprecated by + * :ref:`extra_source_addresses ` + */ + 'additional_source_addresses'?: (_envoy_config_core_v3_SocketAddress)[]; + /** + * Extra source addresses appended to the address specified in the `source_address` + * field. This enables to specify multiple source addresses. Currently, only one extra + * address can be supported, and the extra address should have a different IP version + * with the address in the `source_address` field. The address which has the same IP + * version with the target host's address IP version will be used as bind address. If more + * than one extra address specified, only the first address matched IP version will be + * returned. If there is no same IP version address found, the address in the `source_address` + * will be returned. + */ + 'extra_source_addresses'?: (_envoy_config_core_v3_ExtraSourceAddress)[]; } +/** + * [#next-free-field: 6] + */ export interface BindConfig__Output { /** * The address to bind to when creating a socket. */ 'source_address': (_envoy_config_core_v3_SocketAddress__Output | null); /** - * Whether to set the *IP_FREEBIND* option when creating the socket. When this + * Whether to set the ``IP_FREEBIND`` option when creating the socket. When this * flag is set to true, allows the :ref:`source_address - * ` to be an IP address + * ` to be an IP address * that is not configured on the system running Envoy. When this flag is set - * to false, the option *IP_FREEBIND* is disabled on the socket. When this + * to false, the option ``IP_FREEBIND`` is disabled on the socket. When this * flag is not set (default), the socket is not modified, i.e. the option is * neither enabled nor disabled. */ @@ -46,4 +69,20 @@ export interface BindConfig__Output { * precompiled binaries. */ 'socket_options': (_envoy_config_core_v3_SocketOption__Output)[]; + /** + * Deprecated by + * :ref:`extra_source_addresses ` + */ + 'additional_source_addresses': (_envoy_config_core_v3_SocketAddress__Output)[]; + /** + * Extra source addresses appended to the address specified in the `source_address` + * field. This enables to specify multiple source addresses. Currently, only one extra + * address can be supported, and the extra address should have a different IP version + * with the address in the `source_address` field. The address which has the same IP + * version with the target host's address IP version will be used as bind address. If more + * than one extra address specified, only the first address matched IP version will be + * returned. If there is no same IP version address found, the address in the `source_address` + * will be returned. + */ + 'extra_source_addresses': (_envoy_config_core_v3_ExtraSourceAddress__Output)[]; } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ConfigSource.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ConfigSource.ts index a39c0f077..5438b6e7d 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ConfigSource.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ConfigSource.ts @@ -6,6 +6,7 @@ import type { Duration as _google_protobuf_Duration, Duration__Output as _google import type { SelfConfigSource as _envoy_config_core_v3_SelfConfigSource, SelfConfigSource__Output as _envoy_config_core_v3_SelfConfigSource__Output } from '../../../../envoy/config/core/v3/SelfConfigSource'; import type { ApiVersion as _envoy_config_core_v3_ApiVersion } from '../../../../envoy/config/core/v3/ApiVersion'; import type { Authority as _xds_core_v3_Authority, Authority__Output as _xds_core_v3_Authority__Output } from '../../../../xds/core/v3/Authority'; +import type { PathConfigSource as _envoy_config_core_v3_PathConfigSource, PathConfigSource__Output as _envoy_config_core_v3_PathConfigSource__Output } from '../../../../envoy/config/core/v3/PathConfigSource'; /** * Configuration for :ref:`listeners `, :ref:`clusters @@ -14,23 +15,11 @@ import type { Authority as _xds_core_v3_Authority, Authority__Output as _xds_cor * ` etc. may either be sourced from the * filesystem or from an xDS API source. Filesystem configs are watched with * inotify for updates. - * [#next-free-field: 8] + * [#next-free-field: 9] */ export interface ConfigSource { /** - * Path on the filesystem to source and watch for configuration updates. - * When sourcing configuration for :ref:`secret `, - * the certificate and key files are also watched for updates. - * - * .. note:: - * - * The path to the source must exist at config load time. - * - * .. note:: - * - * Envoy will only watch the file path for *moves.* This is because in general only moves - * are atomic. The same method of swapping files as is demonstrated in the - * :ref:`runtime documentation ` can be used here also. + * Deprecated in favor of ``path_config_source``. Use that field instead. */ 'path'?: (string); /** @@ -74,12 +63,16 @@ export interface ConfigSource { 'resource_api_version'?: (_envoy_config_core_v3_ApiVersion | keyof typeof _envoy_config_core_v3_ApiVersion); /** * Authorities that this config source may be used for. An authority specified in a xdstp:// URL - * is resolved to a *ConfigSource* prior to configuration fetch. This field provides the + * is resolved to a ``ConfigSource`` prior to configuration fetch. This field provides the * association between authority name and configuration source. * [#not-implemented-hide:] */ 'authorities'?: (_xds_core_v3_Authority)[]; - 'config_source_specifier'?: "path"|"api_config_source"|"ads"|"self"; + /** + * Local filesystem path configuration source. + */ + 'path_config_source'?: (_envoy_config_core_v3_PathConfigSource | null); + 'config_source_specifier'?: "path"|"path_config_source"|"api_config_source"|"ads"|"self"; } /** @@ -89,23 +82,11 @@ export interface ConfigSource { * ` etc. may either be sourced from the * filesystem or from an xDS API source. Filesystem configs are watched with * inotify for updates. - * [#next-free-field: 8] + * [#next-free-field: 9] */ export interface ConfigSource__Output { /** - * Path on the filesystem to source and watch for configuration updates. - * When sourcing configuration for :ref:`secret `, - * the certificate and key files are also watched for updates. - * - * .. note:: - * - * The path to the source must exist at config load time. - * - * .. note:: - * - * Envoy will only watch the file path for *moves.* This is because in general only moves - * are atomic. The same method of swapping files as is demonstrated in the - * :ref:`runtime documentation ` can be used here also. + * Deprecated in favor of ``path_config_source``. Use that field instead. */ 'path'?: (string); /** @@ -149,10 +130,14 @@ export interface ConfigSource__Output { 'resource_api_version': (keyof typeof _envoy_config_core_v3_ApiVersion); /** * Authorities that this config source may be used for. An authority specified in a xdstp:// URL - * is resolved to a *ConfigSource* prior to configuration fetch. This field provides the + * is resolved to a ``ConfigSource`` prior to configuration fetch. This field provides the * association between authority name and configuration source. * [#not-implemented-hide:] */ 'authorities': (_xds_core_v3_Authority__Output)[]; - 'config_source_specifier': "path"|"api_config_source"|"ads"|"self"; + /** + * Local filesystem path configuration source. + */ + 'path_config_source'?: (_envoy_config_core_v3_PathConfigSource__Output | null); + 'config_source_specifier': "path"|"path_config_source"|"api_config_source"|"ads"|"self"; } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/DataSource.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/DataSource.ts index cc29e084f..0774fb844 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/DataSource.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/DataSource.ts @@ -2,7 +2,7 @@ /** - * Data source consisting of either a file or an inline value. + * Data source consisting of a file, an inline value, or an environment variable. */ export interface DataSource { /** @@ -17,11 +17,15 @@ export interface DataSource { * String inlined in the configuration. */ 'inline_string'?: (string); - 'specifier'?: "filename"|"inline_bytes"|"inline_string"; + /** + * Environment variable data source. + */ + 'environment_variable'?: (string); + 'specifier'?: "filename"|"inline_bytes"|"inline_string"|"environment_variable"; } /** - * Data source consisting of either a file or an inline value. + * Data source consisting of a file, an inline value, or an environment variable. */ export interface DataSource__Output { /** @@ -36,5 +40,9 @@ export interface DataSource__Output { * String inlined in the configuration. */ 'inline_string'?: (string); - 'specifier': "filename"|"inline_bytes"|"inline_string"; + /** + * Environment variable data source. + */ + 'environment_variable'?: (string); + 'specifier': "filename"|"inline_bytes"|"inline_string"|"environment_variable"; } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/DnsResolutionConfig.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/DnsResolutionConfig.ts index c87be12e2..cf9f7a455 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/DnsResolutionConfig.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/DnsResolutionConfig.ts @@ -11,9 +11,6 @@ export interface DnsResolutionConfig { * A list of dns resolver addresses. If specified, the DNS client library will perform resolution * via the underlying DNS resolvers. Otherwise, the default system resolvers * (e.g., /etc/resolv.conf) will be used. - * Setting this value causes failure if the - * ``envoy.restart_features.use_apple_api_for_dns_lookups`` runtime value is true during - * server startup. Apple's API only allows overriding DNS resolvers via system settings. */ 'resolvers'?: (_envoy_config_core_v3_Address)[]; /** @@ -30,9 +27,6 @@ export interface DnsResolutionConfig__Output { * A list of dns resolver addresses. If specified, the DNS client library will perform resolution * via the underlying DNS resolvers. Otherwise, the default system resolvers * (e.g., /etc/resolv.conf) will be used. - * Setting this value causes failure if the - * ``envoy.restart_features.use_apple_api_for_dns_lookups`` runtime value is true during - * server startup. Apple's API only allows overriding DNS resolvers via system settings. */ 'resolvers': (_envoy_config_core_v3_Address__Output)[]; /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/DnsResolverOptions.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/DnsResolverOptions.ts index 11b68b150..e3f83b72c 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/DnsResolverOptions.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/DnsResolverOptions.ts @@ -7,9 +7,6 @@ export interface DnsResolverOptions { /** * Use TCP for all DNS queries instead of the default protocol UDP. - * Setting this value causes failure if the - * ``envoy.restart_features.use_apple_api_for_dns_lookups`` runtime value is true during - * server startup. Apple's API only uses UDP for DNS resolution. */ 'use_tcp_for_dns_lookups'?: (boolean); /** @@ -24,9 +21,6 @@ export interface DnsResolverOptions { export interface DnsResolverOptions__Output { /** * Use TCP for all DNS queries instead of the default protocol UDP. - * Setting this value causes failure if the - * ``envoy.restart_features.use_apple_api_for_dns_lookups`` runtime value is true during - * server startup. Apple's API only uses UDP for DNS resolution. */ 'use_tcp_for_dns_lookups': (boolean); /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/EnvoyInternalAddress.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/EnvoyInternalAddress.ts index 936e433db..264e65a0a 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/EnvoyInternalAddress.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/EnvoyInternalAddress.ts @@ -2,27 +2,39 @@ /** - * [#not-implemented-hide:] The address represents an envoy internal listener. - * TODO(lambdai): Make this address available for listener and endpoint. - * TODO(asraa): When address available, remove workaround from test/server/server_fuzz_test.cc:30. + * The address represents an envoy internal listener. + * [#comment: TODO(asraa): When address available, remove workaround from test/server/server_fuzz_test.cc:30.] */ export interface EnvoyInternalAddress { /** - * [#not-implemented-hide:] The :ref:`listener name ` of the destination internal listener. + * Specifies the :ref:`name ` of the + * internal listener. */ 'server_listener_name'?: (string); + /** + * Specifies an endpoint identifier to distinguish between multiple endpoints for the same internal listener in a + * single upstream pool. Only used in the upstream addresses for tracking changes to individual endpoints. This, for + * example, may be set to the final destination IP for the target internal listener. + */ + 'endpoint_id'?: (string); 'address_name_specifier'?: "server_listener_name"; } /** - * [#not-implemented-hide:] The address represents an envoy internal listener. - * TODO(lambdai): Make this address available for listener and endpoint. - * TODO(asraa): When address available, remove workaround from test/server/server_fuzz_test.cc:30. + * The address represents an envoy internal listener. + * [#comment: TODO(asraa): When address available, remove workaround from test/server/server_fuzz_test.cc:30.] */ export interface EnvoyInternalAddress__Output { /** - * [#not-implemented-hide:] The :ref:`listener name ` of the destination internal listener. + * Specifies the :ref:`name ` of the + * internal listener. */ 'server_listener_name'?: (string); + /** + * Specifies an endpoint identifier to distinguish between multiple endpoints for the same internal listener in a + * single upstream pool. Only used in the upstream addresses for tracking changes to individual endpoints. This, for + * example, may be set to the final destination IP for the target internal listener. + */ + 'endpoint_id': (string); 'address_name_specifier': "server_listener_name"; } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Extension.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Extension.ts index 5f730bd22..b25c15cce 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Extension.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Extension.ts @@ -4,7 +4,7 @@ import type { BuildVersion as _envoy_config_core_v3_BuildVersion, BuildVersion__ /** * Version and identification for an Envoy extension. - * [#next-free-field: 6] + * [#next-free-field: 7] */ export interface Extension { /** @@ -36,11 +36,15 @@ export interface Extension { * Indicates that the extension is present but was disabled via dynamic configuration. */ 'disabled'?: (boolean); + /** + * Type URLs of extension configuration protos. + */ + 'type_urls'?: (string)[]; } /** * Version and identification for an Envoy extension. - * [#next-free-field: 6] + * [#next-free-field: 7] */ export interface Extension__Output { /** @@ -72,4 +76,8 @@ export interface Extension__Output { * Indicates that the extension is present but was disabled via dynamic configuration. */ 'disabled': (boolean); + /** + * Type URLs of extension configuration protos. + */ + 'type_urls': (string)[]; } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ExtensionConfigSource.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ExtensionConfigSource.ts index 805762ef5..6342f50fc 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ExtensionConfigSource.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ExtensionConfigSource.ts @@ -1,4 +1,4 @@ -// Original file: deps/envoy-api/envoy/config/core/v3/extension.proto +// Original file: deps/envoy-api/envoy/config/core/v3/config_source.proto import type { ConfigSource as _envoy_config_core_v3_ConfigSource, ConfigSource__Output as _envoy_config_core_v3_ConfigSource__Output } from '../../../../envoy/config/core/v3/ConfigSource'; import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../../google/protobuf/Any'; @@ -21,7 +21,7 @@ export interface ExtensionConfigSource { /** * Optional default configuration to use as the initial configuration if * there is a failure to receive the initial extension configuration or if - * `apply_default_config_without_warming` flag is set. + * ``apply_default_config_without_warming`` flag is set. */ 'default_config'?: (_google_protobuf_Any | null); /** @@ -55,7 +55,7 @@ export interface ExtensionConfigSource__Output { /** * Optional default configuration to use as the initial configuration if * there is a failure to receive the initial extension configuration or if - * `apply_default_config_without_warming` flag is set. + * ``apply_default_config_without_warming`` flag is set. */ 'default_config': (_google_protobuf_Any__Output | null); /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ExtraSourceAddress.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ExtraSourceAddress.ts new file mode 100644 index 000000000..78051fafb --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ExtraSourceAddress.ts @@ -0,0 +1,38 @@ +// Original file: deps/envoy-api/envoy/config/core/v3/address.proto + +import type { SocketAddress as _envoy_config_core_v3_SocketAddress, SocketAddress__Output as _envoy_config_core_v3_SocketAddress__Output } from '../../../../envoy/config/core/v3/SocketAddress'; +import type { SocketOptionsOverride as _envoy_config_core_v3_SocketOptionsOverride, SocketOptionsOverride__Output as _envoy_config_core_v3_SocketOptionsOverride__Output } from '../../../../envoy/config/core/v3/SocketOptionsOverride'; + +export interface ExtraSourceAddress { + /** + * The additional address to bind. + */ + 'address'?: (_envoy_config_core_v3_SocketAddress | null); + /** + * Additional socket options that may not be present in Envoy source code or + * precompiled binaries. If specified, this will override the + * :ref:`socket_options ` + * in the BindConfig. If specified with no + * :ref:`socket_options ` + * or an empty list of :ref:`socket_options `, + * it means no socket option will apply. + */ + 'socket_options'?: (_envoy_config_core_v3_SocketOptionsOverride | null); +} + +export interface ExtraSourceAddress__Output { + /** + * The additional address to bind. + */ + 'address': (_envoy_config_core_v3_SocketAddress__Output | null); + /** + * Additional socket options that may not be present in Envoy source code or + * precompiled binaries. If specified, this will override the + * :ref:`socket_options ` + * in the BindConfig. If specified with no + * :ref:`socket_options ` + * or an empty list of :ref:`socket_options `, + * it means no socket option will apply. + */ + 'socket_options': (_envoy_config_core_v3_SocketOptionsOverride__Output | null); +} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/GrpcService.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/GrpcService.ts index 0d81f7340..eaeeff52c 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/GrpcService.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/GrpcService.ts @@ -2,6 +2,7 @@ import type { Duration as _google_protobuf_Duration, Duration__Output as _google_protobuf_Duration__Output } from '../../../../google/protobuf/Duration'; import type { HeaderValue as _envoy_config_core_v3_HeaderValue, HeaderValue__Output as _envoy_config_core_v3_HeaderValue__Output } from '../../../../envoy/config/core/v3/HeaderValue'; +import type { RetryPolicy as _envoy_config_core_v3_RetryPolicy, RetryPolicy__Output as _envoy_config_core_v3_RetryPolicy__Output } from '../../../../envoy/config/core/v3/RetryPolicy'; import type { Struct as _google_protobuf_Struct, Struct__Output as _google_protobuf_Struct__Output } from '../../../../google/protobuf/Struct'; import type { UInt32Value as _google_protobuf_UInt32Value, UInt32Value__Output as _google_protobuf_UInt32Value__Output } from '../../../../google/protobuf/UInt32Value'; import type { DataSource as _envoy_config_core_v3_DataSource, DataSource__Output as _envoy_config_core_v3_DataSource__Output } from '../../../../envoy/config/core/v3/DataSource'; @@ -153,10 +154,17 @@ export interface _envoy_config_core_v3_GrpcService_EnvoyGrpc { */ 'cluster_name'?: (string); /** - * The `:authority` header in the grpc request. If this field is not set, the authority header value will be `cluster_name`. + * The ``:authority`` header in the grpc request. If this field is not set, the authority header value will be ``cluster_name``. * Note that this authority does not override the SNI. The SNI is provided by the transport socket of the cluster. */ 'authority'?: (string); + /** + * Indicates the retry policy for re-establishing the gRPC stream + * This field is optional. If max interval is not provided, it will be set to ten times the provided base interval. + * Currently only supported for xDS gRPC streams. + * If not set, xDS gRPC streams default base interval:500ms, maximum interval:30s will be applied. + */ + 'retry_policy'?: (_envoy_config_core_v3_RetryPolicy | null); } export interface _envoy_config_core_v3_GrpcService_EnvoyGrpc__Output { @@ -167,10 +175,17 @@ export interface _envoy_config_core_v3_GrpcService_EnvoyGrpc__Output { */ 'cluster_name': (string); /** - * The `:authority` header in the grpc request. If this field is not set, the authority header value will be `cluster_name`. + * The ``:authority`` header in the grpc request. If this field is not set, the authority header value will be ``cluster_name``. * Note that this authority does not override the SNI. The SNI is provided by the transport socket of the cluster. */ 'authority': (string); + /** + * Indicates the retry policy for re-establishing the gRPC stream + * This field is optional. If max interval is not provided, it will be set to ten times the provided base interval. + * Currently only supported for xDS gRPC streams. + * If not set, xDS gRPC streams default base interval:500ms, maximum interval:30s will be applied. + */ + 'retry_policy': (_envoy_config_core_v3_RetryPolicy__Output | null); } /** @@ -372,7 +387,7 @@ export interface _envoy_config_core_v3_GrpcService_GoogleGrpc_CallCredentials_St /** * URI of the token exchange service that handles token exchange requests. * [#comment:TODO(asraa): Add URI validation when implemented. Tracked by - * https://github.com/envoyproxy/protoc-gen-validate/issues/303] + * https://github.com/bufbuild/protoc-gen-validate/issues/303] */ 'token_exchange_service_uri'?: (string); /** @@ -426,7 +441,7 @@ export interface _envoy_config_core_v3_GrpcService_GoogleGrpc_CallCredentials_St /** * URI of the token exchange service that handles token exchange requests. * [#comment:TODO(asraa): Add URI validation when implemented. Tracked by - * https://github.com/envoyproxy/protoc-gen-validate/issues/303] + * https://github.com/bufbuild/protoc-gen-validate/issues/303] */ 'token_exchange_service_uri': (string); /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HeaderValue.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HeaderValue.ts index a9fb6c07a..1cac90213 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HeaderValue.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HeaderValue.ts @@ -14,7 +14,7 @@ export interface HeaderValue { * * The same :ref:`format specifier ` as used for * :ref:`HTTP access logging ` applies here, however - * unknown header values are replaced with the empty string instead of `-`. + * unknown header values are replaced with the empty string instead of ``-``. */ 'value'?: (string); } @@ -32,7 +32,7 @@ export interface HeaderValue__Output { * * The same :ref:`format specifier ` as used for * :ref:`HTTP access logging ` applies here, however - * unknown header values are replaced with the empty string instead of `-`. + * unknown header values are replaced with the empty string instead of ``-``. */ 'value': (string); } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HeaderValueOption.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HeaderValueOption.ts index 7ba7e9d8d..efb656303 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HeaderValueOption.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HeaderValueOption.ts @@ -39,13 +39,27 @@ export interface HeaderValueOption { /** * Should the value be appended? If true (default), the value is appended to * existing values. Otherwise it replaces any existing values. + * This field is deprecated and please use + * :ref:`append_action ` as replacement. + * + * .. note:: + * The :ref:`external authorization service ` and + * :ref:`external processor service ` have + * default value (``false``) for this field. */ 'append'?: (_google_protobuf_BoolValue | null); /** - * [#not-implemented-hide:] Describes the action taken to append/overwrite the given value for an existing header - * or to only add this header if it's absent. Value defaults to :ref:`APPEND_IF_EXISTS_OR_ADD`. + * Describes the action taken to append/overwrite the given value for an existing header + * or to only add this header if it's absent. + * Value defaults to :ref:`APPEND_IF_EXISTS_OR_ADD + * `. */ 'append_action'?: (_envoy_config_core_v3_HeaderValueOption_HeaderAppendAction | keyof typeof _envoy_config_core_v3_HeaderValueOption_HeaderAppendAction); + /** + * Is the header value allowed to be empty? If false (default), custom headers with empty values are dropped, + * otherwise they are added. + */ + 'keep_empty_value'?: (boolean); } /** @@ -59,11 +73,25 @@ export interface HeaderValueOption__Output { /** * Should the value be appended? If true (default), the value is appended to * existing values. Otherwise it replaces any existing values. + * This field is deprecated and please use + * :ref:`append_action ` as replacement. + * + * .. note:: + * The :ref:`external authorization service ` and + * :ref:`external processor service ` have + * default value (``false``) for this field. */ 'append': (_google_protobuf_BoolValue__Output | null); /** - * [#not-implemented-hide:] Describes the action taken to append/overwrite the given value for an existing header - * or to only add this header if it's absent. Value defaults to :ref:`APPEND_IF_EXISTS_OR_ADD`. + * Describes the action taken to append/overwrite the given value for an existing header + * or to only add this header if it's absent. + * Value defaults to :ref:`APPEND_IF_EXISTS_OR_ADD + * `. */ 'append_action': (keyof typeof _envoy_config_core_v3_HeaderValueOption_HeaderAppendAction); + /** + * Is the header value allowed to be empty? If false (default), custom headers with empty values are dropped, + * otherwise they are added. + */ + 'keep_empty_value': (boolean); } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HealthCheck.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HealthCheck.ts index 8882de614..e0638df7d 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HealthCheck.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HealthCheck.ts @@ -5,10 +5,13 @@ import type { UInt32Value as _google_protobuf_UInt32Value, UInt32Value__Output a import type { BoolValue as _google_protobuf_BoolValue, BoolValue__Output as _google_protobuf_BoolValue__Output } from '../../../../google/protobuf/BoolValue'; import type { EventServiceConfig as _envoy_config_core_v3_EventServiceConfig, EventServiceConfig__Output as _envoy_config_core_v3_EventServiceConfig__Output } from '../../../../envoy/config/core/v3/EventServiceConfig'; import type { Struct as _google_protobuf_Struct, Struct__Output as _google_protobuf_Struct__Output } from '../../../../google/protobuf/Struct'; +import type { TypedExtensionConfig as _envoy_config_core_v3_TypedExtensionConfig, TypedExtensionConfig__Output as _envoy_config_core_v3_TypedExtensionConfig__Output } from '../../../../envoy/config/core/v3/TypedExtensionConfig'; +import type { UInt64Value as _google_protobuf_UInt64Value, UInt64Value__Output as _google_protobuf_UInt64Value__Output } from '../../../../google/protobuf/UInt64Value'; import type { HeaderValueOption as _envoy_config_core_v3_HeaderValueOption, HeaderValueOption__Output as _envoy_config_core_v3_HeaderValueOption__Output } from '../../../../envoy/config/core/v3/HeaderValueOption'; import type { Int64Range as _envoy_type_v3_Int64Range, Int64Range__Output as _envoy_type_v3_Int64Range__Output } from '../../../../envoy/type/v3/Int64Range'; import type { CodecClientType as _envoy_type_v3_CodecClientType } from '../../../../envoy/type/v3/CodecClientType'; import type { StringMatcher as _envoy_type_matcher_v3_StringMatcher, StringMatcher__Output as _envoy_type_matcher_v3_StringMatcher__Output } from '../../../../envoy/type/matcher/v3/StringMatcher'; +import type { RequestMethod as _envoy_config_core_v3_RequestMethod } from '../../../../envoy/config/core/v3/RequestMethod'; import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../../google/protobuf/Any'; import type { Long } from '@grpc/proto-loader'; @@ -68,6 +71,13 @@ export interface _envoy_config_core_v3_HealthCheck_GrpcHealthCheck { * the :ref:`hostname ` field. */ 'authority'?: (string); + /** + * Specifies a list of key-value pairs that should be added to the metadata of each GRPC call + * that is sent to the health checked cluster. For more information, including details on header value syntax, + * see the documentation on :ref:`custom request headers + * `. + */ + 'initial_metadata'?: (_envoy_config_core_v3_HeaderValueOption)[]; } /** @@ -92,10 +102,17 @@ export interface _envoy_config_core_v3_HealthCheck_GrpcHealthCheck__Output { * the :ref:`hostname ` field. */ 'authority': (string); + /** + * Specifies a list of key-value pairs that should be added to the metadata of each GRPC call + * that is sent to the health checked cluster. For more information, including details on header value syntax, + * see the documentation on :ref:`custom request headers + * `. + */ + 'initial_metadata': (_envoy_config_core_v3_HeaderValueOption__Output)[]; } /** - * [#next-free-field: 13] + * [#next-free-field: 15] */ export interface _envoy_config_core_v3_HealthCheck_HttpHealthCheck { /** @@ -107,7 +124,7 @@ export interface _envoy_config_core_v3_HealthCheck_HttpHealthCheck { 'host'?: (string); /** * Specifies the HTTP path that will be requested during health checking. For example - * * /healthcheck*. + * ``/healthcheck``. */ 'path'?: (string); /** @@ -115,9 +132,22 @@ export interface _envoy_config_core_v3_HealthCheck_HttpHealthCheck { */ 'send'?: (_envoy_config_core_v3_HealthCheck_Payload | null); /** - * [#not-implemented-hide:] HTTP specific response. + * Specifies a list of HTTP expected responses to match in the first ``response_buffer_size`` bytes of the response body. + * If it is set, both the expected response check and status code determine the health check. + * When checking the response, “fuzzy” matching is performed such that each payload block must be found, + * and in the order specified, but not necessarily contiguous. + * + * .. note:: + * + * It is recommended to set ``response_buffer_size`` based on the total Payload size for efficiency. + * The default buffer size is 1024 bytes when it is not set. + */ + 'receive'?: (_envoy_config_core_v3_HealthCheck_Payload)[]; + /** + * Specifies the size of response buffer in bytes that is used to Payload match. + * The default value is 1024. Setting to 0 implies that the Payload will be matched against the entire response. */ - 'receive'?: (_envoy_config_core_v3_HealthCheck_Payload | null); + 'response_buffer_size'?: (_google_protobuf_UInt64Value | null); /** * Specifies a list of HTTP headers that should be added to each request that is sent to the * health checked cluster. For more information, including details on header value syntax, see @@ -161,10 +191,17 @@ export interface _envoy_config_core_v3_HealthCheck_HttpHealthCheck { * ` for more information. */ 'service_name_matcher'?: (_envoy_type_matcher_v3_StringMatcher | null); + /** + * HTTP Method that will be used for health checking, default is "GET". + * GET, HEAD, POST, PUT, DELETE, OPTIONS, TRACE, PATCH methods are supported, but making request body is not supported. + * CONNECT method is disallowed because it is not appropriate for health check request. + * If a non-200 response is expected by the method, it needs to be set in :ref:`expected_statuses `. + */ + 'method'?: (_envoy_config_core_v3_RequestMethod | keyof typeof _envoy_config_core_v3_RequestMethod); } /** - * [#next-free-field: 13] + * [#next-free-field: 15] */ export interface _envoy_config_core_v3_HealthCheck_HttpHealthCheck__Output { /** @@ -176,7 +213,7 @@ export interface _envoy_config_core_v3_HealthCheck_HttpHealthCheck__Output { 'host': (string); /** * Specifies the HTTP path that will be requested during health checking. For example - * * /healthcheck*. + * ``/healthcheck``. */ 'path': (string); /** @@ -184,9 +221,22 @@ export interface _envoy_config_core_v3_HealthCheck_HttpHealthCheck__Output { */ 'send': (_envoy_config_core_v3_HealthCheck_Payload__Output | null); /** - * [#not-implemented-hide:] HTTP specific response. + * Specifies a list of HTTP expected responses to match in the first ``response_buffer_size`` bytes of the response body. + * If it is set, both the expected response check and status code determine the health check. + * When checking the response, “fuzzy” matching is performed such that each payload block must be found, + * and in the order specified, but not necessarily contiguous. + * + * .. note:: + * + * It is recommended to set ``response_buffer_size`` based on the total Payload size for efficiency. + * The default buffer size is 1024 bytes when it is not set. */ - 'receive': (_envoy_config_core_v3_HealthCheck_Payload__Output | null); + 'receive': (_envoy_config_core_v3_HealthCheck_Payload__Output)[]; + /** + * Specifies the size of response buffer in bytes that is used to Payload match. + * The default value is 1024. Setting to 0 implies that the Payload will be matched against the entire response. + */ + 'response_buffer_size': (_google_protobuf_UInt64Value__Output | null); /** * Specifies a list of HTTP headers that should be added to each request that is sent to the * health checked cluster. For more information, including details on header value syntax, see @@ -230,6 +280,13 @@ export interface _envoy_config_core_v3_HealthCheck_HttpHealthCheck__Output { * ` for more information. */ 'service_name_matcher': (_envoy_type_matcher_v3_StringMatcher__Output | null); + /** + * HTTP Method that will be used for health checking, default is "GET". + * GET, HEAD, POST, PUT, DELETE, OPTIONS, TRACE, PATCH methods are supported, but making request body is not supported. + * CONNECT method is disallowed because it is not appropriate for health check request. + * If a non-200 response is expected by the method, it needs to be set in :ref:`expected_statuses `. + */ + 'method': (keyof typeof _envoy_config_core_v3_RequestMethod); } /** @@ -241,7 +298,7 @@ export interface _envoy_config_core_v3_HealthCheck_Payload { */ 'text'?: (string); /** - * [#not-implemented-hide:] Binary payload. + * Binary payload. */ 'binary'?: (Buffer | Uint8Array | string); 'payload'?: "text"|"binary"; @@ -256,7 +313,7 @@ export interface _envoy_config_core_v3_HealthCheck_Payload__Output { */ 'text'?: (string); /** - * [#not-implemented-hide:] Binary payload. + * Binary payload. */ 'binary'?: (Buffer); 'payload': "text"|"binary"; @@ -289,7 +346,7 @@ export interface _envoy_config_core_v3_HealthCheck_TcpHealthCheck { 'send'?: (_envoy_config_core_v3_HealthCheck_Payload | null); /** * When checking the response, “fuzzy” matching is performed such that each - * binary block must be found, and in the order specified, but not + * payload block must be found, and in the order specified, but not * necessarily contiguous. */ 'receive'?: (_envoy_config_core_v3_HealthCheck_Payload)[]; @@ -302,7 +359,7 @@ export interface _envoy_config_core_v3_HealthCheck_TcpHealthCheck__Output { 'send': (_envoy_config_core_v3_HealthCheck_Payload__Output | null); /** * When checking the response, “fuzzy” matching is performed such that each - * binary block must be found, and in the order specified, but not + * payload block must be found, and in the order specified, but not * necessarily contiguous. */ 'receive': (_envoy_config_core_v3_HealthCheck_Payload__Output)[]; @@ -341,7 +398,7 @@ export interface _envoy_config_core_v3_HealthCheck_TlsOptions__Output { } /** - * [#next-free-field: 25] + * [#next-free-field: 26] */ export interface HealthCheck { /** @@ -360,7 +417,7 @@ export interface HealthCheck { 'interval_jitter'?: (_google_protobuf_Duration | null); /** * The number of unhealthy health checks required before a host is marked - * unhealthy. Note that for *http* health checking if a host responds with a code not in + * unhealthy. Note that for ``http`` health checking if a host responds with a code not in * :ref:`expected_statuses ` * or :ref:`retriable_statuses `, * this threshold is ignored and the host is considered immediately unhealthy. @@ -433,14 +490,19 @@ export interface HealthCheck { */ 'healthy_edge_interval'?: (_google_protobuf_Duration | null); /** + * .. attention:: + * This field is deprecated in favor of the extension + * :ref:`event_logger ` and + * :ref:`event_log_path ` + * in the file sink extension. + * * Specifies the path to the :ref:`health check event log `. - * If empty, no event log will be written. */ 'event_log_path'?: (string); /** * An optional jitter amount as a percentage of interval_ms. If specified, - * during every interval Envoy will add interval_ms * - * interval_jitter_percent / 100 to the wait time. + * during every interval Envoy will add ``interval_ms`` * + * ``interval_jitter_percent`` / 100 to the wait time. * * If interval_jitter_ms and interval_jitter_percent are both set, both of * them will be used to increase the wait time. @@ -490,7 +552,7 @@ export interface HealthCheck { * name: envoy.transport_sockets.tls * config: { ... } # tls socket configuration * - * If this field is set, then for health checks it will supersede an entry of *envoy.transport_socket* in the + * If this field is set, then for health checks it will supersede an entry of ``envoy.transport_socket`` in the * :ref:`LbEndpoint.Metadata `. * This allows using different transport socket capabilities for health checking versus proxying to the * endpoint. @@ -507,7 +569,7 @@ export interface HealthCheck { * (including new hosts) when the cluster has received no traffic. * * This is useful for when we want to send frequent health checks with - * `no_traffic_interval` but then revert to lower frequency `no_traffic_healthy_interval` once + * ``no_traffic_interval`` but then revert to lower frequency ``no_traffic_healthy_interval`` once * a host in the cluster is marked as healthy. * * Once a cluster has been used for traffic routing, Envoy will shift back to using the @@ -517,11 +579,16 @@ export interface HealthCheck { * no traffic interval and send that interval regardless of health state. */ 'no_traffic_healthy_interval'?: (_google_protobuf_Duration | null); + /** + * A list of event log sinks to process the health check event. + * [#extension-category: envoy.health_check.event_sinks] + */ + 'event_logger'?: (_envoy_config_core_v3_TypedExtensionConfig)[]; 'health_checker'?: "http_health_check"|"tcp_health_check"|"grpc_health_check"|"custom_health_check"; } /** - * [#next-free-field: 25] + * [#next-free-field: 26] */ export interface HealthCheck__Output { /** @@ -540,7 +607,7 @@ export interface HealthCheck__Output { 'interval_jitter': (_google_protobuf_Duration__Output | null); /** * The number of unhealthy health checks required before a host is marked - * unhealthy. Note that for *http* health checking if a host responds with a code not in + * unhealthy. Note that for ``http`` health checking if a host responds with a code not in * :ref:`expected_statuses ` * or :ref:`retriable_statuses `, * this threshold is ignored and the host is considered immediately unhealthy. @@ -613,14 +680,19 @@ export interface HealthCheck__Output { */ 'healthy_edge_interval': (_google_protobuf_Duration__Output | null); /** + * .. attention:: + * This field is deprecated in favor of the extension + * :ref:`event_logger ` and + * :ref:`event_log_path ` + * in the file sink extension. + * * Specifies the path to the :ref:`health check event log `. - * If empty, no event log will be written. */ 'event_log_path': (string); /** * An optional jitter amount as a percentage of interval_ms. If specified, - * during every interval Envoy will add interval_ms * - * interval_jitter_percent / 100 to the wait time. + * during every interval Envoy will add ``interval_ms`` * + * ``interval_jitter_percent`` / 100 to the wait time. * * If interval_jitter_ms and interval_jitter_percent are both set, both of * them will be used to increase the wait time. @@ -670,7 +742,7 @@ export interface HealthCheck__Output { * name: envoy.transport_sockets.tls * config: { ... } # tls socket configuration * - * If this field is set, then for health checks it will supersede an entry of *envoy.transport_socket* in the + * If this field is set, then for health checks it will supersede an entry of ``envoy.transport_socket`` in the * :ref:`LbEndpoint.Metadata `. * This allows using different transport socket capabilities for health checking versus proxying to the * endpoint. @@ -687,7 +759,7 @@ export interface HealthCheck__Output { * (including new hosts) when the cluster has received no traffic. * * This is useful for when we want to send frequent health checks with - * `no_traffic_interval` but then revert to lower frequency `no_traffic_healthy_interval` once + * ``no_traffic_interval`` but then revert to lower frequency ``no_traffic_healthy_interval`` once * a host in the cluster is marked as healthy. * * Once a cluster has been used for traffic routing, Envoy will shift back to using the @@ -697,5 +769,10 @@ export interface HealthCheck__Output { * no traffic interval and send that interval regardless of health state. */ 'no_traffic_healthy_interval': (_google_protobuf_Duration__Output | null); + /** + * A list of event log sinks to process the health check event. + * [#extension-category: envoy.health_check.event_sinks] + */ + 'event_logger': (_envoy_config_core_v3_TypedExtensionConfig__Output)[]; 'health_checker': "http_health_check"|"tcp_health_check"|"grpc_health_check"|"custom_health_check"; } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HealthStatus.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HealthStatus.ts index 6ecbca272..7d3d76569 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HealthStatus.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HealthStatus.ts @@ -5,7 +5,7 @@ */ export enum HealthStatus { /** - * The health status is not known. This is interpreted by Envoy as *HEALTHY*. + * The health status is not known. This is interpreted by Envoy as ``HEALTHY``. */ UNKNOWN = 0, /** @@ -21,12 +21,12 @@ export enum HealthStatus { * ``_ * or * ``_. - * This is interpreted by Envoy as *UNHEALTHY*. + * This is interpreted by Envoy as ``UNHEALTHY``. */ DRAINING = 3, /** * Health check timed out. This is part of HDS and is interpreted by Envoy as - * *UNHEALTHY*. + * ``UNHEALTHY``. */ TIMEOUT = 4, /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HealthStatusSet.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HealthStatusSet.ts new file mode 100644 index 000000000..c518192d7 --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HealthStatusSet.ts @@ -0,0 +1,17 @@ +// Original file: deps/envoy-api/envoy/config/core/v3/health_check.proto + +import type { HealthStatus as _envoy_config_core_v3_HealthStatus } from '../../../../envoy/config/core/v3/HealthStatus'; + +export interface HealthStatusSet { + /** + * An order-independent set of health status. + */ + 'statuses'?: (_envoy_config_core_v3_HealthStatus | keyof typeof _envoy_config_core_v3_HealthStatus)[]; +} + +export interface HealthStatusSet__Output { + /** + * An order-independent set of health status. + */ + 'statuses': (keyof typeof _envoy_config_core_v3_HealthStatus)[]; +} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Http1ProtocolOptions.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Http1ProtocolOptions.ts index 40b7408f6..d141a946c 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Http1ProtocolOptions.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Http1ProtocolOptions.ts @@ -52,27 +52,27 @@ export interface _envoy_config_core_v3_Http1ProtocolOptions_HeaderKeyFormat_Prop } /** - * [#next-free-field: 8] + * [#next-free-field: 11] */ export interface Http1ProtocolOptions { /** * Handle HTTP requests with absolute URLs in the requests. These requests * are generally sent by clients to forward/explicit proxies. This allows clients to configure * envoy as their HTTP proxy. In Unix, for example, this is typically done by setting the - * *http_proxy* environment variable. + * ``http_proxy`` environment variable. */ 'allow_absolute_url'?: (_google_protobuf_BoolValue | null); /** * Handle incoming HTTP/1.0 and HTTP 0.9 requests. * This is off by default, and not fully standards compliant. There is support for pre-HTTP/1.1 * style connect logic, dechunking, and handling lack of client host iff - * *default_host_for_http_10* is configured. + * ``default_host_for_http_10`` is configured. */ 'accept_http_10'?: (boolean); /** - * A default host for HTTP/1.0 requests. This is highly suggested if *accept_http_10* is true as + * A default host for HTTP/1.0 requests. This is highly suggested if ``accept_http_10`` is true as * Envoy does not otherwise support HTTP/1.0 without a Host header. - * This is a no-op if *accept_http_10* is not true. + * This is a no-op if ``accept_http_10`` is not true. */ 'default_host_for_http_10'?: (string); /** @@ -93,14 +93,17 @@ export interface Http1ProtocolOptions { */ 'enable_trailers'?: (boolean); /** - * Allows Envoy to process requests/responses with both `Content-Length` and `Transfer-Encoding` + * Allows Envoy to process requests/responses with both ``Content-Length`` and ``Transfer-Encoding`` * headers set. By default such messages are rejected, but if option is enabled - Envoy will * remove Content-Length header and process message. - * See `RFC7230, sec. 3.3.3 ` for details. + * See `RFC7230, sec. 3.3.3 `_ for details. * * .. attention:: * Enabling this option might lead to request smuggling vulnerability, especially if traffic * is proxied via multiple layers of proxies. + * [#comment:TODO: This field is ignored when the + * :ref:`header validation configuration ` + * is present.] */ 'allow_chunked_length'?: (boolean); /** @@ -112,30 +115,60 @@ export interface Http1ProtocolOptions { * `. */ 'override_stream_error_on_invalid_http_message'?: (_google_protobuf_BoolValue | null); + /** + * Allows sending fully qualified URLs when proxying the first line of the + * response. By default, Envoy will only send the path components in the first line. + * If this is true, Envoy will create a fully qualified URI composing scheme + * (inferred if not present), host (from the host/:authority header) and path + * (from first line or :path header). + */ + 'send_fully_qualified_url'?: (boolean); + /** + * [#not-implemented-hide:] Hiding so that field can be removed after BalsaParser is rolled out. + * If set, force HTTP/1 parser: BalsaParser if true, http-parser if false. + * If unset, HTTP/1 parser is selected based on + * envoy.reloadable_features.http1_use_balsa_parser. + * See issue #21245. + */ + 'use_balsa_parser'?: (_google_protobuf_BoolValue | null); + /** + * [#not-implemented-hide:] Hiding so that field can be removed. + * If true, and BalsaParser is used (either `use_balsa_parser` above is true, + * or `envoy.reloadable_features.http1_use_balsa_parser` is true and + * `use_balsa_parser` is unset), then every non-empty method with only valid + * characters is accepted. Otherwise, methods not on the hard-coded list are + * rejected. + * Once UHV is enabled, this field should be removed, and BalsaParser should + * allow any method. UHV validates the method, rejecting empty string or + * invalid characters, and provides :ref:`restrict_http_methods + * ` + * to reject custom methods. + */ + 'allow_custom_methods'?: (boolean); } /** - * [#next-free-field: 8] + * [#next-free-field: 11] */ export interface Http1ProtocolOptions__Output { /** * Handle HTTP requests with absolute URLs in the requests. These requests * are generally sent by clients to forward/explicit proxies. This allows clients to configure * envoy as their HTTP proxy. In Unix, for example, this is typically done by setting the - * *http_proxy* environment variable. + * ``http_proxy`` environment variable. */ 'allow_absolute_url': (_google_protobuf_BoolValue__Output | null); /** * Handle incoming HTTP/1.0 and HTTP 0.9 requests. * This is off by default, and not fully standards compliant. There is support for pre-HTTP/1.1 * style connect logic, dechunking, and handling lack of client host iff - * *default_host_for_http_10* is configured. + * ``default_host_for_http_10`` is configured. */ 'accept_http_10': (boolean); /** - * A default host for HTTP/1.0 requests. This is highly suggested if *accept_http_10* is true as + * A default host for HTTP/1.0 requests. This is highly suggested if ``accept_http_10`` is true as * Envoy does not otherwise support HTTP/1.0 without a Host header. - * This is a no-op if *accept_http_10* is not true. + * This is a no-op if ``accept_http_10`` is not true. */ 'default_host_for_http_10': (string); /** @@ -156,14 +189,17 @@ export interface Http1ProtocolOptions__Output { */ 'enable_trailers': (boolean); /** - * Allows Envoy to process requests/responses with both `Content-Length` and `Transfer-Encoding` + * Allows Envoy to process requests/responses with both ``Content-Length`` and ``Transfer-Encoding`` * headers set. By default such messages are rejected, but if option is enabled - Envoy will * remove Content-Length header and process message. - * See `RFC7230, sec. 3.3.3 ` for details. + * See `RFC7230, sec. 3.3.3 `_ for details. * * .. attention:: * Enabling this option might lead to request smuggling vulnerability, especially if traffic * is proxied via multiple layers of proxies. + * [#comment:TODO: This field is ignored when the + * :ref:`header validation configuration ` + * is present.] */ 'allow_chunked_length': (boolean); /** @@ -175,4 +211,34 @@ export interface Http1ProtocolOptions__Output { * `. */ 'override_stream_error_on_invalid_http_message': (_google_protobuf_BoolValue__Output | null); + /** + * Allows sending fully qualified URLs when proxying the first line of the + * response. By default, Envoy will only send the path components in the first line. + * If this is true, Envoy will create a fully qualified URI composing scheme + * (inferred if not present), host (from the host/:authority header) and path + * (from first line or :path header). + */ + 'send_fully_qualified_url': (boolean); + /** + * [#not-implemented-hide:] Hiding so that field can be removed after BalsaParser is rolled out. + * If set, force HTTP/1 parser: BalsaParser if true, http-parser if false. + * If unset, HTTP/1 parser is selected based on + * envoy.reloadable_features.http1_use_balsa_parser. + * See issue #21245. + */ + 'use_balsa_parser': (_google_protobuf_BoolValue__Output | null); + /** + * [#not-implemented-hide:] Hiding so that field can be removed. + * If true, and BalsaParser is used (either `use_balsa_parser` above is true, + * or `envoy.reloadable_features.http1_use_balsa_parser` is true and + * `use_balsa_parser` is unset), then every non-empty method with only valid + * characters is accepted. Otherwise, methods not on the hard-coded list are + * rejected. + * Once UHV is enabled, this field should be removed, and BalsaParser should + * allow any method. UHV validates the method, rejecting empty string or + * invalid characters, and provides :ref:`restrict_http_methods + * ` + * to reject custom methods. + */ + 'allow_custom_methods': (boolean); } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Http2ProtocolOptions.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Http2ProtocolOptions.ts index 786e2a00d..cc22ce44c 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Http2ProtocolOptions.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Http2ProtocolOptions.ts @@ -35,7 +35,7 @@ export interface _envoy_config_core_v3_Http2ProtocolOptions_SettingsParameter__O } /** - * [#next-free-field: 16] + * [#next-free-field: 17] */ export interface Http2ProtocolOptions { /** @@ -74,8 +74,8 @@ export interface Http2ProtocolOptions { */ 'initial_stream_window_size'?: (_google_protobuf_UInt32Value | null); /** - * Similar to *initial_stream_window_size*, but for connection-level flow-control - * window. Currently, this has the same minimum/maximum/default as *initial_stream_window_size*. + * Similar to ``initial_stream_window_size``, but for connection-level flow-control + * window. Currently, this has the same minimum/maximum/default as ``initial_stream_window_size``. */ 'initial_connection_window_size'?: (_google_protobuf_UInt32Value | null); /** @@ -96,8 +96,6 @@ export interface Http2ProtocolOptions { * be written into the socket). Exceeding this limit triggers flood mitigation and connection is * terminated. The ``http2.outbound_flood`` stat tracks the number of terminated connections due * to flood mitigation. The default limit is 10000. - * NOTE: flood and abuse mitigation for upstream connections is presently enabled by the - * `envoy.reloadable_features.upstream_http2_flood_checks` flag. */ 'max_outbound_frames'?: (_google_protobuf_UInt32Value | null); /** @@ -106,8 +104,6 @@ export interface Http2ProtocolOptions { * this limit triggers flood mitigation and connection is terminated. The * ``http2.outbound_control_flood`` stat tracks the number of terminated connections due to flood * mitigation. The default limit is 1000. - * NOTE: flood and abuse mitigation for upstream connections is presently enabled by the - * `envoy.reloadable_features.upstream_http2_flood_checks` flag. */ 'max_outbound_control_frames'?: (_google_protobuf_UInt32Value | null); /** @@ -117,8 +113,6 @@ export interface Http2ProtocolOptions { * stat tracks the number of connections terminated due to flood mitigation. * Setting this to 0 will terminate connection upon receiving first frame with an empty payload * and no end stream flag. The default limit is 1. - * NOTE: flood and abuse mitigation for upstream connections is presently enabled by the - * `envoy.reloadable_features.upstream_http2_flood_checks` flag. */ 'max_consecutive_inbound_frames_with_empty_payload'?: (_google_protobuf_UInt32Value | null); /** @@ -126,15 +120,13 @@ export interface Http2ProtocolOptions { * of PRIORITY frames received over the lifetime of connection exceeds the value calculated * using this formula:: * - * max_inbound_priority_frames_per_stream * (1 + opened_streams) + * ``max_inbound_priority_frames_per_stream`` * (1 + ``opened_streams``) * - * the connection is terminated. For downstream connections the `opened_streams` is incremented when + * the connection is terminated. For downstream connections the ``opened_streams`` is incremented when * Envoy receives complete response headers from the upstream server. For upstream connection the - * `opened_streams` is incremented when Envoy send the HEADERS frame for a new stream. The + * ``opened_streams`` is incremented when Envoy send the HEADERS frame for a new stream. The * ``http2.inbound_priority_frames_flood`` stat tracks * the number of connections terminated due to flood mitigation. The default limit is 100. - * NOTE: flood and abuse mitigation for upstream connections is presently enabled by the - * `envoy.reloadable_features.upstream_http2_flood_checks` flag. */ 'max_inbound_priority_frames_per_stream'?: (_google_protobuf_UInt32Value | null); /** @@ -142,18 +134,16 @@ export interface Http2ProtocolOptions { * of WINDOW_UPDATE frames received over the lifetime of connection exceeds the value calculated * using this formula:: * - * 5 + 2 * (opened_streams + - * max_inbound_window_update_frames_per_data_frame_sent * outbound_data_frames) + * 5 + 2 * (``opened_streams`` + + * ``max_inbound_window_update_frames_per_data_frame_sent`` * ``outbound_data_frames``) * - * the connection is terminated. For downstream connections the `opened_streams` is incremented when + * the connection is terminated. For downstream connections the ``opened_streams`` is incremented when * Envoy receives complete response headers from the upstream server. For upstream connections the - * `opened_streams` is incremented when Envoy sends the HEADERS frame for a new stream. The + * ``opened_streams`` is incremented when Envoy sends the HEADERS frame for a new stream. The * ``http2.inbound_priority_frames_flood`` stat tracks the number of connections terminated due to * flood mitigation. The default max_inbound_window_update_frames_per_data_frame_sent value is 10. * Setting this to 1 should be enough to support HTTP/2 implementations with basic flow control, * but more complex implementations that try to estimate available bandwidth require at least 2. - * NOTE: flood and abuse mitigation for upstream connections is presently enabled by the - * `envoy.reloadable_features.upstream_http2_flood_checks` flag. */ 'max_inbound_window_update_frames_per_data_frame_sent'?: (_google_protobuf_UInt32Value | null); /** @@ -216,10 +206,16 @@ export interface Http2ProtocolOptions { * does not respond within the configured timeout, the connection will be aborted. */ 'connection_keepalive'?: (_envoy_config_core_v3_KeepaliveSettings | null); + /** + * [#not-implemented-hide:] Hiding so that the field can be removed after oghttp2 is rolled out. + * If set, force use of a particular HTTP/2 codec: oghttp2 if true, nghttp2 if false. + * If unset, HTTP/2 codec is selected based on envoy.reloadable_features.http2_use_oghttp2. + */ + 'use_oghttp2_codec'?: (_google_protobuf_BoolValue | null); } /** - * [#next-free-field: 16] + * [#next-free-field: 17] */ export interface Http2ProtocolOptions__Output { /** @@ -258,8 +254,8 @@ export interface Http2ProtocolOptions__Output { */ 'initial_stream_window_size': (_google_protobuf_UInt32Value__Output | null); /** - * Similar to *initial_stream_window_size*, but for connection-level flow-control - * window. Currently, this has the same minimum/maximum/default as *initial_stream_window_size*. + * Similar to ``initial_stream_window_size``, but for connection-level flow-control + * window. Currently, this has the same minimum/maximum/default as ``initial_stream_window_size``. */ 'initial_connection_window_size': (_google_protobuf_UInt32Value__Output | null); /** @@ -280,8 +276,6 @@ export interface Http2ProtocolOptions__Output { * be written into the socket). Exceeding this limit triggers flood mitigation and connection is * terminated. The ``http2.outbound_flood`` stat tracks the number of terminated connections due * to flood mitigation. The default limit is 10000. - * NOTE: flood and abuse mitigation for upstream connections is presently enabled by the - * `envoy.reloadable_features.upstream_http2_flood_checks` flag. */ 'max_outbound_frames': (_google_protobuf_UInt32Value__Output | null); /** @@ -290,8 +284,6 @@ export interface Http2ProtocolOptions__Output { * this limit triggers flood mitigation and connection is terminated. The * ``http2.outbound_control_flood`` stat tracks the number of terminated connections due to flood * mitigation. The default limit is 1000. - * NOTE: flood and abuse mitigation for upstream connections is presently enabled by the - * `envoy.reloadable_features.upstream_http2_flood_checks` flag. */ 'max_outbound_control_frames': (_google_protobuf_UInt32Value__Output | null); /** @@ -301,8 +293,6 @@ export interface Http2ProtocolOptions__Output { * stat tracks the number of connections terminated due to flood mitigation. * Setting this to 0 will terminate connection upon receiving first frame with an empty payload * and no end stream flag. The default limit is 1. - * NOTE: flood and abuse mitigation for upstream connections is presently enabled by the - * `envoy.reloadable_features.upstream_http2_flood_checks` flag. */ 'max_consecutive_inbound_frames_with_empty_payload': (_google_protobuf_UInt32Value__Output | null); /** @@ -310,15 +300,13 @@ export interface Http2ProtocolOptions__Output { * of PRIORITY frames received over the lifetime of connection exceeds the value calculated * using this formula:: * - * max_inbound_priority_frames_per_stream * (1 + opened_streams) + * ``max_inbound_priority_frames_per_stream`` * (1 + ``opened_streams``) * - * the connection is terminated. For downstream connections the `opened_streams` is incremented when + * the connection is terminated. For downstream connections the ``opened_streams`` is incremented when * Envoy receives complete response headers from the upstream server. For upstream connection the - * `opened_streams` is incremented when Envoy send the HEADERS frame for a new stream. The + * ``opened_streams`` is incremented when Envoy send the HEADERS frame for a new stream. The * ``http2.inbound_priority_frames_flood`` stat tracks * the number of connections terminated due to flood mitigation. The default limit is 100. - * NOTE: flood and abuse mitigation for upstream connections is presently enabled by the - * `envoy.reloadable_features.upstream_http2_flood_checks` flag. */ 'max_inbound_priority_frames_per_stream': (_google_protobuf_UInt32Value__Output | null); /** @@ -326,18 +314,16 @@ export interface Http2ProtocolOptions__Output { * of WINDOW_UPDATE frames received over the lifetime of connection exceeds the value calculated * using this formula:: * - * 5 + 2 * (opened_streams + - * max_inbound_window_update_frames_per_data_frame_sent * outbound_data_frames) + * 5 + 2 * (``opened_streams`` + + * ``max_inbound_window_update_frames_per_data_frame_sent`` * ``outbound_data_frames``) * - * the connection is terminated. For downstream connections the `opened_streams` is incremented when + * the connection is terminated. For downstream connections the ``opened_streams`` is incremented when * Envoy receives complete response headers from the upstream server. For upstream connections the - * `opened_streams` is incremented when Envoy sends the HEADERS frame for a new stream. The + * ``opened_streams`` is incremented when Envoy sends the HEADERS frame for a new stream. The * ``http2.inbound_priority_frames_flood`` stat tracks the number of connections terminated due to * flood mitigation. The default max_inbound_window_update_frames_per_data_frame_sent value is 10. * Setting this to 1 should be enough to support HTTP/2 implementations with basic flow control, * but more complex implementations that try to estimate available bandwidth require at least 2. - * NOTE: flood and abuse mitigation for upstream connections is presently enabled by the - * `envoy.reloadable_features.upstream_http2_flood_checks` flag. */ 'max_inbound_window_update_frames_per_data_frame_sent': (_google_protobuf_UInt32Value__Output | null); /** @@ -400,4 +386,10 @@ export interface Http2ProtocolOptions__Output { * does not respond within the configured timeout, the connection will be aborted. */ 'connection_keepalive': (_envoy_config_core_v3_KeepaliveSettings__Output | null); + /** + * [#not-implemented-hide:] Hiding so that the field can be removed after oghttp2 is rolled out. + * If set, force use of a particular HTTP/2 codec: oghttp2 if true, nghttp2 if false. + * If unset, HTTP/2 codec is selected based on envoy.reloadable_features.http2_use_oghttp2. + */ + 'use_oghttp2_codec': (_google_protobuf_BoolValue__Output | null); } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HttpProtocolOptions.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HttpProtocolOptions.ts index 34a4053dd..a3064110f 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HttpProtocolOptions.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HttpProtocolOptions.ts @@ -24,7 +24,7 @@ export enum _envoy_config_core_v3_HttpProtocolOptions_HeadersWithUnderscoresActi */ REJECT_REQUEST = 1, /** - * Drop the header with name containing underscores. The header is dropped before the filter chain is + * Drop the client header with name containing underscores. The header is dropped before the filter chain is * invoked and as such filters will not see dropped headers. The * "httpN.dropped_headers_with_underscores" is incremented for each dropped header. */ @@ -63,11 +63,10 @@ export interface HttpProtocolOptions { /** * The maximum duration of a connection. The duration is defined as a period since a connection * was established. If not set, there is no max duration. When max_connection_duration is reached - * and if there are no active streams, the connection will be closed. If there are any active streams, - * the drain sequence will kick-in, and the connection will be force-closed after the drain period. - * See :ref:`drain_timeout + * and if there are no active streams, the connection will be closed. If the connection is a + * downstream connection and there are any active streams, the drain sequence will kick-in, + * and the connection will be force-closed after the drain period. See :ref:`drain_timeout * `. - * Note: This feature is not yet implemented for the upstream connections. */ 'max_connection_duration'?: (_google_protobuf_Duration | null); /** @@ -79,6 +78,8 @@ export interface HttpProtocolOptions { * Action to take when a client request with a header name containing underscore characters is received. * If this setting is not specified, the value defaults to ALLOW. * Note: upstream responses are not affected by this setting. + * Note: this only affects client headers. It does not affect headers added + * by Envoy filters and does not have any impact if added to cluster config. */ 'headers_with_underscores_action'?: (_envoy_config_core_v3_HttpProtocolOptions_HeadersWithUnderscoresAction | keyof typeof _envoy_config_core_v3_HttpProtocolOptions_HeadersWithUnderscoresAction); /** @@ -122,11 +123,10 @@ export interface HttpProtocolOptions__Output { /** * The maximum duration of a connection. The duration is defined as a period since a connection * was established. If not set, there is no max duration. When max_connection_duration is reached - * and if there are no active streams, the connection will be closed. If there are any active streams, - * the drain sequence will kick-in, and the connection will be force-closed after the drain period. - * See :ref:`drain_timeout + * and if there are no active streams, the connection will be closed. If the connection is a + * downstream connection and there are any active streams, the drain sequence will kick-in, + * and the connection will be force-closed after the drain period. See :ref:`drain_timeout * `. - * Note: This feature is not yet implemented for the upstream connections. */ 'max_connection_duration': (_google_protobuf_Duration__Output | null); /** @@ -138,6 +138,8 @@ export interface HttpProtocolOptions__Output { * Action to take when a client request with a header name containing underscore characters is received. * If this setting is not specified, the value defaults to ALLOW. * Note: upstream responses are not affected by this setting. + * Note: this only affects client headers. It does not affect headers added + * by Envoy filters and does not have any impact if added to cluster config. */ 'headers_with_underscores_action': (keyof typeof _envoy_config_core_v3_HttpProtocolOptions_HeadersWithUnderscoresAction); /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HttpUri.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HttpUri.ts index 0bac9ac51..9a06ba477 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HttpUri.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HttpUri.ts @@ -32,7 +32,7 @@ export interface HttpUri { */ 'timeout'?: (_google_protobuf_Duration | null); /** - * Specify how `uri` is to be fetched. Today, this requires an explicit + * Specify how ``uri`` is to be fetched. Today, this requires an explicit * cluster, but in the future we may support dynamic cluster creation or * inline DNS resolution. See `issue * `_. @@ -70,7 +70,7 @@ export interface HttpUri__Output { */ 'timeout': (_google_protobuf_Duration__Output | null); /** - * Specify how `uri` is to be fetched. Today, this requires an explicit + * Specify how ``uri`` is to be fetched. Today, this requires an explicit * cluster, but in the future we may support dynamic cluster creation or * inline DNS resolution. See `issue * `_. diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/KeepaliveSettings.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/KeepaliveSettings.ts index 2c274c6e1..2d2ef0787 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/KeepaliveSettings.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/KeepaliveSettings.ts @@ -11,7 +11,9 @@ export interface KeepaliveSettings { 'interval'?: (_google_protobuf_Duration | null); /** * How long to wait for a response to a keepalive PING. If a response is not received within this - * time period, the connection will be aborted. + * time period, the connection will be aborted. Note that in order to prevent the influence of + * Head-of-line (HOL) blocking the timeout period is extended when *any* frame is received on + * the connection, under the assumption that if a frame is received the connection is healthy. */ 'timeout'?: (_google_protobuf_Duration | null); /** @@ -26,6 +28,8 @@ export interface KeepaliveSettings { * If this is zero, this type of PING will not be sent. * If an interval ping is outstanding, a second ping will not be sent as the * interval ping will determine if the connection is dead. + * + * The same feature for HTTP/3 is given by inheritance from QUICHE which uses :ref:`connection idle_timeout ` and the current PTO of the connection to decide whether to probe before sending a new request. */ 'connection_idle_interval'?: (_google_protobuf_Duration | null); } @@ -38,7 +42,9 @@ export interface KeepaliveSettings__Output { 'interval': (_google_protobuf_Duration__Output | null); /** * How long to wait for a response to a keepalive PING. If a response is not received within this - * time period, the connection will be aborted. + * time period, the connection will be aborted. Note that in order to prevent the influence of + * Head-of-line (HOL) blocking the timeout period is extended when *any* frame is received on + * the connection, under the assumption that if a frame is received the connection is healthy. */ 'timeout': (_google_protobuf_Duration__Output | null); /** @@ -53,6 +59,8 @@ export interface KeepaliveSettings__Output { * If this is zero, this type of PING will not be sent. * If an interval ping is outstanding, a second ping will not be sent as the * interval ping will determine if the connection is dead. + * + * The same feature for HTTP/3 is given by inheritance from QUICHE which uses :ref:`connection idle_timeout ` and the current PTO of the connection to decide whether to probe before sending a new request. */ 'connection_idle_interval': (_google_protobuf_Duration__Output | null); } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Metadata.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Metadata.ts index fb603c2ba..2bcd3ce36 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Metadata.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Metadata.ts @@ -29,21 +29,21 @@ import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__ */ export interface Metadata { /** - * Key is the reverse DNS filter name, e.g. com.acme.widget. The envoy.* + * Key is the reverse DNS filter name, e.g. com.acme.widget. The ``envoy.*`` * namespace is reserved for Envoy's built-in filters. - * If both *filter_metadata* and + * If both ``filter_metadata`` and * :ref:`typed_filter_metadata ` * fields are present in the metadata with same keys, - * only *typed_filter_metadata* field will be parsed. + * only ``typed_filter_metadata`` field will be parsed. */ 'filter_metadata'?: ({[key: string]: _google_protobuf_Struct}); /** - * Key is the reverse DNS filter name, e.g. com.acme.widget. The envoy.* + * Key is the reverse DNS filter name, e.g. com.acme.widget. The ``envoy.*`` * namespace is reserved for Envoy's built-in filters. * The value is encoded as google.protobuf.Any. * If both :ref:`filter_metadata ` - * and *typed_filter_metadata* fields are present in the metadata with same keys, - * only *typed_filter_metadata* field will be parsed. + * and ``typed_filter_metadata`` fields are present in the metadata with same keys, + * only ``typed_filter_metadata`` field will be parsed. */ 'typed_filter_metadata'?: ({[key: string]: _google_protobuf_Any}); } @@ -74,21 +74,21 @@ export interface Metadata { */ export interface Metadata__Output { /** - * Key is the reverse DNS filter name, e.g. com.acme.widget. The envoy.* + * Key is the reverse DNS filter name, e.g. com.acme.widget. The ``envoy.*`` * namespace is reserved for Envoy's built-in filters. - * If both *filter_metadata* and + * If both ``filter_metadata`` and * :ref:`typed_filter_metadata ` * fields are present in the metadata with same keys, - * only *typed_filter_metadata* field will be parsed. + * only ``typed_filter_metadata`` field will be parsed. */ 'filter_metadata': ({[key: string]: _google_protobuf_Struct__Output}); /** - * Key is the reverse DNS filter name, e.g. com.acme.widget. The envoy.* + * Key is the reverse DNS filter name, e.g. com.acme.widget. The ``envoy.*`` * namespace is reserved for Envoy's built-in filters. * The value is encoded as google.protobuf.Any. * If both :ref:`filter_metadata ` - * and *typed_filter_metadata* fields are present in the metadata with same keys, - * only *typed_filter_metadata* field will be parsed. + * and ``typed_filter_metadata`` fields are present in the metadata with same keys, + * only ``typed_filter_metadata`` field will be parsed. */ 'typed_filter_metadata': ({[key: string]: _google_protobuf_Any__Output}); } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Node.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Node.ts index addd47a68..6aef94d8e 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Node.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Node.ts @@ -68,7 +68,7 @@ export interface Node { /** * Client feature support list. These are well known features described * in the Envoy API repository for a given major version of an API. Client features - * use reverse DNS naming scheme, for example `com.acme.feature`. + * use reverse DNS naming scheme, for example ``com.acme.feature``. * See :ref:`the list of features ` that xDS client may * support. */ @@ -77,7 +77,7 @@ export interface Node { * Known listening ports on the node as a generic hint to the management server * for filtering :ref:`listeners ` to be returned. For example, * if there is a listener bound to port 80, the list can optionally contain the - * SocketAddress `(0.0.0.0,80)`. The field is optional and just a hint. + * SocketAddress ``(0.0.0.0,80)``. The field is optional and just a hint. */ 'listening_addresses'?: (_envoy_config_core_v3_Address)[]; /** @@ -152,7 +152,7 @@ export interface Node__Output { /** * Client feature support list. These are well known features described * in the Envoy API repository for a given major version of an API. Client features - * use reverse DNS naming scheme, for example `com.acme.feature`. + * use reverse DNS naming scheme, for example ``com.acme.feature``. * See :ref:`the list of features ` that xDS client may * support. */ @@ -161,7 +161,7 @@ export interface Node__Output { * Known listening ports on the node as a generic hint to the management server * for filtering :ref:`listeners ` to be returned. For example, * if there is a listener bound to port 80, the list can optionally contain the - * SocketAddress `(0.0.0.0,80)`. The field is optional and just a hint. + * SocketAddress ``(0.0.0.0,80)``. The field is optional and just a hint. */ 'listening_addresses': (_envoy_config_core_v3_Address__Output)[]; /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/PathConfigSource.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/PathConfigSource.ts new file mode 100644 index 000000000..e620f8c52 --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/PathConfigSource.ts @@ -0,0 +1,85 @@ +// Original file: deps/envoy-api/envoy/config/core/v3/config_source.proto + +import type { WatchedDirectory as _envoy_config_core_v3_WatchedDirectory, WatchedDirectory__Output as _envoy_config_core_v3_WatchedDirectory__Output } from '../../../../envoy/config/core/v3/WatchedDirectory'; + +/** + * Local filesystem path configuration source. + */ +export interface PathConfigSource { + /** + * Path on the filesystem to source and watch for configuration updates. + * When sourcing configuration for a :ref:`secret `, + * the certificate and key files are also watched for updates. + * + * .. note:: + * + * The path to the source must exist at config load time. + * + * .. note:: + * + * If ``watched_directory`` is *not* configured, Envoy will watch the file path for *moves*. + * This is because in general only moves are atomic. The same method of swapping files as is + * demonstrated in the :ref:`runtime documentation ` can be + * used here also. If ``watched_directory`` is configured, no watch will be placed directly on + * this path. Instead, the configured ``watched_directory`` will be used to trigger reloads of + * this path. This is required in certain deployment scenarios. See below for more information. + */ + 'path'?: (string); + /** + * If configured, this directory will be watched for *moves*. When an entry in this directory is + * moved to, the ``path`` will be reloaded. This is required in certain deployment scenarios. + * + * Specifically, if trying to load an xDS resource using a + * `Kubernetes ConfigMap `_, the + * following configuration might be used: + * 1. Store xds.yaml inside a ConfigMap. + * 2. Mount the ConfigMap to ``/config_map/xds`` + * 3. Configure path ``/config_map/xds/xds.yaml`` + * 4. Configure watched directory ``/config_map/xds`` + * + * The above configuration will ensure that Envoy watches the owning directory for moves which is + * required due to how Kubernetes manages ConfigMap symbolic links during atomic updates. + */ + 'watched_directory'?: (_envoy_config_core_v3_WatchedDirectory | null); +} + +/** + * Local filesystem path configuration source. + */ +export interface PathConfigSource__Output { + /** + * Path on the filesystem to source and watch for configuration updates. + * When sourcing configuration for a :ref:`secret `, + * the certificate and key files are also watched for updates. + * + * .. note:: + * + * The path to the source must exist at config load time. + * + * .. note:: + * + * If ``watched_directory`` is *not* configured, Envoy will watch the file path for *moves*. + * This is because in general only moves are atomic. The same method of swapping files as is + * demonstrated in the :ref:`runtime documentation ` can be + * used here also. If ``watched_directory`` is configured, no watch will be placed directly on + * this path. Instead, the configured ``watched_directory`` will be used to trigger reloads of + * this path. This is required in certain deployment scenarios. See below for more information. + */ + 'path': (string); + /** + * If configured, this directory will be watched for *moves*. When an entry in this directory is + * moved to, the ``path`` will be reloaded. This is required in certain deployment scenarios. + * + * Specifically, if trying to load an xDS resource using a + * `Kubernetes ConfigMap `_, the + * following configuration might be used: + * 1. Store xds.yaml inside a ConfigMap. + * 2. Mount the ConfigMap to ``/config_map/xds`` + * 3. Configure path ``/config_map/xds/xds.yaml`` + * 4. Configure watched directory ``/config_map/xds`` + * + * The above configuration will ensure that Envoy watches the owning directory for moves which is + * required due to how Kubernetes manages ConfigMap symbolic links during atomic updates. + */ + 'watched_directory': (_envoy_config_core_v3_WatchedDirectory__Output | null); +} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ProxyProtocolConfig.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ProxyProtocolConfig.ts index a28de2dbe..7da9d569e 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ProxyProtocolConfig.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ProxyProtocolConfig.ts @@ -1,5 +1,6 @@ // Original file: deps/envoy-api/envoy/config/core/v3/proxy_protocol.proto +import type { ProxyProtocolPassThroughTLVs as _envoy_config_core_v3_ProxyProtocolPassThroughTLVs, ProxyProtocolPassThroughTLVs__Output as _envoy_config_core_v3_ProxyProtocolPassThroughTLVs__Output } from '../../../../envoy/config/core/v3/ProxyProtocolPassThroughTLVs'; // Original file: deps/envoy-api/envoy/config/core/v3/proxy_protocol.proto @@ -19,6 +20,11 @@ export interface ProxyProtocolConfig { * The PROXY protocol version to use. See https://www.haproxy.org/download/2.1/doc/proxy-protocol.txt for details */ 'version'?: (_envoy_config_core_v3_ProxyProtocolConfig_Version | keyof typeof _envoy_config_core_v3_ProxyProtocolConfig_Version); + /** + * This config controls which TLVs can be passed to upstream if it is Proxy Protocol + * V2 header. If there is no setting for this field, no TLVs will be passed through. + */ + 'pass_through_tlvs'?: (_envoy_config_core_v3_ProxyProtocolPassThroughTLVs | null); } export interface ProxyProtocolConfig__Output { @@ -26,4 +32,9 @@ export interface ProxyProtocolConfig__Output { * The PROXY protocol version to use. See https://www.haproxy.org/download/2.1/doc/proxy-protocol.txt for details */ 'version': (keyof typeof _envoy_config_core_v3_ProxyProtocolConfig_Version); + /** + * This config controls which TLVs can be passed to upstream if it is Proxy Protocol + * V2 header. If there is no setting for this field, no TLVs will be passed through. + */ + 'pass_through_tlvs': (_envoy_config_core_v3_ProxyProtocolPassThroughTLVs__Output | null); } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ProxyProtocolPassThroughTLVs.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ProxyProtocolPassThroughTLVs.ts new file mode 100644 index 000000000..0dddbf79f --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ProxyProtocolPassThroughTLVs.ts @@ -0,0 +1,43 @@ +// Original file: deps/envoy-api/envoy/config/core/v3/proxy_protocol.proto + + +// Original file: deps/envoy-api/envoy/config/core/v3/proxy_protocol.proto + +export enum _envoy_config_core_v3_ProxyProtocolPassThroughTLVs_PassTLVsMatchType { + /** + * Pass all TLVs. + */ + INCLUDE_ALL = 0, + /** + * Pass specific TLVs defined in tlv_type. + */ + INCLUDE = 1, +} + +export interface ProxyProtocolPassThroughTLVs { + /** + * The strategy to pass through TLVs. Default is INCLUDE_ALL. + * If INCLUDE_ALL is set, all TLVs will be passed through no matter the tlv_type field. + */ + 'match_type'?: (_envoy_config_core_v3_ProxyProtocolPassThroughTLVs_PassTLVsMatchType | keyof typeof _envoy_config_core_v3_ProxyProtocolPassThroughTLVs_PassTLVsMatchType); + /** + * The TLV types that are applied based on match_type. + * TLV type is defined as uint8_t in proxy protocol. See `the spec + * `_ for details. + */ + 'tlv_type'?: (number)[]; +} + +export interface ProxyProtocolPassThroughTLVs__Output { + /** + * The strategy to pass through TLVs. Default is INCLUDE_ALL. + * If INCLUDE_ALL is set, all TLVs will be passed through no matter the tlv_type field. + */ + 'match_type': (keyof typeof _envoy_config_core_v3_ProxyProtocolPassThroughTLVs_PassTLVsMatchType); + /** + * The TLV types that are applied based on match_type. + * TLV type is defined as uint8_t in proxy protocol. See `the spec + * `_ for details. + */ + 'tlv_type': (number)[]; +} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/QuicKeepAliveSettings.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/QuicKeepAliveSettings.ts new file mode 100644 index 000000000..2bb2aa872 --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/QuicKeepAliveSettings.ts @@ -0,0 +1,53 @@ +// Original file: deps/envoy-api/envoy/config/core/v3/protocol.proto + +import type { Duration as _google_protobuf_Duration, Duration__Output as _google_protobuf_Duration__Output } from '../../../../google/protobuf/Duration'; + +/** + * Config for keepalive probes in a QUIC connection. + * Note that QUIC keep-alive probing packets work differently from HTTP/2 keep-alive PINGs in a sense that the probing packet + * itself doesn't timeout waiting for a probing response. Quic has a shorter idle timeout than TCP, so it doesn't rely on such probing to discover dead connections. If the peer fails to respond, the connection will idle timeout eventually. Thus, they are configured differently from :ref:`connection_keepalive `. + */ +export interface QuicKeepAliveSettings { + /** + * The max interval for a connection to send keep-alive probing packets (with PING or PATH_RESPONSE). The value should be smaller than :ref:`connection idle_timeout ` to prevent idle timeout while not less than 1s to avoid throttling the connection or flooding the peer with probes. + * + * If :ref:`initial_interval ` is absent or zero, a client connection will use this value to start probing. + * + * If zero, disable keepalive probing. + * If absent, use the QUICHE default interval to probe. + */ + 'max_interval'?: (_google_protobuf_Duration | null); + /** + * The interval to send the first few keep-alive probing packets to prevent connection from hitting the idle timeout. Subsequent probes will be sent, each one with an interval exponentially longer than previous one, till it reaches :ref:`max_interval `. And the probes afterwards will always use :ref:`max_interval `. + * + * The value should be smaller than :ref:`connection idle_timeout ` to prevent idle timeout and smaller than max_interval to take effect. + * + * If absent or zero, disable keepalive probing for a server connection. For a client connection, if :ref:`max_interval ` is also zero, do not keepalive, otherwise use max_interval or QUICHE default to probe all the time. + */ + 'initial_interval'?: (_google_protobuf_Duration | null); +} + +/** + * Config for keepalive probes in a QUIC connection. + * Note that QUIC keep-alive probing packets work differently from HTTP/2 keep-alive PINGs in a sense that the probing packet + * itself doesn't timeout waiting for a probing response. Quic has a shorter idle timeout than TCP, so it doesn't rely on such probing to discover dead connections. If the peer fails to respond, the connection will idle timeout eventually. Thus, they are configured differently from :ref:`connection_keepalive `. + */ +export interface QuicKeepAliveSettings__Output { + /** + * The max interval for a connection to send keep-alive probing packets (with PING or PATH_RESPONSE). The value should be smaller than :ref:`connection idle_timeout ` to prevent idle timeout while not less than 1s to avoid throttling the connection or flooding the peer with probes. + * + * If :ref:`initial_interval ` is absent or zero, a client connection will use this value to start probing. + * + * If zero, disable keepalive probing. + * If absent, use the QUICHE default interval to probe. + */ + 'max_interval': (_google_protobuf_Duration__Output | null); + /** + * The interval to send the first few keep-alive probing packets to prevent connection from hitting the idle timeout. Subsequent probes will be sent, each one with an interval exponentially longer than previous one, till it reaches :ref:`max_interval `. And the probes afterwards will always use :ref:`max_interval `. + * + * The value should be smaller than :ref:`connection idle_timeout ` to prevent idle timeout and smaller than max_interval to take effect. + * + * If absent or zero, disable keepalive probing for a server connection. For a client connection, if :ref:`max_interval ` is also zero, do not keepalive, otherwise use max_interval or QUICHE default to probe all the time. + */ + 'initial_interval': (_google_protobuf_Duration__Output | null); +} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/QuicProtocolOptions.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/QuicProtocolOptions.ts index 6a653b54d..b33feab51 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/QuicProtocolOptions.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/QuicProtocolOptions.ts @@ -1,9 +1,11 @@ // Original file: deps/envoy-api/envoy/config/core/v3/protocol.proto import type { UInt32Value as _google_protobuf_UInt32Value, UInt32Value__Output as _google_protobuf_UInt32Value__Output } from '../../../../google/protobuf/UInt32Value'; +import type { QuicKeepAliveSettings as _envoy_config_core_v3_QuicKeepAliveSettings, QuicKeepAliveSettings__Output as _envoy_config_core_v3_QuicKeepAliveSettings__Output } from '../../../../envoy/config/core/v3/QuicKeepAliveSettings'; /** * QUIC protocol options which apply to both downstream and upstream connections. + * [#next-free-field: 6] */ export interface QuicProtocolOptions { /** @@ -25,18 +27,31 @@ export interface QuicProtocolOptions { */ 'initial_stream_window_size'?: (_google_protobuf_UInt32Value | null); /** - * Similar to *initial_stream_window_size*, but for connection-level + * Similar to ``initial_stream_window_size``, but for connection-level * flow-control. Valid values rage from 1 to 25165824 (24MB, maximum supported by QUICHE) and defaults to 65536 (2^16). - * window. Currently, this has the same minimum/default as *initial_stream_window_size*. + * window. Currently, this has the same minimum/default as ``initial_stream_window_size``. * * NOTE: 16384 (2^14) is the minimum window size supported in Google QUIC. We only support increasing the default * window size now, so it's also the minimum. */ 'initial_connection_window_size'?: (_google_protobuf_UInt32Value | null); + /** + * The number of timeouts that can occur before port migration is triggered for QUIC clients. + * This defaults to 1. If set to 0, port migration will not occur on path degrading. + * Timeout here refers to QUIC internal path degrading timeout mechanism, such as PTO. + * This has no effect on server sessions. + */ + 'num_timeouts_to_trigger_port_migration'?: (_google_protobuf_UInt32Value | null); + /** + * Probes the peer at the configured interval to solicit traffic, i.e. ACK or PATH_RESPONSE, from the peer to push back connection idle timeout. + * If absent, use the default keepalive behavior of which a client connection sends PINGs every 15s, and a server connection doesn't do anything. + */ + 'connection_keepalive'?: (_envoy_config_core_v3_QuicKeepAliveSettings | null); } /** * QUIC protocol options which apply to both downstream and upstream connections. + * [#next-free-field: 6] */ export interface QuicProtocolOptions__Output { /** @@ -58,12 +73,24 @@ export interface QuicProtocolOptions__Output { */ 'initial_stream_window_size': (_google_protobuf_UInt32Value__Output | null); /** - * Similar to *initial_stream_window_size*, but for connection-level + * Similar to ``initial_stream_window_size``, but for connection-level * flow-control. Valid values rage from 1 to 25165824 (24MB, maximum supported by QUICHE) and defaults to 65536 (2^16). - * window. Currently, this has the same minimum/default as *initial_stream_window_size*. + * window. Currently, this has the same minimum/default as ``initial_stream_window_size``. * * NOTE: 16384 (2^14) is the minimum window size supported in Google QUIC. We only support increasing the default * window size now, so it's also the minimum. */ 'initial_connection_window_size': (_google_protobuf_UInt32Value__Output | null); + /** + * The number of timeouts that can occur before port migration is triggered for QUIC clients. + * This defaults to 1. If set to 0, port migration will not occur on path degrading. + * Timeout here refers to QUIC internal path degrading timeout mechanism, such as PTO. + * This has no effect on server sessions. + */ + 'num_timeouts_to_trigger_port_migration': (_google_protobuf_UInt32Value__Output | null); + /** + * Probes the peer at the configured interval to solicit traffic, i.e. ACK or PATH_RESPONSE, from the peer to push back connection idle timeout. + * If absent, use the default keepalive behavior of which a client connection sends PINGs every 15s, and a server connection doesn't do anything. + */ + 'connection_keepalive': (_envoy_config_core_v3_QuicKeepAliveSettings__Output | null); } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/RuntimeFractionalPercent.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/RuntimeFractionalPercent.ts index 3a2073294..d976b4375 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/RuntimeFractionalPercent.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/RuntimeFractionalPercent.ts @@ -12,7 +12,7 @@ import type { FractionalPercent as _envoy_type_v3_FractionalPercent, FractionalP * :ref:`FractionalPercent ` proto represented as JSON/YAML * and may also be represented as an integer with the assumption that the value is an integral * percentage out of 100. For instance, a runtime key lookup returning the value "42" would parse - * as a `FractionalPercent` whose numerator is 42 and denominator is HUNDRED. + * as a ``FractionalPercent`` whose numerator is 42 and denominator is HUNDRED. */ export interface RuntimeFractionalPercent { /** @@ -35,7 +35,7 @@ export interface RuntimeFractionalPercent { * :ref:`FractionalPercent ` proto represented as JSON/YAML * and may also be represented as an integer with the assumption that the value is an integral * percentage out of 100. For instance, a runtime key lookup returning the value "42" would parse - * as a `FractionalPercent` whose numerator is 42 and denominator is HUNDRED. + * as a ``FractionalPercent`` whose numerator is 42 and denominator is HUNDRED. */ export interface RuntimeFractionalPercent__Output { /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SocketAddress.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SocketAddress.ts index 3966dc04c..ae87b9edd 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SocketAddress.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SocketAddress.ts @@ -22,8 +22,8 @@ export interface SocketAddress { * within an upstream :ref:`BindConfig `, the address * controls the source address of outbound connections. For :ref:`clusters * `, the cluster type determines whether the - * address must be an IP (*STATIC* or *EDS* clusters) or a hostname resolved by DNS - * (*STRICT_DNS* or *LOGICAL_DNS* clusters). Address resolution can be customized + * address must be an IP (``STATIC`` or ``EDS`` clusters) or a hostname resolved by DNS + * (``STRICT_DNS`` or ``LOGICAL_DNS`` clusters). Address resolution can be customized * via :ref:`resolver_name `. */ 'address'?: (string); @@ -39,7 +39,7 @@ export interface SocketAddress { * this is empty, a context dependent default applies. If the address is a concrete * IP address, no resolution will occur. If address is a hostname this * should be set for resolution other than DNS. Specifying a custom resolver with - * *STRICT_DNS* or *LOGICAL_DNS* will generate an error at runtime. + * ``STRICT_DNS`` or ``LOGICAL_DNS`` will generate an error at runtime. */ 'resolver_name'?: (string); /** @@ -66,8 +66,8 @@ export interface SocketAddress__Output { * within an upstream :ref:`BindConfig `, the address * controls the source address of outbound connections. For :ref:`clusters * `, the cluster type determines whether the - * address must be an IP (*STATIC* or *EDS* clusters) or a hostname resolved by DNS - * (*STRICT_DNS* or *LOGICAL_DNS* clusters). Address resolution can be customized + * address must be an IP (``STATIC`` or ``EDS`` clusters) or a hostname resolved by DNS + * (``STRICT_DNS`` or ``LOGICAL_DNS`` clusters). Address resolution can be customized * via :ref:`resolver_name `. */ 'address': (string); @@ -83,7 +83,7 @@ export interface SocketAddress__Output { * this is empty, a context dependent default applies. If the address is a concrete * IP address, no resolution will occur. If address is a hostname this * should be set for resolution other than DNS. Specifying a custom resolver with - * *STRICT_DNS* or *LOGICAL_DNS* will generate an error at runtime. + * ``STRICT_DNS`` or ``LOGICAL_DNS`` will generate an error at runtime. */ 'resolver_name': (string); /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SocketOption.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SocketOption.ts index 56f13c339..edff50a63 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SocketOption.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SocketOption.ts @@ -22,6 +22,26 @@ export enum _envoy_config_core_v3_SocketOption_SocketState { /** * Generic socket option message. This would be used to set socket options that * might not exist in upstream kernels or precompiled Envoy binaries. + * + * For example: + * + * .. code-block:: json + * + * { + * "description": "support tcp keep alive", + * "state": 0, + * "level": 1, + * "name": 9, + * "int_value": 1, + * } + * + * 1 means SOL_SOCKET and 9 means SO_KEEPALIVE on Linux. + * With the above configuration, `TCP Keep-Alives `_ + * can be enabled in socket with Linux, which can be used in + * :ref:`listener's` or + * :ref:`admin's ` socket_options etc. + * + * It should be noted that the name or level may have different values on different platforms. * [#next-free-field: 7] */ export interface SocketOption { @@ -57,6 +77,26 @@ export interface SocketOption { /** * Generic socket option message. This would be used to set socket options that * might not exist in upstream kernels or precompiled Envoy binaries. + * + * For example: + * + * .. code-block:: json + * + * { + * "description": "support tcp keep alive", + * "state": 0, + * "level": 1, + * "name": 9, + * "int_value": 1, + * } + * + * 1 means SOL_SOCKET and 9 means SO_KEEPALIVE on Linux. + * With the above configuration, `TCP Keep-Alives `_ + * can be enabled in socket with Linux, which can be used in + * :ref:`listener's` or + * :ref:`admin's ` socket_options etc. + * + * It should be noted that the name or level may have different values on different platforms. * [#next-free-field: 7] */ export interface SocketOption__Output { diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SocketOptionsOverride.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SocketOptionsOverride.ts new file mode 100644 index 000000000..5df984579 --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SocketOptionsOverride.ts @@ -0,0 +1,11 @@ +// Original file: deps/envoy-api/envoy/config/core/v3/socket_option.proto + +import type { SocketOption as _envoy_config_core_v3_SocketOption, SocketOption__Output as _envoy_config_core_v3_SocketOption__Output } from '../../../../envoy/config/core/v3/SocketOption'; + +export interface SocketOptionsOverride { + 'socket_options'?: (_envoy_config_core_v3_SocketOption)[]; +} + +export interface SocketOptionsOverride__Output { + 'socket_options': (_envoy_config_core_v3_SocketOption__Output)[]; +} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SubstitutionFormatString.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SubstitutionFormatString.ts index a935fc1ec..be0237e38 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SubstitutionFormatString.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SubstitutionFormatString.ts @@ -63,9 +63,9 @@ export interface SubstitutionFormatString { */ 'omit_empty_values'?: (boolean); /** - * Specify a *content_type* field. - * If this field is not set then ``text/plain`` is used for *text_format* and - * ``application/json`` is used for *json_format*. + * Specify a ``content_type`` field. + * If this field is not set then ``text/plain`` is used for ``text_format`` and + * ``application/json`` is used for ``json_format``. * * .. validated-code-block:: yaml * :type-name: envoy.config.core.v3.SubstitutionFormatString @@ -160,9 +160,9 @@ export interface SubstitutionFormatString__Output { */ 'omit_empty_values': (boolean); /** - * Specify a *content_type* field. - * If this field is not set then ``text/plain`` is used for *text_format* and - * ``application/json`` is used for *json_format*. + * Specify a ``content_type`` field. + * If this field is not set then ``text/plain`` is used for ``text_format`` and + * ``application/json`` is used for ``json_format``. * * .. validated-code-block:: yaml * :type-name: envoy.config.core.v3.SubstitutionFormatString diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/TypedExtensionConfig.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/TypedExtensionConfig.ts index d653f9373..d46751d4f 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/TypedExtensionConfig.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/TypedExtensionConfig.ts @@ -14,8 +14,9 @@ export interface TypedExtensionConfig { 'name'?: (string); /** * The typed config for the extension. The type URL will be used to identify - * the extension. In the case that the type URL is *udpa.type.v1.TypedStruct*, - * the inner type URL of *TypedStruct* will be utilized. See the + * the extension. In the case that the type URL is ``xds.type.v3.TypedStruct`` + * (or, for historical reasons, ``udpa.type.v1.TypedStruct``), the inner type + * URL of ``TypedStruct`` will be utilized. See the * :ref:`extension configuration overview * ` for further details. */ @@ -34,8 +35,9 @@ export interface TypedExtensionConfig__Output { 'name': (string); /** * The typed config for the extension. The type URL will be used to identify - * the extension. In the case that the type URL is *udpa.type.v1.TypedStruct*, - * the inner type URL of *TypedStruct* will be utilized. See the + * the extension. In the case that the type URL is ``xds.type.v3.TypedStruct`` + * (or, for historical reasons, ``udpa.type.v1.TypedStruct``), the inner type + * URL of ``TypedStruct`` will be utilized. See the * :ref:`extension configuration overview * ` for further details. */ diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/UpstreamHttpProtocolOptions.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/UpstreamHttpProtocolOptions.ts index c0da4159f..91e9f0b53 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/UpstreamHttpProtocolOptions.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/UpstreamHttpProtocolOptions.ts @@ -7,13 +7,15 @@ export interface UpstreamHttpProtocolOptions { * upstream connections based on the downstream HTTP host/authority header or any other arbitrary * header when :ref:`override_auto_sni_header ` * is set, as seen by the :ref:`router filter `. + * Does nothing if a filter before the http router filter sets the corresponding metadata. */ 'auto_sni'?: (boolean); /** * Automatic validate upstream presented certificate for new upstream connections based on the * downstream HTTP host/authority header or any other arbitrary header when :ref:`override_auto_sni_header ` * is set, as seen by the :ref:`router filter `. - * This field is intended to be set with `auto_sni` field. + * This field is intended to be set with ``auto_sni`` field. + * Does nothing if a filter before the http router filter sets the corresponding metadata. */ 'auto_san_validation'?: (boolean); /** @@ -22,8 +24,9 @@ export interface UpstreamHttpProtocolOptions { * :ref:`router filter `. * If unset, host/authority header will be used for populating the SNI. If the specified header * is not found or the value is empty, host/authority header will be used instead. - * This field is intended to be set with `auto_sni` and/or `auto_san_validation` fields. + * This field is intended to be set with ``auto_sni`` and/or ``auto_san_validation`` fields. * If none of these fields are set then setting this would be a no-op. + * Does nothing if a filter before the http router filter sets the corresponding metadata. */ 'override_auto_sni_header'?: (string); } @@ -34,13 +37,15 @@ export interface UpstreamHttpProtocolOptions__Output { * upstream connections based on the downstream HTTP host/authority header or any other arbitrary * header when :ref:`override_auto_sni_header ` * is set, as seen by the :ref:`router filter `. + * Does nothing if a filter before the http router filter sets the corresponding metadata. */ 'auto_sni': (boolean); /** * Automatic validate upstream presented certificate for new upstream connections based on the * downstream HTTP host/authority header or any other arbitrary header when :ref:`override_auto_sni_header ` * is set, as seen by the :ref:`router filter `. - * This field is intended to be set with `auto_sni` field. + * This field is intended to be set with ``auto_sni`` field. + * Does nothing if a filter before the http router filter sets the corresponding metadata. */ 'auto_san_validation': (boolean); /** @@ -49,8 +54,9 @@ export interface UpstreamHttpProtocolOptions__Output { * :ref:`router filter `. * If unset, host/authority header will be used for populating the SNI. If the specified header * is not found or the value is empty, host/authority header will be used instead. - * This field is intended to be set with `auto_sni` and/or `auto_san_validation` fields. + * This field is intended to be set with ``auto_sni`` and/or ``auto_san_validation`` fields. * If none of these fields are set then setting this would be a no-op. + * Does nothing if a filter before the http router filter sets the corresponding metadata. */ 'override_auto_sni_header': (string); } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/endpoint/v3/ClusterStats.ts b/packages/grpc-js-xds/src/generated/envoy/config/endpoint/v3/ClusterStats.ts index c160333b2..d65383885 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/endpoint/v3/ClusterStats.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/endpoint/v3/ClusterStats.ts @@ -52,9 +52,9 @@ export interface ClusterStats { 'total_dropped_requests'?: (number | string | Long); /** * Period over which the actual load report occurred. This will be guaranteed to include every - * request reported. Due to system load and delays between the *LoadStatsRequest* sent from Envoy - * and the *LoadStatsResponse* message sent from the management server, this may be longer than - * the requested load reporting interval in the *LoadStatsResponse*. + * request reported. Due to system load and delays between the ``LoadStatsRequest`` sent from Envoy + * and the ``LoadStatsResponse`` message sent from the management server, this may be longer than + * the requested load reporting interval in the ``LoadStatsResponse``. */ 'load_report_interval'?: (_google_protobuf_Duration | null); /** @@ -96,9 +96,9 @@ export interface ClusterStats__Output { 'total_dropped_requests': (string); /** * Period over which the actual load report occurred. This will be guaranteed to include every - * request reported. Due to system load and delays between the *LoadStatsRequest* sent from Envoy - * and the *LoadStatsResponse* message sent from the management server, this may be longer than - * the requested load reporting interval in the *LoadStatsResponse*. + * request reported. Due to system load and delays between the ``LoadStatsRequest`` sent from Envoy + * and the ``LoadStatsResponse`` message sent from the management server, this may be longer than + * the requested load reporting interval in the ``LoadStatsResponse``. */ 'load_report_interval': (_google_protobuf_Duration__Output | null); /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/endpoint/v3/Endpoint.ts b/packages/grpc-js-xds/src/generated/envoy/config/endpoint/v3/Endpoint.ts index 31eb09055..ef56d7068 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/endpoint/v3/Endpoint.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/endpoint/v3/Endpoint.ts @@ -23,6 +23,19 @@ export interface _envoy_config_endpoint_v3_Endpoint_HealthCheckConfig { * endpoint. */ 'hostname'?: (string); + /** + * Optional alternative health check host address. + * + * .. attention:: + * + * The form of the health check host address is expected to be a direct IP address. + */ + 'address'?: (_envoy_config_core_v3_Address | null); + /** + * Optional flag to control if perform active health check for this endpoint. + * Active health check is enabled by default if there is a health checker. + */ + 'disable_active_health_check'?: (boolean); } /** @@ -46,6 +59,19 @@ export interface _envoy_config_endpoint_v3_Endpoint_HealthCheckConfig__Output { * endpoint. */ 'hostname': (string); + /** + * Optional alternative health check host address. + * + * .. attention:: + * + * The form of the health check host address is expected to be a direct IP address. + */ + 'address': (_envoy_config_core_v3_Address__Output | null); + /** + * Optional flag to control if perform active health check for this endpoint. + * Active health check is enabled by default if there is a health checker. + */ + 'disable_active_health_check': (boolean); } /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/endpoint/v3/LbEndpoint.ts b/packages/grpc-js-xds/src/generated/envoy/config/endpoint/v3/LbEndpoint.ts index 6025ae3a7..8d184b8a0 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/endpoint/v3/LbEndpoint.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/endpoint/v3/LbEndpoint.ts @@ -18,8 +18,8 @@ export interface LbEndpoint { /** * The endpoint metadata specifies values that may be used by the load * balancer to select endpoints in a cluster for a given request. The filter - * name should be specified as *envoy.lb*. An example boolean key-value pair - * is *canary*, providing the optional canary status of the upstream host. + * name should be specified as ``envoy.lb``. An example boolean key-value pair + * is ``canary``, providing the optional canary status of the upstream host. * This may be matched against in a route's * :ref:`RouteAction ` metadata_match field * to subset the endpoints considered in cluster load balancing. @@ -32,9 +32,9 @@ export interface LbEndpoint { * of the weights of all endpoints in the endpoint's locality to produce a * percentage of traffic for the endpoint. This percentage is then further * weighted by the endpoint's locality's load balancing weight from - * LocalityLbEndpoints. If unspecified, each host is presumed to have equal - * weight in a locality. The sum of the weights of all endpoints in the - * endpoint's locality must not exceed uint32_t maximal value (4294967295). + * LocalityLbEndpoints. If unspecified, will be treated as 1. The sum + * of the weights of all endpoints in the endpoint's locality must not + * exceed uint32_t maximal value (4294967295). */ 'load_balancing_weight'?: (_google_protobuf_UInt32Value | null); /** @@ -60,8 +60,8 @@ export interface LbEndpoint__Output { /** * The endpoint metadata specifies values that may be used by the load * balancer to select endpoints in a cluster for a given request. The filter - * name should be specified as *envoy.lb*. An example boolean key-value pair - * is *canary*, providing the optional canary status of the upstream host. + * name should be specified as ``envoy.lb``. An example boolean key-value pair + * is ``canary``, providing the optional canary status of the upstream host. * This may be matched against in a route's * :ref:`RouteAction ` metadata_match field * to subset the endpoints considered in cluster load balancing. @@ -74,9 +74,9 @@ export interface LbEndpoint__Output { * of the weights of all endpoints in the endpoint's locality to produce a * percentage of traffic for the endpoint. This percentage is then further * weighted by the endpoint's locality's load balancing weight from - * LocalityLbEndpoints. If unspecified, each host is presumed to have equal - * weight in a locality. The sum of the weights of all endpoints in the - * endpoint's locality must not exceed uint32_t maximal value (4294967295). + * LocalityLbEndpoints. If unspecified, will be treated as 1. The sum + * of the weights of all endpoints in the endpoint's locality must not + * exceed uint32_t maximal value (4294967295). */ 'load_balancing_weight': (_google_protobuf_UInt32Value__Output | null); /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/endpoint/v3/LocalityLbEndpoints.ts b/packages/grpc-js-xds/src/generated/envoy/config/endpoint/v3/LocalityLbEndpoints.ts index 182e27c9c..4540792d6 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/endpoint/v3/LocalityLbEndpoints.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/endpoint/v3/LocalityLbEndpoints.ts @@ -23,9 +23,8 @@ export interface _envoy_config_endpoint_v3_LocalityLbEndpoints_LbEndpointList__O /** * A group of endpoints belonging to a Locality. - * One can have multiple LocalityLbEndpoints for a locality, but this is - * generally only done if the different groups need to have different load - * balancing weights or different priorities. + * One can have multiple LocalityLbEndpoints for a locality, but only if + * they have different priorities. * [#next-free-field: 9] */ export interface LocalityLbEndpoints { @@ -36,7 +35,7 @@ export interface LocalityLbEndpoints { /** * The group of endpoints belonging to the locality specified. * [#comment:TODO(adisuissa): Once LEDS is implemented this field needs to be - * deprecated and replaced by *load_balancer_endpoints*.] + * deprecated and replaced by ``load_balancer_endpoints``.] */ 'lb_endpoints'?: (_envoy_config_endpoint_v3_LbEndpoint)[]; /** @@ -76,7 +75,7 @@ export interface LocalityLbEndpoints { 'proximity'?: (_google_protobuf_UInt32Value | null); /** * The group of endpoints belonging to the locality. - * [#comment:TODO(adisuissa): Once LEDS is implemented the *lb_endpoints* field + * [#comment:TODO(adisuissa): Once LEDS is implemented the ``lb_endpoints`` field * needs to be deprecated.] */ 'load_balancer_endpoints'?: (_envoy_config_endpoint_v3_LocalityLbEndpoints_LbEndpointList | null); @@ -92,9 +91,8 @@ export interface LocalityLbEndpoints { /** * A group of endpoints belonging to a Locality. - * One can have multiple LocalityLbEndpoints for a locality, but this is - * generally only done if the different groups need to have different load - * balancing weights or different priorities. + * One can have multiple LocalityLbEndpoints for a locality, but only if + * they have different priorities. * [#next-free-field: 9] */ export interface LocalityLbEndpoints__Output { @@ -105,7 +103,7 @@ export interface LocalityLbEndpoints__Output { /** * The group of endpoints belonging to the locality specified. * [#comment:TODO(adisuissa): Once LEDS is implemented this field needs to be - * deprecated and replaced by *load_balancer_endpoints*.] + * deprecated and replaced by ``load_balancer_endpoints``.] */ 'lb_endpoints': (_envoy_config_endpoint_v3_LbEndpoint__Output)[]; /** @@ -145,7 +143,7 @@ export interface LocalityLbEndpoints__Output { 'proximity': (_google_protobuf_UInt32Value__Output | null); /** * The group of endpoints belonging to the locality. - * [#comment:TODO(adisuissa): Once LEDS is implemented the *lb_endpoints* field + * [#comment:TODO(adisuissa): Once LEDS is implemented the ``lb_endpoints`` field * needs to be deprecated.] */ 'load_balancer_endpoints'?: (_envoy_config_endpoint_v3_LocalityLbEndpoints_LbEndpointList__Output | null); diff --git a/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/AdditionalAddress.ts b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/AdditionalAddress.ts new file mode 100644 index 000000000..cc1f5e42f --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/AdditionalAddress.ts @@ -0,0 +1,38 @@ +// Original file: deps/envoy-api/envoy/config/listener/v3/listener.proto + +import type { Address as _envoy_config_core_v3_Address, Address__Output as _envoy_config_core_v3_Address__Output } from '../../../../envoy/config/core/v3/Address'; +import type { SocketOptionsOverride as _envoy_config_core_v3_SocketOptionsOverride, SocketOptionsOverride__Output as _envoy_config_core_v3_SocketOptionsOverride__Output } from '../../../../envoy/config/core/v3/SocketOptionsOverride'; + +/** + * The additional address the listener is listening on. + */ +export interface AdditionalAddress { + 'address'?: (_envoy_config_core_v3_Address | null); + /** + * Additional socket options that may not be present in Envoy source code or + * precompiled binaries. If specified, this will override the + * :ref:`socket_options ` + * in the listener. If specified with no + * :ref:`socket_options ` + * or an empty list of :ref:`socket_options `, + * it means no socket option will apply. + */ + 'socket_options'?: (_envoy_config_core_v3_SocketOptionsOverride | null); +} + +/** + * The additional address the listener is listening on. + */ +export interface AdditionalAddress__Output { + 'address': (_envoy_config_core_v3_Address__Output | null); + /** + * Additional socket options that may not be present in Envoy source code or + * precompiled binaries. If specified, this will override the + * :ref:`socket_options ` + * in the listener. If specified with no + * :ref:`socket_options ` + * or an empty list of :ref:`socket_options `, + * it means no socket option will apply. + */ + 'socket_options': (_envoy_config_core_v3_SocketOptionsOverride__Output | null); +} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/ApiListenerManager.ts b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/ApiListenerManager.ts new file mode 100644 index 000000000..125875161 --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/ApiListenerManager.ts @@ -0,0 +1,18 @@ +// Original file: deps/envoy-api/envoy/config/listener/v3/listener.proto + + +/** + * A placeholder proto so that users can explicitly configure the API + * Listener Manager via the bootstrap's :ref:`listener_manager `. + * [#not-implemented-hide:] + */ +export interface ApiListenerManager { +} + +/** + * A placeholder proto so that users can explicitly configure the API + * Listener Manager via the bootstrap's :ref:`listener_manager `. + * [#not-implemented-hide:] + */ +export interface ApiListenerManager__Output { +} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/Filter.ts b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/Filter.ts index 66e903b28..b95b36418 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/Filter.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/Filter.ts @@ -8,8 +8,7 @@ import type { ExtensionConfigSource as _envoy_config_core_v3_ExtensionConfigSour */ export interface Filter { /** - * The name of the filter to instantiate. The name must match a - * :ref:`supported filter `. + * The name of the filter configuration. */ 'name'?: (string); /** @@ -33,8 +32,7 @@ export interface Filter { */ export interface Filter__Output { /** - * The name of the filter to instantiate. The name must match a - * :ref:`supported filter `. + * The name of the filter configuration. */ 'name': (string); /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/FilterChain.ts b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/FilterChain.ts index e65f433c4..f27000d71 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/FilterChain.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/FilterChain.ts @@ -60,6 +60,12 @@ export interface FilterChain { * connections established with the listener. Order matters as the filters are * processed sequentially as connection events happen. Note: If the filter * list is empty, the connection will close by default. + * + * For QUIC listeners, network filters other than HTTP Connection Manager (HCM) + * can be created, but due to differences in the connection implementation compared + * to TCP, the onData() method will never be called. Therefore, network filters + * for QUIC listeners should only expect to do work at the start of a new connection + * (i.e. in onNewConnection()). HCM must be the last (or only) filter in the chain. */ 'filters'?: (_envoy_config_listener_v3_Filter)[]; /** @@ -81,17 +87,18 @@ export interface FilterChain { 'metadata'?: (_envoy_config_core_v3_Metadata | null); /** * Optional custom transport socket implementation to use for downstream connections. - * To setup TLS, set a transport socket with name `envoy.transport_sockets.tls` and - * :ref:`DownstreamTlsContext ` in the `typed_config`. + * To setup TLS, set a transport socket with name ``envoy.transport_sockets.tls`` and + * :ref:`DownstreamTlsContext ` in the ``typed_config``. * If no transport socket configuration is specified, new connections * will be set up with plaintext. * [#extension-category: envoy.transport_sockets.downstream] */ 'transport_socket'?: (_envoy_config_core_v3_TransportSocket | null); /** - * [#not-implemented-hide:] The unique name (or empty) by which this filter chain is known. If no - * name is provided, Envoy will allocate an internal UUID for the filter chain. If the filter - * chain is to be dynamically updated or removed via FCDS a unique name must be provided. + * The unique name (or empty) by which this filter chain is known. + * Note: :ref:`filter_chain_matcher + * ` + * requires that filter chains are uniquely named within a listener. */ 'name'?: (string); /** @@ -123,6 +130,12 @@ export interface FilterChain__Output { * connections established with the listener. Order matters as the filters are * processed sequentially as connection events happen. Note: If the filter * list is empty, the connection will close by default. + * + * For QUIC listeners, network filters other than HTTP Connection Manager (HCM) + * can be created, but due to differences in the connection implementation compared + * to TCP, the onData() method will never be called. Therefore, network filters + * for QUIC listeners should only expect to do work at the start of a new connection + * (i.e. in onNewConnection()). HCM must be the last (or only) filter in the chain. */ 'filters': (_envoy_config_listener_v3_Filter__Output)[]; /** @@ -144,17 +157,18 @@ export interface FilterChain__Output { 'metadata': (_envoy_config_core_v3_Metadata__Output | null); /** * Optional custom transport socket implementation to use for downstream connections. - * To setup TLS, set a transport socket with name `envoy.transport_sockets.tls` and - * :ref:`DownstreamTlsContext ` in the `typed_config`. + * To setup TLS, set a transport socket with name ``envoy.transport_sockets.tls`` and + * :ref:`DownstreamTlsContext ` in the ``typed_config``. * If no transport socket configuration is specified, new connections * will be set up with plaintext. * [#extension-category: envoy.transport_sockets.downstream] */ 'transport_socket': (_envoy_config_core_v3_TransportSocket__Output | null); /** - * [#not-implemented-hide:] The unique name (or empty) by which this filter chain is known. If no - * name is provided, Envoy will allocate an internal UUID for the filter chain. If the filter - * chain is to be dynamically updated or removed via FCDS a unique name must be provided. + * The unique name (or empty) by which this filter chain is known. + * Note: :ref:`filter_chain_matcher + * ` + * requires that filter chains are uniquely named within a listener. */ 'name': (string); /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/FilterChainMatch.ts b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/FilterChainMatch.ts index 259888217..87b1503cb 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/FilterChainMatch.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/FilterChainMatch.ts @@ -143,6 +143,7 @@ export interface FilterChainMatch { * will be first matched against ``www.example.com``, then ``*.example.com``, then ``*.com``. * * Note that partial wildcards are not supported, and values like ``*w.example.com`` are invalid. + * The value ``*`` is also not supported, and ``server_names`` should be omitted instead. * * .. attention:: * @@ -285,6 +286,7 @@ export interface FilterChainMatch__Output { * will be first matched against ``www.example.com``, then ``*.example.com``, then ``*.com``. * * Note that partial wildcards are not supported, and values like ``*w.example.com`` are invalid. + * The value ``*`` is also not supported, and ``server_names`` should be omitted instead. * * .. attention:: * diff --git a/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/Listener.ts b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/Listener.ts index 3df1006b7..ca12fc6de 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/Listener.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/Listener.ts @@ -12,6 +12,9 @@ import type { TrafficDirection as _envoy_config_core_v3_TrafficDirection } from import type { UdpListenerConfig as _envoy_config_listener_v3_UdpListenerConfig, UdpListenerConfig__Output as _envoy_config_listener_v3_UdpListenerConfig__Output } from '../../../../envoy/config/listener/v3/UdpListenerConfig'; import type { ApiListener as _envoy_config_listener_v3_ApiListener, ApiListener__Output as _envoy_config_listener_v3_ApiListener__Output } from '../../../../envoy/config/listener/v3/ApiListener'; import type { AccessLog as _envoy_config_accesslog_v3_AccessLog, AccessLog__Output as _envoy_config_accesslog_v3_AccessLog__Output } from '../../../../envoy/config/accesslog/v3/AccessLog'; +import type { Matcher as _xds_type_matcher_v3_Matcher, Matcher__Output as _xds_type_matcher_v3_Matcher__Output } from '../../../../xds/type/matcher/v3/Matcher'; +import type { AdditionalAddress as _envoy_config_listener_v3_AdditionalAddress, AdditionalAddress__Output as _envoy_config_listener_v3_AdditionalAddress__Output } from '../../../../envoy/config/listener/v3/AdditionalAddress'; +import type { TypedExtensionConfig as _envoy_config_core_v3_TypedExtensionConfig, TypedExtensionConfig__Output as _envoy_config_core_v3_TypedExtensionConfig__Output } from '../../../../envoy/config/core/v3/TypedExtensionConfig'; /** * Configuration for listener connection balancing. @@ -21,7 +24,13 @@ export interface _envoy_config_listener_v3_Listener_ConnectionBalanceConfig { * If specified, the listener will use the exact connection balancer. */ 'exact_balance'?: (_envoy_config_listener_v3_Listener_ConnectionBalanceConfig_ExactBalance | null); - 'balance_type'?: "exact_balance"; + /** + * The listener will use the connection balancer according to ``type_url``. If ``type_url`` is invalid, + * Envoy will not attempt to balance active connections between worker threads. + * [#extension-category: envoy.network.connection_balance] + */ + 'extend_balance'?: (_envoy_config_core_v3_TypedExtensionConfig | null); + 'balance_type'?: "exact_balance"|"extend_balance"; } /** @@ -32,7 +41,13 @@ export interface _envoy_config_listener_v3_Listener_ConnectionBalanceConfig__Out * If specified, the listener will use the exact connection balancer. */ 'exact_balance'?: (_envoy_config_listener_v3_Listener_ConnectionBalanceConfig_ExactBalance__Output | null); - 'balance_type': "exact_balance"; + /** + * The listener will use the connection balancer according to ``type_url``. If ``type_url`` is invalid, + * Envoy will not attempt to balance active connections between worker threads. + * [#extension-category: envoy.network.connection_balance] + */ + 'extend_balance'?: (_envoy_config_core_v3_TypedExtensionConfig__Output | null); + 'balance_type': "exact_balance"|"extend_balance"; } /** @@ -105,20 +120,18 @@ export interface _envoy_config_listener_v3_Listener_ConnectionBalanceConfig_Exac /** * Configuration for envoy internal listener. All the future internal listener features should be added here. - * [#not-implemented-hide:] */ export interface _envoy_config_listener_v3_Listener_InternalListenerConfig { } /** * Configuration for envoy internal listener. All the future internal listener features should be added here. - * [#not-implemented-hide:] */ export interface _envoy_config_listener_v3_Listener_InternalListenerConfig__Output { } /** - * [#next-free-field: 30] + * [#next-free-field: 34] */ export interface Listener { /** @@ -131,6 +144,7 @@ export interface Listener { * The address that the listener should listen on. In general, the address must be unique, though * that is governed by the bind rules of the OS. E.g., multiple listeners can listen on port 0 on * Linux as the actual port will be allocated by the OS. + * Required unless ``api_listener`` or ``listener_specifier`` is populated. */ 'address'?: (_envoy_config_core_v3_Address | null); /** @@ -144,7 +158,7 @@ export interface Listener { */ 'filter_chains'?: (_envoy_config_listener_v3_FilterChain)[]; /** - * If a connection is redirected using *iptables*, the port on which the proxy + * If a connection is redirected using ``iptables``, the port on which the proxy * receives it might be different from the original destination address. When this flag is set to * true, the listener hands off redirected connections to the listener associated with the * original destination address. If there is no listener associated with the original destination @@ -177,31 +191,30 @@ export interface Listener { * UDP Listener filters can be specified when the protocol in the listener socket address in * :ref:`protocol ` is :ref:`UDP * `. - * UDP listeners currently support a single filter. */ 'listener_filters'?: (_envoy_config_listener_v3_ListenerFilter)[]; /** * Whether the listener should be set as a transparent socket. * When this flag is set to true, connections can be redirected to the listener using an - * *iptables* *TPROXY* target, in which case the original source and destination addresses and + * ``iptables`` ``TPROXY`` target, in which case the original source and destination addresses and * ports are preserved on accepted connections. This flag should be used in combination with * :ref:`an original_dst ` :ref:`listener filter * ` to mark the connections' local addresses as * "restored." This can be used to hand off each redirected connection to another listener * associated with the connection's destination address. Direct connections to the socket without - * using *TPROXY* cannot be distinguished from connections redirected using *TPROXY* and are + * using ``TPROXY`` cannot be distinguished from connections redirected using ``TPROXY`` and are * therefore treated as if they were redirected. * When this flag is set to false, the listener's socket is explicitly reset as non-transparent. - * Setting this flag requires Envoy to run with the *CAP_NET_ADMIN* capability. + * Setting this flag requires Envoy to run with the ``CAP_NET_ADMIN`` capability. * When this flag is not set (default), the socket is not modified, i.e. the transparent option * is neither set nor reset. */ 'transparent'?: (_google_protobuf_BoolValue | null); /** - * Whether the listener should set the *IP_FREEBIND* socket option. When this + * Whether the listener should set the ``IP_FREEBIND`` socket option. When this * flag is set to true, listeners can be bound to an IP address that is not * configured on the system running Envoy. When this flag is set to false, the - * option *IP_FREEBIND* is disabled on the socket. When this flag is not set + * option ``IP_FREEBIND`` is disabled on the socket. When this flag is not set * (default), the socket is not modified, i.e. the option is neither enabled * nor disabled. */ @@ -225,13 +238,16 @@ export interface Listener { 'tcp_fast_open_queue_length'?: (_google_protobuf_UInt32Value | null); /** * Additional socket options that may not be present in Envoy source code or - * precompiled binaries. + * precompiled binaries. The socket options can be updated for a listener when + * :ref:`enable_reuse_port ` + * is `true`. Otherwise, if socket options change during a listener update the update will be rejected + * to make it clear that the options were not updated. */ 'socket_options'?: (_envoy_config_core_v3_SocketOption)[]; /** * The timeout to wait for all listener filters to complete operation. If the timeout is reached, * the accepted socket is closed without a connection being created unless - * `continue_on_listener_filters_timeout` is set to true. Specify 0 to disable the + * ``continue_on_listener_filters_timeout`` is set to true. Specify 0 to disable the * timeout. If not specified, a default timeout of 15s is used. */ 'listener_filters_timeout'?: (_google_protobuf_Duration | null); @@ -290,7 +306,7 @@ export interface Listener { */ 'connection_balance_config'?: (_envoy_config_listener_v3_Listener_ConnectionBalanceConfig | null); /** - * Deprecated. Use `enable_reuse_port` instead. + * Deprecated. Use ``enable_reuse_port`` instead. */ 'reuse_port'?: (boolean); /** @@ -318,32 +334,34 @@ export interface Listener { /** * Used to represent an internal listener which does not listen on OSI L4 address but can be used by the * :ref:`envoy cluster ` to create a user space connection to. - * The internal listener acts as a tcp listener. It supports listener filters and network filter chains. - * The internal listener require :ref:`address ` has - * field `envoy_internal_address`. + * The internal listener acts as a TCP listener. It supports listener filters and network filter chains. + * Upstream clusters refer to the internal listeners by their :ref:`name + * `. :ref:`Address + * ` must not be set on the internal listeners. * - * There are some limitations are derived from the implementation. The known limitations include + * There are some limitations that are derived from the implementation. The known limitations include: * * * :ref:`ConnectionBalanceConfig ` is not - * allowed because both cluster connection and listener connection must be owned by the same dispatcher. + * allowed because both the cluster connection and the listener connection must be owned by the same dispatcher. * * :ref:`tcp_backlog_size ` * * :ref:`freebind ` * * :ref:`transparent ` - * [#not-implemented-hide:] */ 'internal_listener'?: (_envoy_config_listener_v3_Listener_InternalListenerConfig | null); /** * Optional prefix to use on listener stats. If empty, the stats will be rooted at - * `listener.
.`. If non-empty, stats will be rooted at - * `listener..`. + * ``listener.
.``. If non-empty, stats will be rooted at + * ``listener..``. */ 'stat_prefix'?: (string); /** - * When this flag is set to true, listeners set the *SO_REUSEPORT* socket option and + * When this flag is set to true, listeners set the ``SO_REUSEPORT`` socket option and * create one socket for each worker thread. This makes inbound connections * distribute among worker threads roughly evenly in cases where there are a high number * of connections. When this flag is set to false, all worker threads share one socket. This field - * defaults to true. + * defaults to true. The change of field will be rejected during an listener update when the + * runtime flag ``envoy.reloadable_features.enable_update_listener_socket_options`` is enabled. + * Otherwise, the update of this field will be ignored quietly. * * .. attention:: * @@ -361,17 +379,49 @@ export interface Listener { * is warned similar to macOS. It is left enabled for UDP with undefined behavior currently. */ 'enable_reuse_port'?: (_google_protobuf_BoolValue | null); + /** + * Enable MPTCP (multi-path TCP) on this listener. Clients will be allowed to establish + * MPTCP connections. Non-MPTCP clients will fall back to regular TCP. + */ + 'enable_mptcp'?: (boolean); + /** + * Whether the listener should limit connections based upon the value of + * :ref:`global_downstream_max_connections `. + */ + 'ignore_global_conn_limit'?: (boolean); + /** + * :ref:`Matcher API ` resolving the filter chain name from the + * network properties. This matcher is used as a replacement for the filter chain match condition + * :ref:`filter_chain_match + * `. If specified, all + * :ref:`filter_chains ` must have a + * non-empty and unique :ref:`name ` field + * and not specify :ref:`filter_chain_match + * ` field. + * + * .. note:: + * + * Once matched, each connection is permanently bound to its filter chain. + * If the matcher changes but the filter chain remains the same, the + * connections bound to the filter chain are not drained. If, however, the + * filter chain is removed or structurally modified, then the drain for its + * connections is initiated. + */ + 'filter_chain_matcher'?: (_xds_type_matcher_v3_Matcher | null); + /** + * The additional addresses the listener should listen on. The addresses must be unique across all + * listeners. Multiple addresses with port 0 can be supplied. When using multiple addresses in a single listener, + * all addresses use the same protocol, and multiple internal addresses are not supported. + */ + 'additional_addresses'?: (_envoy_config_listener_v3_AdditionalAddress)[]; /** * The exclusive listener type and the corresponding config. - * TODO(lambdai): https://github.com/envoyproxy/envoy/issues/15372 - * Will create and add TcpListenerConfig. Will add UdpListenerConfig and ApiListener. - * [#not-implemented-hide:] */ 'listener_specifier'?: "internal_listener"; } /** - * [#next-free-field: 30] + * [#next-free-field: 34] */ export interface Listener__Output { /** @@ -384,6 +434,7 @@ export interface Listener__Output { * The address that the listener should listen on. In general, the address must be unique, though * that is governed by the bind rules of the OS. E.g., multiple listeners can listen on port 0 on * Linux as the actual port will be allocated by the OS. + * Required unless ``api_listener`` or ``listener_specifier`` is populated. */ 'address': (_envoy_config_core_v3_Address__Output | null); /** @@ -397,7 +448,7 @@ export interface Listener__Output { */ 'filter_chains': (_envoy_config_listener_v3_FilterChain__Output)[]; /** - * If a connection is redirected using *iptables*, the port on which the proxy + * If a connection is redirected using ``iptables``, the port on which the proxy * receives it might be different from the original destination address. When this flag is set to * true, the listener hands off redirected connections to the listener associated with the * original destination address. If there is no listener associated with the original destination @@ -430,31 +481,30 @@ export interface Listener__Output { * UDP Listener filters can be specified when the protocol in the listener socket address in * :ref:`protocol ` is :ref:`UDP * `. - * UDP listeners currently support a single filter. */ 'listener_filters': (_envoy_config_listener_v3_ListenerFilter__Output)[]; /** * Whether the listener should be set as a transparent socket. * When this flag is set to true, connections can be redirected to the listener using an - * *iptables* *TPROXY* target, in which case the original source and destination addresses and + * ``iptables`` ``TPROXY`` target, in which case the original source and destination addresses and * ports are preserved on accepted connections. This flag should be used in combination with * :ref:`an original_dst ` :ref:`listener filter * ` to mark the connections' local addresses as * "restored." This can be used to hand off each redirected connection to another listener * associated with the connection's destination address. Direct connections to the socket without - * using *TPROXY* cannot be distinguished from connections redirected using *TPROXY* and are + * using ``TPROXY`` cannot be distinguished from connections redirected using ``TPROXY`` and are * therefore treated as if they were redirected. * When this flag is set to false, the listener's socket is explicitly reset as non-transparent. - * Setting this flag requires Envoy to run with the *CAP_NET_ADMIN* capability. + * Setting this flag requires Envoy to run with the ``CAP_NET_ADMIN`` capability. * When this flag is not set (default), the socket is not modified, i.e. the transparent option * is neither set nor reset. */ 'transparent': (_google_protobuf_BoolValue__Output | null); /** - * Whether the listener should set the *IP_FREEBIND* socket option. When this + * Whether the listener should set the ``IP_FREEBIND`` socket option. When this * flag is set to true, listeners can be bound to an IP address that is not * configured on the system running Envoy. When this flag is set to false, the - * option *IP_FREEBIND* is disabled on the socket. When this flag is not set + * option ``IP_FREEBIND`` is disabled on the socket. When this flag is not set * (default), the socket is not modified, i.e. the option is neither enabled * nor disabled. */ @@ -478,13 +528,16 @@ export interface Listener__Output { 'tcp_fast_open_queue_length': (_google_protobuf_UInt32Value__Output | null); /** * Additional socket options that may not be present in Envoy source code or - * precompiled binaries. + * precompiled binaries. The socket options can be updated for a listener when + * :ref:`enable_reuse_port ` + * is `true`. Otherwise, if socket options change during a listener update the update will be rejected + * to make it clear that the options were not updated. */ 'socket_options': (_envoy_config_core_v3_SocketOption__Output)[]; /** * The timeout to wait for all listener filters to complete operation. If the timeout is reached, * the accepted socket is closed without a connection being created unless - * `continue_on_listener_filters_timeout` is set to true. Specify 0 to disable the + * ``continue_on_listener_filters_timeout`` is set to true. Specify 0 to disable the * timeout. If not specified, a default timeout of 15s is used. */ 'listener_filters_timeout': (_google_protobuf_Duration__Output | null); @@ -543,7 +596,7 @@ export interface Listener__Output { */ 'connection_balance_config': (_envoy_config_listener_v3_Listener_ConnectionBalanceConfig__Output | null); /** - * Deprecated. Use `enable_reuse_port` instead. + * Deprecated. Use ``enable_reuse_port`` instead. */ 'reuse_port': (boolean); /** @@ -571,32 +624,34 @@ export interface Listener__Output { /** * Used to represent an internal listener which does not listen on OSI L4 address but can be used by the * :ref:`envoy cluster ` to create a user space connection to. - * The internal listener acts as a tcp listener. It supports listener filters and network filter chains. - * The internal listener require :ref:`address ` has - * field `envoy_internal_address`. + * The internal listener acts as a TCP listener. It supports listener filters and network filter chains. + * Upstream clusters refer to the internal listeners by their :ref:`name + * `. :ref:`Address + * ` must not be set on the internal listeners. * - * There are some limitations are derived from the implementation. The known limitations include + * There are some limitations that are derived from the implementation. The known limitations include: * * * :ref:`ConnectionBalanceConfig ` is not - * allowed because both cluster connection and listener connection must be owned by the same dispatcher. + * allowed because both the cluster connection and the listener connection must be owned by the same dispatcher. * * :ref:`tcp_backlog_size ` * * :ref:`freebind ` * * :ref:`transparent ` - * [#not-implemented-hide:] */ 'internal_listener'?: (_envoy_config_listener_v3_Listener_InternalListenerConfig__Output | null); /** * Optional prefix to use on listener stats. If empty, the stats will be rooted at - * `listener.
.`. If non-empty, stats will be rooted at - * `listener..`. + * ``listener.
.``. If non-empty, stats will be rooted at + * ``listener..``. */ 'stat_prefix': (string); /** - * When this flag is set to true, listeners set the *SO_REUSEPORT* socket option and + * When this flag is set to true, listeners set the ``SO_REUSEPORT`` socket option and * create one socket for each worker thread. This makes inbound connections * distribute among worker threads roughly evenly in cases where there are a high number * of connections. When this flag is set to false, all worker threads share one socket. This field - * defaults to true. + * defaults to true. The change of field will be rejected during an listener update when the + * runtime flag ``envoy.reloadable_features.enable_update_listener_socket_options`` is enabled. + * Otherwise, the update of this field will be ignored quietly. * * .. attention:: * @@ -614,11 +669,43 @@ export interface Listener__Output { * is warned similar to macOS. It is left enabled for UDP with undefined behavior currently. */ 'enable_reuse_port': (_google_protobuf_BoolValue__Output | null); + /** + * Enable MPTCP (multi-path TCP) on this listener. Clients will be allowed to establish + * MPTCP connections. Non-MPTCP clients will fall back to regular TCP. + */ + 'enable_mptcp': (boolean); + /** + * Whether the listener should limit connections based upon the value of + * :ref:`global_downstream_max_connections `. + */ + 'ignore_global_conn_limit': (boolean); + /** + * :ref:`Matcher API ` resolving the filter chain name from the + * network properties. This matcher is used as a replacement for the filter chain match condition + * :ref:`filter_chain_match + * `. If specified, all + * :ref:`filter_chains ` must have a + * non-empty and unique :ref:`name ` field + * and not specify :ref:`filter_chain_match + * ` field. + * + * .. note:: + * + * Once matched, each connection is permanently bound to its filter chain. + * If the matcher changes but the filter chain remains the same, the + * connections bound to the filter chain are not drained. If, however, the + * filter chain is removed or structurally modified, then the drain for its + * connections is initiated. + */ + 'filter_chain_matcher': (_xds_type_matcher_v3_Matcher__Output | null); + /** + * The additional addresses the listener should listen on. The addresses must be unique across all + * listeners. Multiple addresses with port 0 can be supplied. When using multiple addresses in a single listener, + * all addresses use the same protocol, and multiple internal addresses are not supported. + */ + 'additional_addresses': (_envoy_config_listener_v3_AdditionalAddress__Output)[]; /** * The exclusive listener type and the corresponding config. - * TODO(lambdai): https://github.com/envoyproxy/envoy/issues/15372 - * Will create and add TcpListenerConfig. Will add UdpListenerConfig and ApiListener. - * [#not-implemented-hide:] */ 'listener_specifier': "internal_listener"; } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/ListenerCollection.ts b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/ListenerCollection.ts index 590ca8ed3..a1e7a10ca 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/ListenerCollection.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/ListenerCollection.ts @@ -3,7 +3,7 @@ import type { CollectionEntry as _xds_core_v3_CollectionEntry, CollectionEntry__Output as _xds_core_v3_CollectionEntry__Output } from '../../../../xds/core/v3/CollectionEntry'; /** - * Listener list collections. Entries are *Listener* resources or references. + * Listener list collections. Entries are ``Listener`` resources or references. * [#not-implemented-hide:] */ export interface ListenerCollection { @@ -11,7 +11,7 @@ export interface ListenerCollection { } /** - * Listener list collections. Entries are *Listener* resources or references. + * Listener list collections. Entries are ``Listener`` resources or references. * [#not-implemented-hide:] */ export interface ListenerCollection__Output { diff --git a/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/ListenerFilter.ts b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/ListenerFilter.ts index be7242849..5844c4bbe 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/ListenerFilter.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/ListenerFilter.ts @@ -2,11 +2,14 @@ import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../../google/protobuf/Any'; import type { ListenerFilterChainMatchPredicate as _envoy_config_listener_v3_ListenerFilterChainMatchPredicate, ListenerFilterChainMatchPredicate__Output as _envoy_config_listener_v3_ListenerFilterChainMatchPredicate__Output } from '../../../../envoy/config/listener/v3/ListenerFilterChainMatchPredicate'; +import type { ExtensionConfigSource as _envoy_config_core_v3_ExtensionConfigSource, ExtensionConfigSource__Output as _envoy_config_core_v3_ExtensionConfigSource__Output } from '../../../../envoy/config/core/v3/ExtensionConfigSource'; +/** + * [#next-free-field: 6] + */ export interface ListenerFilter { /** - * The name of the filter to instantiate. The name must match a - * :ref:`supported filter `. + * The name of the filter configuration. */ 'name'?: (string); /** @@ -21,13 +24,21 @@ export interface ListenerFilter { * for further examples. */ 'filter_disabled'?: (_envoy_config_listener_v3_ListenerFilterChainMatchPredicate | null); - 'config_type'?: "typed_config"; + /** + * Configuration source specifier for an extension configuration discovery + * service. In case of a failure and without the default configuration, the + * listener closes the connections. + */ + 'config_discovery'?: (_envoy_config_core_v3_ExtensionConfigSource | null); + 'config_type'?: "typed_config"|"config_discovery"; } +/** + * [#next-free-field: 6] + */ export interface ListenerFilter__Output { /** - * The name of the filter to instantiate. The name must match a - * :ref:`supported filter `. + * The name of the filter configuration. */ 'name': (string); /** @@ -42,5 +53,11 @@ export interface ListenerFilter__Output { * for further examples. */ 'filter_disabled': (_envoy_config_listener_v3_ListenerFilterChainMatchPredicate__Output | null); - 'config_type': "typed_config"; + /** + * Configuration source specifier for an extension configuration discovery + * service. In case of a failure and without the default configuration, the + * listener closes the connections. + */ + 'config_discovery'?: (_envoy_config_core_v3_ExtensionConfigSource__Output | null); + 'config_type': "typed_config"|"config_discovery"; } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/ListenerManager.ts b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/ListenerManager.ts new file mode 100644 index 000000000..d27a10c68 --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/ListenerManager.ts @@ -0,0 +1,18 @@ +// Original file: deps/envoy-api/envoy/config/listener/v3/listener.proto + + +/** + * A placeholder proto so that users can explicitly configure the standard + * Listener Manager via the bootstrap's :ref:`listener_manager `. + * [#not-implemented-hide:] + */ +export interface ListenerManager { +} + +/** + * A placeholder proto so that users can explicitly configure the standard + * Listener Manager via the bootstrap's :ref:`listener_manager `. + * [#not-implemented-hide:] + */ +export interface ListenerManager__Output { +} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/QuicProtocolOptions.ts b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/QuicProtocolOptions.ts index 5e01a772f..e88ab26a9 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/QuicProtocolOptions.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/QuicProtocolOptions.ts @@ -8,18 +8,21 @@ import type { TypedExtensionConfig as _envoy_config_core_v3_TypedExtensionConfig /** * Configuration specific to the UDP QUIC listener. - * [#next-free-field: 8] + * [#next-free-field: 10] */ export interface QuicProtocolOptions { 'quic_protocol_options'?: (_envoy_config_core_v3_QuicProtocolOptions | null); /** * Maximum number of milliseconds that connection will be alive when there is - * no network activity. 300000ms if not specified. + * no network activity. + * + * If it is less than 1ms, Envoy will use 1ms. 300000ms if not specified. */ 'idle_timeout'?: (_google_protobuf_Duration | null); /** * Connection timeout in milliseconds before the crypto handshake is finished. - * 20000ms if not specified. + * + * If it is less than 5000ms, Envoy will use 5000ms. 20000ms if not specified. */ 'crypto_handshake_timeout'?: (_google_protobuf_Duration | null); /** @@ -38,33 +41,49 @@ export interface QuicProtocolOptions { */ 'packets_to_read_to_connection_count_ratio'?: (_google_protobuf_UInt32Value | null); /** - * Configure which implementation of `quic::QuicCryptoClientStreamBase` to be used for this listener. + * Configure which implementation of ``quic::QuicCryptoClientStreamBase`` to be used for this listener. * If not specified the :ref:`QUICHE default one configured by ` will be used. * [#extension-category: envoy.quic.server.crypto_stream] */ 'crypto_stream_config'?: (_envoy_config_core_v3_TypedExtensionConfig | null); /** - * Configure which implementation of `quic::ProofSource` to be used for this listener. + * Configure which implementation of ``quic::ProofSource`` to be used for this listener. * If not specified the :ref:`default one configured by ` will be used. * [#extension-category: envoy.quic.proof_source] */ 'proof_source_config'?: (_envoy_config_core_v3_TypedExtensionConfig | null); + /** + * Config which implementation of ``quic::ConnectionIdGeneratorInterface`` to be used for this listener. + * If not specified the :ref:`default one configured by ` will be used. + * [#extension-category: envoy.quic.connection_id_generator] + */ + 'connection_id_generator_config'?: (_envoy_config_core_v3_TypedExtensionConfig | null); + /** + * Configure the server's preferred address to advertise so that client can migrate to it. See :ref:`example ` which configures a pair of v4 and v6 preferred addresses. + * The current QUICHE implementation will advertise only one of the preferred IPv4 and IPv6 addresses based on the address family the client initially connects with, and only if the client is also QUICHE-based. + * If not specified, Envoy will not advertise any server's preferred address. + * [#extension-category: envoy.quic.server_preferred_address] + */ + 'server_preferred_address_config'?: (_envoy_config_core_v3_TypedExtensionConfig | null); } /** * Configuration specific to the UDP QUIC listener. - * [#next-free-field: 8] + * [#next-free-field: 10] */ export interface QuicProtocolOptions__Output { 'quic_protocol_options': (_envoy_config_core_v3_QuicProtocolOptions__Output | null); /** * Maximum number of milliseconds that connection will be alive when there is - * no network activity. 300000ms if not specified. + * no network activity. + * + * If it is less than 1ms, Envoy will use 1ms. 300000ms if not specified. */ 'idle_timeout': (_google_protobuf_Duration__Output | null); /** * Connection timeout in milliseconds before the crypto handshake is finished. - * 20000ms if not specified. + * + * If it is less than 5000ms, Envoy will use 5000ms. 20000ms if not specified. */ 'crypto_handshake_timeout': (_google_protobuf_Duration__Output | null); /** @@ -83,15 +102,28 @@ export interface QuicProtocolOptions__Output { */ 'packets_to_read_to_connection_count_ratio': (_google_protobuf_UInt32Value__Output | null); /** - * Configure which implementation of `quic::QuicCryptoClientStreamBase` to be used for this listener. + * Configure which implementation of ``quic::QuicCryptoClientStreamBase`` to be used for this listener. * If not specified the :ref:`QUICHE default one configured by ` will be used. * [#extension-category: envoy.quic.server.crypto_stream] */ 'crypto_stream_config': (_envoy_config_core_v3_TypedExtensionConfig__Output | null); /** - * Configure which implementation of `quic::ProofSource` to be used for this listener. + * Configure which implementation of ``quic::ProofSource`` to be used for this listener. * If not specified the :ref:`default one configured by ` will be used. * [#extension-category: envoy.quic.proof_source] */ 'proof_source_config': (_envoy_config_core_v3_TypedExtensionConfig__Output | null); + /** + * Config which implementation of ``quic::ConnectionIdGeneratorInterface`` to be used for this listener. + * If not specified the :ref:`default one configured by ` will be used. + * [#extension-category: envoy.quic.connection_id_generator] + */ + 'connection_id_generator_config': (_envoy_config_core_v3_TypedExtensionConfig__Output | null); + /** + * Configure the server's preferred address to advertise so that client can migrate to it. See :ref:`example ` which configures a pair of v4 and v6 preferred addresses. + * The current QUICHE implementation will advertise only one of the preferred IPv4 and IPv6 addresses based on the address family the client initially connects with, and only if the client is also QUICHE-based. + * If not specified, Envoy will not advertise any server's preferred address. + * [#extension-category: envoy.quic.server_preferred_address] + */ + 'server_preferred_address_config': (_envoy_config_core_v3_TypedExtensionConfig__Output | null); } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/UdpListenerConfig.ts b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/UdpListenerConfig.ts index f4c220e20..63f9666e0 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/UdpListenerConfig.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/UdpListenerConfig.ts @@ -2,9 +2,10 @@ import type { UdpSocketConfig as _envoy_config_core_v3_UdpSocketConfig, UdpSocketConfig__Output as _envoy_config_core_v3_UdpSocketConfig__Output } from '../../../../envoy/config/core/v3/UdpSocketConfig'; import type { QuicProtocolOptions as _envoy_config_listener_v3_QuicProtocolOptions, QuicProtocolOptions__Output as _envoy_config_listener_v3_QuicProtocolOptions__Output } from '../../../../envoy/config/listener/v3/QuicProtocolOptions'; +import type { TypedExtensionConfig as _envoy_config_core_v3_TypedExtensionConfig, TypedExtensionConfig__Output as _envoy_config_core_v3_TypedExtensionConfig__Output } from '../../../../envoy/config/core/v3/TypedExtensionConfig'; /** - * [#next-free-field: 8] + * [#next-free-field: 9] */ export interface UdpListenerConfig { /** @@ -17,16 +18,21 @@ export interface UdpListenerConfig { /** * Configuration for QUIC protocol. If empty, QUIC will not be enabled on this listener. Set * to the default object to enable QUIC without modifying any additional options. - * - * .. warning:: - * QUIC support is currently alpha and should be used with caution. Please - * see :ref:`here ` for details. */ 'quic_options'?: (_envoy_config_listener_v3_QuicProtocolOptions | null); + /** + * Configuration for the UDP packet writer. If empty, HTTP/3 will use GSO if available + * (:ref:`UdpDefaultWriterFactory `) + * or the default kernel sendmsg if not, + * (:ref:`UdpDefaultWriterFactory `) + * and raw UDP will use kernel sendmsg. + * [#extension-category: envoy.udp_packet_writer] + */ + 'udp_packet_packet_writer_config'?: (_envoy_config_core_v3_TypedExtensionConfig | null); } /** - * [#next-free-field: 8] + * [#next-free-field: 9] */ export interface UdpListenerConfig__Output { /** @@ -39,10 +45,15 @@ export interface UdpListenerConfig__Output { /** * Configuration for QUIC protocol. If empty, QUIC will not be enabled on this listener. Set * to the default object to enable QUIC without modifying any additional options. - * - * .. warning:: - * QUIC support is currently alpha and should be used with caution. Please - * see :ref:`here ` for details. */ 'quic_options': (_envoy_config_listener_v3_QuicProtocolOptions__Output | null); + /** + * Configuration for the UDP packet writer. If empty, HTTP/3 will use GSO if available + * (:ref:`UdpDefaultWriterFactory `) + * or the default kernel sendmsg if not, + * (:ref:`UdpDefaultWriterFactory `) + * and raw UDP will use kernel sendmsg. + * [#extension-category: envoy.udp_packet_writer] + */ + 'udp_packet_packet_writer_config': (_envoy_config_core_v3_TypedExtensionConfig__Output | null); } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/ValidationListenerManager.ts b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/ValidationListenerManager.ts new file mode 100644 index 000000000..3b6ccc3a6 --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/ValidationListenerManager.ts @@ -0,0 +1,18 @@ +// Original file: deps/envoy-api/envoy/config/listener/v3/listener.proto + + +/** + * A placeholder proto so that users can explicitly configure the standard + * Validation Listener Manager via the bootstrap's :ref:`listener_manager `. + * [#not-implemented-hide:] + */ +export interface ValidationListenerManager { +} + +/** + * A placeholder proto so that users can explicitly configure the standard + * Validation Listener Manager via the bootstrap's :ref:`listener_manager `. + * [#not-implemented-hide:] + */ +export interface ValidationListenerManager__Output { +} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/ClusterSpecifierPlugin.ts b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/ClusterSpecifierPlugin.ts index 14724412a..3742eeb6b 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/ClusterSpecifierPlugin.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/ClusterSpecifierPlugin.ts @@ -1,4 +1,4 @@ -// Original file: deps/envoy-api/envoy/config/route/v3/route.proto +// Original file: deps/envoy-api/envoy/config/route/v3/route_components.proto import type { TypedExtensionConfig as _envoy_config_core_v3_TypedExtensionConfig, TypedExtensionConfig__Output as _envoy_config_core_v3_TypedExtensionConfig__Output } from '../../../../envoy/config/core/v3/TypedExtensionConfig'; @@ -10,6 +10,14 @@ export interface ClusterSpecifierPlugin { * The name of the plugin and its opaque configuration. */ 'extension'?: (_envoy_config_core_v3_TypedExtensionConfig | null); + /** + * If is_optional is not set or is set to false and the plugin defined by this message is not a + * supported type, the containing resource is NACKed. If is_optional is set to true, the resource + * would not be NACKed for this reason. In this case, routes referencing this plugin's name would + * not be treated as an illegal configuration, but would result in a failure if the route is + * selected. + */ + 'is_optional'?: (boolean); } /** @@ -20,4 +28,12 @@ export interface ClusterSpecifierPlugin__Output { * The name of the plugin and its opaque configuration. */ 'extension': (_envoy_config_core_v3_TypedExtensionConfig__Output | null); + /** + * If is_optional is not set or is set to false and the plugin defined by this message is not a + * supported type, the containing resource is NACKed. If is_optional is set to true, the resource + * would not be NACKed for this reason. In this case, routes referencing this plugin's name would + * not be treated as an illegal configuration, but would result in a failure if the route is + * selected. + */ + 'is_optional': (boolean); } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/CorsPolicy.ts b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/CorsPolicy.ts index 40634448a..8a74b0658 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/CorsPolicy.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/CorsPolicy.ts @@ -5,23 +5,31 @@ import type { RuntimeFractionalPercent as _envoy_config_core_v3_RuntimeFractiona import type { StringMatcher as _envoy_type_matcher_v3_StringMatcher, StringMatcher__Output as _envoy_type_matcher_v3_StringMatcher__Output } from '../../../../envoy/type/matcher/v3/StringMatcher'; /** - * [#next-free-field: 12] + * Cors policy configuration. + * + * .. attention:: + * + * This message has been deprecated. Please use + * :ref:`CorsPolicy in filter extension ` + * as as alternative. + * + * [#next-free-field: 13] */ export interface CorsPolicy { /** - * Specifies the content for the *access-control-allow-methods* header. + * Specifies the content for the ``access-control-allow-methods`` header. */ 'allow_methods'?: (string); /** - * Specifies the content for the *access-control-allow-headers* header. + * Specifies the content for the ``access-control-allow-headers`` header. */ 'allow_headers'?: (string); /** - * Specifies the content for the *access-control-expose-headers* header. + * Specifies the content for the ``access-control-expose-headers`` header. */ 'expose_headers'?: (string); /** - * Specifies the content for the *access-control-max-age* header. + * Specifies the content for the ``access-control-max-age`` header. */ 'max_age'?: (string); /** @@ -47,7 +55,7 @@ export interface CorsPolicy { * * If :ref:`runtime_key ` is specified, * Envoy will lookup the runtime key to get the percentage of requests for which it will evaluate - * and track the request's *Origin* to determine if it's valid but will not enforce any policies. + * and track the request's ``Origin`` to determine if it's valid but will not enforce any policies. */ 'shadow_enabled'?: (_envoy_config_core_v3_RuntimeFractionalPercent | null); /** @@ -55,27 +63,42 @@ export interface CorsPolicy { * string matchers match. */ 'allow_origin_string_match'?: (_envoy_type_matcher_v3_StringMatcher)[]; + /** + * Specify whether allow requests whose target server's IP address is more private than that from + * which the request initiator was fetched. + * + * More details refer to https://developer.chrome.com/blog/private-network-access-preflight. + */ + 'allow_private_network_access'?: (_google_protobuf_BoolValue | null); 'enabled_specifier'?: "filter_enabled"; } /** - * [#next-free-field: 12] + * Cors policy configuration. + * + * .. attention:: + * + * This message has been deprecated. Please use + * :ref:`CorsPolicy in filter extension ` + * as as alternative. + * + * [#next-free-field: 13] */ export interface CorsPolicy__Output { /** - * Specifies the content for the *access-control-allow-methods* header. + * Specifies the content for the ``access-control-allow-methods`` header. */ 'allow_methods': (string); /** - * Specifies the content for the *access-control-allow-headers* header. + * Specifies the content for the ``access-control-allow-headers`` header. */ 'allow_headers': (string); /** - * Specifies the content for the *access-control-expose-headers* header. + * Specifies the content for the ``access-control-expose-headers`` header. */ 'expose_headers': (string); /** - * Specifies the content for the *access-control-max-age* header. + * Specifies the content for the ``access-control-max-age`` header. */ 'max_age': (string); /** @@ -101,7 +124,7 @@ export interface CorsPolicy__Output { * * If :ref:`runtime_key ` is specified, * Envoy will lookup the runtime key to get the percentage of requests for which it will evaluate - * and track the request's *Origin* to determine if it's valid but will not enforce any policies. + * and track the request's ``Origin`` to determine if it's valid but will not enforce any policies. */ 'shadow_enabled': (_envoy_config_core_v3_RuntimeFractionalPercent__Output | null); /** @@ -109,5 +132,12 @@ export interface CorsPolicy__Output { * string matchers match. */ 'allow_origin_string_match': (_envoy_type_matcher_v3_StringMatcher__Output)[]; + /** + * Specify whether allow requests whose target server's IP address is more private than that from + * which the request initiator was fetched. + * + * More details refer to https://developer.chrome.com/blog/private-network-access-preflight. + */ + 'allow_private_network_access': (_google_protobuf_BoolValue__Output | null); 'enabled_specifier': "filter_enabled"; } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/DirectResponseAction.ts b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/DirectResponseAction.ts index 794ae510a..7c0f9ee67 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/DirectResponseAction.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/DirectResponseAction.ts @@ -13,7 +13,7 @@ export interface DirectResponseAction { * * .. note:: * - * Headers can be specified using *response_headers_to_add* in the enclosing + * Headers can be specified using ``response_headers_to_add`` in the enclosing * :ref:`envoy_v3_api_msg_config.route.v3.Route`, :ref:`envoy_v3_api_msg_config.route.v3.RouteConfiguration` or * :ref:`envoy_v3_api_msg_config.route.v3.VirtualHost`. */ @@ -31,7 +31,7 @@ export interface DirectResponseAction__Output { * * .. note:: * - * Headers can be specified using *response_headers_to_add* in the enclosing + * Headers can be specified using ``response_headers_to_add`` in the enclosing * :ref:`envoy_v3_api_msg_config.route.v3.Route`, :ref:`envoy_v3_api_msg_config.route.v3.RouteConfiguration` or * :ref:`envoy_v3_api_msg_config.route.v3.VirtualHost`. */ diff --git a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/FilterConfig.ts b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/FilterConfig.ts index 2c960419c..e5a3b5778 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/FilterConfig.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/FilterConfig.ts @@ -9,7 +9,6 @@ import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__ * :ref:`Route.typed_per_filter_config`, * or :ref:`WeightedCluster.ClusterWeight.typed_per_filter_config` * to add additional flags to the filter. - * [#not-implemented-hide:] */ export interface FilterConfig { /** @@ -22,6 +21,23 @@ export interface FilterConfig { * than rejecting the config. */ 'is_optional'?: (boolean); + /** + * If true, the filter is disabled in the route or virtual host and the ``config`` field is ignored. + * + * .. note:: + * + * This field will take effect when the request arrive and filter chain is created for the request. + * If initial route is selected for the request and a filter is disabled in the initial route, then + * the filter will not be added to the filter chain. + * And if the request is mutated later and re-match to another route, the disabled filter by the + * initial route will not be added back to the filter chain because the filter chain is already + * created and it is too late to change the chain. + * + * This field only make sense for the downstream HTTP filters for now. + * + * [#not-implemented-hide:] + */ + 'disabled'?: (boolean); } /** @@ -31,7 +47,6 @@ export interface FilterConfig { * :ref:`Route.typed_per_filter_config`, * or :ref:`WeightedCluster.ClusterWeight.typed_per_filter_config` * to add additional flags to the filter. - * [#not-implemented-hide:] */ export interface FilterConfig__Output { /** @@ -44,4 +59,21 @@ export interface FilterConfig__Output { * than rejecting the config. */ 'is_optional': (boolean); + /** + * If true, the filter is disabled in the route or virtual host and the ``config`` field is ignored. + * + * .. note:: + * + * This field will take effect when the request arrive and filter chain is created for the request. + * If initial route is selected for the request and a filter is disabled in the initial route, then + * the filter will not be added to the filter chain. + * And if the request is mutated later and re-match to another route, the disabled filter by the + * initial route will not be added back to the filter chain because the filter chain is already + * created and it is too late to change the chain. + * + * This field only make sense for the downstream HTTP filters for now. + * + * [#not-implemented-hide:] + */ + 'disabled': (boolean); } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/HeaderMatcher.ts b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/HeaderMatcher.ts index bde8f28cd..e073a8f13 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/HeaderMatcher.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/HeaderMatcher.ts @@ -8,19 +8,21 @@ import type { Long } from '@grpc/proto-loader'; /** * .. attention:: * - * Internally, Envoy always uses the HTTP/2 *:authority* header to represent the HTTP/1 *Host* - * header. Thus, if attempting to match on *Host*, match on *:authority* instead. + * Internally, Envoy always uses the HTTP/2 ``:authority`` header to represent the HTTP/1 ``Host`` + * header. Thus, if attempting to match on ``Host``, match on ``:authority`` instead. * * .. attention:: * - * To route on HTTP method, use the special HTTP/2 *:method* header. This works for both + * To route on HTTP method, use the special HTTP/2 ``:method`` header. This works for both * HTTP/1 and HTTP/2 as Envoy normalizes headers. E.g., * * .. code-block:: json * * { * "name": ":method", - * "exact_match": "POST" + * "string_match": { + * "exact": "POST" + * } * } * * .. attention:: @@ -30,7 +32,7 @@ import type { Long } from '@grpc/proto-loader'; * value. * * [#next-major-version: HeaderMatcher should be refactored to use StringMatcher.] - * [#next-free-field: 14] + * [#next-free-field: 15] */ export interface HeaderMatcher { /** @@ -52,8 +54,8 @@ export interface HeaderMatcher { * * Examples: * - * * For range [-10,0), route will match for header value -1, but not for 0, "somestring", 10.9, - * "-1somestring" + * * For range [-10,0), route will match for header value -1, but not for 0, ``somestring``, 10.9, + * ``-1somestring`` */ 'range_match'?: (_envoy_type_v3_Int64Range | null); /** @@ -66,7 +68,7 @@ export interface HeaderMatcher { * * Examples: * - * * The regex ``\d{3}`` does not match the value *1234*, so it will match when inverted. + * * The regex ``\d{3}`` does not match the value ``1234``, so it will match when inverted. * * The range [-10,0) will match the value -1, so it will not match when inverted. */ 'invert_match'?: (boolean); @@ -77,7 +79,7 @@ export interface HeaderMatcher { * * Examples: * - * * The prefix *abcd* matches the value *abcdxyz*, but not for *abcxyz*. + * * The prefix ``abcd`` matches the value ``abcdxyz``, but not for ``abcxyz``. */ 'prefix_match'?: (string); /** @@ -87,7 +89,7 @@ export interface HeaderMatcher { * * Examples: * - * * The suffix *abcd* matches the value *xyzabcd*, but not for *xyzbcd*. + * * The suffix ``abcd`` matches the value ``xyzabcd``, but not for ``xyzbcd``. */ 'suffix_match'?: (string); /** @@ -105,13 +107,42 @@ export interface HeaderMatcher { * * Examples: * - * * The value *abcd* matches the value *xyzabcdpqr*, but not for *xyzbcdpqr*. + * * The value ``abcd`` matches the value ``xyzabcdpqr``, but not for ``xyzbcdpqr``. */ 'contains_match'?: (string); /** * If specified, header match will be performed based on the string match of the header value. */ 'string_match'?: (_envoy_type_matcher_v3_StringMatcher | null); + /** + * If specified, for any header match rule, if the header match rule specified header + * does not exist, this header value will be treated as empty. Defaults to false. + * + * Examples: + * + * * The header match rule specified header "header1" to range match of [0, 10], + * :ref:`invert_match ` + * is set to true and :ref:`treat_missing_header_as_empty ` + * is set to true; The "header1" header is not present. The match rule will + * treat the "header1" as an empty header. The empty header does not match the range, + * so it will match when inverted. + * * The header match rule specified header "header2" to range match of [0, 10], + * :ref:`invert_match ` + * is set to true and :ref:`treat_missing_header_as_empty ` + * is set to false; The "header2" header is not present and the header + * matcher rule for "header2" will be ignored so it will not match. + * * The header match rule specified header "header3" to a string regex match + * ``^$`` which means an empty string, and + * :ref:`treat_missing_header_as_empty ` + * is set to true; The "header3" header is not present. + * The match rule will treat the "header3" header as an empty header so it will match. + * * The header match rule specified header "header4" to a string regex match + * ``^$`` which means an empty string, and + * :ref:`treat_missing_header_as_empty ` + * is set to false; The "header4" header is not present. + * The match rule for "header4" will be ignored so it will not match. + */ + 'treat_missing_header_as_empty'?: (boolean); /** * Specifies how the header match will be performed to route the request. */ @@ -121,19 +152,21 @@ export interface HeaderMatcher { /** * .. attention:: * - * Internally, Envoy always uses the HTTP/2 *:authority* header to represent the HTTP/1 *Host* - * header. Thus, if attempting to match on *Host*, match on *:authority* instead. + * Internally, Envoy always uses the HTTP/2 ``:authority`` header to represent the HTTP/1 ``Host`` + * header. Thus, if attempting to match on ``Host``, match on ``:authority`` instead. * * .. attention:: * - * To route on HTTP method, use the special HTTP/2 *:method* header. This works for both + * To route on HTTP method, use the special HTTP/2 ``:method`` header. This works for both * HTTP/1 and HTTP/2 as Envoy normalizes headers. E.g., * * .. code-block:: json * * { * "name": ":method", - * "exact_match": "POST" + * "string_match": { + * "exact": "POST" + * } * } * * .. attention:: @@ -143,7 +176,7 @@ export interface HeaderMatcher { * value. * * [#next-major-version: HeaderMatcher should be refactored to use StringMatcher.] - * [#next-free-field: 14] + * [#next-free-field: 15] */ export interface HeaderMatcher__Output { /** @@ -165,8 +198,8 @@ export interface HeaderMatcher__Output { * * Examples: * - * * For range [-10,0), route will match for header value -1, but not for 0, "somestring", 10.9, - * "-1somestring" + * * For range [-10,0), route will match for header value -1, but not for 0, ``somestring``, 10.9, + * ``-1somestring`` */ 'range_match'?: (_envoy_type_v3_Int64Range__Output | null); /** @@ -179,7 +212,7 @@ export interface HeaderMatcher__Output { * * Examples: * - * * The regex ``\d{3}`` does not match the value *1234*, so it will match when inverted. + * * The regex ``\d{3}`` does not match the value ``1234``, so it will match when inverted. * * The range [-10,0) will match the value -1, so it will not match when inverted. */ 'invert_match': (boolean); @@ -190,7 +223,7 @@ export interface HeaderMatcher__Output { * * Examples: * - * * The prefix *abcd* matches the value *abcdxyz*, but not for *abcxyz*. + * * The prefix ``abcd`` matches the value ``abcdxyz``, but not for ``abcxyz``. */ 'prefix_match'?: (string); /** @@ -200,7 +233,7 @@ export interface HeaderMatcher__Output { * * Examples: * - * * The suffix *abcd* matches the value *xyzabcd*, but not for *xyzbcd*. + * * The suffix ``abcd`` matches the value ``xyzabcd``, but not for ``xyzbcd``. */ 'suffix_match'?: (string); /** @@ -218,13 +251,42 @@ export interface HeaderMatcher__Output { * * Examples: * - * * The value *abcd* matches the value *xyzabcdpqr*, but not for *xyzbcdpqr*. + * * The value ``abcd`` matches the value ``xyzabcdpqr``, but not for ``xyzbcdpqr``. */ 'contains_match'?: (string); /** * If specified, header match will be performed based on the string match of the header value. */ 'string_match'?: (_envoy_type_matcher_v3_StringMatcher__Output | null); + /** + * If specified, for any header match rule, if the header match rule specified header + * does not exist, this header value will be treated as empty. Defaults to false. + * + * Examples: + * + * * The header match rule specified header "header1" to range match of [0, 10], + * :ref:`invert_match ` + * is set to true and :ref:`treat_missing_header_as_empty ` + * is set to true; The "header1" header is not present. The match rule will + * treat the "header1" as an empty header. The empty header does not match the range, + * so it will match when inverted. + * * The header match rule specified header "header2" to range match of [0, 10], + * :ref:`invert_match ` + * is set to true and :ref:`treat_missing_header_as_empty ` + * is set to false; The "header2" header is not present and the header + * matcher rule for "header2" will be ignored so it will not match. + * * The header match rule specified header "header3" to a string regex match + * ``^$`` which means an empty string, and + * :ref:`treat_missing_header_as_empty ` + * is set to true; The "header3" header is not present. + * The match rule will treat the "header3" header as an empty header so it will match. + * * The header match rule specified header "header4" to a string regex match + * ``^$`` which means an empty string, and + * :ref:`treat_missing_header_as_empty ` + * is set to false; The "header4" header is not present. + * The match rule for "header4" will be ignored so it will not match. + */ + 'treat_missing_header_as_empty': (boolean); /** * Specifies how the header match will be performed to route the request. */ diff --git a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/QueryParameterMatcher.ts b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/QueryParameterMatcher.ts index d511259b0..b98b6329b 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/QueryParameterMatcher.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/QueryParameterMatcher.ts @@ -10,7 +10,7 @@ import type { StringMatcher as _envoy_type_matcher_v3_StringMatcher, StringMatch export interface QueryParameterMatcher { /** * Specifies the name of a key that must be present in the requested - * *path*'s query string. + * ``path``'s query string. */ 'name'?: (string); /** @@ -32,7 +32,7 @@ export interface QueryParameterMatcher { export interface QueryParameterMatcher__Output { /** * Specifies the name of a key that must be present in the requested - * *path*'s query string. + * ``path``'s query string. */ 'name': (string); /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RateLimit.ts b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RateLimit.ts index f1d49537c..cd47e471a 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RateLimit.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RateLimit.ts @@ -5,9 +5,10 @@ import type { TypedExtensionConfig as _envoy_config_core_v3_TypedExtensionConfig import type { BoolValue as _google_protobuf_BoolValue, BoolValue__Output as _google_protobuf_BoolValue__Output } from '../../../../google/protobuf/BoolValue'; import type { HeaderMatcher as _envoy_config_route_v3_HeaderMatcher, HeaderMatcher__Output as _envoy_config_route_v3_HeaderMatcher__Output } from '../../../../envoy/config/route/v3/HeaderMatcher'; import type { MetadataKey as _envoy_type_metadata_v3_MetadataKey, MetadataKey__Output as _envoy_type_metadata_v3_MetadataKey__Output } from '../../../../envoy/type/metadata/v3/MetadataKey'; +import type { QueryParameterMatcher as _envoy_config_route_v3_QueryParameterMatcher, QueryParameterMatcher__Output as _envoy_config_route_v3_QueryParameterMatcher__Output } from '../../../../envoy/config/route/v3/QueryParameterMatcher'; /** - * [#next-free-field: 10] + * [#next-free-field: 12] */ export interface _envoy_config_route_v3_RateLimit_Action { /** @@ -47,14 +48,28 @@ export interface _envoy_config_route_v3_RateLimit_Action { 'metadata'?: (_envoy_config_route_v3_RateLimit_Action_MetaData | null); /** * Rate limit descriptor extension. See the rate limit descriptor extensions documentation. + * + * :ref:`HTTP matching input functions ` are + * permitted as descriptor extensions. The input functions are only + * looked up if there is no rate limit descriptor extension matching + * the type URL. + * * [#extension-category: envoy.rate_limit_descriptors] */ 'extension'?: (_envoy_config_core_v3_TypedExtensionConfig | null); - 'action_specifier'?: "source_cluster"|"destination_cluster"|"request_headers"|"remote_address"|"generic_key"|"header_value_match"|"dynamic_metadata"|"metadata"|"extension"; + /** + * Rate limit on masked remote address. + */ + 'masked_remote_address'?: (_envoy_config_route_v3_RateLimit_Action_MaskedRemoteAddress | null); + /** + * Rate limit on the existence of query parameters. + */ + 'query_parameter_value_match'?: (_envoy_config_route_v3_RateLimit_Action_QueryParameterValueMatch | null); + 'action_specifier'?: "source_cluster"|"destination_cluster"|"request_headers"|"remote_address"|"generic_key"|"header_value_match"|"dynamic_metadata"|"metadata"|"extension"|"masked_remote_address"|"query_parameter_value_match"; } /** - * [#next-free-field: 10] + * [#next-free-field: 12] */ export interface _envoy_config_route_v3_RateLimit_Action__Output { /** @@ -94,10 +109,24 @@ export interface _envoy_config_route_v3_RateLimit_Action__Output { 'metadata'?: (_envoy_config_route_v3_RateLimit_Action_MetaData__Output | null); /** * Rate limit descriptor extension. See the rate limit descriptor extensions documentation. + * + * :ref:`HTTP matching input functions ` are + * permitted as descriptor extensions. The input functions are only + * looked up if there is no rate limit descriptor extension matching + * the type URL. + * * [#extension-category: envoy.rate_limit_descriptors] */ 'extension'?: (_envoy_config_core_v3_TypedExtensionConfig__Output | null); - 'action_specifier': "source_cluster"|"destination_cluster"|"request_headers"|"remote_address"|"generic_key"|"header_value_match"|"dynamic_metadata"|"metadata"|"extension"; + /** + * Rate limit on masked remote address. + */ + 'masked_remote_address'?: (_envoy_config_route_v3_RateLimit_Action_MaskedRemoteAddress__Output | null); + /** + * Rate limit on the existence of query parameters. + */ + 'query_parameter_value_match'?: (_envoy_config_route_v3_RateLimit_Action_QueryParameterValueMatch__Output | null); + 'action_specifier': "source_cluster"|"destination_cluster"|"request_headers"|"remote_address"|"generic_key"|"header_value_match"|"dynamic_metadata"|"metadata"|"extension"|"masked_remote_address"|"query_parameter_value_match"; } /** @@ -164,7 +193,7 @@ export interface _envoy_config_route_v3_RateLimit_Action_DynamicMetaData { */ 'metadata_key'?: (_envoy_type_metadata_v3_MetadataKey | null); /** - * An optional value to use if *metadata_key* is empty. If not set and + * An optional value to use if ``metadata_key`` is empty. If not set and * no value is present under the metadata_key then no descriptor is generated. */ 'default_value'?: (string); @@ -192,7 +221,7 @@ export interface _envoy_config_route_v3_RateLimit_Action_DynamicMetaData__Output */ 'metadata_key': (_envoy_type_metadata_v3_MetadataKey__Output | null); /** - * An optional value to use if *metadata_key* is empty. If not set and + * An optional value to use if ``metadata_key`` is empty. If not set and * no value is present under the metadata_key then no descriptor is generated. */ 'default_value': (string); @@ -270,6 +299,10 @@ export interface _envoy_config_route_v3_RateLimit_Action_GenericKey__Output { * ("header_match", "") */ export interface _envoy_config_route_v3_RateLimit_Action_HeaderValueMatch { + /** + * The key to use in the descriptor entry. Defaults to ``header_match``. + */ + 'descriptor_key'?: (string); /** * The value to use in the descriptor entry. */ @@ -299,6 +332,10 @@ export interface _envoy_config_route_v3_RateLimit_Action_HeaderValueMatch { * ("header_match", "") */ export interface _envoy_config_route_v3_RateLimit_Action_HeaderValueMatch__Output { + /** + * The key to use in the descriptor entry. Defaults to ``header_match``. + */ + 'descriptor_key': (string); /** * The value to use in the descriptor entry. */ @@ -320,12 +357,67 @@ export interface _envoy_config_route_v3_RateLimit_Action_HeaderValueMatch__Outpu 'headers': (_envoy_config_route_v3_HeaderMatcher__Output)[]; } +/** + * The following descriptor entry is appended to the descriptor and is populated using the + * masked address from :ref:`x-forwarded-for `: + * + * .. code-block:: cpp + * + * ("masked_remote_address", "") + */ +export interface _envoy_config_route_v3_RateLimit_Action_MaskedRemoteAddress { + /** + * Length of prefix mask len for IPv4 (e.g. 0, 32). + * Defaults to 32 when unset. + * For example, trusted address from x-forwarded-for is ``192.168.1.1``, + * the descriptor entry is ("masked_remote_address", "192.168.1.1/32"); + * if mask len is 24, the descriptor entry is ("masked_remote_address", "192.168.1.0/24"). + */ + 'v4_prefix_mask_len'?: (_google_protobuf_UInt32Value | null); + /** + * Length of prefix mask len for IPv6 (e.g. 0, 128). + * Defaults to 128 when unset. + * For example, trusted address from x-forwarded-for is ``2001:abcd:ef01:2345:6789:abcd:ef01:234``, + * the descriptor entry is ("masked_remote_address", "2001:abcd:ef01:2345:6789:abcd:ef01:234/128"); + * if mask len is 64, the descriptor entry is ("masked_remote_address", "2001:abcd:ef01:2345::/64"). + */ + 'v6_prefix_mask_len'?: (_google_protobuf_UInt32Value | null); +} + +/** + * The following descriptor entry is appended to the descriptor and is populated using the + * masked address from :ref:`x-forwarded-for `: + * + * .. code-block:: cpp + * + * ("masked_remote_address", "") + */ +export interface _envoy_config_route_v3_RateLimit_Action_MaskedRemoteAddress__Output { + /** + * Length of prefix mask len for IPv4 (e.g. 0, 32). + * Defaults to 32 when unset. + * For example, trusted address from x-forwarded-for is ``192.168.1.1``, + * the descriptor entry is ("masked_remote_address", "192.168.1.1/32"); + * if mask len is 24, the descriptor entry is ("masked_remote_address", "192.168.1.0/24"). + */ + 'v4_prefix_mask_len': (_google_protobuf_UInt32Value__Output | null); + /** + * Length of prefix mask len for IPv6 (e.g. 0, 128). + * Defaults to 128 when unset. + * For example, trusted address from x-forwarded-for is ``2001:abcd:ef01:2345:6789:abcd:ef01:234``, + * the descriptor entry is ("masked_remote_address", "2001:abcd:ef01:2345:6789:abcd:ef01:234/128"); + * if mask len is 64, the descriptor entry is ("masked_remote_address", "2001:abcd:ef01:2345::/64"). + */ + 'v6_prefix_mask_len': (_google_protobuf_UInt32Value__Output | null); +} + /** * The following descriptor entry is appended when the metadata contains a key value: * * .. code-block:: cpp * * ("", "") + * [#next-free-field: 6] */ export interface _envoy_config_route_v3_RateLimit_Action_MetaData { /** @@ -338,14 +430,21 @@ export interface _envoy_config_route_v3_RateLimit_Action_MetaData { */ 'metadata_key'?: (_envoy_type_metadata_v3_MetadataKey | null); /** - * An optional value to use if *metadata_key* is empty. If not set and - * no value is present under the metadata_key then no descriptor is generated. + * An optional value to use if ``metadata_key`` is empty. If not set and + * no value is present under the metadata_key then ``skip_if_absent`` is followed to + * skip calling the rate limiting service or skip the descriptor. */ 'default_value'?: (string); /** * Source of metadata */ 'source'?: (_envoy_config_route_v3_RateLimit_Action_MetaData_Source | keyof typeof _envoy_config_route_v3_RateLimit_Action_MetaData_Source); + /** + * If set to true, Envoy skips the descriptor while calling rate limiting service + * when ``metadata_key`` is empty and ``default_value`` is not set. By default it skips calling the + * rate limiting service in that case. + */ + 'skip_if_absent'?: (boolean); } /** @@ -354,6 +453,7 @@ export interface _envoy_config_route_v3_RateLimit_Action_MetaData { * .. code-block:: cpp * * ("", "") + * [#next-free-field: 6] */ export interface _envoy_config_route_v3_RateLimit_Action_MetaData__Output { /** @@ -366,14 +466,21 @@ export interface _envoy_config_route_v3_RateLimit_Action_MetaData__Output { */ 'metadata_key': (_envoy_type_metadata_v3_MetadataKey__Output | null); /** - * An optional value to use if *metadata_key* is empty. If not set and - * no value is present under the metadata_key then no descriptor is generated. + * An optional value to use if ``metadata_key`` is empty. If not set and + * no value is present under the metadata_key then ``skip_if_absent`` is followed to + * skip calling the rate limiting service or skip the descriptor. */ 'default_value': (string); /** * Source of metadata */ 'source': (keyof typeof _envoy_config_route_v3_RateLimit_Action_MetaData_Source); + /** + * If set to true, Envoy skips the descriptor while calling rate limiting service + * when ``metadata_key`` is empty and ``default_value`` is not set. By default it skips calling the + * rate limiting service in that case. + */ + 'skip_if_absent': (boolean); } export interface _envoy_config_route_v3_RateLimit_Override { @@ -392,6 +499,72 @@ export interface _envoy_config_route_v3_RateLimit_Override__Output { 'override_specifier': "dynamic_metadata"; } +/** + * The following descriptor entry is appended to the descriptor: + * + * .. code-block:: cpp + * + * ("query_match", "") + */ +export interface _envoy_config_route_v3_RateLimit_Action_QueryParameterValueMatch { + /** + * The key to use in the descriptor entry. Defaults to ``query_match``. + */ + 'descriptor_key'?: (string); + /** + * The value to use in the descriptor entry. + */ + 'descriptor_value'?: (string); + /** + * If set to true, the action will append a descriptor entry when the + * request matches the headers. If set to false, the action will append a + * descriptor entry when the request does not match the headers. The + * default value is true. + */ + 'expect_match'?: (_google_protobuf_BoolValue | null); + /** + * Specifies a set of query parameters that the rate limit action should match + * on. The action will check the request’s query parameters against all the + * specified query parameters in the config. A match will happen if all the + * query parameters in the config are present in the request with the same values + * (or based on presence if the value field is not in the config). + */ + 'query_parameters'?: (_envoy_config_route_v3_QueryParameterMatcher)[]; +} + +/** + * The following descriptor entry is appended to the descriptor: + * + * .. code-block:: cpp + * + * ("query_match", "") + */ +export interface _envoy_config_route_v3_RateLimit_Action_QueryParameterValueMatch__Output { + /** + * The key to use in the descriptor entry. Defaults to ``query_match``. + */ + 'descriptor_key': (string); + /** + * The value to use in the descriptor entry. + */ + 'descriptor_value': (string); + /** + * If set to true, the action will append a descriptor entry when the + * request matches the headers. If set to false, the action will append a + * descriptor entry when the request does not match the headers. The + * default value is true. + */ + 'expect_match': (_google_protobuf_BoolValue__Output | null); + /** + * Specifies a set of query parameters that the rate limit action should match + * on. The action will check the request’s query parameters against all the + * specified query parameters in the config. A match will happen if all the + * query parameters in the config are present in the request with the same values + * (or based on presence if the value field is not in the config). + */ + 'query_parameters': (_envoy_config_route_v3_QueryParameterMatcher__Output)[]; +} + /** * The following descriptor entry is appended to the descriptor and is populated using the * trusted address from :ref:`x-forwarded-for `: @@ -416,7 +589,7 @@ export interface _envoy_config_route_v3_RateLimit_Action_RemoteAddress__Output { /** * The following descriptor entry is appended when a header contains a key that matches the - * *header_name*: + * ``header_name``: * * .. code-block:: cpp * @@ -443,7 +616,7 @@ export interface _envoy_config_route_v3_RateLimit_Action_RequestHeaders { /** * The following descriptor entry is appended when a header contains a key that matches the - * *header_name*: + * ``header_name``: * * .. code-block:: cpp * diff --git a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RedirectAction.ts b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RedirectAction.ts index e6d41fd7b..fd11a681b 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RedirectAction.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RedirectAction.ts @@ -115,10 +115,10 @@ export interface RedirectAction { 'regex_rewrite'?: (_envoy_type_matcher_v3_RegexMatchAndSubstitute | null); /** * When the scheme redirection take place, the following rules apply: - * 1. If the source URI scheme is `http` and the port is explicitly - * set to `:80`, the port will be removed after the redirection - * 2. If the source URI scheme is `https` and the port is explicitly - * set to `:443`, the port will be removed after the redirection + * 1. If the source URI scheme is ``http`` and the port is explicitly + * set to ``:80``, the port will be removed after the redirection + * 2. If the source URI scheme is ``https`` and the port is explicitly + * set to ``:443``, the port will be removed after the redirection */ 'scheme_rewrite_specifier'?: "https_redirect"|"scheme_redirect"; 'path_rewrite_specifier'?: "path_redirect"|"prefix_rewrite"|"regex_rewrite"; @@ -212,10 +212,10 @@ export interface RedirectAction__Output { 'regex_rewrite'?: (_envoy_type_matcher_v3_RegexMatchAndSubstitute__Output | null); /** * When the scheme redirection take place, the following rules apply: - * 1. If the source URI scheme is `http` and the port is explicitly - * set to `:80`, the port will be removed after the redirection - * 2. If the source URI scheme is `https` and the port is explicitly - * set to `:443`, the port will be removed after the redirection + * 1. If the source URI scheme is ``http`` and the port is explicitly + * set to ``:80``, the port will be removed after the redirection + * 2. If the source URI scheme is ``https`` and the port is explicitly + * set to ``:443``, the port will be removed after the redirection */ 'scheme_rewrite_specifier': "https_redirect"|"scheme_redirect"; 'path_rewrite_specifier': "path_redirect"|"prefix_rewrite"|"regex_rewrite"; diff --git a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RetryPolicy.ts b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RetryPolicy.ts index 0d523b52e..d60458728 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RetryPolicy.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RetryPolicy.ts @@ -176,8 +176,8 @@ export interface _envoy_config_route_v3_RetryPolicy_RetryBackOff { 'base_interval'?: (_google_protobuf_Duration | null); /** * Specifies the maximum interval between retries. This parameter is optional, but must be - * greater than or equal to the `base_interval` if set. The default is 10 times the - * `base_interval`. See :ref:`config_http_filters_router_x-envoy-max-retries` for a discussion + * greater than or equal to the ``base_interval`` if set. The default is 10 times the + * ``base_interval``. See :ref:`config_http_filters_router_x-envoy-max-retries` for a discussion * of Envoy's back-off algorithm. */ 'max_interval'?: (_google_protobuf_Duration | null); @@ -193,8 +193,8 @@ export interface _envoy_config_route_v3_RetryPolicy_RetryBackOff__Output { 'base_interval': (_google_protobuf_Duration__Output | null); /** * Specifies the maximum interval between retries. This parameter is optional, but must be - * greater than or equal to the `base_interval` if set. The default is 10 times the - * `base_interval`. See :ref:`config_http_filters_router_x-envoy-max-retries` for a discussion + * greater than or equal to the ``base_interval`` if set. The default is 10 times the + * ``base_interval``. See :ref:`config_http_filters_router_x-envoy-max-retries` for a discussion * of Envoy's back-off algorithm. */ 'max_interval': (_google_protobuf_Duration__Output | null); @@ -293,7 +293,7 @@ export interface RetryPolicy { /** * Specifies parameters that control exponential retry back off. This parameter is optional, in which case the * default base interval is 25 milliseconds or, if set, the current value of the - * `upstream.base_retry_backoff_ms` runtime parameter. The default maximum interval is 10 times + * ``upstream.base_retry_backoff_ms`` runtime parameter. The default maximum interval is 10 times * the base interval. The documentation for :ref:`config_http_filters_router_x-envoy-max-retries` * describes Envoy's back-off algorithm. */ @@ -314,7 +314,7 @@ export interface RetryPolicy { * return a response header like ``Retry-After`` or ``X-RateLimit-Reset`` to * provide feedback to the client on how long to wait before retrying. If * configured, this back-off strategy will be used instead of the - * default exponential back off strategy (configured using `retry_back_off`) + * default exponential back off strategy (configured using ``retry_back_off``) * whenever a response includes the matching headers. */ 'rate_limited_retry_back_off'?: (_envoy_config_route_v3_RetryPolicy_RateLimitedRetryBackOff | null); @@ -405,7 +405,7 @@ export interface RetryPolicy__Output { /** * Specifies parameters that control exponential retry back off. This parameter is optional, in which case the * default base interval is 25 milliseconds or, if set, the current value of the - * `upstream.base_retry_backoff_ms` runtime parameter. The default maximum interval is 10 times + * ``upstream.base_retry_backoff_ms`` runtime parameter. The default maximum interval is 10 times * the base interval. The documentation for :ref:`config_http_filters_router_x-envoy-max-retries` * describes Envoy's back-off algorithm. */ @@ -426,7 +426,7 @@ export interface RetryPolicy__Output { * return a response header like ``Retry-After`` or ``X-RateLimit-Reset`` to * provide feedback to the client on how long to wait before retrying. If * configured, this back-off strategy will be used instead of the - * default exponential back off strategy (configured using `retry_back_off`) + * default exponential back off strategy (configured using ``retry_back_off``) * whenever a response includes the matching headers. */ 'rate_limited_retry_back_off': (_envoy_config_route_v3_RetryPolicy_RateLimitedRetryBackOff__Output | null); diff --git a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/Route.ts b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/Route.ts index d48b554d0..beda9395d 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/Route.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/Route.ts @@ -21,7 +21,7 @@ import type { NonForwardingAction as _envoy_config_route_v3_NonForwardingAction, * * Envoy supports routing on HTTP method via :ref:`header matching * `. - * [#next-free-field: 19] + * [#next-free-field: 20] */ export interface Route { /** @@ -41,7 +41,7 @@ export interface Route { * about the route. It can be used for configuration, stats, and logging. * The metadata should go under the filter namespace that will need it. * For instance, if the metadata is intended for the Router filter, - * the filter name should be specified as *envoy.filters.http.router*. + * the filter name should be specified as ``envoy.filters.http.router``. */ 'metadata'?: (_envoy_config_core_v3_Metadata | null); /** @@ -81,11 +81,15 @@ export interface Route { */ 'request_headers_to_remove'?: (string)[]; /** - * The typed_per_filter_config field can be used to provide route-specific - * configurations for filters. The key should match the filter name, such as - * *envoy.filters.http.buffer* for the HTTP buffer filter. Use of this field is filter - * specific; see the :ref:`HTTP filter documentation ` for - * if and how it is utilized. + * The per_filter_config field can be used to provide route-specific configurations for filters. + * The key should match the :ref:`filter config name + * `. + * The canonical filter name (e.g., ``envoy.filters.http.buffer`` for the HTTP buffer filter) can also + * be used for the backwards compatibility. If there is no entry referred by the filter config name, the + * entry referred by the canonical filter name will be provided to the filters as fallback. + * + * Use of this field is filter specific; + * see the :ref:`HTTP filter documentation ` for if and how it is utilized. * [#comment: An entry's value may be wrapped in a * :ref:`FilterConfig` * message to specify additional options.] @@ -121,6 +125,22 @@ export interface Route { * in Envoy for a filter that directly generates responses for requests. */ 'non_forwarding_action'?: (_envoy_config_route_v3_NonForwardingAction | null); + /** + * The human readable prefix to use when emitting statistics for this endpoint. + * The statistics are rooted at vhost..route.. + * This should be set for highly critical + * endpoints that one wishes to get “per-route” statistics on. + * If not set, endpoint statistics are not generated. + * + * The emitted statistics are the same as those documented for :ref:`virtual clusters `. + * + * .. warning:: + * + * We do not recommend setting up a stat prefix for + * every application endpoint. This is both not easily maintainable and + * statistics use a non-trivial amount of memory(approximately 1KiB per route). + */ + 'stat_prefix'?: (string); 'action'?: "route"|"redirect"|"direct_response"|"filter_action"|"non_forwarding_action"; } @@ -132,7 +152,7 @@ export interface Route { * * Envoy supports routing on HTTP method via :ref:`header matching * `. - * [#next-free-field: 19] + * [#next-free-field: 20] */ export interface Route__Output { /** @@ -152,7 +172,7 @@ export interface Route__Output { * about the route. It can be used for configuration, stats, and logging. * The metadata should go under the filter namespace that will need it. * For instance, if the metadata is intended for the Router filter, - * the filter name should be specified as *envoy.filters.http.router*. + * the filter name should be specified as ``envoy.filters.http.router``. */ 'metadata': (_envoy_config_core_v3_Metadata__Output | null); /** @@ -192,11 +212,15 @@ export interface Route__Output { */ 'request_headers_to_remove': (string)[]; /** - * The typed_per_filter_config field can be used to provide route-specific - * configurations for filters. The key should match the filter name, such as - * *envoy.filters.http.buffer* for the HTTP buffer filter. Use of this field is filter - * specific; see the :ref:`HTTP filter documentation ` for - * if and how it is utilized. + * The per_filter_config field can be used to provide route-specific configurations for filters. + * The key should match the :ref:`filter config name + * `. + * The canonical filter name (e.g., ``envoy.filters.http.buffer`` for the HTTP buffer filter) can also + * be used for the backwards compatibility. If there is no entry referred by the filter config name, the + * entry referred by the canonical filter name will be provided to the filters as fallback. + * + * Use of this field is filter specific; + * see the :ref:`HTTP filter documentation ` for if and how it is utilized. * [#comment: An entry's value may be wrapped in a * :ref:`FilterConfig` * message to specify additional options.] @@ -232,5 +256,21 @@ export interface Route__Output { * in Envoy for a filter that directly generates responses for requests. */ 'non_forwarding_action'?: (_envoy_config_route_v3_NonForwardingAction__Output | null); + /** + * The human readable prefix to use when emitting statistics for this endpoint. + * The statistics are rooted at vhost..route.. + * This should be set for highly critical + * endpoints that one wishes to get “per-route” statistics on. + * If not set, endpoint statistics are not generated. + * + * The emitted statistics are the same as those documented for :ref:`virtual clusters `. + * + * .. warning:: + * + * We do not recommend setting up a stat prefix for + * every application endpoint. This is both not easily maintainable and + * statistics use a non-trivial amount of memory(approximately 1KiB per route). + */ + 'stat_prefix': (string); 'action': "route"|"redirect"|"direct_response"|"filter_action"|"non_forwarding_action"; } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RouteAction.ts b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RouteAction.ts index 43bd51723..9dd8b7c2c 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RouteAction.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RouteAction.ts @@ -13,6 +13,8 @@ import type { UInt32Value as _google_protobuf_UInt32Value, UInt32Value__Output a import type { RegexMatchAndSubstitute as _envoy_type_matcher_v3_RegexMatchAndSubstitute, RegexMatchAndSubstitute__Output as _envoy_type_matcher_v3_RegexMatchAndSubstitute__Output } from '../../../../envoy/type/matcher/v3/RegexMatchAndSubstitute'; import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../../google/protobuf/Any'; import type { InternalRedirectPolicy as _envoy_config_route_v3_InternalRedirectPolicy, InternalRedirectPolicy__Output as _envoy_config_route_v3_InternalRedirectPolicy__Output } from '../../../../envoy/config/route/v3/InternalRedirectPolicy'; +import type { ClusterSpecifierPlugin as _envoy_config_route_v3_ClusterSpecifierPlugin, ClusterSpecifierPlugin__Output as _envoy_config_route_v3_ClusterSpecifierPlugin__Output } from '../../../../envoy/config/route/v3/ClusterSpecifierPlugin'; +import type { TypedExtensionConfig as _envoy_config_core_v3_TypedExtensionConfig, TypedExtensionConfig__Output as _envoy_config_core_v3_TypedExtensionConfig__Output } from '../../../../envoy/config/core/v3/TypedExtensionConfig'; import type { RuntimeFractionalPercent as _envoy_config_core_v3_RuntimeFractionalPercent, RuntimeFractionalPercent__Output as _envoy_config_core_v3_RuntimeFractionalPercent__Output } from '../../../../envoy/config/core/v3/RuntimeFractionalPercent'; import type { ProxyProtocolConfig as _envoy_config_core_v3_ProxyProtocolConfig, ProxyProtocolConfig__Output as _envoy_config_core_v3_ProxyProtocolConfig__Output } from '../../../../envoy/config/core/v3/ProxyProtocolConfig'; @@ -27,6 +29,10 @@ export enum _envoy_config_route_v3_RouteAction_ClusterNotFoundResponseCode { * HTTP status code - 404 Not Found. */ NOT_FOUND = 1, + /** + * HTTP status code - 500 Internal Server Error. + */ + INTERNAL_SERVER_ERROR = 2, } /** @@ -148,8 +154,8 @@ export interface _envoy_config_route_v3_RouteAction_HashPolicy_Cookie__Output { export interface _envoy_config_route_v3_RouteAction_HashPolicy_FilterState { /** * The name of the Object in the per-request filterState, which is an - * Envoy::Http::Hashable object. If there is no data associated with the key, - * or the stored object is not Envoy::Http::Hashable, no hash will be produced. + * Envoy::Hashable object. If there is no data associated with the key, + * or the stored object is not Envoy::Hashable, no hash will be produced. */ 'key'?: (string); } @@ -157,8 +163,8 @@ export interface _envoy_config_route_v3_RouteAction_HashPolicy_FilterState { export interface _envoy_config_route_v3_RouteAction_HashPolicy_FilterState__Output { /** * The name of the Object in the per-request filterState, which is an - * Envoy::Http::Hashable object. If there is no data associated with the key, - * or the stored object is not Envoy::Http::Hashable, no hash will be produced. + * Envoy::Hashable object. If there is no data associated with the key, + * or the stored object is not Envoy::Hashable, no hash will be produced. */ 'key': (string); } @@ -317,12 +323,12 @@ export interface _envoy_config_route_v3_RouteAction_MaxStreamDuration { /** * If present, and the request contains a `grpc-timeout header * `_, use that value as the - * *max_stream_duration*, but limit the applied timeout to the maximum value specified here. - * If set to 0, the `grpc-timeout` header is used without modification. + * ``max_stream_duration``, but limit the applied timeout to the maximum value specified here. + * If set to 0, the ``grpc-timeout`` header is used without modification. */ 'grpc_timeout_header_max'?: (_google_protobuf_Duration | null); /** - * If present, Envoy will adjust the timeout provided by the `grpc-timeout` header by + * If present, Envoy will adjust the timeout provided by the ``grpc-timeout`` header by * subtracting the provided duration from the header. This is useful for allowing Envoy to set * its global timeout to be less than that of the deadline imposed by the calling client, which * makes it more likely that Envoy will handle the timeout instead of having the call canceled @@ -347,12 +353,12 @@ export interface _envoy_config_route_v3_RouteAction_MaxStreamDuration__Output { /** * If present, and the request contains a `grpc-timeout header * `_, use that value as the - * *max_stream_duration*, but limit the applied timeout to the maximum value specified here. - * If set to 0, the `grpc-timeout` header is used without modification. + * ``max_stream_duration``, but limit the applied timeout to the maximum value specified here. + * If set to 0, the ``grpc-timeout`` header is used without modification. */ 'grpc_timeout_header_max': (_google_protobuf_Duration__Output | null); /** - * If present, Envoy will adjust the timeout provided by the `grpc-timeout` header by + * If present, Envoy will adjust the timeout provided by the ``grpc-timeout`` header by * subtracting the provided duration from the header. This is useful for allowing Envoy to set * its global timeout to be less than that of the deadline imposed by the calling client, which * makes it more likely that Envoy will handle the timeout instead of having the call canceled @@ -386,23 +392,47 @@ export interface _envoy_config_route_v3_RouteAction_HashPolicy_QueryParameter__O * respond before returning the response from the primary cluster. All normal statistics are * collected for the shadow cluster making this feature useful for testing. * - * During shadowing, the host/authority header is altered such that *-shadow* is appended. This is - * useful for logging. For example, *cluster1* becomes *cluster1-shadow*. + * During shadowing, the host/authority header is altered such that ``-shadow`` is appended. This is + * useful for logging. For example, ``cluster1`` becomes ``cluster1-shadow``. * * .. note:: * * Shadowing will not be triggered if the primary cluster does not exist. + * + * .. note:: + * + * Shadowing doesn't support Http CONNECT and upgrades. + * [#next-free-field: 6] */ export interface _envoy_config_route_v3_RouteAction_RequestMirrorPolicy { /** + * Only one of ``cluster`` and ``cluster_header`` can be specified. + * [#next-major-version: Need to add back the validation rule: (validate.rules).string = {min_len: 1}] * Specifies the cluster that requests will be mirrored to. The cluster must * exist in the cluster manager configuration. */ 'cluster'?: (string); + /** + * Only one of ``cluster`` and ``cluster_header`` can be specified. + * Envoy will determine the cluster to route to by reading the value of the + * HTTP header named by cluster_header from the request headers. Only the first value in header is used, + * and no shadow request will happen if the value is not found in headers. Envoy will not wait for + * the shadow cluster to respond before returning the response from the primary cluster. + * + * .. attention:: + * + * Internally, Envoy always uses the HTTP/2 ``:authority`` header to represent the HTTP/1 + * ``Host`` header. Thus, if attempting to match on ``Host``, match on ``:authority`` instead. + * + * .. note:: + * + * If the header appears multiple times only the first value is used. + */ + 'cluster_header'?: (string); /** * If not specified, all requests to the target cluster will be mirrored. * - * If specified, this field takes precedence over the `runtime_key` field and requests must also + * If specified, this field takes precedence over the ``runtime_key`` field and requests must also * fall under the percentage of matches indicated by this field. * * For some fraction N/D, a random number in the range [0,D) is selected. If the @@ -422,23 +452,47 @@ export interface _envoy_config_route_v3_RouteAction_RequestMirrorPolicy { * respond before returning the response from the primary cluster. All normal statistics are * collected for the shadow cluster making this feature useful for testing. * - * During shadowing, the host/authority header is altered such that *-shadow* is appended. This is - * useful for logging. For example, *cluster1* becomes *cluster1-shadow*. + * During shadowing, the host/authority header is altered such that ``-shadow`` is appended. This is + * useful for logging. For example, ``cluster1`` becomes ``cluster1-shadow``. * * .. note:: * * Shadowing will not be triggered if the primary cluster does not exist. + * + * .. note:: + * + * Shadowing doesn't support Http CONNECT and upgrades. + * [#next-free-field: 6] */ export interface _envoy_config_route_v3_RouteAction_RequestMirrorPolicy__Output { /** + * Only one of ``cluster`` and ``cluster_header`` can be specified. + * [#next-major-version: Need to add back the validation rule: (validate.rules).string = {min_len: 1}] * Specifies the cluster that requests will be mirrored to. The cluster must * exist in the cluster manager configuration. */ 'cluster': (string); + /** + * Only one of ``cluster`` and ``cluster_header`` can be specified. + * Envoy will determine the cluster to route to by reading the value of the + * HTTP header named by cluster_header from the request headers. Only the first value in header is used, + * and no shadow request will happen if the value is not found in headers. Envoy will not wait for + * the shadow cluster to respond before returning the response from the primary cluster. + * + * .. attention:: + * + * Internally, Envoy always uses the HTTP/2 ``:authority`` header to represent the HTTP/1 + * ``Host`` header. Thus, if attempting to match on ``Host``, match on ``:authority`` instead. + * + * .. note:: + * + * If the header appears multiple times only the first value is used. + */ + 'cluster_header': (string); /** * If not specified, all requests to the target cluster will be mirrored. * - * If specified, this field takes precedence over the `runtime_key` field and requests must also + * If specified, this field takes precedence over the ``runtime_key`` field and requests must also * fall under the percentage of matches indicated by this field. * * For some fraction N/D, a random number in the range [0,D) is selected. If the @@ -509,7 +563,7 @@ export interface _envoy_config_route_v3_RouteAction_UpgradeConfig__Output { } /** - * [#next-free-field: 38] + * [#next-free-field: 42] */ export interface RouteAction { /** @@ -525,8 +579,8 @@ export interface RouteAction { * * .. attention:: * - * Internally, Envoy always uses the HTTP/2 *:authority* header to represent the HTTP/1 - * *Host* header. Thus, if attempting to match on *Host*, match on *:authority* instead. + * Internally, Envoy always uses the HTTP/2 ``:authority`` header to represent the HTTP/1 + * ``Host`` header. Thus, if attempting to match on ``Host``, match on ``:authority`` instead. * * .. note:: * @@ -546,7 +600,7 @@ export interface RouteAction { * in the upstream cluster with metadata matching what's set in this field will be considered * for load balancing. If using :ref:`weighted_clusters * `, metadata will be merged, with values - * provided there taking precedence. The filter name should be specified as *envoy.lb*. + * provided there taking precedence. The filter name should be specified as ``envoy.lb``. */ 'metadata_match'?: (_envoy_config_core_v3_Metadata | null); /** @@ -556,16 +610,16 @@ export interface RouteAction { * place the original path before rewrite into the :ref:`x-envoy-original-path * ` header. * - * Only one of *prefix_rewrite* or - * :ref:`regex_rewrite ` - * may be specified. + * Only one of :ref:`regex_rewrite ` + * :ref:`path_rewrite_policy `, + * or :ref:`prefix_rewrite ` may be specified. * * .. attention:: * * Pay careful attention to the use of trailing slashes in the * :ref:`route's match ` prefix value. * Stripping a prefix from a path requires multiple Routes to handle all cases. For example, - * rewriting * /prefix* to * /* and * /prefix/etc* to * /etc* cannot be done in a single + * rewriting ``/prefix`` to ``/`` and ``/prefix/etc`` to ``/etc`` cannot be done in a single * :ref:`Route `, as shown by the below config entries: * * .. code-block:: yaml @@ -579,21 +633,27 @@ export interface RouteAction { * route: * prefix_rewrite: "/" * - * Having above entries in the config, requests to * /prefix* will be stripped to * /*, while - * requests to * /prefix/etc* will be stripped to * /etc*. + * Having above entries in the config, requests to ``/prefix`` will be stripped to ``/``, while + * requests to ``/prefix/etc`` will be stripped to ``/etc``. */ 'prefix_rewrite'?: (string); /** * Indicates that during forwarding, the host header will be swapped with - * this value. + * this value. Using this option will append the + * :ref:`config_http_conn_man_headers_x-forwarded-host` header if + * :ref:`append_x_forwarded_host ` + * is set. */ 'host_rewrite_literal'?: (string); /** * Indicates that during forwarding, the host header will be swapped with * the hostname of the upstream host chosen by the cluster manager. This * option is applicable only when the destination cluster for a route is of - * type *strict_dns* or *logical_dns*. Setting this to true with other cluster - * types has no effect. + * type ``strict_dns`` or ``logical_dns``. Setting this to true with other cluster types + * has no effect. Using this option will append the + * :ref:`config_http_conn_man_headers_x-forwarded-host` header if + * :ref:`append_x_forwarded_host ` + * is set. */ 'auto_host_rewrite'?: (_google_protobuf_BoolValue | null); /** @@ -650,7 +710,16 @@ export interface RouteAction { */ 'hash_policy'?: (_envoy_config_route_v3_RouteAction_HashPolicy)[]; /** - * Indicates that the route has a CORS policy. + * Indicates that the route has a CORS policy. This field is ignored if related cors policy is + * found in the :ref:`Route.typed_per_filter_config` or + * :ref:`WeightedCluster.ClusterWeight.typed_per_filter_config`. + * + * .. attention:: + * + * This option has been deprecated. Please use + * :ref:`Route.typed_per_filter_config` or + * :ref:`WeightedCluster.ClusterWeight.typed_per_filter_config` + * to configure the CORS HTTP filter. */ 'cors'?: (_envoy_config_route_v3_CorsPolicy | null); /** @@ -665,7 +734,7 @@ export interface RouteAction { * or its default value (infinity) instead of * :ref:`timeout `, but limit the applied timeout * to the maximum value specified here. If configured as 0, the maximum allowed timeout for - * gRPC requests is infinity. If not configured at all, the `grpc-timeout` header is not used + * gRPC requests is infinity. If not configured at all, the ``grpc-timeout`` header is not used * and gRPC requests time out like any other requests using * :ref:`timeout ` or its default. * This can be used to prevent unexpected upstream request timeouts due to potentially long @@ -716,7 +785,7 @@ export interface RouteAction { 'hedge_policy'?: (_envoy_config_route_v3_HedgePolicy | null); /** * Deprecated by :ref:`grpc_timeout_header_offset `. - * If present, Envoy will adjust the timeout provided by the `grpc-timeout` header by subtracting + * If present, Envoy will adjust the timeout provided by the ``grpc-timeout`` header by subtracting * the provided duration from the header. This is useful in allowing Envoy to set its global * timeout to be less than that of the deadline imposed by the calling client, which makes it more * likely that Envoy will handle the timeout instead of having the call canceled by the client. @@ -728,7 +797,10 @@ export interface RouteAction { /** * Indicates that during forwarding, the host header will be swapped with the content of given * downstream or :ref:`custom ` header. - * If header value is empty, host header is left intact. + * If header value is empty, host header is left intact. Using this option will append the + * :ref:`config_http_conn_man_headers_x-forwarded-host` header if + * :ref:`append_x_forwarded_host ` + * is set. * * .. attention:: * @@ -741,7 +813,9 @@ export interface RouteAction { */ 'host_rewrite_header'?: (string); /** - * Indicates that the route has request mirroring policies. + * Specify a set of route request mirroring policies. + * It takes precedence over the virtual host and route config mirror policy entirely. + * That is, policies are not merged, the most specific non-empty one becomes the mirror policies. */ 'request_mirror_policies'?: (_envoy_config_route_v3_RouteAction_RequestMirrorPolicy)[]; /** @@ -771,8 +845,10 @@ export interface RouteAction { * before the rewrite into the :ref:`x-envoy-original-path * ` header. * - * Only one of :ref:`prefix_rewrite ` - * or *regex_rewrite* may be specified. + * Only one of :ref:`regex_rewrite `, + * :ref:`prefix_rewrite `, or + * :ref:`path_rewrite_policy `] + * may be specified. * * Examples using Google's `RE2 `_ engine: * @@ -811,6 +887,10 @@ export interface RouteAction { * Indicates that during forwarding, the host header will be swapped with * the result of the regex substitution executed on path value with query and fragment removed. * This is useful for transitioning variable content between path segment and subdomain. + * Using this option will append the + * :ref:`config_http_conn_man_headers_x-forwarded-host` header if + * :ref:`append_x_forwarded_host ` + * is set. * * For example with the following config: * @@ -822,7 +902,7 @@ export interface RouteAction { * regex: "^/(.+)/.+$" * substitution: \1 * - * Would rewrite the host header to `envoyproxy.io` given the path `/envoyproxy.io/some/path`. + * Would rewrite the host header to ``envoyproxy.io`` given the path ``/envoyproxy.io/some/path``. */ 'host_rewrite_path_regex'?: (_envoy_type_matcher_v3_RegexMatchAndSubstitute | null); /** @@ -830,20 +910,44 @@ export interface RouteAction { */ 'max_stream_duration'?: (_envoy_config_route_v3_RouteAction_MaxStreamDuration | null); /** - * [#not-implemented-hide:] - * Name of the cluster specifier plugin to use to determine the cluster for - * requests on this route. The plugin name must be defined in the associated - * :ref:`envoy_v3_api_field_config.route.v3.RouteConfiguration.cluster_specifier_plugins` - * in the - * :ref:`envoy_v3_api_field_config.core.v3.TypedExtensionConfig.name` field. + * Name of the cluster specifier plugin to use to determine the cluster for requests on this route. + * The cluster specifier plugin name must be defined in the associated + * :ref:`cluster specifier plugins ` + * in the :ref:`name ` field. */ 'cluster_specifier_plugin'?: (string); - 'cluster_specifier'?: "cluster"|"cluster_header"|"weighted_clusters"|"cluster_specifier_plugin"; + /** + * If set, then a host rewrite action (one of + * :ref:`host_rewrite_literal `, + * :ref:`auto_host_rewrite `, + * :ref:`host_rewrite_header `, or + * :ref:`host_rewrite_path_regex `) + * causes the original value of the host header, if any, to be appended to the + * :ref:`config_http_conn_man_headers_x-forwarded-host` HTTP header if it is different to the last value appended. + * This can be disabled by setting the runtime guard `envoy_reloadable_features_append_xfh_idempotent` to false. + */ + 'append_x_forwarded_host'?: (boolean); + /** + * Custom cluster specifier plugin configuration to use to determine the cluster for requests + * on this route. + */ + 'inline_cluster_specifier_plugin'?: (_envoy_config_route_v3_ClusterSpecifierPlugin | null); + /** + * Specifies how to send request over TLS early data. + * If absent, allows `safe HTTP requests `_ to be sent on early data. + * [#extension-category: envoy.route.early_data_policy] + */ + 'early_data_policy'?: (_envoy_config_core_v3_TypedExtensionConfig | null); + /** + * [#extension-category: envoy.path.rewrite] + */ + 'path_rewrite_policy'?: (_envoy_config_core_v3_TypedExtensionConfig | null); + 'cluster_specifier'?: "cluster"|"cluster_header"|"weighted_clusters"|"cluster_specifier_plugin"|"inline_cluster_specifier_plugin"; 'host_rewrite_specifier'?: "host_rewrite_literal"|"auto_host_rewrite"|"host_rewrite_header"|"host_rewrite_path_regex"; } /** - * [#next-free-field: 38] + * [#next-free-field: 42] */ export interface RouteAction__Output { /** @@ -859,8 +963,8 @@ export interface RouteAction__Output { * * .. attention:: * - * Internally, Envoy always uses the HTTP/2 *:authority* header to represent the HTTP/1 - * *Host* header. Thus, if attempting to match on *Host*, match on *:authority* instead. + * Internally, Envoy always uses the HTTP/2 ``:authority`` header to represent the HTTP/1 + * ``Host`` header. Thus, if attempting to match on ``Host``, match on ``:authority`` instead. * * .. note:: * @@ -880,7 +984,7 @@ export interface RouteAction__Output { * in the upstream cluster with metadata matching what's set in this field will be considered * for load balancing. If using :ref:`weighted_clusters * `, metadata will be merged, with values - * provided there taking precedence. The filter name should be specified as *envoy.lb*. + * provided there taking precedence. The filter name should be specified as ``envoy.lb``. */ 'metadata_match': (_envoy_config_core_v3_Metadata__Output | null); /** @@ -890,16 +994,16 @@ export interface RouteAction__Output { * place the original path before rewrite into the :ref:`x-envoy-original-path * ` header. * - * Only one of *prefix_rewrite* or - * :ref:`regex_rewrite ` - * may be specified. + * Only one of :ref:`regex_rewrite ` + * :ref:`path_rewrite_policy `, + * or :ref:`prefix_rewrite ` may be specified. * * .. attention:: * * Pay careful attention to the use of trailing slashes in the * :ref:`route's match ` prefix value. * Stripping a prefix from a path requires multiple Routes to handle all cases. For example, - * rewriting * /prefix* to * /* and * /prefix/etc* to * /etc* cannot be done in a single + * rewriting ``/prefix`` to ``/`` and ``/prefix/etc`` to ``/etc`` cannot be done in a single * :ref:`Route `, as shown by the below config entries: * * .. code-block:: yaml @@ -913,21 +1017,27 @@ export interface RouteAction__Output { * route: * prefix_rewrite: "/" * - * Having above entries in the config, requests to * /prefix* will be stripped to * /*, while - * requests to * /prefix/etc* will be stripped to * /etc*. + * Having above entries in the config, requests to ``/prefix`` will be stripped to ``/``, while + * requests to ``/prefix/etc`` will be stripped to ``/etc``. */ 'prefix_rewrite': (string); /** * Indicates that during forwarding, the host header will be swapped with - * this value. + * this value. Using this option will append the + * :ref:`config_http_conn_man_headers_x-forwarded-host` header if + * :ref:`append_x_forwarded_host ` + * is set. */ 'host_rewrite_literal'?: (string); /** * Indicates that during forwarding, the host header will be swapped with * the hostname of the upstream host chosen by the cluster manager. This * option is applicable only when the destination cluster for a route is of - * type *strict_dns* or *logical_dns*. Setting this to true with other cluster - * types has no effect. + * type ``strict_dns`` or ``logical_dns``. Setting this to true with other cluster types + * has no effect. Using this option will append the + * :ref:`config_http_conn_man_headers_x-forwarded-host` header if + * :ref:`append_x_forwarded_host ` + * is set. */ 'auto_host_rewrite'?: (_google_protobuf_BoolValue__Output | null); /** @@ -984,7 +1094,16 @@ export interface RouteAction__Output { */ 'hash_policy': (_envoy_config_route_v3_RouteAction_HashPolicy__Output)[]; /** - * Indicates that the route has a CORS policy. + * Indicates that the route has a CORS policy. This field is ignored if related cors policy is + * found in the :ref:`Route.typed_per_filter_config` or + * :ref:`WeightedCluster.ClusterWeight.typed_per_filter_config`. + * + * .. attention:: + * + * This option has been deprecated. Please use + * :ref:`Route.typed_per_filter_config` or + * :ref:`WeightedCluster.ClusterWeight.typed_per_filter_config` + * to configure the CORS HTTP filter. */ 'cors': (_envoy_config_route_v3_CorsPolicy__Output | null); /** @@ -999,7 +1118,7 @@ export interface RouteAction__Output { * or its default value (infinity) instead of * :ref:`timeout `, but limit the applied timeout * to the maximum value specified here. If configured as 0, the maximum allowed timeout for - * gRPC requests is infinity. If not configured at all, the `grpc-timeout` header is not used + * gRPC requests is infinity. If not configured at all, the ``grpc-timeout`` header is not used * and gRPC requests time out like any other requests using * :ref:`timeout ` or its default. * This can be used to prevent unexpected upstream request timeouts due to potentially long @@ -1050,7 +1169,7 @@ export interface RouteAction__Output { 'hedge_policy': (_envoy_config_route_v3_HedgePolicy__Output | null); /** * Deprecated by :ref:`grpc_timeout_header_offset `. - * If present, Envoy will adjust the timeout provided by the `grpc-timeout` header by subtracting + * If present, Envoy will adjust the timeout provided by the ``grpc-timeout`` header by subtracting * the provided duration from the header. This is useful in allowing Envoy to set its global * timeout to be less than that of the deadline imposed by the calling client, which makes it more * likely that Envoy will handle the timeout instead of having the call canceled by the client. @@ -1062,7 +1181,10 @@ export interface RouteAction__Output { /** * Indicates that during forwarding, the host header will be swapped with the content of given * downstream or :ref:`custom ` header. - * If header value is empty, host header is left intact. + * If header value is empty, host header is left intact. Using this option will append the + * :ref:`config_http_conn_man_headers_x-forwarded-host` header if + * :ref:`append_x_forwarded_host ` + * is set. * * .. attention:: * @@ -1075,7 +1197,9 @@ export interface RouteAction__Output { */ 'host_rewrite_header'?: (string); /** - * Indicates that the route has request mirroring policies. + * Specify a set of route request mirroring policies. + * It takes precedence over the virtual host and route config mirror policy entirely. + * That is, policies are not merged, the most specific non-empty one becomes the mirror policies. */ 'request_mirror_policies': (_envoy_config_route_v3_RouteAction_RequestMirrorPolicy__Output)[]; /** @@ -1105,8 +1229,10 @@ export interface RouteAction__Output { * before the rewrite into the :ref:`x-envoy-original-path * ` header. * - * Only one of :ref:`prefix_rewrite ` - * or *regex_rewrite* may be specified. + * Only one of :ref:`regex_rewrite `, + * :ref:`prefix_rewrite `, or + * :ref:`path_rewrite_policy `] + * may be specified. * * Examples using Google's `RE2 `_ engine: * @@ -1145,6 +1271,10 @@ export interface RouteAction__Output { * Indicates that during forwarding, the host header will be swapped with * the result of the regex substitution executed on path value with query and fragment removed. * This is useful for transitioning variable content between path segment and subdomain. + * Using this option will append the + * :ref:`config_http_conn_man_headers_x-forwarded-host` header if + * :ref:`append_x_forwarded_host ` + * is set. * * For example with the following config: * @@ -1156,7 +1286,7 @@ export interface RouteAction__Output { * regex: "^/(.+)/.+$" * substitution: \1 * - * Would rewrite the host header to `envoyproxy.io` given the path `/envoyproxy.io/some/path`. + * Would rewrite the host header to ``envoyproxy.io`` given the path ``/envoyproxy.io/some/path``. */ 'host_rewrite_path_regex'?: (_envoy_type_matcher_v3_RegexMatchAndSubstitute__Output | null); /** @@ -1164,14 +1294,38 @@ export interface RouteAction__Output { */ 'max_stream_duration': (_envoy_config_route_v3_RouteAction_MaxStreamDuration__Output | null); /** - * [#not-implemented-hide:] - * Name of the cluster specifier plugin to use to determine the cluster for - * requests on this route. The plugin name must be defined in the associated - * :ref:`envoy_v3_api_field_config.route.v3.RouteConfiguration.cluster_specifier_plugins` - * in the - * :ref:`envoy_v3_api_field_config.core.v3.TypedExtensionConfig.name` field. + * Name of the cluster specifier plugin to use to determine the cluster for requests on this route. + * The cluster specifier plugin name must be defined in the associated + * :ref:`cluster specifier plugins ` + * in the :ref:`name ` field. */ 'cluster_specifier_plugin'?: (string); - 'cluster_specifier': "cluster"|"cluster_header"|"weighted_clusters"|"cluster_specifier_plugin"; + /** + * If set, then a host rewrite action (one of + * :ref:`host_rewrite_literal `, + * :ref:`auto_host_rewrite `, + * :ref:`host_rewrite_header `, or + * :ref:`host_rewrite_path_regex `) + * causes the original value of the host header, if any, to be appended to the + * :ref:`config_http_conn_man_headers_x-forwarded-host` HTTP header if it is different to the last value appended. + * This can be disabled by setting the runtime guard `envoy_reloadable_features_append_xfh_idempotent` to false. + */ + 'append_x_forwarded_host': (boolean); + /** + * Custom cluster specifier plugin configuration to use to determine the cluster for requests + * on this route. + */ + 'inline_cluster_specifier_plugin'?: (_envoy_config_route_v3_ClusterSpecifierPlugin__Output | null); + /** + * Specifies how to send request over TLS early data. + * If absent, allows `safe HTTP requests `_ to be sent on early data. + * [#extension-category: envoy.route.early_data_policy] + */ + 'early_data_policy': (_envoy_config_core_v3_TypedExtensionConfig__Output | null); + /** + * [#extension-category: envoy.path.rewrite] + */ + 'path_rewrite_policy': (_envoy_config_core_v3_TypedExtensionConfig__Output | null); + 'cluster_specifier': "cluster"|"cluster_header"|"weighted_clusters"|"cluster_specifier_plugin"|"inline_cluster_specifier_plugin"; 'host_rewrite_specifier': "host_rewrite_literal"|"auto_host_rewrite"|"host_rewrite_header"|"host_rewrite_path_regex"; } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RouteConfiguration.ts b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RouteConfiguration.ts index 516f4b06b..1eddc6528 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RouteConfiguration.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RouteConfiguration.ts @@ -6,9 +6,11 @@ import type { BoolValue as _google_protobuf_BoolValue, BoolValue__Output as _goo import type { Vhds as _envoy_config_route_v3_Vhds, Vhds__Output as _envoy_config_route_v3_Vhds__Output } from '../../../../envoy/config/route/v3/Vhds'; import type { UInt32Value as _google_protobuf_UInt32Value, UInt32Value__Output as _google_protobuf_UInt32Value__Output } from '../../../../google/protobuf/UInt32Value'; import type { ClusterSpecifierPlugin as _envoy_config_route_v3_ClusterSpecifierPlugin, ClusterSpecifierPlugin__Output as _envoy_config_route_v3_ClusterSpecifierPlugin__Output } from '../../../../envoy/config/route/v3/ClusterSpecifierPlugin'; +import type { _envoy_config_route_v3_RouteAction_RequestMirrorPolicy, _envoy_config_route_v3_RouteAction_RequestMirrorPolicy__Output } from '../../../../envoy/config/route/v3/RouteAction'; +import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../../google/protobuf/Any'; /** - * [#next-free-field: 13] + * [#next-free-field: 17] */ export interface RouteConfiguration { /** @@ -75,10 +77,10 @@ export interface RouteConfiguration { 'request_headers_to_remove'?: (string)[]; /** * An array of virtual hosts will be dynamically loaded via the VHDS API. - * Both *virtual_hosts* and *vhds* fields will be used when present. *virtual_hosts* can be used - * for a base routing table or for infrequently changing virtual hosts. *vhds* is used for + * Both ``virtual_hosts`` and ``vhds`` fields will be used when present. ``virtual_hosts`` can be used + * for a base routing table or for infrequently changing virtual hosts. ``vhds`` is used for * on-demand discovery of virtual hosts. The contents of these two fields will be merged to - * generate a routing table for a given RouteConfiguration, with *vhds* derived configuration + * generate a routing table for a given RouteConfiguration, with ``vhds`` derived configuration * taking precedence. */ 'vhds'?: (_envoy_config_route_v3_Vhds | null); @@ -91,8 +93,6 @@ export interface RouteConfiguration { * * To allow setting overrides at the route or virtual host level, this order can be reversed * by setting this option to true. Defaults to false. - * - * [#next-major-version: In the v3 API, this will default to true.] */ 'most_specific_header_mutations_wins'?: (boolean); /** @@ -109,16 +109,49 @@ export interface RouteConfiguration { */ 'max_direct_response_body_size_bytes'?: (_google_protobuf_UInt32Value | null); /** - * [#not-implemented-hide:] * A list of plugins and their configurations which may be used by a - * :ref:`envoy_v3_api_field_config.route.v3.RouteAction.cluster_specifier_plugin` - * within the route. All *extension.name* fields in this list must be unique. + * :ref:`cluster specifier plugin name ` + * within the route. All ``extension.name`` fields in this list must be unique. */ 'cluster_specifier_plugins'?: (_envoy_config_route_v3_ClusterSpecifierPlugin)[]; + /** + * Specify a set of default request mirroring policies which apply to all routes under its virtual hosts. + * Note that policies are not merged, the most specific non-empty one becomes the mirror policies. + */ + 'request_mirror_policies'?: (_envoy_config_route_v3_RouteAction_RequestMirrorPolicy)[]; + /** + * By default, port in :authority header (if any) is used in host matching. + * With this option enabled, Envoy will ignore the port number in the :authority header (if any) when picking VirtualHost. + * NOTE: this option will not strip the port number (if any) contained in route config + * :ref:`envoy_v3_api_msg_config.route.v3.VirtualHost`.domains field. + */ + 'ignore_port_in_host_matching'?: (boolean); + /** + * Ignore path-parameters in path-matching. + * Before RFC3986, URI were like(RFC1808): :///;?# + * Envoy by default takes ":path" as ";". + * For users who want to only match path on the "" portion, this option should be true. + */ + 'ignore_path_parameters_in_path_matching'?: (boolean); + /** + * The typed_per_filter_config field can be used to provide RouteConfiguration level per filter config. + * The key should match the :ref:`filter config name + * `. + * The canonical filter name (e.g., ``envoy.filters.http.buffer`` for the HTTP buffer filter) can also + * be used for the backwards compatibility. If there is no entry referred by the filter config name, the + * entry referred by the canonical filter name will be provided to the filters as fallback. + * + * Use of this field is filter specific; + * see the :ref:`HTTP filter documentation ` for if and how it is utilized. + * [#comment: An entry's value may be wrapped in a + * :ref:`FilterConfig` + * message to specify additional options.] + */ + 'typed_per_filter_config'?: ({[key: string]: _google_protobuf_Any}); } /** - * [#next-free-field: 13] + * [#next-free-field: 17] */ export interface RouteConfiguration__Output { /** @@ -185,10 +218,10 @@ export interface RouteConfiguration__Output { 'request_headers_to_remove': (string)[]; /** * An array of virtual hosts will be dynamically loaded via the VHDS API. - * Both *virtual_hosts* and *vhds* fields will be used when present. *virtual_hosts* can be used - * for a base routing table or for infrequently changing virtual hosts. *vhds* is used for + * Both ``virtual_hosts`` and ``vhds`` fields will be used when present. ``virtual_hosts`` can be used + * for a base routing table or for infrequently changing virtual hosts. ``vhds`` is used for * on-demand discovery of virtual hosts. The contents of these two fields will be merged to - * generate a routing table for a given RouteConfiguration, with *vhds* derived configuration + * generate a routing table for a given RouteConfiguration, with ``vhds`` derived configuration * taking precedence. */ 'vhds': (_envoy_config_route_v3_Vhds__Output | null); @@ -201,8 +234,6 @@ export interface RouteConfiguration__Output { * * To allow setting overrides at the route or virtual host level, this order can be reversed * by setting this option to true. Defaults to false. - * - * [#next-major-version: In the v3 API, this will default to true.] */ 'most_specific_header_mutations_wins': (boolean); /** @@ -219,10 +250,43 @@ export interface RouteConfiguration__Output { */ 'max_direct_response_body_size_bytes': (_google_protobuf_UInt32Value__Output | null); /** - * [#not-implemented-hide:] * A list of plugins and their configurations which may be used by a - * :ref:`envoy_v3_api_field_config.route.v3.RouteAction.cluster_specifier_plugin` - * within the route. All *extension.name* fields in this list must be unique. + * :ref:`cluster specifier plugin name ` + * within the route. All ``extension.name`` fields in this list must be unique. */ 'cluster_specifier_plugins': (_envoy_config_route_v3_ClusterSpecifierPlugin__Output)[]; + /** + * Specify a set of default request mirroring policies which apply to all routes under its virtual hosts. + * Note that policies are not merged, the most specific non-empty one becomes the mirror policies. + */ + 'request_mirror_policies': (_envoy_config_route_v3_RouteAction_RequestMirrorPolicy__Output)[]; + /** + * By default, port in :authority header (if any) is used in host matching. + * With this option enabled, Envoy will ignore the port number in the :authority header (if any) when picking VirtualHost. + * NOTE: this option will not strip the port number (if any) contained in route config + * :ref:`envoy_v3_api_msg_config.route.v3.VirtualHost`.domains field. + */ + 'ignore_port_in_host_matching': (boolean); + /** + * Ignore path-parameters in path-matching. + * Before RFC3986, URI were like(RFC1808): :///;?# + * Envoy by default takes ":path" as ";". + * For users who want to only match path on the "" portion, this option should be true. + */ + 'ignore_path_parameters_in_path_matching': (boolean); + /** + * The typed_per_filter_config field can be used to provide RouteConfiguration level per filter config. + * The key should match the :ref:`filter config name + * `. + * The canonical filter name (e.g., ``envoy.filters.http.buffer`` for the HTTP buffer filter) can also + * be used for the backwards compatibility. If there is no entry referred by the filter config name, the + * entry referred by the canonical filter name will be provided to the filters as fallback. + * + * Use of this field is filter specific; + * see the :ref:`HTTP filter documentation ` for if and how it is utilized. + * [#comment: An entry's value may be wrapped in a + * :ref:`FilterConfig` + * message to specify additional options.] + */ + 'typed_per_filter_config': ({[key: string]: _google_protobuf_Any__Output}); } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RouteList.ts b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RouteList.ts new file mode 100644 index 000000000..676d38554 --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RouteList.ts @@ -0,0 +1,25 @@ +// Original file: deps/envoy-api/envoy/config/route/v3/route_components.proto + +import type { Route as _envoy_config_route_v3_Route, Route__Output as _envoy_config_route_v3_Route__Output } from '../../../../envoy/config/route/v3/Route'; + +/** + * This can be used in route matcher :ref:`VirtualHost.matcher `. + * When the matcher matches, routes will be matched and run. + */ +export interface RouteList { + /** + * The list of routes that will be matched and run, in order. The first route that matches will be used. + */ + 'routes'?: (_envoy_config_route_v3_Route)[]; +} + +/** + * This can be used in route matcher :ref:`VirtualHost.matcher `. + * When the matcher matches, routes will be matched and run. + */ +export interface RouteList__Output { + /** + * The list of routes that will be matched and run, in order. The first route that matches will be used. + */ + 'routes': (_envoy_config_route_v3_Route__Output)[]; +} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RouteMatch.ts b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RouteMatch.ts index 9d872ed18..06982a82f 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RouteMatch.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RouteMatch.ts @@ -6,6 +6,7 @@ import type { QueryParameterMatcher as _envoy_config_route_v3_QueryParameterMatc import type { RuntimeFractionalPercent as _envoy_config_core_v3_RuntimeFractionalPercent, RuntimeFractionalPercent__Output as _envoy_config_core_v3_RuntimeFractionalPercent__Output } from '../../../../envoy/config/core/v3/RuntimeFractionalPercent'; import type { RegexMatcher as _envoy_type_matcher_v3_RegexMatcher, RegexMatcher__Output as _envoy_type_matcher_v3_RegexMatcher__Output } from '../../../../envoy/type/matcher/v3/RegexMatcher'; import type { MetadataMatcher as _envoy_type_matcher_v3_MetadataMatcher, MetadataMatcher__Output as _envoy_type_matcher_v3_MetadataMatcher__Output } from '../../../../envoy/type/matcher/v3/MetadataMatcher'; +import type { TypedExtensionConfig as _envoy_config_core_v3_TypedExtensionConfig, TypedExtensionConfig__Output as _envoy_config_core_v3_TypedExtensionConfig__Output } from '../../../../envoy/config/core/v3/TypedExtensionConfig'; /** * An extensible message for matching CONNECT requests. @@ -52,17 +53,17 @@ export interface _envoy_config_route_v3_RouteMatch_TlsContextMatchOptions__Outpu } /** - * [#next-free-field: 14] + * [#next-free-field: 16] */ export interface RouteMatch { /** * If specified, the route is a prefix rule meaning that the prefix must - * match the beginning of the *:path* header. + * match the beginning of the ``:path`` header. */ 'prefix'?: (string); /** * If specified, the route is an exact path rule meaning that the path must - * exactly match the *:path* header once the query string is removed. + * exactly match the ``:path`` header once the query string is removed. */ 'path'?: (string); /** @@ -80,9 +81,9 @@ export interface RouteMatch { 'headers'?: (_envoy_config_route_v3_HeaderMatcher)[]; /** * Specifies a set of URL query parameters on which the route should - * match. The router will check the query string from the *path* header + * match. The router will check the query string from the ``path`` header * against all the specified query parameters. If the number of specified - * query parameters is nonzero, they all must match the *path* header's + * query parameters is nonzero, they all must match the ``path`` header's * query string for a match to occur. * * .. note:: @@ -121,9 +122,9 @@ export interface RouteMatch { 'runtime_fraction'?: (_envoy_config_core_v3_RuntimeFractionalPercent | null); /** * If specified, the route is a regular expression rule meaning that the - * regex must match the *:path* header once the query string is removed. The entire path + * regex must match the ``:path`` header once the query string is removed. The entire path * (without the query string) must match the regex. The rule will not match if only a - * subsequence of the *:path* header matches the regex. + * subsequence of the ``:path`` header matches the regex. * * [#next-major-version: In the v3 API we should redo how path specification works such * that we utilize StringMatcher, and additionally have consistent options around whether we @@ -160,21 +161,37 @@ export interface RouteMatch { * dynamic metadata for a match to occur. */ 'dynamic_metadata'?: (_envoy_type_matcher_v3_MetadataMatcher)[]; - 'path_specifier'?: "prefix"|"path"|"safe_regex"|"connect_matcher"; + /** + * If specified, the route is a path-separated prefix rule meaning that the + * ``:path`` header (without the query string) must either exactly match the + * ``path_separated_prefix`` or have it as a prefix, followed by ``/`` + * + * For example, ``/api/dev`` would match + * ``/api/dev``, ``/api/dev/``, ``/api/dev/v1``, and ``/api/dev?param=true`` + * but would not match ``/api/developer`` + * + * Expect the value to not contain ``?`` or ``#`` and not to end in ``/`` + */ + 'path_separated_prefix'?: (string); + /** + * [#extension-category: envoy.path.match] + */ + 'path_match_policy'?: (_envoy_config_core_v3_TypedExtensionConfig | null); + 'path_specifier'?: "prefix"|"path"|"safe_regex"|"connect_matcher"|"path_separated_prefix"|"path_match_policy"; } /** - * [#next-free-field: 14] + * [#next-free-field: 16] */ export interface RouteMatch__Output { /** * If specified, the route is a prefix rule meaning that the prefix must - * match the beginning of the *:path* header. + * match the beginning of the ``:path`` header. */ 'prefix'?: (string); /** * If specified, the route is an exact path rule meaning that the path must - * exactly match the *:path* header once the query string is removed. + * exactly match the ``:path`` header once the query string is removed. */ 'path'?: (string); /** @@ -192,9 +209,9 @@ export interface RouteMatch__Output { 'headers': (_envoy_config_route_v3_HeaderMatcher__Output)[]; /** * Specifies a set of URL query parameters on which the route should - * match. The router will check the query string from the *path* header + * match. The router will check the query string from the ``path`` header * against all the specified query parameters. If the number of specified - * query parameters is nonzero, they all must match the *path* header's + * query parameters is nonzero, they all must match the ``path`` header's * query string for a match to occur. * * .. note:: @@ -233,9 +250,9 @@ export interface RouteMatch__Output { 'runtime_fraction': (_envoy_config_core_v3_RuntimeFractionalPercent__Output | null); /** * If specified, the route is a regular expression rule meaning that the - * regex must match the *:path* header once the query string is removed. The entire path + * regex must match the ``:path`` header once the query string is removed. The entire path * (without the query string) must match the regex. The rule will not match if only a - * subsequence of the *:path* header matches the regex. + * subsequence of the ``:path`` header matches the regex. * * [#next-major-version: In the v3 API we should redo how path specification works such * that we utilize StringMatcher, and additionally have consistent options around whether we @@ -272,5 +289,21 @@ export interface RouteMatch__Output { * dynamic metadata for a match to occur. */ 'dynamic_metadata': (_envoy_type_matcher_v3_MetadataMatcher__Output)[]; - 'path_specifier': "prefix"|"path"|"safe_regex"|"connect_matcher"; + /** + * If specified, the route is a path-separated prefix rule meaning that the + * ``:path`` header (without the query string) must either exactly match the + * ``path_separated_prefix`` or have it as a prefix, followed by ``/`` + * + * For example, ``/api/dev`` would match + * ``/api/dev``, ``/api/dev/``, ``/api/dev/v1``, and ``/api/dev?param=true`` + * but would not match ``/api/developer`` + * + * Expect the value to not contain ``?`` or ``#`` and not to end in ``/`` + */ + 'path_separated_prefix'?: (string); + /** + * [#extension-category: envoy.path.match] + */ + 'path_match_policy'?: (_envoy_config_core_v3_TypedExtensionConfig__Output | null); + 'path_specifier': "prefix"|"path"|"safe_regex"|"connect_matcher"|"path_separated_prefix"|"path_match_policy"; } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/ScopedRouteConfiguration.ts b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/ScopedRouteConfiguration.ts index 5865eadd3..e13c537d4 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/ScopedRouteConfiguration.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/ScopedRouteConfiguration.ts @@ -1,5 +1,6 @@ // Original file: deps/envoy-api/envoy/config/route/v3/scoped_route.proto +import type { RouteConfiguration as _envoy_config_route_v3_RouteConfiguration, RouteConfiguration__Output as _envoy_config_route_v3_RouteConfiguration__Output } from '../../../../envoy/config/route/v3/RouteConfiguration'; export interface _envoy_config_route_v3_ScopedRouteConfiguration_Key_Fragment { /** @@ -52,7 +53,10 @@ export interface _envoy_config_route_v3_ScopedRouteConfiguration_Key__Output { /** * Specifies a routing scope, which associates a * :ref:`Key` to a - * :ref:`envoy_v3_api_msg_config.route.v3.RouteConfiguration` (identified by its resource name). + * :ref:`envoy_v3_api_msg_config.route.v3.RouteConfiguration`. + * The :ref:`envoy_v3_api_msg_config.route.v3.RouteConfiguration` can be obtained dynamically + * via RDS (:ref:`route_configuration_name`) + * or specified inline (:ref:`route_configuration`). * * The HTTP connection manager builds up a table consisting of these Key to * RouteConfiguration mappings, and looks up the RouteConfiguration to use per @@ -106,8 +110,10 @@ export interface _envoy_config_route_v3_ScopedRouteConfiguration_Key__Output { * Host: foo.com * X-Route-Selector: vip=172.10.10.20 * - * would result in the routing table defined by the `route-config1` + * would result in the routing table defined by the ``route-config1`` * RouteConfiguration being assigned to the HTTP request/stream. + * + * [#next-free-field: 6] */ export interface ScopedRouteConfiguration { /** @@ -128,12 +134,19 @@ export interface ScopedRouteConfiguration { * Whether the RouteConfiguration should be loaded on demand. */ 'on_demand'?: (boolean); + /** + * The :ref:`envoy_v3_api_msg_config.route.v3.RouteConfiguration` associated with the scope. + */ + 'route_configuration'?: (_envoy_config_route_v3_RouteConfiguration | null); } /** * Specifies a routing scope, which associates a * :ref:`Key` to a - * :ref:`envoy_v3_api_msg_config.route.v3.RouteConfiguration` (identified by its resource name). + * :ref:`envoy_v3_api_msg_config.route.v3.RouteConfiguration`. + * The :ref:`envoy_v3_api_msg_config.route.v3.RouteConfiguration` can be obtained dynamically + * via RDS (:ref:`route_configuration_name`) + * or specified inline (:ref:`route_configuration`). * * The HTTP connection manager builds up a table consisting of these Key to * RouteConfiguration mappings, and looks up the RouteConfiguration to use per @@ -187,8 +200,10 @@ export interface ScopedRouteConfiguration { * Host: foo.com * X-Route-Selector: vip=172.10.10.20 * - * would result in the routing table defined by the `route-config1` + * would result in the routing table defined by the ``route-config1`` * RouteConfiguration being assigned to the HTTP request/stream. + * + * [#next-free-field: 6] */ export interface ScopedRouteConfiguration__Output { /** @@ -209,4 +224,8 @@ export interface ScopedRouteConfiguration__Output { * Whether the RouteConfiguration should be loaded on demand. */ 'on_demand': (boolean); + /** + * The :ref:`envoy_v3_api_msg_config.route.v3.RouteConfiguration` associated with the scope. + */ + 'route_configuration': (_envoy_config_route_v3_RouteConfiguration__Output | null); } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/Tracing.ts b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/Tracing.ts index 962e9d51a..29995243b 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/Tracing.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/Tracing.ts @@ -8,7 +8,7 @@ export interface Tracing { * Target percentage of requests managed by this HTTP connection manager that will be force * traced if the :ref:`x-client-trace-id ` * header is set. This field is a direct analog for the runtime variable - * 'tracing.client_sampling' in the :ref:`HTTP Connection Manager + * 'tracing.client_enabled' in the :ref:`HTTP Connection Manager * `. * Default: 100% */ @@ -48,7 +48,7 @@ export interface Tracing__Output { * Target percentage of requests managed by this HTTP connection manager that will be force * traced if the :ref:`x-client-trace-id ` * header is set. This field is a direct analog for the runtime variable - * 'tracing.client_sampling' in the :ref:`HTTP Connection Manager + * 'tracing.client_enabled' in the :ref:`HTTP Connection Manager * `. * Default: 100% */ diff --git a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/VirtualCluster.ts b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/VirtualCluster.ts index 7674da733..3c65d6a34 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/VirtualCluster.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/VirtualCluster.ts @@ -30,7 +30,7 @@ export interface VirtualCluster { 'name'?: (string); /** * Specifies a list of header matchers to use for matching requests. Each specified header must - * match. The pseudo-headers `:path` and `:method` can be used to match the request path and + * match. The pseudo-headers ``:path`` and ``:method`` can be used to match the request path and * method, respectively. */ 'headers'?: (_envoy_config_route_v3_HeaderMatcher)[]; @@ -64,7 +64,7 @@ export interface VirtualCluster__Output { 'name': (string); /** * Specifies a list of header matchers to use for matching requests. Each specified header must - * match. The pseudo-headers `:path` and `:method` can be used to match the request path and + * match. The pseudo-headers ``:path`` and ``:method`` can be used to match the request path and * method, respectively. */ 'headers': (_envoy_config_route_v3_HeaderMatcher__Output)[]; diff --git a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/VirtualHost.ts b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/VirtualHost.ts index 017900ed9..b2c344fff 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/VirtualHost.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/VirtualHost.ts @@ -9,6 +9,8 @@ import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__ import type { RetryPolicy as _envoy_config_route_v3_RetryPolicy, RetryPolicy__Output as _envoy_config_route_v3_RetryPolicy__Output } from '../../../../envoy/config/route/v3/RetryPolicy'; import type { HedgePolicy as _envoy_config_route_v3_HedgePolicy, HedgePolicy__Output as _envoy_config_route_v3_HedgePolicy__Output } from '../../../../envoy/config/route/v3/HedgePolicy'; import type { UInt32Value as _google_protobuf_UInt32Value, UInt32Value__Output as _google_protobuf_UInt32Value__Output } from '../../../../google/protobuf/UInt32Value'; +import type { Matcher as _xds_type_matcher_v3_Matcher, Matcher__Output as _xds_type_matcher_v3_Matcher__Output } from '../../../../xds/type/matcher/v3/Matcher'; +import type { _envoy_config_route_v3_RouteAction_RequestMirrorPolicy, _envoy_config_route_v3_RouteAction_RequestMirrorPolicy__Output } from '../../../../envoy/config/route/v3/RouteAction'; // Original file: deps/envoy-api/envoy/config/route/v3/route_components.proto @@ -35,7 +37,7 @@ export enum _envoy_config_route_v3_VirtualHost_TlsRequirementType { * host header. This allows a single listener to service multiple top level domain path trees. Once * a virtual host is selected based on the domain, the routes are processed in order to see which * upstream cluster to route to or whether to perform a redirect. - * [#next-free-field: 21] + * [#next-free-field: 24] */ export interface VirtualHost { /** @@ -67,6 +69,7 @@ export interface VirtualHost { /** * The list of routes that will be matched, in order, for incoming requests. * The first route that matches will be used. + * Only one of this and ``matcher`` can be specified. */ 'routes'?: (_envoy_config_route_v3_Route)[]; /** @@ -94,7 +97,15 @@ export interface VirtualHost { */ 'request_headers_to_add'?: (_envoy_config_core_v3_HeaderValueOption)[]; /** - * Indicates that the virtual host has a CORS policy. + * Indicates that the virtual host has a CORS policy. This field is ignored if related cors policy is + * found in the + * :ref:`VirtualHost.typed_per_filter_config`. + * + * .. attention:: + * + * This option has been deprecated. Please use + * :ref:`VirtualHost.typed_per_filter_config` + * to configure the CORS HTTP filter. */ 'cors'?: (_envoy_config_route_v3_CorsPolicy | null); /** @@ -130,11 +141,15 @@ export interface VirtualHost { */ 'include_request_attempt_count'?: (boolean); /** - * The per_filter_config field can be used to provide virtual host-specific - * configurations for filters. The key should match the filter name, such as - * *envoy.filters.http.buffer* for the HTTP buffer filter. Use of this field is filter - * specific; see the :ref:`HTTP filter documentation ` - * for if and how it is utilized. + * The per_filter_config field can be used to provide virtual host-specific configurations for filters. + * The key should match the :ref:`filter config name + * `. + * The canonical filter name (e.g., ``envoy.filters.http.buffer`` for the HTTP buffer filter) can also + * be used for the backwards compatibility. If there is no entry referred by the filter config name, the + * entry referred by the canonical filter name will be provided to the filters as fallback. + * + * Use of this field is filter specific; + * see the :ref:`HTTP filter documentation ` for if and how it is utilized. * [#comment: An entry's value may be wrapped in a * :ref:`FilterConfig` * message to specify additional options.] @@ -177,6 +192,23 @@ export interface VirtualHost { * set if this field is used. */ 'retry_policy_typed_config'?: (_google_protobuf_Any | null); + /** + * [#next-major-version: This should be included in a oneof with routes wrapped in a message.] + * The match tree to use when resolving route actions for incoming requests. Only one of this and ``routes`` + * can be specified. + */ + 'matcher'?: (_xds_type_matcher_v3_Matcher | null); + /** + * Specify a set of default request mirroring policies for every route under this virtual host. + * It takes precedence over the route config mirror policy entirely. + * That is, policies are not merged, the most specific non-empty one becomes the mirror policies. + */ + 'request_mirror_policies'?: (_envoy_config_route_v3_RouteAction_RequestMirrorPolicy)[]; + /** + * Decides whether to include the :ref:`x-envoy-is-timeout-retry ` + * request header in retries initiated by per try timeouts. + */ + 'include_is_timeout_retry_header'?: (boolean); } /** @@ -185,7 +217,7 @@ export interface VirtualHost { * host header. This allows a single listener to service multiple top level domain path trees. Once * a virtual host is selected based on the domain, the routes are processed in order to see which * upstream cluster to route to or whether to perform a redirect. - * [#next-free-field: 21] + * [#next-free-field: 24] */ export interface VirtualHost__Output { /** @@ -217,6 +249,7 @@ export interface VirtualHost__Output { /** * The list of routes that will be matched, in order, for incoming requests. * The first route that matches will be used. + * Only one of this and ``matcher`` can be specified. */ 'routes': (_envoy_config_route_v3_Route__Output)[]; /** @@ -244,7 +277,15 @@ export interface VirtualHost__Output { */ 'request_headers_to_add': (_envoy_config_core_v3_HeaderValueOption__Output)[]; /** - * Indicates that the virtual host has a CORS policy. + * Indicates that the virtual host has a CORS policy. This field is ignored if related cors policy is + * found in the + * :ref:`VirtualHost.typed_per_filter_config`. + * + * .. attention:: + * + * This option has been deprecated. Please use + * :ref:`VirtualHost.typed_per_filter_config` + * to configure the CORS HTTP filter. */ 'cors': (_envoy_config_route_v3_CorsPolicy__Output | null); /** @@ -280,11 +321,15 @@ export interface VirtualHost__Output { */ 'include_request_attempt_count': (boolean); /** - * The per_filter_config field can be used to provide virtual host-specific - * configurations for filters. The key should match the filter name, such as - * *envoy.filters.http.buffer* for the HTTP buffer filter. Use of this field is filter - * specific; see the :ref:`HTTP filter documentation ` - * for if and how it is utilized. + * The per_filter_config field can be used to provide virtual host-specific configurations for filters. + * The key should match the :ref:`filter config name + * `. + * The canonical filter name (e.g., ``envoy.filters.http.buffer`` for the HTTP buffer filter) can also + * be used for the backwards compatibility. If there is no entry referred by the filter config name, the + * entry referred by the canonical filter name will be provided to the filters as fallback. + * + * Use of this field is filter specific; + * see the :ref:`HTTP filter documentation ` for if and how it is utilized. * [#comment: An entry's value may be wrapped in a * :ref:`FilterConfig` * message to specify additional options.] @@ -327,4 +372,21 @@ export interface VirtualHost__Output { * set if this field is used. */ 'retry_policy_typed_config': (_google_protobuf_Any__Output | null); + /** + * [#next-major-version: This should be included in a oneof with routes wrapped in a message.] + * The match tree to use when resolving route actions for incoming requests. Only one of this and ``routes`` + * can be specified. + */ + 'matcher': (_xds_type_matcher_v3_Matcher__Output | null); + /** + * Specify a set of default request mirroring policies for every route under this virtual host. + * It takes precedence over the route config mirror policy entirely. + * That is, policies are not merged, the most specific non-empty one becomes the mirror policies. + */ + 'request_mirror_policies': (_envoy_config_route_v3_RouteAction_RequestMirrorPolicy__Output)[]; + /** + * Decides whether to include the :ref:`x-envoy-is-timeout-retry ` + * request header in retries initiated by per try timeouts. + */ + 'include_is_timeout_retry_header': (boolean); } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/WeightedCluster.ts b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/WeightedCluster.ts index e734073be..cc820654d 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/WeightedCluster.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/WeightedCluster.ts @@ -10,14 +10,14 @@ import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__ */ export interface _envoy_config_route_v3_WeightedCluster_ClusterWeight { /** - * Only one of *name* and *cluster_header* may be specified. + * Only one of ``name`` and ``cluster_header`` may be specified. * [#next-major-version: Need to add back the validation rule: (validate.rules).string = {min_len: 1}] * Name of the upstream cluster. The cluster must exist in the * :ref:`cluster manager configuration `. */ 'name'?: (string); /** - * Only one of *name* and *cluster_header* may be specified. + * Only one of ``name`` and ``cluster_header`` may be specified. * [#next-major-version: Need to add back the validation rule: (validate.rules).string = {min_len: 1 }] * Envoy will determine the cluster to route to by reading the value of the * HTTP header named by cluster_header from the request headers. If the @@ -26,8 +26,8 @@ export interface _envoy_config_route_v3_WeightedCluster_ClusterWeight { * * .. attention:: * - * Internally, Envoy always uses the HTTP/2 *:authority* header to represent the HTTP/1 - * *Host* header. Thus, if attempting to match on *Host*, match on *:authority* instead. + * Internally, Envoy always uses the HTTP/2 ``:authority`` header to represent the HTTP/1 + * ``Host`` header. Thus, if attempting to match on ``Host``, match on ``:authority`` instead. * * .. note:: * @@ -35,10 +35,11 @@ export interface _envoy_config_route_v3_WeightedCluster_ClusterWeight { */ 'cluster_header'?: (string); /** - * An integer between 0 and :ref:`total_weight - * `. When a request matches the route, - * the choice of an upstream cluster is determined by its weight. The sum of weights across all - * entries in the clusters array must add up to the total_weight, which defaults to 100. + * The weight of the cluster. This value is relative to the other clusters' + * weights. When a request matches the route, the choice of an upstream cluster + * is determined by its weight. The sum of weights across all + * entries in the clusters array must be greater than 0, and must not exceed + * uint32_t maximal value (4294967295). */ 'weight'?: (_google_protobuf_UInt32Value | null); /** @@ -46,7 +47,7 @@ export interface _envoy_config_route_v3_WeightedCluster_ClusterWeight { * the upstream cluster with metadata matching what is set in this field will be considered for * load balancing. Note that this will be merged with what's provided in * :ref:`RouteAction.metadata_match `, with - * values here taking precedence. The filter name should be specified as *envoy.lb*. + * values here taking precedence. The filter name should be specified as ``envoy.lb``. */ 'metadata_match'?: (_envoy_config_core_v3_Metadata | null); /** @@ -80,11 +81,16 @@ export interface _envoy_config_route_v3_WeightedCluster_ClusterWeight { */ 'response_headers_to_remove'?: (string)[]; /** - * The per_filter_config field can be used to provide weighted cluster-specific - * configurations for filters. The key should match the filter name, such as - * *envoy.filters.http.buffer* for the HTTP buffer filter. Use of this field is filter - * specific; see the :ref:`HTTP filter documentation ` - * for if and how it is utilized. + * The per_filter_config field can be used to provide weighted cluster-specific configurations + * for filters. + * The key should match the :ref:`filter config name + * `. + * The canonical filter name (e.g., ``envoy.filters.http.buffer`` for the HTTP buffer filter) can also + * be used for the backwards compatibility. If there is no entry referred by the filter config name, the + * entry referred by the canonical filter name will be provided to the filters as fallback. + * + * Use of this field is filter specific; + * see the :ref:`HTTP filter documentation ` for if and how it is utilized. * [#comment: An entry's value may be wrapped in a * :ref:`FilterConfig` * message to specify additional options.] @@ -103,14 +109,14 @@ export interface _envoy_config_route_v3_WeightedCluster_ClusterWeight { */ export interface _envoy_config_route_v3_WeightedCluster_ClusterWeight__Output { /** - * Only one of *name* and *cluster_header* may be specified. + * Only one of ``name`` and ``cluster_header`` may be specified. * [#next-major-version: Need to add back the validation rule: (validate.rules).string = {min_len: 1}] * Name of the upstream cluster. The cluster must exist in the * :ref:`cluster manager configuration `. */ 'name': (string); /** - * Only one of *name* and *cluster_header* may be specified. + * Only one of ``name`` and ``cluster_header`` may be specified. * [#next-major-version: Need to add back the validation rule: (validate.rules).string = {min_len: 1 }] * Envoy will determine the cluster to route to by reading the value of the * HTTP header named by cluster_header from the request headers. If the @@ -119,8 +125,8 @@ export interface _envoy_config_route_v3_WeightedCluster_ClusterWeight__Output { * * .. attention:: * - * Internally, Envoy always uses the HTTP/2 *:authority* header to represent the HTTP/1 - * *Host* header. Thus, if attempting to match on *Host*, match on *:authority* instead. + * Internally, Envoy always uses the HTTP/2 ``:authority`` header to represent the HTTP/1 + * ``Host`` header. Thus, if attempting to match on ``Host``, match on ``:authority`` instead. * * .. note:: * @@ -128,10 +134,11 @@ export interface _envoy_config_route_v3_WeightedCluster_ClusterWeight__Output { */ 'cluster_header': (string); /** - * An integer between 0 and :ref:`total_weight - * `. When a request matches the route, - * the choice of an upstream cluster is determined by its weight. The sum of weights across all - * entries in the clusters array must add up to the total_weight, which defaults to 100. + * The weight of the cluster. This value is relative to the other clusters' + * weights. When a request matches the route, the choice of an upstream cluster + * is determined by its weight. The sum of weights across all + * entries in the clusters array must be greater than 0, and must not exceed + * uint32_t maximal value (4294967295). */ 'weight': (_google_protobuf_UInt32Value__Output | null); /** @@ -139,7 +146,7 @@ export interface _envoy_config_route_v3_WeightedCluster_ClusterWeight__Output { * the upstream cluster with metadata matching what is set in this field will be considered for * load balancing. Note that this will be merged with what's provided in * :ref:`RouteAction.metadata_match `, with - * values here taking precedence. The filter name should be specified as *envoy.lb*. + * values here taking precedence. The filter name should be specified as ``envoy.lb``. */ 'metadata_match': (_envoy_config_core_v3_Metadata__Output | null); /** @@ -173,11 +180,16 @@ export interface _envoy_config_route_v3_WeightedCluster_ClusterWeight__Output { */ 'response_headers_to_remove': (string)[]; /** - * The per_filter_config field can be used to provide weighted cluster-specific - * configurations for filters. The key should match the filter name, such as - * *envoy.filters.http.buffer* for the HTTP buffer filter. Use of this field is filter - * specific; see the :ref:`HTTP filter documentation ` - * for if and how it is utilized. + * The per_filter_config field can be used to provide weighted cluster-specific configurations + * for filters. + * The key should match the :ref:`filter config name + * `. + * The canonical filter name (e.g., ``envoy.filters.http.buffer`` for the HTTP buffer filter) can also + * be used for the backwards compatibility. If there is no entry referred by the filter config name, the + * entry referred by the canonical filter name will be provided to the filters as fallback. + * + * Use of this field is filter specific; + * see the :ref:`HTTP filter documentation ` for if and how it is utilized. * [#comment: An entry's value may be wrapped in a * :ref:`FilterConfig` * message to specify additional options.] @@ -206,10 +218,10 @@ export interface WeightedCluster { 'clusters'?: (_envoy_config_route_v3_WeightedCluster_ClusterWeight)[]; /** * Specifies the runtime key prefix that should be used to construct the - * runtime keys associated with each cluster. When the *runtime_key_prefix* is + * runtime keys associated with each cluster. When the ``runtime_key_prefix`` is * specified, the router will look for weights associated with each upstream - * cluster under the key *runtime_key_prefix* + "." + *cluster[i].name* where - * *cluster[i]* denotes an entry in the clusters array field. If the runtime + * cluster under the key ``runtime_key_prefix`` + ``.`` + ``cluster[i].name`` where + * ``cluster[i]`` denotes an entry in the clusters array field. If the runtime * key for the cluster does not exist, the value specified in the * configuration file will be used as the default weight. See the :ref:`runtime documentation * ` for how key names map to the underlying implementation. @@ -217,9 +229,20 @@ export interface WeightedCluster { 'runtime_key_prefix'?: (string); /** * Specifies the total weight across all clusters. The sum of all cluster weights must equal this - * value, which must be greater than 0. Defaults to 100. + * value, if this is greater than 0. + * This field is now deprecated, and the client will use the sum of all + * cluster weights. It is up to the management server to supply the correct weights. */ 'total_weight'?: (_google_protobuf_UInt32Value | null); + /** + * Specifies the header name that is used to look up the random value passed in the request header. + * This is used to ensure consistent cluster picking across multiple proxy levels for weighted traffic. + * If header is not present or invalid, Envoy will fall back to use the internally generated random value. + * This header is expected to be single-valued header as we only want to have one selected value throughout + * the process for the consistency. And the value is a unsigned number between 0 and UINT64_MAX. + */ + 'header_name'?: (string); + 'random_value_specifier'?: "header_name"; } /** @@ -237,10 +260,10 @@ export interface WeightedCluster__Output { 'clusters': (_envoy_config_route_v3_WeightedCluster_ClusterWeight__Output)[]; /** * Specifies the runtime key prefix that should be used to construct the - * runtime keys associated with each cluster. When the *runtime_key_prefix* is + * runtime keys associated with each cluster. When the ``runtime_key_prefix`` is * specified, the router will look for weights associated with each upstream - * cluster under the key *runtime_key_prefix* + "." + *cluster[i].name* where - * *cluster[i]* denotes an entry in the clusters array field. If the runtime + * cluster under the key ``runtime_key_prefix`` + ``.`` + ``cluster[i].name`` where + * ``cluster[i]`` denotes an entry in the clusters array field. If the runtime * key for the cluster does not exist, the value specified in the * configuration file will be used as the default weight. See the :ref:`runtime documentation * ` for how key names map to the underlying implementation. @@ -248,7 +271,18 @@ export interface WeightedCluster__Output { 'runtime_key_prefix': (string); /** * Specifies the total weight across all clusters. The sum of all cluster weights must equal this - * value, which must be greater than 0. Defaults to 100. + * value, if this is greater than 0. + * This field is now deprecated, and the client will use the sum of all + * cluster weights. It is up to the management server to supply the correct weights. */ 'total_weight': (_google_protobuf_UInt32Value__Output | null); + /** + * Specifies the header name that is used to look up the random value passed in the request header. + * This is used to ensure consistent cluster picking across multiple proxy levels for weighted traffic. + * If header is not present or invalid, Envoy will fall back to use the internally generated random value. + * This header is expected to be single-valued header as we only want to have one selected value throughout + * the process for the consistency. And the value is a unsigned number between 0 and UINT64_MAX. + */ + 'header_name'?: (string); + 'random_value_specifier': "header_name"; } diff --git a/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/AccessLogCommon.ts b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/AccessLogCommon.ts new file mode 100644 index 000000000..7679dfac1 --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/AccessLogCommon.ts @@ -0,0 +1,421 @@ +// Original file: deps/envoy-api/envoy/data/accesslog/v3/accesslog.proto + +import type { Address as _envoy_config_core_v3_Address, Address__Output as _envoy_config_core_v3_Address__Output } from '../../../../envoy/config/core/v3/Address'; +import type { TLSProperties as _envoy_data_accesslog_v3_TLSProperties, TLSProperties__Output as _envoy_data_accesslog_v3_TLSProperties__Output } from '../../../../envoy/data/accesslog/v3/TLSProperties'; +import type { Timestamp as _google_protobuf_Timestamp, Timestamp__Output as _google_protobuf_Timestamp__Output } from '../../../../google/protobuf/Timestamp'; +import type { Duration as _google_protobuf_Duration, Duration__Output as _google_protobuf_Duration__Output } from '../../../../google/protobuf/Duration'; +import type { ResponseFlags as _envoy_data_accesslog_v3_ResponseFlags, ResponseFlags__Output as _envoy_data_accesslog_v3_ResponseFlags__Output } from '../../../../envoy/data/accesslog/v3/ResponseFlags'; +import type { Metadata as _envoy_config_core_v3_Metadata, Metadata__Output as _envoy_config_core_v3_Metadata__Output } from '../../../../envoy/config/core/v3/Metadata'; +import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../../google/protobuf/Any'; +import type { AccessLogType as _envoy_data_accesslog_v3_AccessLogType } from '../../../../envoy/data/accesslog/v3/AccessLogType'; +import type { Long } from '@grpc/proto-loader'; + +/** + * Defines fields that are shared by all Envoy access logs. + * [#next-free-field: 34] + */ +export interface AccessLogCommon { + /** + * [#not-implemented-hide:] + * This field indicates the rate at which this log entry was sampled. + * Valid range is (0.0, 1.0]. + */ + 'sample_rate'?: (number | string); + /** + * This field is the remote/origin address on which the request from the user was received. + * Note: This may not be the physical peer. E.g, if the remote address is inferred from for + * example the x-forwarder-for header, proxy protocol, etc. + */ + 'downstream_remote_address'?: (_envoy_config_core_v3_Address | null); + /** + * This field is the local/destination address on which the request from the user was received. + */ + 'downstream_local_address'?: (_envoy_config_core_v3_Address | null); + /** + * If the connection is secure,S this field will contain TLS properties. + */ + 'tls_properties'?: (_envoy_data_accesslog_v3_TLSProperties | null); + /** + * The time that Envoy started servicing this request. This is effectively the time that the first + * downstream byte is received. + */ + 'start_time'?: (_google_protobuf_Timestamp | null); + /** + * Interval between the first downstream byte received and the last + * downstream byte received (i.e. time it takes to receive a request). + */ + 'time_to_last_rx_byte'?: (_google_protobuf_Duration | null); + /** + * Interval between the first downstream byte received and the first upstream byte sent. There may + * by considerable delta between ``time_to_last_rx_byte`` and this value due to filters. + * Additionally, the same caveats apply as documented in ``time_to_last_downstream_tx_byte`` about + * not accounting for kernel socket buffer time, etc. + */ + 'time_to_first_upstream_tx_byte'?: (_google_protobuf_Duration | null); + /** + * Interval between the first downstream byte received and the last upstream byte sent. There may + * by considerable delta between ``time_to_last_rx_byte`` and this value due to filters. + * Additionally, the same caveats apply as documented in ``time_to_last_downstream_tx_byte`` about + * not accounting for kernel socket buffer time, etc. + */ + 'time_to_last_upstream_tx_byte'?: (_google_protobuf_Duration | null); + /** + * Interval between the first downstream byte received and the first upstream + * byte received (i.e. time it takes to start receiving a response). + */ + 'time_to_first_upstream_rx_byte'?: (_google_protobuf_Duration | null); + /** + * Interval between the first downstream byte received and the last upstream + * byte received (i.e. time it takes to receive a complete response). + */ + 'time_to_last_upstream_rx_byte'?: (_google_protobuf_Duration | null); + /** + * Interval between the first downstream byte received and the first downstream byte sent. + * There may be a considerable delta between the ``time_to_first_upstream_rx_byte`` and this field + * due to filters. Additionally, the same caveats apply as documented in + * ``time_to_last_downstream_tx_byte`` about not accounting for kernel socket buffer time, etc. + */ + 'time_to_first_downstream_tx_byte'?: (_google_protobuf_Duration | null); + /** + * Interval between the first downstream byte received and the last downstream byte sent. + * Depending on protocol, buffering, windowing, filters, etc. there may be a considerable delta + * between ``time_to_last_upstream_rx_byte`` and this field. Note also that this is an approximate + * time. In the current implementation it does not include kernel socket buffer time. In the + * current implementation it also does not include send window buffering inside the HTTP/2 codec. + * In the future it is likely that work will be done to make this duration more accurate. + */ + 'time_to_last_downstream_tx_byte'?: (_google_protobuf_Duration | null); + /** + * The upstream remote/destination address that handles this exchange. This does not include + * retries. + */ + 'upstream_remote_address'?: (_envoy_config_core_v3_Address | null); + /** + * The upstream local/origin address that handles this exchange. This does not include retries. + */ + 'upstream_local_address'?: (_envoy_config_core_v3_Address | null); + /** + * The upstream cluster that ``upstream_remote_address`` belongs to. + */ + 'upstream_cluster'?: (string); + /** + * Flags indicating occurrences during request/response processing. + */ + 'response_flags'?: (_envoy_data_accesslog_v3_ResponseFlags | null); + /** + * All metadata encountered during request processing, including endpoint + * selection. + * + * This can be used to associate IDs attached to the various configurations + * used to process this request with the access log entry. For example, a + * route created from a higher level forwarding rule with some ID can place + * that ID in this field and cross reference later. It can also be used to + * determine if a canary endpoint was used or not. + */ + 'metadata'?: (_envoy_config_core_v3_Metadata | null); + /** + * If upstream connection failed due to transport socket (e.g. TLS handshake), provides the + * failure reason from the transport socket. The format of this field depends on the configured + * upstream transport socket. Common TLS failures are in + * :ref:`TLS trouble shooting `. + */ + 'upstream_transport_failure_reason'?: (string); + /** + * The name of the route + */ + 'route_name'?: (string); + /** + * This field is the downstream direct remote address on which the request from the user was + * received. Note: This is always the physical peer, even if the remote address is inferred from + * for example the x-forwarder-for header, proxy protocol, etc. + */ + 'downstream_direct_remote_address'?: (_envoy_config_core_v3_Address | null); + /** + * Map of filter state in stream info that have been configured to be logged. If the filter + * state serialized to any message other than ``google.protobuf.Any`` it will be packed into + * ``google.protobuf.Any``. + */ + 'filter_state_objects'?: ({[key: string]: _google_protobuf_Any}); + /** + * A list of custom tags, which annotate logs with additional information. + * To configure this value, users should configure + * :ref:`custom_tags `. + */ + 'custom_tags'?: ({[key: string]: string}); + /** + * For HTTP: Total duration in milliseconds of the request from the start time to the last byte out. + * For TCP: Total duration in milliseconds of the downstream connection. + * This is the total duration of the request (i.e., when the request's ActiveStream is destroyed) + * and may be longer than ``time_to_last_downstream_tx_byte``. + */ + 'duration'?: (_google_protobuf_Duration | null); + /** + * For HTTP: Number of times the request is attempted upstream. Note that the field is omitted when the request was never attempted upstream. + * For TCP: Number of times the connection request is attempted upstream. Note that the field is omitted when the connect request was never attempted upstream. + */ + 'upstream_request_attempt_count'?: (number); + /** + * Connection termination details may provide additional information about why the connection was terminated by Envoy for L4 reasons. + */ + 'connection_termination_details'?: (string); + /** + * Optional unique id of stream (TCP connection, long-live HTTP2 stream, HTTP request) for logging and tracing. + * This could be any format string that could be used to identify one stream. + */ + 'stream_id'?: (string); + /** + * If this log entry is final log entry that flushed after the stream completed or + * intermediate log entry that flushed periodically during the stream. + * There may be multiple intermediate log entries and only one final log entry for each + * long-live stream (TCP connection, long-live HTTP2 stream). + * And if it is necessary, unique ID or identifier can be added to the log entry + * :ref:`stream_id ` to + * correlate all these intermediate log entries and final log entry. + * + * .. attention:: + * + * This field is deprecated in favor of ``access_log_type`` for better indication of the + * type of the access log record. + */ + 'intermediate_log_entry'?: (boolean); + /** + * If downstream connection in listener failed due to transport socket (e.g. TLS handshake), provides the + * failure reason from the transport socket. The format of this field depends on the configured downstream + * transport socket. Common TLS failures are in :ref:`TLS trouble shooting `. + */ + 'downstream_transport_failure_reason'?: (string); + /** + * For HTTP: Total number of bytes sent to the downstream by the http stream. + * For TCP: Total number of bytes sent to the downstream by the tcp proxy. + */ + 'downstream_wire_bytes_sent'?: (number | string | Long); + /** + * For HTTP: Total number of bytes received from the downstream by the http stream. Envoy over counts sizes of received HTTP/1.1 pipelined requests by adding up bytes of requests in the pipeline to the one currently being processed. + * For TCP: Total number of bytes received from the downstream by the tcp proxy. + */ + 'downstream_wire_bytes_received'?: (number | string | Long); + /** + * For HTTP: Total number of bytes sent to the upstream by the http stream. This value accumulates during upstream retries. + * For TCP: Total number of bytes sent to the upstream by the tcp proxy. + */ + 'upstream_wire_bytes_sent'?: (number | string | Long); + /** + * For HTTP: Total number of bytes received from the upstream by the http stream. + * For TCP: Total number of bytes sent to the upstream by the tcp proxy. + */ + 'upstream_wire_bytes_received'?: (number | string | Long); + /** + * The type of the access log, which indicates when the log was recorded. + * See :ref:`ACCESS_LOG_TYPE ` for the available values. + * In case the access log was recorded by a flow which does not correspond to one of the supported + * values, then the default value will be ``NotSet``. + * For more information about how access log behaves and when it is being recorded, + * please refer to :ref:`access logging `. + */ + 'access_log_type'?: (_envoy_data_accesslog_v3_AccessLogType | keyof typeof _envoy_data_accesslog_v3_AccessLogType); +} + +/** + * Defines fields that are shared by all Envoy access logs. + * [#next-free-field: 34] + */ +export interface AccessLogCommon__Output { + /** + * [#not-implemented-hide:] + * This field indicates the rate at which this log entry was sampled. + * Valid range is (0.0, 1.0]. + */ + 'sample_rate': (number); + /** + * This field is the remote/origin address on which the request from the user was received. + * Note: This may not be the physical peer. E.g, if the remote address is inferred from for + * example the x-forwarder-for header, proxy protocol, etc. + */ + 'downstream_remote_address': (_envoy_config_core_v3_Address__Output | null); + /** + * This field is the local/destination address on which the request from the user was received. + */ + 'downstream_local_address': (_envoy_config_core_v3_Address__Output | null); + /** + * If the connection is secure,S this field will contain TLS properties. + */ + 'tls_properties': (_envoy_data_accesslog_v3_TLSProperties__Output | null); + /** + * The time that Envoy started servicing this request. This is effectively the time that the first + * downstream byte is received. + */ + 'start_time': (_google_protobuf_Timestamp__Output | null); + /** + * Interval between the first downstream byte received and the last + * downstream byte received (i.e. time it takes to receive a request). + */ + 'time_to_last_rx_byte': (_google_protobuf_Duration__Output | null); + /** + * Interval between the first downstream byte received and the first upstream byte sent. There may + * by considerable delta between ``time_to_last_rx_byte`` and this value due to filters. + * Additionally, the same caveats apply as documented in ``time_to_last_downstream_tx_byte`` about + * not accounting for kernel socket buffer time, etc. + */ + 'time_to_first_upstream_tx_byte': (_google_protobuf_Duration__Output | null); + /** + * Interval between the first downstream byte received and the last upstream byte sent. There may + * by considerable delta between ``time_to_last_rx_byte`` and this value due to filters. + * Additionally, the same caveats apply as documented in ``time_to_last_downstream_tx_byte`` about + * not accounting for kernel socket buffer time, etc. + */ + 'time_to_last_upstream_tx_byte': (_google_protobuf_Duration__Output | null); + /** + * Interval between the first downstream byte received and the first upstream + * byte received (i.e. time it takes to start receiving a response). + */ + 'time_to_first_upstream_rx_byte': (_google_protobuf_Duration__Output | null); + /** + * Interval between the first downstream byte received and the last upstream + * byte received (i.e. time it takes to receive a complete response). + */ + 'time_to_last_upstream_rx_byte': (_google_protobuf_Duration__Output | null); + /** + * Interval between the first downstream byte received and the first downstream byte sent. + * There may be a considerable delta between the ``time_to_first_upstream_rx_byte`` and this field + * due to filters. Additionally, the same caveats apply as documented in + * ``time_to_last_downstream_tx_byte`` about not accounting for kernel socket buffer time, etc. + */ + 'time_to_first_downstream_tx_byte': (_google_protobuf_Duration__Output | null); + /** + * Interval between the first downstream byte received and the last downstream byte sent. + * Depending on protocol, buffering, windowing, filters, etc. there may be a considerable delta + * between ``time_to_last_upstream_rx_byte`` and this field. Note also that this is an approximate + * time. In the current implementation it does not include kernel socket buffer time. In the + * current implementation it also does not include send window buffering inside the HTTP/2 codec. + * In the future it is likely that work will be done to make this duration more accurate. + */ + 'time_to_last_downstream_tx_byte': (_google_protobuf_Duration__Output | null); + /** + * The upstream remote/destination address that handles this exchange. This does not include + * retries. + */ + 'upstream_remote_address': (_envoy_config_core_v3_Address__Output | null); + /** + * The upstream local/origin address that handles this exchange. This does not include retries. + */ + 'upstream_local_address': (_envoy_config_core_v3_Address__Output | null); + /** + * The upstream cluster that ``upstream_remote_address`` belongs to. + */ + 'upstream_cluster': (string); + /** + * Flags indicating occurrences during request/response processing. + */ + 'response_flags': (_envoy_data_accesslog_v3_ResponseFlags__Output | null); + /** + * All metadata encountered during request processing, including endpoint + * selection. + * + * This can be used to associate IDs attached to the various configurations + * used to process this request with the access log entry. For example, a + * route created from a higher level forwarding rule with some ID can place + * that ID in this field and cross reference later. It can also be used to + * determine if a canary endpoint was used or not. + */ + 'metadata': (_envoy_config_core_v3_Metadata__Output | null); + /** + * If upstream connection failed due to transport socket (e.g. TLS handshake), provides the + * failure reason from the transport socket. The format of this field depends on the configured + * upstream transport socket. Common TLS failures are in + * :ref:`TLS trouble shooting `. + */ + 'upstream_transport_failure_reason': (string); + /** + * The name of the route + */ + 'route_name': (string); + /** + * This field is the downstream direct remote address on which the request from the user was + * received. Note: This is always the physical peer, even if the remote address is inferred from + * for example the x-forwarder-for header, proxy protocol, etc. + */ + 'downstream_direct_remote_address': (_envoy_config_core_v3_Address__Output | null); + /** + * Map of filter state in stream info that have been configured to be logged. If the filter + * state serialized to any message other than ``google.protobuf.Any`` it will be packed into + * ``google.protobuf.Any``. + */ + 'filter_state_objects': ({[key: string]: _google_protobuf_Any__Output}); + /** + * A list of custom tags, which annotate logs with additional information. + * To configure this value, users should configure + * :ref:`custom_tags `. + */ + 'custom_tags': ({[key: string]: string}); + /** + * For HTTP: Total duration in milliseconds of the request from the start time to the last byte out. + * For TCP: Total duration in milliseconds of the downstream connection. + * This is the total duration of the request (i.e., when the request's ActiveStream is destroyed) + * and may be longer than ``time_to_last_downstream_tx_byte``. + */ + 'duration': (_google_protobuf_Duration__Output | null); + /** + * For HTTP: Number of times the request is attempted upstream. Note that the field is omitted when the request was never attempted upstream. + * For TCP: Number of times the connection request is attempted upstream. Note that the field is omitted when the connect request was never attempted upstream. + */ + 'upstream_request_attempt_count': (number); + /** + * Connection termination details may provide additional information about why the connection was terminated by Envoy for L4 reasons. + */ + 'connection_termination_details': (string); + /** + * Optional unique id of stream (TCP connection, long-live HTTP2 stream, HTTP request) for logging and tracing. + * This could be any format string that could be used to identify one stream. + */ + 'stream_id': (string); + /** + * If this log entry is final log entry that flushed after the stream completed or + * intermediate log entry that flushed periodically during the stream. + * There may be multiple intermediate log entries and only one final log entry for each + * long-live stream (TCP connection, long-live HTTP2 stream). + * And if it is necessary, unique ID or identifier can be added to the log entry + * :ref:`stream_id ` to + * correlate all these intermediate log entries and final log entry. + * + * .. attention:: + * + * This field is deprecated in favor of ``access_log_type`` for better indication of the + * type of the access log record. + */ + 'intermediate_log_entry': (boolean); + /** + * If downstream connection in listener failed due to transport socket (e.g. TLS handshake), provides the + * failure reason from the transport socket. The format of this field depends on the configured downstream + * transport socket. Common TLS failures are in :ref:`TLS trouble shooting `. + */ + 'downstream_transport_failure_reason': (string); + /** + * For HTTP: Total number of bytes sent to the downstream by the http stream. + * For TCP: Total number of bytes sent to the downstream by the tcp proxy. + */ + 'downstream_wire_bytes_sent': (string); + /** + * For HTTP: Total number of bytes received from the downstream by the http stream. Envoy over counts sizes of received HTTP/1.1 pipelined requests by adding up bytes of requests in the pipeline to the one currently being processed. + * For TCP: Total number of bytes received from the downstream by the tcp proxy. + */ + 'downstream_wire_bytes_received': (string); + /** + * For HTTP: Total number of bytes sent to the upstream by the http stream. This value accumulates during upstream retries. + * For TCP: Total number of bytes sent to the upstream by the tcp proxy. + */ + 'upstream_wire_bytes_sent': (string); + /** + * For HTTP: Total number of bytes received from the upstream by the http stream. + * For TCP: Total number of bytes sent to the upstream by the tcp proxy. + */ + 'upstream_wire_bytes_received': (string); + /** + * The type of the access log, which indicates when the log was recorded. + * See :ref:`ACCESS_LOG_TYPE ` for the available values. + * In case the access log was recorded by a flow which does not correspond to one of the supported + * values, then the default value will be ``NotSet``. + * For more information about how access log behaves and when it is being recorded, + * please refer to :ref:`access logging `. + */ + 'access_log_type': (keyof typeof _envoy_data_accesslog_v3_AccessLogType); +} diff --git a/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/AccessLogType.ts b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/AccessLogType.ts new file mode 100644 index 000000000..a50bb42c1 --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/AccessLogType.ts @@ -0,0 +1,15 @@ +// Original file: deps/envoy-api/envoy/data/accesslog/v3/accesslog.proto + +export enum AccessLogType { + NotSet = 0, + TcpUpstreamConnected = 1, + TcpPeriodic = 2, + TcpConnectionEnd = 3, + DownstreamStart = 4, + DownstreamPeriodic = 5, + DownstreamEnd = 6, + UpstreamPoolReady = 7, + UpstreamPeriodic = 8, + UpstreamEnd = 9, + DownstreamTunnelSuccessfullyEstablished = 10, +} diff --git a/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/ConnectionProperties.ts b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/ConnectionProperties.ts new file mode 100644 index 000000000..a0cdfc75f --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/ConnectionProperties.ts @@ -0,0 +1,31 @@ +// Original file: deps/envoy-api/envoy/data/accesslog/v3/accesslog.proto + +import type { Long } from '@grpc/proto-loader'; + +/** + * Defines fields for a connection + */ +export interface ConnectionProperties { + /** + * Number of bytes received from downstream. + */ + 'received_bytes'?: (number | string | Long); + /** + * Number of bytes sent to downstream. + */ + 'sent_bytes'?: (number | string | Long); +} + +/** + * Defines fields for a connection + */ +export interface ConnectionProperties__Output { + /** + * Number of bytes received from downstream. + */ + 'received_bytes': (string); + /** + * Number of bytes sent to downstream. + */ + 'sent_bytes': (string); +} diff --git a/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/HTTPAccessLogEntry.ts b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/HTTPAccessLogEntry.ts new file mode 100644 index 000000000..760954bb1 --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/HTTPAccessLogEntry.ts @@ -0,0 +1,50 @@ +// Original file: deps/envoy-api/envoy/data/accesslog/v3/accesslog.proto + +import type { AccessLogCommon as _envoy_data_accesslog_v3_AccessLogCommon, AccessLogCommon__Output as _envoy_data_accesslog_v3_AccessLogCommon__Output } from '../../../../envoy/data/accesslog/v3/AccessLogCommon'; +import type { HTTPRequestProperties as _envoy_data_accesslog_v3_HTTPRequestProperties, HTTPRequestProperties__Output as _envoy_data_accesslog_v3_HTTPRequestProperties__Output } from '../../../../envoy/data/accesslog/v3/HTTPRequestProperties'; +import type { HTTPResponseProperties as _envoy_data_accesslog_v3_HTTPResponseProperties, HTTPResponseProperties__Output as _envoy_data_accesslog_v3_HTTPResponseProperties__Output } from '../../../../envoy/data/accesslog/v3/HTTPResponseProperties'; + +// Original file: deps/envoy-api/envoy/data/accesslog/v3/accesslog.proto + +/** + * HTTP version + */ +export enum _envoy_data_accesslog_v3_HTTPAccessLogEntry_HTTPVersion { + PROTOCOL_UNSPECIFIED = 0, + HTTP10 = 1, + HTTP11 = 2, + HTTP2 = 3, + HTTP3 = 4, +} + +export interface HTTPAccessLogEntry { + /** + * Common properties shared by all Envoy access logs. + */ + 'common_properties'?: (_envoy_data_accesslog_v3_AccessLogCommon | null); + 'protocol_version'?: (_envoy_data_accesslog_v3_HTTPAccessLogEntry_HTTPVersion | keyof typeof _envoy_data_accesslog_v3_HTTPAccessLogEntry_HTTPVersion); + /** + * Description of the incoming HTTP request. + */ + 'request'?: (_envoy_data_accesslog_v3_HTTPRequestProperties | null); + /** + * Description of the outgoing HTTP response. + */ + 'response'?: (_envoy_data_accesslog_v3_HTTPResponseProperties | null); +} + +export interface HTTPAccessLogEntry__Output { + /** + * Common properties shared by all Envoy access logs. + */ + 'common_properties': (_envoy_data_accesslog_v3_AccessLogCommon__Output | null); + 'protocol_version': (keyof typeof _envoy_data_accesslog_v3_HTTPAccessLogEntry_HTTPVersion); + /** + * Description of the incoming HTTP request. + */ + 'request': (_envoy_data_accesslog_v3_HTTPRequestProperties__Output | null); + /** + * Description of the outgoing HTTP response. + */ + 'response': (_envoy_data_accesslog_v3_HTTPResponseProperties__Output | null); +} diff --git a/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/HTTPRequestProperties.ts b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/HTTPRequestProperties.ts new file mode 100644 index 000000000..9e145503c --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/HTTPRequestProperties.ts @@ -0,0 +1,163 @@ +// Original file: deps/envoy-api/envoy/data/accesslog/v3/accesslog.proto + +import type { RequestMethod as _envoy_config_core_v3_RequestMethod } from '../../../../envoy/config/core/v3/RequestMethod'; +import type { UInt32Value as _google_protobuf_UInt32Value, UInt32Value__Output as _google_protobuf_UInt32Value__Output } from '../../../../google/protobuf/UInt32Value'; +import type { Long } from '@grpc/proto-loader'; + +/** + * [#next-free-field: 16] + */ +export interface HTTPRequestProperties { + /** + * The request method (RFC 7231/2616). + */ + 'request_method'?: (_envoy_config_core_v3_RequestMethod | keyof typeof _envoy_config_core_v3_RequestMethod); + /** + * The scheme portion of the incoming request URI. + */ + 'scheme'?: (string); + /** + * HTTP/2 ``:authority`` or HTTP/1.1 ``Host`` header value. + */ + 'authority'?: (string); + /** + * The port of the incoming request URI + * (unused currently, as port is composed onto authority). + */ + 'port'?: (_google_protobuf_UInt32Value | null); + /** + * The path portion from the incoming request URI. + */ + 'path'?: (string); + /** + * Value of the ``User-Agent`` request header. + */ + 'user_agent'?: (string); + /** + * Value of the ``Referer`` request header. + */ + 'referer'?: (string); + /** + * Value of the ``X-Forwarded-For`` request header. + */ + 'forwarded_for'?: (string); + /** + * Value of the ``X-Request-Id`` request header + * + * This header is used by Envoy to uniquely identify a request. + * It will be generated for all external requests and internal requests that + * do not already have a request ID. + */ + 'request_id'?: (string); + /** + * Value of the ``X-Envoy-Original-Path`` request header. + */ + 'original_path'?: (string); + /** + * Size of the HTTP request headers in bytes. + * + * This value is captured from the OSI layer 7 perspective, i.e. it does not + * include overhead from framing or encoding at other networking layers. + */ + 'request_headers_bytes'?: (number | string | Long); + /** + * Size of the HTTP request body in bytes. + * + * This value is captured from the OSI layer 7 perspective, i.e. it does not + * include overhead from framing or encoding at other networking layers. + */ + 'request_body_bytes'?: (number | string | Long); + /** + * Map of additional headers that have been configured to be logged. + */ + 'request_headers'?: ({[key: string]: string}); + /** + * Number of header bytes sent to the upstream by the http stream, including protocol overhead. + * + * This value accumulates during upstream retries. + */ + 'upstream_header_bytes_sent'?: (number | string | Long); + /** + * Number of header bytes received from the downstream by the http stream, including protocol overhead. + */ + 'downstream_header_bytes_received'?: (number | string | Long); +} + +/** + * [#next-free-field: 16] + */ +export interface HTTPRequestProperties__Output { + /** + * The request method (RFC 7231/2616). + */ + 'request_method': (keyof typeof _envoy_config_core_v3_RequestMethod); + /** + * The scheme portion of the incoming request URI. + */ + 'scheme': (string); + /** + * HTTP/2 ``:authority`` or HTTP/1.1 ``Host`` header value. + */ + 'authority': (string); + /** + * The port of the incoming request URI + * (unused currently, as port is composed onto authority). + */ + 'port': (_google_protobuf_UInt32Value__Output | null); + /** + * The path portion from the incoming request URI. + */ + 'path': (string); + /** + * Value of the ``User-Agent`` request header. + */ + 'user_agent': (string); + /** + * Value of the ``Referer`` request header. + */ + 'referer': (string); + /** + * Value of the ``X-Forwarded-For`` request header. + */ + 'forwarded_for': (string); + /** + * Value of the ``X-Request-Id`` request header + * + * This header is used by Envoy to uniquely identify a request. + * It will be generated for all external requests and internal requests that + * do not already have a request ID. + */ + 'request_id': (string); + /** + * Value of the ``X-Envoy-Original-Path`` request header. + */ + 'original_path': (string); + /** + * Size of the HTTP request headers in bytes. + * + * This value is captured from the OSI layer 7 perspective, i.e. it does not + * include overhead from framing or encoding at other networking layers. + */ + 'request_headers_bytes': (string); + /** + * Size of the HTTP request body in bytes. + * + * This value is captured from the OSI layer 7 perspective, i.e. it does not + * include overhead from framing or encoding at other networking layers. + */ + 'request_body_bytes': (string); + /** + * Map of additional headers that have been configured to be logged. + */ + 'request_headers': ({[key: string]: string}); + /** + * Number of header bytes sent to the upstream by the http stream, including protocol overhead. + * + * This value accumulates during upstream retries. + */ + 'upstream_header_bytes_sent': (string); + /** + * Number of header bytes received from the downstream by the http stream, including protocol overhead. + */ + 'downstream_header_bytes_received': (string); +} diff --git a/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/HTTPResponseProperties.ts b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/HTTPResponseProperties.ts new file mode 100644 index 000000000..1368fc8cd --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/HTTPResponseProperties.ts @@ -0,0 +1,92 @@ +// Original file: deps/envoy-api/envoy/data/accesslog/v3/accesslog.proto + +import type { UInt32Value as _google_protobuf_UInt32Value, UInt32Value__Output as _google_protobuf_UInt32Value__Output } from '../../../../google/protobuf/UInt32Value'; +import type { Long } from '@grpc/proto-loader'; + +/** + * [#next-free-field: 9] + */ +export interface HTTPResponseProperties { + /** + * The HTTP response code returned by Envoy. + */ + 'response_code'?: (_google_protobuf_UInt32Value | null); + /** + * Size of the HTTP response headers in bytes. + * + * This value is captured from the OSI layer 7 perspective, i.e. it does not + * include protocol overhead or overhead from framing or encoding at other networking layers. + */ + 'response_headers_bytes'?: (number | string | Long); + /** + * Size of the HTTP response body in bytes. + * + * This value is captured from the OSI layer 7 perspective, i.e. it does not + * include overhead from framing or encoding at other networking layers. + */ + 'response_body_bytes'?: (number | string | Long); + /** + * Map of additional headers configured to be logged. + */ + 'response_headers'?: ({[key: string]: string}); + /** + * Map of trailers configured to be logged. + */ + 'response_trailers'?: ({[key: string]: string}); + /** + * The HTTP response code details. + */ + 'response_code_details'?: (string); + /** + * Number of header bytes received from the upstream by the http stream, including protocol overhead. + */ + 'upstream_header_bytes_received'?: (number | string | Long); + /** + * Number of header bytes sent to the downstream by the http stream, including protocol overhead. + */ + 'downstream_header_bytes_sent'?: (number | string | Long); +} + +/** + * [#next-free-field: 9] + */ +export interface HTTPResponseProperties__Output { + /** + * The HTTP response code returned by Envoy. + */ + 'response_code': (_google_protobuf_UInt32Value__Output | null); + /** + * Size of the HTTP response headers in bytes. + * + * This value is captured from the OSI layer 7 perspective, i.e. it does not + * include protocol overhead or overhead from framing or encoding at other networking layers. + */ + 'response_headers_bytes': (string); + /** + * Size of the HTTP response body in bytes. + * + * This value is captured from the OSI layer 7 perspective, i.e. it does not + * include overhead from framing or encoding at other networking layers. + */ + 'response_body_bytes': (string); + /** + * Map of additional headers configured to be logged. + */ + 'response_headers': ({[key: string]: string}); + /** + * Map of trailers configured to be logged. + */ + 'response_trailers': ({[key: string]: string}); + /** + * The HTTP response code details. + */ + 'response_code_details': (string); + /** + * Number of header bytes received from the upstream by the http stream, including protocol overhead. + */ + 'upstream_header_bytes_received': (string); + /** + * Number of header bytes sent to the downstream by the http stream, including protocol overhead. + */ + 'downstream_header_bytes_sent': (string); +} diff --git a/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/ResponseFlags.ts b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/ResponseFlags.ts new file mode 100644 index 000000000..ec45824b3 --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/ResponseFlags.ts @@ -0,0 +1,255 @@ +// Original file: deps/envoy-api/envoy/data/accesslog/v3/accesslog.proto + + +// Original file: deps/envoy-api/envoy/data/accesslog/v3/accesslog.proto + +/** + * Reasons why the request was unauthorized + */ +export enum _envoy_data_accesslog_v3_ResponseFlags_Unauthorized_Reason { + REASON_UNSPECIFIED = 0, + /** + * The request was denied by the external authorization service. + */ + EXTERNAL_SERVICE = 1, +} + +export interface _envoy_data_accesslog_v3_ResponseFlags_Unauthorized { + 'reason'?: (_envoy_data_accesslog_v3_ResponseFlags_Unauthorized_Reason | keyof typeof _envoy_data_accesslog_v3_ResponseFlags_Unauthorized_Reason); +} + +export interface _envoy_data_accesslog_v3_ResponseFlags_Unauthorized__Output { + 'reason': (keyof typeof _envoy_data_accesslog_v3_ResponseFlags_Unauthorized_Reason); +} + +/** + * Flags indicating occurrences during request/response processing. + * [#next-free-field: 28] + */ +export interface ResponseFlags { + /** + * Indicates local server healthcheck failed. + */ + 'failed_local_healthcheck'?: (boolean); + /** + * Indicates there was no healthy upstream. + */ + 'no_healthy_upstream'?: (boolean); + /** + * Indicates an there was an upstream request timeout. + */ + 'upstream_request_timeout'?: (boolean); + /** + * Indicates local codec level reset was sent on the stream. + */ + 'local_reset'?: (boolean); + /** + * Indicates remote codec level reset was received on the stream. + */ + 'upstream_remote_reset'?: (boolean); + /** + * Indicates there was a local reset by a connection pool due to an initial connection failure. + */ + 'upstream_connection_failure'?: (boolean); + /** + * Indicates the stream was reset due to an upstream connection termination. + */ + 'upstream_connection_termination'?: (boolean); + /** + * Indicates the stream was reset because of a resource overflow. + */ + 'upstream_overflow'?: (boolean); + /** + * Indicates no route was found for the request. + */ + 'no_route_found'?: (boolean); + /** + * Indicates that the request was delayed before proxying. + */ + 'delay_injected'?: (boolean); + /** + * Indicates that the request was aborted with an injected error code. + */ + 'fault_injected'?: (boolean); + /** + * Indicates that the request was rate-limited locally. + */ + 'rate_limited'?: (boolean); + /** + * Indicates if the request was deemed unauthorized and the reason for it. + */ + 'unauthorized_details'?: (_envoy_data_accesslog_v3_ResponseFlags_Unauthorized | null); + /** + * Indicates that the request was rejected because there was an error in rate limit service. + */ + 'rate_limit_service_error'?: (boolean); + /** + * Indicates the stream was reset due to a downstream connection termination. + */ + 'downstream_connection_termination'?: (boolean); + /** + * Indicates that the upstream retry limit was exceeded, resulting in a downstream error. + */ + 'upstream_retry_limit_exceeded'?: (boolean); + /** + * Indicates that the stream idle timeout was hit, resulting in a downstream 408. + */ + 'stream_idle_timeout'?: (boolean); + /** + * Indicates that the request was rejected because an envoy request header failed strict + * validation. + */ + 'invalid_envoy_request_headers'?: (boolean); + /** + * Indicates there was an HTTP protocol error on the downstream request. + */ + 'downstream_protocol_error'?: (boolean); + /** + * Indicates there was a max stream duration reached on the upstream request. + */ + 'upstream_max_stream_duration_reached'?: (boolean); + /** + * Indicates the response was served from a cache filter. + */ + 'response_from_cache_filter'?: (boolean); + /** + * Indicates that a filter configuration is not available. + */ + 'no_filter_config_found'?: (boolean); + /** + * Indicates that request or connection exceeded the downstream connection duration. + */ + 'duration_timeout'?: (boolean); + /** + * Indicates there was an HTTP protocol error in the upstream response. + */ + 'upstream_protocol_error'?: (boolean); + /** + * Indicates no cluster was found for the request. + */ + 'no_cluster_found'?: (boolean); + /** + * Indicates overload manager terminated the request. + */ + 'overload_manager'?: (boolean); + /** + * Indicates a DNS resolution failed. + */ + 'dns_resolution_failure'?: (boolean); +} + +/** + * Flags indicating occurrences during request/response processing. + * [#next-free-field: 28] + */ +export interface ResponseFlags__Output { + /** + * Indicates local server healthcheck failed. + */ + 'failed_local_healthcheck': (boolean); + /** + * Indicates there was no healthy upstream. + */ + 'no_healthy_upstream': (boolean); + /** + * Indicates an there was an upstream request timeout. + */ + 'upstream_request_timeout': (boolean); + /** + * Indicates local codec level reset was sent on the stream. + */ + 'local_reset': (boolean); + /** + * Indicates remote codec level reset was received on the stream. + */ + 'upstream_remote_reset': (boolean); + /** + * Indicates there was a local reset by a connection pool due to an initial connection failure. + */ + 'upstream_connection_failure': (boolean); + /** + * Indicates the stream was reset due to an upstream connection termination. + */ + 'upstream_connection_termination': (boolean); + /** + * Indicates the stream was reset because of a resource overflow. + */ + 'upstream_overflow': (boolean); + /** + * Indicates no route was found for the request. + */ + 'no_route_found': (boolean); + /** + * Indicates that the request was delayed before proxying. + */ + 'delay_injected': (boolean); + /** + * Indicates that the request was aborted with an injected error code. + */ + 'fault_injected': (boolean); + /** + * Indicates that the request was rate-limited locally. + */ + 'rate_limited': (boolean); + /** + * Indicates if the request was deemed unauthorized and the reason for it. + */ + 'unauthorized_details': (_envoy_data_accesslog_v3_ResponseFlags_Unauthorized__Output | null); + /** + * Indicates that the request was rejected because there was an error in rate limit service. + */ + 'rate_limit_service_error': (boolean); + /** + * Indicates the stream was reset due to a downstream connection termination. + */ + 'downstream_connection_termination': (boolean); + /** + * Indicates that the upstream retry limit was exceeded, resulting in a downstream error. + */ + 'upstream_retry_limit_exceeded': (boolean); + /** + * Indicates that the stream idle timeout was hit, resulting in a downstream 408. + */ + 'stream_idle_timeout': (boolean); + /** + * Indicates that the request was rejected because an envoy request header failed strict + * validation. + */ + 'invalid_envoy_request_headers': (boolean); + /** + * Indicates there was an HTTP protocol error on the downstream request. + */ + 'downstream_protocol_error': (boolean); + /** + * Indicates there was a max stream duration reached on the upstream request. + */ + 'upstream_max_stream_duration_reached': (boolean); + /** + * Indicates the response was served from a cache filter. + */ + 'response_from_cache_filter': (boolean); + /** + * Indicates that a filter configuration is not available. + */ + 'no_filter_config_found': (boolean); + /** + * Indicates that request or connection exceeded the downstream connection duration. + */ + 'duration_timeout': (boolean); + /** + * Indicates there was an HTTP protocol error in the upstream response. + */ + 'upstream_protocol_error': (boolean); + /** + * Indicates no cluster was found for the request. + */ + 'no_cluster_found': (boolean); + /** + * Indicates overload manager terminated the request. + */ + 'overload_manager': (boolean); + /** + * Indicates a DNS resolution failed. + */ + 'dns_resolution_failure': (boolean); +} diff --git a/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/TCPAccessLogEntry.ts b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/TCPAccessLogEntry.ts new file mode 100644 index 000000000..55e9cace0 --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/TCPAccessLogEntry.ts @@ -0,0 +1,26 @@ +// Original file: deps/envoy-api/envoy/data/accesslog/v3/accesslog.proto + +import type { AccessLogCommon as _envoy_data_accesslog_v3_AccessLogCommon, AccessLogCommon__Output as _envoy_data_accesslog_v3_AccessLogCommon__Output } from '../../../../envoy/data/accesslog/v3/AccessLogCommon'; +import type { ConnectionProperties as _envoy_data_accesslog_v3_ConnectionProperties, ConnectionProperties__Output as _envoy_data_accesslog_v3_ConnectionProperties__Output } from '../../../../envoy/data/accesslog/v3/ConnectionProperties'; + +export interface TCPAccessLogEntry { + /** + * Common properties shared by all Envoy access logs. + */ + 'common_properties'?: (_envoy_data_accesslog_v3_AccessLogCommon | null); + /** + * Properties of the TCP connection. + */ + 'connection_properties'?: (_envoy_data_accesslog_v3_ConnectionProperties | null); +} + +export interface TCPAccessLogEntry__Output { + /** + * Common properties shared by all Envoy access logs. + */ + 'common_properties': (_envoy_data_accesslog_v3_AccessLogCommon__Output | null); + /** + * Properties of the TCP connection. + */ + 'connection_properties': (_envoy_data_accesslog_v3_ConnectionProperties__Output | null); +} diff --git a/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/TLSProperties.ts b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/TLSProperties.ts new file mode 100644 index 000000000..106d24d09 --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/TLSProperties.ts @@ -0,0 +1,131 @@ +// Original file: deps/envoy-api/envoy/data/accesslog/v3/accesslog.proto + +import type { UInt32Value as _google_protobuf_UInt32Value, UInt32Value__Output as _google_protobuf_UInt32Value__Output } from '../../../../google/protobuf/UInt32Value'; + +export interface _envoy_data_accesslog_v3_TLSProperties_CertificateProperties { + /** + * SANs present in the certificate. + */ + 'subject_alt_name'?: (_envoy_data_accesslog_v3_TLSProperties_CertificateProperties_SubjectAltName)[]; + /** + * The subject field of the certificate. + */ + 'subject'?: (string); +} + +export interface _envoy_data_accesslog_v3_TLSProperties_CertificateProperties__Output { + /** + * SANs present in the certificate. + */ + 'subject_alt_name': (_envoy_data_accesslog_v3_TLSProperties_CertificateProperties_SubjectAltName__Output)[]; + /** + * The subject field of the certificate. + */ + 'subject': (string); +} + +export interface _envoy_data_accesslog_v3_TLSProperties_CertificateProperties_SubjectAltName { + 'uri'?: (string); + /** + * [#not-implemented-hide:] + */ + 'dns'?: (string); + 'san'?: "uri"|"dns"; +} + +export interface _envoy_data_accesslog_v3_TLSProperties_CertificateProperties_SubjectAltName__Output { + 'uri'?: (string); + /** + * [#not-implemented-hide:] + */ + 'dns'?: (string); + 'san': "uri"|"dns"; +} + +// Original file: deps/envoy-api/envoy/data/accesslog/v3/accesslog.proto + +export enum _envoy_data_accesslog_v3_TLSProperties_TLSVersion { + VERSION_UNSPECIFIED = 0, + TLSv1 = 1, + TLSv1_1 = 2, + TLSv1_2 = 3, + TLSv1_3 = 4, +} + +/** + * Properties of a negotiated TLS connection. + * [#next-free-field: 8] + */ +export interface TLSProperties { + /** + * Version of TLS that was negotiated. + */ + 'tls_version'?: (_envoy_data_accesslog_v3_TLSProperties_TLSVersion | keyof typeof _envoy_data_accesslog_v3_TLSProperties_TLSVersion); + /** + * TLS cipher suite negotiated during handshake. The value is a + * four-digit hex code defined by the IANA TLS Cipher Suite Registry + * (e.g. ``009C`` for ``TLS_RSA_WITH_AES_128_GCM_SHA256``). + * + * Here it is expressed as an integer. + */ + 'tls_cipher_suite'?: (_google_protobuf_UInt32Value | null); + /** + * SNI hostname from handshake. + */ + 'tls_sni_hostname'?: (string); + /** + * Properties of the local certificate used to negotiate TLS. + */ + 'local_certificate_properties'?: (_envoy_data_accesslog_v3_TLSProperties_CertificateProperties | null); + /** + * Properties of the peer certificate used to negotiate TLS. + */ + 'peer_certificate_properties'?: (_envoy_data_accesslog_v3_TLSProperties_CertificateProperties | null); + /** + * The TLS session ID. + */ + 'tls_session_id'?: (string); + /** + * The ``JA3`` fingerprint when ``JA3`` fingerprinting is enabled. + */ + 'ja3_fingerprint'?: (string); +} + +/** + * Properties of a negotiated TLS connection. + * [#next-free-field: 8] + */ +export interface TLSProperties__Output { + /** + * Version of TLS that was negotiated. + */ + 'tls_version': (keyof typeof _envoy_data_accesslog_v3_TLSProperties_TLSVersion); + /** + * TLS cipher suite negotiated during handshake. The value is a + * four-digit hex code defined by the IANA TLS Cipher Suite Registry + * (e.g. ``009C`` for ``TLS_RSA_WITH_AES_128_GCM_SHA256``). + * + * Here it is expressed as an integer. + */ + 'tls_cipher_suite': (_google_protobuf_UInt32Value__Output | null); + /** + * SNI hostname from handshake. + */ + 'tls_sni_hostname': (string); + /** + * Properties of the local certificate used to negotiate TLS. + */ + 'local_certificate_properties': (_envoy_data_accesslog_v3_TLSProperties_CertificateProperties__Output | null); + /** + * Properties of the peer certificate used to negotiate TLS. + */ + 'peer_certificate_properties': (_envoy_data_accesslog_v3_TLSProperties_CertificateProperties__Output | null); + /** + * The TLS session ID. + */ + 'tls_session_id': (string); + /** + * The ``JA3`` fingerprint when ``JA3`` fingerprinting is enabled. + */ + 'ja3_fingerprint': (string); +} diff --git a/packages/grpc-js-xds/src/generated/envoy/extensions/filters/http/fault/v3/HTTPFault.ts b/packages/grpc-js-xds/src/generated/envoy/extensions/filters/http/fault/v3/HTTPFault.ts index d78bd9e4a..dd3a3b50c 100644 --- a/packages/grpc-js-xds/src/generated/envoy/extensions/filters/http/fault/v3/HTTPFault.ts +++ b/packages/grpc-js-xds/src/generated/envoy/extensions/filters/http/fault/v3/HTTPFault.ts @@ -5,9 +5,10 @@ import type { FaultAbort as _envoy_extensions_filters_http_fault_v3_FaultAbort, import type { HeaderMatcher as _envoy_config_route_v3_HeaderMatcher, HeaderMatcher__Output as _envoy_config_route_v3_HeaderMatcher__Output } from '../../../../../../envoy/config/route/v3/HeaderMatcher'; import type { UInt32Value as _google_protobuf_UInt32Value, UInt32Value__Output as _google_protobuf_UInt32Value__Output } from '../../../../../../google/protobuf/UInt32Value'; import type { FaultRateLimit as _envoy_extensions_filters_common_fault_v3_FaultRateLimit, FaultRateLimit__Output as _envoy_extensions_filters_common_fault_v3_FaultRateLimit__Output } from '../../../../../../envoy/extensions/filters/common/fault/v3/FaultRateLimit'; +import type { Struct as _google_protobuf_Struct, Struct__Output as _google_protobuf_Struct__Output } from '../../../../../../google/protobuf/Struct'; /** - * [#next-free-field: 16] + * [#next-free-field: 17] */ export interface HTTPFault { /** @@ -17,7 +18,7 @@ export interface HTTPFault { 'delay'?: (_envoy_extensions_filters_common_fault_v3_FaultDelay | null); /** * If specified, the filter will abort requests based on the values in - * the object. At least *abort* or *delay* must be specified. + * the object. At least ``abort`` or ``delay`` must be specified. */ 'abort'?: (_envoy_extensions_filters_http_fault_v3_FaultAbort | null); /** @@ -35,7 +36,7 @@ export interface HTTPFault { * The filter will check the request's headers against all the specified * headers in the filter config. A match will happen if all the headers in the * config are present in the request with the same values (or based on - * presence if the *value* field is not in the config). + * presence if the ``value`` field is not in the config). */ 'headers'?: (_envoy_config_route_v3_HeaderMatcher)[]; /** @@ -52,9 +53,9 @@ export interface HTTPFault { * filter. Note that because this setting can be overridden at the route level, it's possible * for the number of active faults to be greater than this value (if injected via a different * route). If not specified, defaults to unlimited. This setting can be overridden via - * `runtime ` and any faults that are not injected - * due to overflow will be indicated via the `faults_overflow - * ` stat. + * ``runtime `` and any faults that are not injected + * due to overflow will be indicated via the ``faults_overflow + * `` stat. * * .. attention:: * Like other :ref:`circuit breakers ` in Envoy, this is a fuzzy @@ -114,10 +115,18 @@ export interface HTTPFault { * Default value is false. */ 'disable_downstream_cluster_stats'?: (boolean); + /** + * When an abort or delay fault is executed, the metadata struct provided here will be added to the + * request's dynamic metadata under the namespace corresponding to the name of the fault filter. + * This data can be logged as part of Access Logs using the :ref:`command operator + * ` %DYNAMIC_METADATA(NAMESPACE)%, where NAMESPACE is the name of + * the fault filter. + */ + 'filter_metadata'?: (_google_protobuf_Struct | null); } /** - * [#next-free-field: 16] + * [#next-free-field: 17] */ export interface HTTPFault__Output { /** @@ -127,7 +136,7 @@ export interface HTTPFault__Output { 'delay': (_envoy_extensions_filters_common_fault_v3_FaultDelay__Output | null); /** * If specified, the filter will abort requests based on the values in - * the object. At least *abort* or *delay* must be specified. + * the object. At least ``abort`` or ``delay`` must be specified. */ 'abort': (_envoy_extensions_filters_http_fault_v3_FaultAbort__Output | null); /** @@ -145,7 +154,7 @@ export interface HTTPFault__Output { * The filter will check the request's headers against all the specified * headers in the filter config. A match will happen if all the headers in the * config are present in the request with the same values (or based on - * presence if the *value* field is not in the config). + * presence if the ``value`` field is not in the config). */ 'headers': (_envoy_config_route_v3_HeaderMatcher__Output)[]; /** @@ -162,9 +171,9 @@ export interface HTTPFault__Output { * filter. Note that because this setting can be overridden at the route level, it's possible * for the number of active faults to be greater than this value (if injected via a different * route). If not specified, defaults to unlimited. This setting can be overridden via - * `runtime ` and any faults that are not injected - * due to overflow will be indicated via the `faults_overflow - * ` stat. + * ``runtime `` and any faults that are not injected + * due to overflow will be indicated via the ``faults_overflow + * `` stat. * * .. attention:: * Like other :ref:`circuit breakers ` in Envoy, this is a fuzzy @@ -224,4 +233,12 @@ export interface HTTPFault__Output { * Default value is false. */ 'disable_downstream_cluster_stats': (boolean); + /** + * When an abort or delay fault is executed, the metadata struct provided here will be added to the + * request's dynamic metadata under the namespace corresponding to the name of the fault filter. + * This data can be logged as part of Access Logs using the :ref:`command operator + * ` %DYNAMIC_METADATA(NAMESPACE)%, where NAMESPACE is the name of + * the fault filter. + */ + 'filter_metadata': (_google_protobuf_Struct__Output | null); } diff --git a/packages/grpc-js-xds/src/generated/envoy/extensions/filters/network/http_connection_manager/v3/HttpConnectionManager.ts b/packages/grpc-js-xds/src/generated/envoy/extensions/filters/network/http_connection_manager/v3/HttpConnectionManager.ts index fdf7084fe..1b4f36fd8 100644 --- a/packages/grpc-js-xds/src/generated/envoy/extensions/filters/network/http_connection_manager/v3/HttpConnectionManager.ts +++ b/packages/grpc-js-xds/src/generated/envoy/extensions/filters/network/http_connection_manager/v3/HttpConnectionManager.ts @@ -19,6 +19,7 @@ import type { SchemeHeaderTransformation as _envoy_config_core_v3_SchemeHeaderTr import type { Percent as _envoy_type_v3_Percent, Percent__Output as _envoy_type_v3_Percent__Output } from '../../../../../../envoy/type/v3/Percent'; import type { CustomTag as _envoy_type_tracing_v3_CustomTag, CustomTag__Output as _envoy_type_tracing_v3_CustomTag__Output } from '../../../../../../envoy/type/tracing/v3/CustomTag'; import type { _envoy_config_trace_v3_Tracing_Http, _envoy_config_trace_v3_Tracing_Http__Output } from '../../../../../../envoy/config/trace/v3/Tracing'; +import type { CidrRange as _envoy_config_core_v3_CidrRange, CidrRange__Output as _envoy_config_core_v3_CidrRange__Output } from '../../../../../../envoy/config/core/v3/CidrRange'; import type { PathTransformation as _envoy_type_http_v3_PathTransformation, PathTransformation__Output as _envoy_type_http_v3_PathTransformation__Output } from '../../../../../../envoy/type/http/v3/PathTransformation'; // Original file: deps/envoy-api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto @@ -83,11 +84,68 @@ export enum _envoy_extensions_filters_network_http_connection_manager_v3_HttpCon ALWAYS_FORWARD_ONLY = 4, } +export interface _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_HcmAccessLogOptions { + /** + * The interval to flush the above access logs. By default, the HCM will flush exactly one access log + * on stream close, when the HTTP request is complete. If this field is set, the HCM will flush access + * logs periodically at the specified interval. This is especially useful in the case of long-lived + * requests, such as CONNECT and Websockets. Final access logs can be detected via the + * `requestComplete()` method of `StreamInfo` in access log filters, or thru the `%DURATION%` substitution + * string. + * The interval must be at least 1 millisecond. + */ + 'access_log_flush_interval'?: (_google_protobuf_Duration | null); + /** + * If set to true, HCM will flush an access log when a new HTTP request is received, after request + * headers have been evaluated, before iterating through the HTTP filter chain. + * This log record, if enabled, does not depend on periodic log records or request completion log. + * Details related to upstream cluster, such as upstream host, will not be available for this log. + */ + 'flush_access_log_on_new_request'?: (boolean); + /** + * If true, the HCM will flush an access log when a tunnel is successfully established. For example, + * this could be when an upstream has successfully returned 101 Switching Protocols, or when the proxy + * has returned 200 to a CONNECT request. + */ + 'flush_log_on_tunnel_successfully_established'?: (boolean); +} + +export interface _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_HcmAccessLogOptions__Output { + /** + * The interval to flush the above access logs. By default, the HCM will flush exactly one access log + * on stream close, when the HTTP request is complete. If this field is set, the HCM will flush access + * logs periodically at the specified interval. This is especially useful in the case of long-lived + * requests, such as CONNECT and Websockets. Final access logs can be detected via the + * `requestComplete()` method of `StreamInfo` in access log filters, or thru the `%DURATION%` substitution + * string. + * The interval must be at least 1 millisecond. + */ + 'access_log_flush_interval': (_google_protobuf_Duration__Output | null); + /** + * If set to true, HCM will flush an access log when a new HTTP request is received, after request + * headers have been evaluated, before iterating through the HTTP filter chain. + * This log record, if enabled, does not depend on periodic log records or request completion log. + * Details related to upstream cluster, such as upstream host, will not be available for this log. + */ + 'flush_access_log_on_new_request': (boolean); + /** + * If true, the HCM will flush an access log when a tunnel is successfully established. For example, + * this could be when an upstream has successfully returned 101 Switching Protocols, or when the proxy + * has returned 200 to a CONNECT request. + */ + 'flush_log_on_tunnel_successfully_established': (boolean); +} + export interface _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_InternalAddressConfig { /** * Whether unix socket addresses should be considered internal. */ 'unix_sockets'?: (boolean); + /** + * List of CIDR ranges that are treated as internal. If unset, then RFC1918 / RFC4193 + * IP addresses will be considered internal. + */ + 'cidr_ranges'?: (_envoy_config_core_v3_CidrRange)[]; } export interface _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_InternalAddressConfig__Output { @@ -95,6 +153,11 @@ export interface _envoy_extensions_filters_network_http_connection_manager_v3_Ht * Whether unix socket addresses should be considered internal. */ 'unix_sockets': (boolean); + /** + * List of CIDR ranges that are treated as internal. If unset, then RFC1918 / RFC4193 + * IP addresses will be considered internal. + */ + 'cidr_ranges': (_envoy_config_core_v3_CidrRange__Output)[]; } // Original file: deps/envoy-api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto @@ -116,15 +179,15 @@ export enum _envoy_extensions_filters_network_http_connection_manager_v3_HttpCon * path will be visible internally if a transformation is enabled. Any path rewrites that the * router performs (e.g. :ref:`regex_rewrite * ` or :ref:`prefix_rewrite - * `) will apply to the *:path* header + * `) will apply to the ``:path`` header * destined for the upstream. * - * Note: access logging and tracing will show the original *:path* header. + * Note: access logging and tracing will show the original ``:path`` header. */ export interface _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_PathNormalizationOptions { /** * [#not-implemented-hide:] Normalization applies internally before any processing of requests by - * HTTP filters, routing, and matching *and* will affect the forwarded *:path* header. Defaults + * HTTP filters, routing, and matching *and* will affect the forwarded ``:path`` header. Defaults * to :ref:`NormalizePathRFC3986 * `. When not * specified, this value may be overridden by the runtime variable @@ -136,7 +199,7 @@ export interface _envoy_extensions_filters_network_http_connection_manager_v3_Ht /** * [#not-implemented-hide:] Normalization only applies internally before any processing of * requests by HTTP filters, routing, and matching. These will be applied after full - * transformation is applied. The *:path* header before this transformation will be restored in + * transformation is applied. The ``:path`` header before this transformation will be restored in * the router filter and sent upstream unless it was mutated by a filter. Defaults to no * transformations. * Multiple actions can be applied in the same Transformation, forming a sequential @@ -153,15 +216,15 @@ export interface _envoy_extensions_filters_network_http_connection_manager_v3_Ht * path will be visible internally if a transformation is enabled. Any path rewrites that the * router performs (e.g. :ref:`regex_rewrite * ` or :ref:`prefix_rewrite - * `) will apply to the *:path* header + * `) will apply to the ``:path`` header * destined for the upstream. * - * Note: access logging and tracing will show the original *:path* header. + * Note: access logging and tracing will show the original ``:path`` header. */ export interface _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_PathNormalizationOptions__Output { /** * [#not-implemented-hide:] Normalization applies internally before any processing of requests by - * HTTP filters, routing, and matching *and* will affect the forwarded *:path* header. Defaults + * HTTP filters, routing, and matching *and* will affect the forwarded ``:path`` header. Defaults * to :ref:`NormalizePathRFC3986 * `. When not * specified, this value may be overridden by the runtime variable @@ -173,7 +236,7 @@ export interface _envoy_extensions_filters_network_http_connection_manager_v3_Ht /** * [#not-implemented-hide:] Normalization only applies internally before any processing of * requests by HTTP filters, routing, and matching. These will be applied after full - * transformation is applied. The *:path* header before this transformation will be restored in + * transformation is applied. The ``:path`` header before this transformation will be restored in * the router filter and sent upstream unless it was mutated by a filter. Defaults to no * transformations. * Multiple actions can be applied in the same Transformation, forming a sequential @@ -224,6 +287,122 @@ export enum _envoy_extensions_filters_network_http_connection_manager_v3_HttpCon UNESCAPE_AND_FORWARD = 4, } +/** + * Configures the manner in which the Proxy-Status HTTP response header is + * populated. + * + * See the [Proxy-Status + * RFC](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-proxy-status-08). + * [#comment:TODO: Update this with the non-draft URL when finalized.] + * + * The Proxy-Status header is a string of the form: + * + * "; error=; details=
" + * [#next-free-field: 7] + */ +export interface _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_ProxyStatusConfig { + /** + * If true, the details field of the Proxy-Status header is not populated with stream_info.response_code_details. + * This value defaults to ``false``, i.e. the ``details`` field is populated by default. + */ + 'remove_details'?: (boolean); + /** + * If true, the details field of the Proxy-Status header will not contain + * connection termination details. This value defaults to ``false``, i.e. the + * ``details`` field will contain connection termination details by default. + */ + 'remove_connection_termination_details'?: (boolean); + /** + * If true, the details field of the Proxy-Status header will not contain an + * enumeration of the Envoy ResponseFlags. This value defaults to ``false``, + * i.e. the ``details`` field will contain a list of ResponseFlags by default. + */ + 'remove_response_flags'?: (boolean); + /** + * If true, overwrites the existing Status header with the response code + * recommended by the Proxy-Status spec. + * This value defaults to ``false``, i.e. the HTTP response code is not + * overwritten. + */ + 'set_recommended_response_code'?: (boolean); + /** + * If ``use_node_id`` is set, Proxy-Status headers will use the Envoy's node + * ID as the name of the proxy. + */ + 'use_node_id'?: (boolean); + /** + * If ``literal_proxy_name`` is set, Proxy-Status headers will use this + * value as the name of the proxy. + */ + 'literal_proxy_name'?: (string); + /** + * The name of the proxy as it appears at the start of the Proxy-Status + * header. + * + * If neither of these values are set, this value defaults to ``server_name``, + * which itself defaults to "envoy". + */ + 'proxy_name'?: "use_node_id"|"literal_proxy_name"; +} + +/** + * Configures the manner in which the Proxy-Status HTTP response header is + * populated. + * + * See the [Proxy-Status + * RFC](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-proxy-status-08). + * [#comment:TODO: Update this with the non-draft URL when finalized.] + * + * The Proxy-Status header is a string of the form: + * + * "; error=; details=
" + * [#next-free-field: 7] + */ +export interface _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_ProxyStatusConfig__Output { + /** + * If true, the details field of the Proxy-Status header is not populated with stream_info.response_code_details. + * This value defaults to ``false``, i.e. the ``details`` field is populated by default. + */ + 'remove_details': (boolean); + /** + * If true, the details field of the Proxy-Status header will not contain + * connection termination details. This value defaults to ``false``, i.e. the + * ``details`` field will contain connection termination details by default. + */ + 'remove_connection_termination_details': (boolean); + /** + * If true, the details field of the Proxy-Status header will not contain an + * enumeration of the Envoy ResponseFlags. This value defaults to ``false``, + * i.e. the ``details`` field will contain a list of ResponseFlags by default. + */ + 'remove_response_flags': (boolean); + /** + * If true, overwrites the existing Status header with the response code + * recommended by the Proxy-Status spec. + * This value defaults to ``false``, i.e. the HTTP response code is not + * overwritten. + */ + 'set_recommended_response_code': (boolean); + /** + * If ``use_node_id`` is set, Proxy-Status headers will use the Envoy's node + * ID as the name of the proxy. + */ + 'use_node_id'?: (boolean); + /** + * If ``literal_proxy_name`` is set, Proxy-Status headers will use this + * value as the name of the proxy. + */ + 'literal_proxy_name'?: (string); + /** + * The name of the proxy as it appears at the start of the Proxy-Status + * header. + * + * If neither of these values are set, this value defaults to ``server_name``, + * which itself defaults to "envoy". + */ + 'proxy_name': "use_node_id"|"literal_proxy_name"; +} + // Original file: deps/envoy-api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto export enum _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_ServerHeaderTransformation { @@ -317,7 +496,7 @@ export interface _envoy_extensions_filters_network_http_connection_manager_v3_Ht * Target percentage of requests managed by this HTTP connection manager that will be force * traced if the :ref:`x-client-trace-id ` * header is set. This field is a direct analog for the runtime variable - * 'tracing.client_sampling' in the :ref:`HTTP Connection Manager + * 'tracing.client_enabled' in the :ref:`HTTP Connection Manager * `. * Default: 100% */ @@ -361,7 +540,7 @@ export interface _envoy_extensions_filters_network_http_connection_manager_v3_Ht * If not specified, no tracing will be performed. * * .. attention:: - * Please be aware that *envoy.tracers.opencensus* provider can only be configured once + * Please be aware that ``envoy.tracers.opencensus`` provider can only be configured once * in Envoy lifetime. * Any attempts to reconfigure it or to use different configurations for different HCM filters * will be rejected. @@ -379,7 +558,7 @@ export interface _envoy_extensions_filters_network_http_connection_manager_v3_Ht * Target percentage of requests managed by this HTTP connection manager that will be force * traced if the :ref:`x-client-trace-id ` * header is set. This field is a direct analog for the runtime variable - * 'tracing.client_sampling' in the :ref:`HTTP Connection Manager + * 'tracing.client_enabled' in the :ref:`HTTP Connection Manager * `. * Default: 100% */ @@ -423,7 +602,7 @@ export interface _envoy_extensions_filters_network_http_connection_manager_v3_Ht * If not specified, no tracing will be performed. * * .. attention:: - * Please be aware that *envoy.tracers.opencensus* provider can only be configured once + * Please be aware that ``envoy.tracers.opencensus`` provider can only be configured once * in Envoy lifetime. * Any attempts to reconfigure it or to use different configurations for different HCM filters * will be rejected. @@ -508,7 +687,7 @@ export interface _envoy_extensions_filters_network_http_connection_manager_v3_Ht } /** - * [#next-free-field: 49] + * [#next-free-field: 57] */ export interface HttpConnectionManager { /** @@ -549,6 +728,10 @@ export interface HttpConnectionManager { 'tracing'?: (_envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_Tracing | null); /** * Additional HTTP/1 settings that are passed to the HTTP/1 codec. + * [#comment:TODO: The following fields are ignored when the + * :ref:`header validation configuration ` + * is present: + * 1. :ref:`allow_chunked_length `] */ 'http_protocol_options'?: (_envoy_config_core_v3_Http1ProtocolOptions | null); /** @@ -557,7 +740,7 @@ export interface HttpConnectionManager { 'http2_protocol_options'?: (_envoy_config_core_v3_Http2ProtocolOptions | null); /** * An optional override that the connection manager will write to the server - * header in responses. If not set, the default is *envoy*. + * header in responses. If not set, the default is ``envoy``. */ 'server_name'?: (string); /** @@ -604,8 +787,8 @@ export interface HttpConnectionManager { * ` * is APPEND_FORWARD or SANITIZE_SET and the client connection is mTLS. It specifies the fields in * the client certificate to be forwarded. Note that in the - * :ref:`config_http_conn_man_headers_x-forwarded-client-cert` header, *Hash* is always set, and - * *By* is always set when the client certificate presents the URI type Subject Alternative Name + * :ref:`config_http_conn_man_headers_x-forwarded-client-cert` header, ``Hash`` is always set, and + * ``By`` is always set when the client certificate presents the URI type Subject Alternative Name * value. */ 'set_current_client_cert_details'?: (_envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_SetCurrentClientCertDetails | null); @@ -629,7 +812,7 @@ export interface HttpConnectionManager { * :ref:`use_remote_address * ` * is true and represent_ipv4_remote_address_as_ipv4_mapped_ipv6 is true and the remote address is - * an IPv4 address, the address will be mapped to IPv6 before it is appended to *x-forwarded-for*. + * an IPv4 address, the address will be mapped to IPv6 before it is appended to ``x-forwarded-for``. * This is useful for testing compatibility of upstream services that parse the header value. For * example, 50.0.0.1 is represented as ::FFFF:50.0.0.1. See `IPv4-Mapped IPv6 Addresses * `_ for details. This will also affect the @@ -647,7 +830,7 @@ export interface HttpConnectionManager { * has mutated the request headers. While :ref:`use_remote_address * ` * will also suppress XFF addition, it has consequences for logging and other - * Envoy uses of the remote address, so *skip_xff_append* should be used + * Envoy uses of the remote address, so ``skip_xff_append`` should be used * when only an elision of XFF addition is intended. */ 'skip_xff_append'?: (boolean); @@ -754,7 +937,7 @@ export interface HttpConnectionManager { 'max_request_headers_kb'?: (_google_protobuf_UInt32Value | null); /** * Should paths be normalized according to RFC 3986 before any processing of - * requests by HTTP filters or routing? This affects the upstream *:path* header + * requests by HTTP filters or routing? This affects the upstream ``:path`` header * as well. For paths that fail this check, Envoy will respond with 400 to * paths that are malformed. This defaults to false currently but will default * true in the future. When not specified, this value may be overridden by the @@ -764,6 +947,9 @@ export interface HttpConnectionManager { * for details of normalization. * Note that Envoy does not perform * `case normalization `_ + * [#comment:TODO: This field is ignored when the + * :ref:`header validation configuration ` + * is present.] */ 'normalize_path'?: (_google_protobuf_BoolValue | null); /** @@ -781,10 +967,13 @@ export interface HttpConnectionManager { 'preserve_external_request_id'?: (boolean); /** * Determines if adjacent slashes in the path are merged into one before any processing of - * requests by HTTP filters or routing. This affects the upstream *:path* header as well. Without - * setting this option, incoming requests with path `//dir///file` will not match against route - * with `prefix` match set to `/dir`. Defaults to `false`. Note that slash merging is not part of + * requests by HTTP filters or routing. This affects the upstream ``:path`` header as well. Without + * setting this option, incoming requests with path ``//dir///file`` will not match against route + * with ``prefix`` match set to ``/dir``. Defaults to ``false``. Note that slash merging is not part of * `HTTP spec `_ and is provided for convenience. + * [#comment:TODO: This field is ignored when the + * :ref:`header validation configuration ` + * is present.] */ 'merge_slashes'?: (boolean); /** @@ -835,10 +1024,10 @@ export interface HttpConnectionManager { * local port. This affects the upstream host header unless the method is * CONNECT in which case if no filter adds a port the original port will be restored before headers are * sent upstream. - * Without setting this option, incoming requests with host `example:443` will not match against - * route with :ref:`domains` match set to `example`. Defaults to `false`. Note that port removal is not part + * Without setting this option, incoming requests with host ``example:443`` will not match against + * route with :ref:`domains` match set to ``example``. Defaults to ``false``. Note that port removal is not part * of `HTTP spec `_ and is provided for convenience. - * Only one of `strip_matching_host_port` or `strip_any_host_port` can be set. + * Only one of ``strip_matching_host_port`` or ``strip_any_host_port`` can be set. */ 'strip_matching_host_port'?: (boolean); /** @@ -856,7 +1045,7 @@ export interface HttpConnectionManager { * ` or the new HTTP/2 option * :ref:`override_stream_error_on_invalid_http_message * ` - * *not* the deprecated but similarly named :ref:`stream_error_on_invalid_http_messaging + * ``not`` the deprecated but similarly named :ref:`stream_error_on_invalid_http_messaging * ` */ 'stream_error_on_invalid_http_message'?: (_google_protobuf_BoolValue | null); @@ -871,17 +1060,17 @@ export interface HttpConnectionManager { * of request by HTTP filters or routing. * This affects the upstream host header unless the method is CONNECT in * which case if no filter adds a port the original port will be restored before headers are sent upstream. - * Without setting this option, incoming requests with host `example:443` will not match against - * route with :ref:`domains` match set to `example`. Defaults to `false`. Note that port removal is not part + * Without setting this option, incoming requests with host ``example:443`` will not match against + * route with :ref:`domains` match set to ``example``. Defaults to ``false``. Note that port removal is not part * of `HTTP spec `_ and is provided for convenience. - * Only one of `strip_matching_host_port` or `strip_any_host_port` can be set. + * Only one of ``strip_matching_host_port`` or ``strip_any_host_port`` can be set. */ 'strip_any_host_port'?: (boolean); /** * [#not-implemented-hide:] Path normalization configuration. This includes * configurations for transformations (e.g. RFC 3986 normalization or merge * adjacent slashes) and the policy to apply them. The policy determines - * whether transformations affect the forwarded *:path* header. RFC 3986 path + * whether transformations affect the forwarded ``:path`` header. RFC 3986 path * normalization is enabled by default and the default policy is that the * normalized header will be forwarded. See :ref:`PathNormalizationOptions * ` @@ -899,6 +1088,9 @@ export interface HttpConnectionManager { * runtime variable. * The :ref:`http_connection_manager.path_with_escaped_slashes_action_sampling` runtime * variable can be used to apply the action to a portion of all requests. + * [#comment:TODO: This field is ignored when the + * :ref:`header validation configuration ` + * is present.] */ 'path_with_escaped_slashes_action'?: (_envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_PathWithEscapedSlashesAction | keyof typeof _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_PathWithEscapedSlashesAction); /** @@ -925,11 +1117,11 @@ export interface HttpConnectionManager { * Determines if trailing dot of the host should be removed from host/authority header before any * processing of request by HTTP filters or routing. * This affects the upstream host header. - * Without setting this option, incoming requests with host `example.com.` will not match against - * route with :ref:`domains` match set to `example.com`. Defaults to `false`. + * Without setting this option, incoming requests with host ``example.com.`` will not match against + * route with :ref:`domains` match set to ``example.com``. Defaults to ``false``. * When the incoming request contains a host/authority header that includes a port number, * setting this option will strip a trailing dot, if present, from the host section, - * leaving the port as is (e.g. host value `example.com.:443` will be updated to `example.com:443`). + * leaving the port as is (e.g. host value ``example.com.:443`` will be updated to ``example.com:443``). */ 'strip_trailing_host_dot'?: (boolean); /** @@ -938,12 +1130,88 @@ export interface HttpConnectionManager { * handling applies. */ 'scheme_header_transformation'?: (_envoy_config_core_v3_SchemeHeaderTransformation | null); + /** + * Proxy-Status HTTP response header configuration. + * If this config is set, the Proxy-Status HTTP response header field is + * populated. By default, it is not. + */ + 'proxy_status_config'?: (_envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_ProxyStatusConfig | null); + /** + * Configuration options for Header Validation (UHV). + * UHV is an extensible mechanism for checking validity of HTTP requests as well as providing + * normalization for request attributes, such as URI path. + * If the typed_header_validation_config is present it overrides the following options: + * ``normalize_path``, ``merge_slashes``, ``path_with_escaped_slashes_action`` + * ``http_protocol_options.allow_chunked_length``, ``common_http_protocol_options.headers_with_underscores_action``. + * + * The default UHV checks the following: + * + * #. HTTP/1 header map validity according to `RFC 7230 section 3.2`_ + * #. Syntax of HTTP/1 request target URI and response status + * #. HTTP/2 header map validity according to `RFC 7540 section 8.1.2`_ + * #. Syntax of HTTP/3 pseudo headers + * #. Syntax of ``Content-Length`` and ``Transfer-Encoding`` + * #. Validation of HTTP/1 requests with both ``Content-Length`` and ``Transfer-Encoding`` headers + * #. Normalization of the URI path according to `Normalization and Comparison `_ + * without `case normalization `_ + * + * [#not-implemented-hide:] + * [#extension-category: envoy.http.header_validators] + */ + 'typed_header_validation_config'?: (_envoy_config_core_v3_TypedExtensionConfig | null); + /** + * Append the `x-forwarded-port` header with the port value client used to connect to Envoy. It + * will be ignored if the `x-forwarded-port` header has been set by any trusted proxy in front of Envoy. + */ + 'append_x_forwarded_port'?: (boolean); + /** + * The configuration for the early header mutation extensions. + * + * When configured the extensions will be called before any routing, tracing, or any filter processing. + * Each extension will be applied in the order they are configured. + * If the same header is mutated by multiple extensions, then the last extension will win. + * + * [#extension-category: envoy.http.early_header_mutation] + */ + 'early_header_mutation_extensions'?: (_envoy_config_core_v3_TypedExtensionConfig)[]; + /** + * Whether the HCM will add ProxyProtocolFilterState to the Connection lifetime filter state. Defaults to `true`. + * This should be set to `false` in cases where Envoy's view of the downstream address may not correspond to the + * actual client address, for example, if there's another proxy in front of the Envoy. + */ + 'add_proxy_protocol_connection_state'?: (_google_protobuf_BoolValue | null); + /** + * .. attention:: + * This field is deprecated in favor of + * :ref:`access_log_flush_interval + * `. + * Note that if both this field and :ref:`access_log_flush_interval + * ` + * are specified, the former (deprecated field) is ignored. + */ + 'access_log_flush_interval'?: (_google_protobuf_Duration | null); + /** + * .. attention:: + * This field is deprecated in favor of + * :ref:`flush_access_log_on_new_request + * `. + * Note that if both this field and :ref:`flush_access_log_on_new_request + * ` + * are specified, the former (deprecated field) is ignored. + */ + 'flush_access_log_on_new_request'?: (boolean); + /** + * Additional access log options for HTTP connection manager. + */ + 'access_log_options'?: (_envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_HcmAccessLogOptions | null); 'route_specifier'?: "rds"|"route_config"|"scoped_routes"; 'strip_port_mode'?: "strip_any_host_port"; } /** - * [#next-free-field: 49] + * [#next-free-field: 57] */ export interface HttpConnectionManager__Output { /** @@ -984,6 +1252,10 @@ export interface HttpConnectionManager__Output { 'tracing': (_envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_Tracing__Output | null); /** * Additional HTTP/1 settings that are passed to the HTTP/1 codec. + * [#comment:TODO: The following fields are ignored when the + * :ref:`header validation configuration ` + * is present: + * 1. :ref:`allow_chunked_length `] */ 'http_protocol_options': (_envoy_config_core_v3_Http1ProtocolOptions__Output | null); /** @@ -992,7 +1264,7 @@ export interface HttpConnectionManager__Output { 'http2_protocol_options': (_envoy_config_core_v3_Http2ProtocolOptions__Output | null); /** * An optional override that the connection manager will write to the server - * header in responses. If not set, the default is *envoy*. + * header in responses. If not set, the default is ``envoy``. */ 'server_name': (string); /** @@ -1039,8 +1311,8 @@ export interface HttpConnectionManager__Output { * ` * is APPEND_FORWARD or SANITIZE_SET and the client connection is mTLS. It specifies the fields in * the client certificate to be forwarded. Note that in the - * :ref:`config_http_conn_man_headers_x-forwarded-client-cert` header, *Hash* is always set, and - * *By* is always set when the client certificate presents the URI type Subject Alternative Name + * :ref:`config_http_conn_man_headers_x-forwarded-client-cert` header, ``Hash`` is always set, and + * ``By`` is always set when the client certificate presents the URI type Subject Alternative Name * value. */ 'set_current_client_cert_details': (_envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_SetCurrentClientCertDetails__Output | null); @@ -1064,7 +1336,7 @@ export interface HttpConnectionManager__Output { * :ref:`use_remote_address * ` * is true and represent_ipv4_remote_address_as_ipv4_mapped_ipv6 is true and the remote address is - * an IPv4 address, the address will be mapped to IPv6 before it is appended to *x-forwarded-for*. + * an IPv4 address, the address will be mapped to IPv6 before it is appended to ``x-forwarded-for``. * This is useful for testing compatibility of upstream services that parse the header value. For * example, 50.0.0.1 is represented as ::FFFF:50.0.0.1. See `IPv4-Mapped IPv6 Addresses * `_ for details. This will also affect the @@ -1082,7 +1354,7 @@ export interface HttpConnectionManager__Output { * has mutated the request headers. While :ref:`use_remote_address * ` * will also suppress XFF addition, it has consequences for logging and other - * Envoy uses of the remote address, so *skip_xff_append* should be used + * Envoy uses of the remote address, so ``skip_xff_append`` should be used * when only an elision of XFF addition is intended. */ 'skip_xff_append': (boolean); @@ -1189,7 +1461,7 @@ export interface HttpConnectionManager__Output { 'max_request_headers_kb': (_google_protobuf_UInt32Value__Output | null); /** * Should paths be normalized according to RFC 3986 before any processing of - * requests by HTTP filters or routing? This affects the upstream *:path* header + * requests by HTTP filters or routing? This affects the upstream ``:path`` header * as well. For paths that fail this check, Envoy will respond with 400 to * paths that are malformed. This defaults to false currently but will default * true in the future. When not specified, this value may be overridden by the @@ -1199,6 +1471,9 @@ export interface HttpConnectionManager__Output { * for details of normalization. * Note that Envoy does not perform * `case normalization `_ + * [#comment:TODO: This field is ignored when the + * :ref:`header validation configuration ` + * is present.] */ 'normalize_path': (_google_protobuf_BoolValue__Output | null); /** @@ -1216,10 +1491,13 @@ export interface HttpConnectionManager__Output { 'preserve_external_request_id': (boolean); /** * Determines if adjacent slashes in the path are merged into one before any processing of - * requests by HTTP filters or routing. This affects the upstream *:path* header as well. Without - * setting this option, incoming requests with path `//dir///file` will not match against route - * with `prefix` match set to `/dir`. Defaults to `false`. Note that slash merging is not part of + * requests by HTTP filters or routing. This affects the upstream ``:path`` header as well. Without + * setting this option, incoming requests with path ``//dir///file`` will not match against route + * with ``prefix`` match set to ``/dir``. Defaults to ``false``. Note that slash merging is not part of * `HTTP spec `_ and is provided for convenience. + * [#comment:TODO: This field is ignored when the + * :ref:`header validation configuration ` + * is present.] */ 'merge_slashes': (boolean); /** @@ -1270,10 +1548,10 @@ export interface HttpConnectionManager__Output { * local port. This affects the upstream host header unless the method is * CONNECT in which case if no filter adds a port the original port will be restored before headers are * sent upstream. - * Without setting this option, incoming requests with host `example:443` will not match against - * route with :ref:`domains` match set to `example`. Defaults to `false`. Note that port removal is not part + * Without setting this option, incoming requests with host ``example:443`` will not match against + * route with :ref:`domains` match set to ``example``. Defaults to ``false``. Note that port removal is not part * of `HTTP spec `_ and is provided for convenience. - * Only one of `strip_matching_host_port` or `strip_any_host_port` can be set. + * Only one of ``strip_matching_host_port`` or ``strip_any_host_port`` can be set. */ 'strip_matching_host_port': (boolean); /** @@ -1291,7 +1569,7 @@ export interface HttpConnectionManager__Output { * ` or the new HTTP/2 option * :ref:`override_stream_error_on_invalid_http_message * ` - * *not* the deprecated but similarly named :ref:`stream_error_on_invalid_http_messaging + * ``not`` the deprecated but similarly named :ref:`stream_error_on_invalid_http_messaging * ` */ 'stream_error_on_invalid_http_message': (_google_protobuf_BoolValue__Output | null); @@ -1306,17 +1584,17 @@ export interface HttpConnectionManager__Output { * of request by HTTP filters or routing. * This affects the upstream host header unless the method is CONNECT in * which case if no filter adds a port the original port will be restored before headers are sent upstream. - * Without setting this option, incoming requests with host `example:443` will not match against - * route with :ref:`domains` match set to `example`. Defaults to `false`. Note that port removal is not part + * Without setting this option, incoming requests with host ``example:443`` will not match against + * route with :ref:`domains` match set to ``example``. Defaults to ``false``. Note that port removal is not part * of `HTTP spec `_ and is provided for convenience. - * Only one of `strip_matching_host_port` or `strip_any_host_port` can be set. + * Only one of ``strip_matching_host_port`` or ``strip_any_host_port`` can be set. */ 'strip_any_host_port'?: (boolean); /** * [#not-implemented-hide:] Path normalization configuration. This includes * configurations for transformations (e.g. RFC 3986 normalization or merge * adjacent slashes) and the policy to apply them. The policy determines - * whether transformations affect the forwarded *:path* header. RFC 3986 path + * whether transformations affect the forwarded ``:path`` header. RFC 3986 path * normalization is enabled by default and the default policy is that the * normalized header will be forwarded. See :ref:`PathNormalizationOptions * ` @@ -1334,6 +1612,9 @@ export interface HttpConnectionManager__Output { * runtime variable. * The :ref:`http_connection_manager.path_with_escaped_slashes_action_sampling` runtime * variable can be used to apply the action to a portion of all requests. + * [#comment:TODO: This field is ignored when the + * :ref:`header validation configuration ` + * is present.] */ 'path_with_escaped_slashes_action': (keyof typeof _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_PathWithEscapedSlashesAction); /** @@ -1360,11 +1641,11 @@ export interface HttpConnectionManager__Output { * Determines if trailing dot of the host should be removed from host/authority header before any * processing of request by HTTP filters or routing. * This affects the upstream host header. - * Without setting this option, incoming requests with host `example.com.` will not match against - * route with :ref:`domains` match set to `example.com`. Defaults to `false`. + * Without setting this option, incoming requests with host ``example.com.`` will not match against + * route with :ref:`domains` match set to ``example.com``. Defaults to ``false``. * When the incoming request contains a host/authority header that includes a port number, * setting this option will strip a trailing dot, if present, from the host section, - * leaving the port as is (e.g. host value `example.com.:443` will be updated to `example.com:443`). + * leaving the port as is (e.g. host value ``example.com.:443`` will be updated to ``example.com:443``). */ 'strip_trailing_host_dot': (boolean); /** @@ -1373,6 +1654,82 @@ export interface HttpConnectionManager__Output { * handling applies. */ 'scheme_header_transformation': (_envoy_config_core_v3_SchemeHeaderTransformation__Output | null); + /** + * Proxy-Status HTTP response header configuration. + * If this config is set, the Proxy-Status HTTP response header field is + * populated. By default, it is not. + */ + 'proxy_status_config': (_envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_ProxyStatusConfig__Output | null); + /** + * Configuration options for Header Validation (UHV). + * UHV is an extensible mechanism for checking validity of HTTP requests as well as providing + * normalization for request attributes, such as URI path. + * If the typed_header_validation_config is present it overrides the following options: + * ``normalize_path``, ``merge_slashes``, ``path_with_escaped_slashes_action`` + * ``http_protocol_options.allow_chunked_length``, ``common_http_protocol_options.headers_with_underscores_action``. + * + * The default UHV checks the following: + * + * #. HTTP/1 header map validity according to `RFC 7230 section 3.2`_ + * #. Syntax of HTTP/1 request target URI and response status + * #. HTTP/2 header map validity according to `RFC 7540 section 8.1.2`_ + * #. Syntax of HTTP/3 pseudo headers + * #. Syntax of ``Content-Length`` and ``Transfer-Encoding`` + * #. Validation of HTTP/1 requests with both ``Content-Length`` and ``Transfer-Encoding`` headers + * #. Normalization of the URI path according to `Normalization and Comparison `_ + * without `case normalization `_ + * + * [#not-implemented-hide:] + * [#extension-category: envoy.http.header_validators] + */ + 'typed_header_validation_config': (_envoy_config_core_v3_TypedExtensionConfig__Output | null); + /** + * Append the `x-forwarded-port` header with the port value client used to connect to Envoy. It + * will be ignored if the `x-forwarded-port` header has been set by any trusted proxy in front of Envoy. + */ + 'append_x_forwarded_port': (boolean); + /** + * The configuration for the early header mutation extensions. + * + * When configured the extensions will be called before any routing, tracing, or any filter processing. + * Each extension will be applied in the order they are configured. + * If the same header is mutated by multiple extensions, then the last extension will win. + * + * [#extension-category: envoy.http.early_header_mutation] + */ + 'early_header_mutation_extensions': (_envoy_config_core_v3_TypedExtensionConfig__Output)[]; + /** + * Whether the HCM will add ProxyProtocolFilterState to the Connection lifetime filter state. Defaults to `true`. + * This should be set to `false` in cases where Envoy's view of the downstream address may not correspond to the + * actual client address, for example, if there's another proxy in front of the Envoy. + */ + 'add_proxy_protocol_connection_state': (_google_protobuf_BoolValue__Output | null); + /** + * .. attention:: + * This field is deprecated in favor of + * :ref:`access_log_flush_interval + * `. + * Note that if both this field and :ref:`access_log_flush_interval + * ` + * are specified, the former (deprecated field) is ignored. + */ + 'access_log_flush_interval': (_google_protobuf_Duration__Output | null); + /** + * .. attention:: + * This field is deprecated in favor of + * :ref:`flush_access_log_on_new_request + * `. + * Note that if both this field and :ref:`flush_access_log_on_new_request + * ` + * are specified, the former (deprecated field) is ignored. + */ + 'flush_access_log_on_new_request': (boolean); + /** + * Additional access log options for HTTP connection manager. + */ + 'access_log_options': (_envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_HcmAccessLogOptions__Output | null); 'route_specifier': "rds"|"route_config"|"scoped_routes"; 'strip_port_mode': "strip_any_host_port"; } diff --git a/packages/grpc-js-xds/src/generated/envoy/extensions/filters/network/http_connection_manager/v3/HttpFilter.ts b/packages/grpc-js-xds/src/generated/envoy/extensions/filters/network/http_connection_manager/v3/HttpFilter.ts index d1c1cbc06..1550ca237 100644 --- a/packages/grpc-js-xds/src/generated/envoy/extensions/filters/network/http_connection_manager/v3/HttpFilter.ts +++ b/packages/grpc-js-xds/src/generated/envoy/extensions/filters/network/http_connection_manager/v3/HttpFilter.ts @@ -8,9 +8,7 @@ import type { ExtensionConfigSource as _envoy_config_core_v3_ExtensionConfigSour */ export interface HttpFilter { /** - * The name of the filter configuration. The name is used as a fallback to - * select an extension if the type of the configuration proto is not - * sufficient. It also serves as a resource name in ExtensionConfigDS. + * The name of the filter configuration. It also serves as a resource name in ExtensionConfigDS. */ 'name'?: (string); /** @@ -38,7 +36,6 @@ export interface HttpFilter { * If true, clients that do not support this filter may ignore the * filter but otherwise accept the config. * Otherwise, clients that do not support this filter must reject the config. - * This is also same with typed per filter config. */ 'is_optional'?: (boolean); 'config_type'?: "typed_config"|"config_discovery"; @@ -49,9 +46,7 @@ export interface HttpFilter { */ export interface HttpFilter__Output { /** - * The name of the filter configuration. The name is used as a fallback to - * select an extension if the type of the configuration proto is not - * sufficient. It also serves as a resource name in ExtensionConfigDS. + * The name of the filter configuration. It also serves as a resource name in ExtensionConfigDS. */ 'name': (string); /** @@ -79,7 +74,6 @@ export interface HttpFilter__Output { * If true, clients that do not support this filter may ignore the * filter but otherwise accept the config. * Otherwise, clients that do not support this filter must reject the config. - * This is also same with typed per filter config. */ 'is_optional': (boolean); 'config_type': "typed_config"|"config_discovery"; diff --git a/packages/grpc-js-xds/src/generated/envoy/extensions/filters/network/http_connection_manager/v3/ResponseMapper.ts b/packages/grpc-js-xds/src/generated/envoy/extensions/filters/network/http_connection_manager/v3/ResponseMapper.ts index 26d14a07d..66533b411 100644 --- a/packages/grpc-js-xds/src/generated/envoy/extensions/filters/network/http_connection_manager/v3/ResponseMapper.ts +++ b/packages/grpc-js-xds/src/generated/envoy/extensions/filters/network/http_connection_manager/v3/ResponseMapper.ts @@ -20,12 +20,12 @@ export interface ResponseMapper { */ 'status_code'?: (_google_protobuf_UInt32Value | null); /** - * The new local reply body text if specified. It will be used in the `%LOCAL_REPLY_BODY%` - * command operator in the `body_format`. + * The new local reply body text if specified. It will be used in the ``%LOCAL_REPLY_BODY%`` + * command operator in the ``body_format``. */ 'body'?: (_envoy_config_core_v3_DataSource | null); /** - * A per mapper `body_format` to override the :ref:`body_format `. + * A per mapper ``body_format`` to override the :ref:`body_format `. * It will be used when this mapper is matched. */ 'body_format_override'?: (_envoy_config_core_v3_SubstitutionFormatString | null); @@ -50,12 +50,12 @@ export interface ResponseMapper__Output { */ 'status_code': (_google_protobuf_UInt32Value__Output | null); /** - * The new local reply body text if specified. It will be used in the `%LOCAL_REPLY_BODY%` - * command operator in the `body_format`. + * The new local reply body text if specified. It will be used in the ``%LOCAL_REPLY_BODY%`` + * command operator in the ``body_format``. */ 'body': (_envoy_config_core_v3_DataSource__Output | null); /** - * A per mapper `body_format` to override the :ref:`body_format `. + * A per mapper ``body_format`` to override the :ref:`body_format `. * It will be used when this mapper is matched. */ 'body_format_override': (_envoy_config_core_v3_SubstitutionFormatString__Output | null); diff --git a/packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/wrr_locality/v3/WrrLocality.ts b/packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/wrr_locality/v3/WrrLocality.ts new file mode 100644 index 000000000..d35fb06e5 --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/wrr_locality/v3/WrrLocality.ts @@ -0,0 +1,25 @@ +// Original file: deps/envoy-api/envoy/extensions/load_balancing_policies/wrr_locality/v3/wrr_locality.proto + +import type { LoadBalancingPolicy as _envoy_config_cluster_v3_LoadBalancingPolicy, LoadBalancingPolicy__Output as _envoy_config_cluster_v3_LoadBalancingPolicy__Output } from '../../../../../envoy/config/cluster/v3/LoadBalancingPolicy'; + +/** + * Configuration for the wrr_locality LB policy. See the :ref:`load balancing architecture overview + * ` for more information. + */ +export interface WrrLocality { + /** + * The child LB policy to create for endpoint-picking within the chosen locality. + */ + 'endpoint_picking_policy'?: (_envoy_config_cluster_v3_LoadBalancingPolicy | null); +} + +/** + * Configuration for the wrr_locality LB policy. See the :ref:`load balancing architecture overview + * ` for more information. + */ +export interface WrrLocality__Output { + /** + * The child LB policy to create for endpoint-picking within the chosen locality. + */ + 'endpoint_picking_policy': (_envoy_config_cluster_v3_LoadBalancingPolicy__Output | null); +} diff --git a/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/AggregatedDiscoveryService.ts b/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/AggregatedDiscoveryService.ts index e6498177b..e8d7df1f2 100644 --- a/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/AggregatedDiscoveryService.ts +++ b/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/AggregatedDiscoveryService.ts @@ -8,7 +8,7 @@ import type { DiscoveryRequest as _envoy_service_discovery_v3_DiscoveryRequest, import type { DiscoveryResponse as _envoy_service_discovery_v3_DiscoveryResponse, DiscoveryResponse__Output as _envoy_service_discovery_v3_DiscoveryResponse__Output } from '../../../../envoy/service/discovery/v3/DiscoveryResponse'; /** - * See https://github.com/lyft/envoy-api#apis for a description of the role of + * See https://github.com/envoyproxy/envoy-api#apis for a description of the role of * ADS and how it is intended to be used by a management server. ADS requests * have the same structure as their singleton xDS counterparts, but can * multiplex many resource types on a single stream. The type_url in the @@ -35,7 +35,7 @@ export interface AggregatedDiscoveryServiceClient extends grpc.Client { } /** - * See https://github.com/lyft/envoy-api#apis for a description of the role of + * See https://github.com/envoyproxy/envoy-api#apis for a description of the role of * ADS and how it is intended to be used by a management server. ADS requests * have the same structure as their singleton xDS counterparts, but can * multiplex many resource types on a single stream. The type_url in the diff --git a/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/DeltaDiscoveryRequest.ts b/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/DeltaDiscoveryRequest.ts index 6e900970a..6c1a3cd95 100644 --- a/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/DeltaDiscoveryRequest.ts +++ b/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/DeltaDiscoveryRequest.ts @@ -2,6 +2,7 @@ import type { Node as _envoy_config_core_v3_Node, Node__Output as _envoy_config_core_v3_Node__Output } from '../../../../envoy/config/core/v3/Node'; import type { Status as _google_rpc_Status, Status__Output as _google_rpc_Status__Output } from '../../../../google/rpc/Status'; +import type { ResourceLocator as _envoy_service_discovery_v3_ResourceLocator, ResourceLocator__Output as _envoy_service_discovery_v3_ResourceLocator__Output } from '../../../../envoy/service/discovery/v3/ResourceLocator'; /** * DeltaDiscoveryRequest and DeltaDiscoveryResponse are used in a new gRPC @@ -36,7 +37,7 @@ import type { Status as _google_rpc_Status, Status__Output as _google_rpc_Status * In particular, initial_resource_versions being sent at the "start" of every * gRPC stream actually entails a message for each type_url, each with its own * initial_resource_versions. - * [#next-free-field: 8] + * [#next-free-field: 10] */ export interface DeltaDiscoveryRequest { /** @@ -45,9 +46,9 @@ export interface DeltaDiscoveryRequest { 'node'?: (_envoy_config_core_v3_Node | null); /** * Type of the resource that is being requested, e.g. - * "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment". This does not need to be set if - * resources are only referenced via *xds_resource_subscribe* and - * *xds_resources_unsubscribe*. + * ``type.googleapis.com/envoy.api.v2.ClusterLoadAssignment``. This does not need to be set if + * resources are only referenced via ``xds_resource_subscribe`` and + * ``xds_resources_unsubscribe``. */ 'type_url'?: (string); /** @@ -98,10 +99,26 @@ export interface DeltaDiscoveryRequest { 'response_nonce'?: (string); /** * This is populated when the previous :ref:`DiscoveryResponse ` - * failed to update configuration. The *message* field in *error_details* + * failed to update configuration. The ``message`` field in ``error_details`` * provides the Envoy internal exception related to the failure. */ 'error_detail'?: (_google_rpc_Status | null); + /** + * [#not-implemented-hide:] + * Alternative to ``resource_names_subscribe`` field that allows specifying dynamic parameters + * along with each resource name. + * Note that it is legal for a request to have some resources listed + * in ``resource_names_subscribe`` and others in ``resource_locators_subscribe``. + */ + 'resource_locators_subscribe'?: (_envoy_service_discovery_v3_ResourceLocator)[]; + /** + * [#not-implemented-hide:] + * Alternative to ``resource_names_unsubscribe`` field that allows specifying dynamic parameters + * along with each resource name. + * Note that it is legal for a request to have some resources listed + * in ``resource_names_unsubscribe`` and others in ``resource_locators_unsubscribe``. + */ + 'resource_locators_unsubscribe'?: (_envoy_service_discovery_v3_ResourceLocator)[]; } /** @@ -137,7 +154,7 @@ export interface DeltaDiscoveryRequest { * In particular, initial_resource_versions being sent at the "start" of every * gRPC stream actually entails a message for each type_url, each with its own * initial_resource_versions. - * [#next-free-field: 8] + * [#next-free-field: 10] */ export interface DeltaDiscoveryRequest__Output { /** @@ -146,9 +163,9 @@ export interface DeltaDiscoveryRequest__Output { 'node': (_envoy_config_core_v3_Node__Output | null); /** * Type of the resource that is being requested, e.g. - * "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment". This does not need to be set if - * resources are only referenced via *xds_resource_subscribe* and - * *xds_resources_unsubscribe*. + * ``type.googleapis.com/envoy.api.v2.ClusterLoadAssignment``. This does not need to be set if + * resources are only referenced via ``xds_resource_subscribe`` and + * ``xds_resources_unsubscribe``. */ 'type_url': (string); /** @@ -199,8 +216,24 @@ export interface DeltaDiscoveryRequest__Output { 'response_nonce': (string); /** * This is populated when the previous :ref:`DiscoveryResponse ` - * failed to update configuration. The *message* field in *error_details* + * failed to update configuration. The ``message`` field in ``error_details`` * provides the Envoy internal exception related to the failure. */ 'error_detail': (_google_rpc_Status__Output | null); + /** + * [#not-implemented-hide:] + * Alternative to ``resource_names_subscribe`` field that allows specifying dynamic parameters + * along with each resource name. + * Note that it is legal for a request to have some resources listed + * in ``resource_names_subscribe`` and others in ``resource_locators_subscribe``. + */ + 'resource_locators_subscribe': (_envoy_service_discovery_v3_ResourceLocator__Output)[]; + /** + * [#not-implemented-hide:] + * Alternative to ``resource_names_unsubscribe`` field that allows specifying dynamic parameters + * along with each resource name. + * Note that it is legal for a request to have some resources listed + * in ``resource_names_unsubscribe`` and others in ``resource_locators_unsubscribe``. + */ + 'resource_locators_unsubscribe': (_envoy_service_discovery_v3_ResourceLocator__Output)[]; } diff --git a/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/DeltaDiscoveryResponse.ts b/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/DeltaDiscoveryResponse.ts index efbbed85c..0728140ee 100644 --- a/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/DeltaDiscoveryResponse.ts +++ b/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/DeltaDiscoveryResponse.ts @@ -2,9 +2,10 @@ import type { Resource as _envoy_service_discovery_v3_Resource, Resource__Output as _envoy_service_discovery_v3_Resource__Output } from '../../../../envoy/service/discovery/v3/Resource'; import type { ControlPlane as _envoy_config_core_v3_ControlPlane, ControlPlane__Output as _envoy_config_core_v3_ControlPlane__Output } from '../../../../envoy/config/core/v3/ControlPlane'; +import type { ResourceName as _envoy_service_discovery_v3_ResourceName, ResourceName__Output as _envoy_service_discovery_v3_ResourceName__Output } from '../../../../envoy/service/discovery/v3/ResourceName'; /** - * [#next-free-field: 8] + * [#next-free-field: 9] */ export interface DeltaDiscoveryResponse { /** @@ -36,10 +37,16 @@ export interface DeltaDiscoveryResponse { * The control plane instance that sent the response. */ 'control_plane'?: (_envoy_config_core_v3_ControlPlane | null); + /** + * Alternative to removed_resources that allows specifying which variant of + * a resource is being removed. This variant must be used for any resource + * for which dynamic parameter constraints were sent to the client. + */ + 'removed_resource_names'?: (_envoy_service_discovery_v3_ResourceName)[]; } /** - * [#next-free-field: 8] + * [#next-free-field: 9] */ export interface DeltaDiscoveryResponse__Output { /** @@ -71,4 +78,10 @@ export interface DeltaDiscoveryResponse__Output { * The control plane instance that sent the response. */ 'control_plane': (_envoy_config_core_v3_ControlPlane__Output | null); + /** + * Alternative to removed_resources that allows specifying which variant of + * a resource is being removed. This variant must be used for any resource + * for which dynamic parameter constraints were sent to the client. + */ + 'removed_resource_names': (_envoy_service_discovery_v3_ResourceName__Output)[]; } diff --git a/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/DiscoveryRequest.ts b/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/DiscoveryRequest.ts index f392ab8ae..95f1299cb 100644 --- a/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/DiscoveryRequest.ts +++ b/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/DiscoveryRequest.ts @@ -2,11 +2,12 @@ import type { Node as _envoy_config_core_v3_Node, Node__Output as _envoy_config_core_v3_Node__Output } from '../../../../envoy/config/core/v3/Node'; import type { Status as _google_rpc_Status, Status__Output as _google_rpc_Status__Output } from '../../../../google/rpc/Status'; +import type { ResourceLocator as _envoy_service_discovery_v3_ResourceLocator, ResourceLocator__Output as _envoy_service_discovery_v3_ResourceLocator__Output } from '../../../../envoy/service/discovery/v3/ResourceLocator'; /** * A DiscoveryRequest requests a set of versioned resources of the same type for * a given Envoy node on some API. - * [#next-free-field: 7] + * [#next-free-field: 8] */ export interface DiscoveryRequest { /** @@ -49,17 +50,27 @@ export interface DiscoveryRequest { 'response_nonce'?: (string); /** * This is populated when the previous :ref:`DiscoveryResponse ` - * failed to update configuration. The *message* field in *error_details* provides the Envoy + * failed to update configuration. The ``message`` field in ``error_details`` provides the Envoy * internal exception related to the failure. It is only intended for consumption during manual * debugging, the string provided is not guaranteed to be stable across Envoy versions. */ 'error_detail'?: (_google_rpc_Status | null); + /** + * [#not-implemented-hide:] + * Alternative to ``resource_names`` field that allows specifying dynamic + * parameters along with each resource name. Clients that populate this + * field must be able to handle responses from the server where resources + * are wrapped in a Resource message. + * Note that it is legal for a request to have some resources listed + * in ``resource_names`` and others in ``resource_locators``. + */ + 'resource_locators'?: (_envoy_service_discovery_v3_ResourceLocator)[]; } /** * A DiscoveryRequest requests a set of versioned resources of the same type for * a given Envoy node on some API. - * [#next-free-field: 7] + * [#next-free-field: 8] */ export interface DiscoveryRequest__Output { /** @@ -102,9 +113,19 @@ export interface DiscoveryRequest__Output { 'response_nonce': (string); /** * This is populated when the previous :ref:`DiscoveryResponse ` - * failed to update configuration. The *message* field in *error_details* provides the Envoy + * failed to update configuration. The ``message`` field in ``error_details`` provides the Envoy * internal exception related to the failure. It is only intended for consumption during manual * debugging, the string provided is not guaranteed to be stable across Envoy versions. */ 'error_detail': (_google_rpc_Status__Output | null); + /** + * [#not-implemented-hide:] + * Alternative to ``resource_names`` field that allows specifying dynamic + * parameters along with each resource name. Clients that populate this + * field must be able to handle responses from the server where resources + * are wrapped in a Resource message. + * Note that it is legal for a request to have some resources listed + * in ``resource_names`` and others in ``resource_locators``. + */ + 'resource_locators': (_envoy_service_discovery_v3_ResourceLocator__Output)[]; } diff --git a/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/DynamicParameterConstraints.ts b/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/DynamicParameterConstraints.ts new file mode 100644 index 000000000..5bba10719 --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/DynamicParameterConstraints.ts @@ -0,0 +1,119 @@ +// Original file: deps/envoy-api/envoy/service/discovery/v3/discovery.proto + +import type { DynamicParameterConstraints as _envoy_service_discovery_v3_DynamicParameterConstraints, DynamicParameterConstraints__Output as _envoy_service_discovery_v3_DynamicParameterConstraints__Output } from '../../../../envoy/service/discovery/v3/DynamicParameterConstraints'; + +export interface _envoy_service_discovery_v3_DynamicParameterConstraints_ConstraintList { + 'constraints'?: (_envoy_service_discovery_v3_DynamicParameterConstraints)[]; +} + +export interface _envoy_service_discovery_v3_DynamicParameterConstraints_ConstraintList__Output { + 'constraints': (_envoy_service_discovery_v3_DynamicParameterConstraints__Output)[]; +} + +export interface _envoy_service_discovery_v3_DynamicParameterConstraints_SingleConstraint_Exists { +} + +export interface _envoy_service_discovery_v3_DynamicParameterConstraints_SingleConstraint_Exists__Output { +} + +/** + * A single constraint for a given key. + */ +export interface _envoy_service_discovery_v3_DynamicParameterConstraints_SingleConstraint { + /** + * The key to match against. + */ + 'key'?: (string); + /** + * Matches this exact value. + */ + 'value'?: (string); + /** + * Key is present (matches any value except for the key being absent). + * This allows setting a default constraint for clients that do + * not send a key at all, while there may be other clients that need + * special configuration based on that key. + */ + 'exists'?: (_envoy_service_discovery_v3_DynamicParameterConstraints_SingleConstraint_Exists | null); + 'constraint_type'?: "value"|"exists"; +} + +/** + * A single constraint for a given key. + */ +export interface _envoy_service_discovery_v3_DynamicParameterConstraints_SingleConstraint__Output { + /** + * The key to match against. + */ + 'key': (string); + /** + * Matches this exact value. + */ + 'value'?: (string); + /** + * Key is present (matches any value except for the key being absent). + * This allows setting a default constraint for clients that do + * not send a key at all, while there may be other clients that need + * special configuration based on that key. + */ + 'exists'?: (_envoy_service_discovery_v3_DynamicParameterConstraints_SingleConstraint_Exists__Output | null); + 'constraint_type': "value"|"exists"; +} + +/** + * A set of dynamic parameter constraints associated with a variant of an individual xDS resource. + * These constraints determine whether the resource matches a subscription based on the set of + * dynamic parameters in the subscription, as specified in the + * :ref:`ResourceLocator.dynamic_parameters` + * field. This allows xDS implementations (clients, servers, and caching proxies) to determine + * which variant of a resource is appropriate for a given client. + */ +export interface DynamicParameterConstraints { + /** + * A single constraint to evaluate. + */ + 'constraint'?: (_envoy_service_discovery_v3_DynamicParameterConstraints_SingleConstraint | null); + /** + * A list of constraints that match if any one constraint in the list + * matches. + */ + 'or_constraints'?: (_envoy_service_discovery_v3_DynamicParameterConstraints_ConstraintList | null); + /** + * A list of constraints that must all match. + */ + 'and_constraints'?: (_envoy_service_discovery_v3_DynamicParameterConstraints_ConstraintList | null); + /** + * The inverse (NOT) of a set of constraints. + */ + 'not_constraints'?: (_envoy_service_discovery_v3_DynamicParameterConstraints | null); + 'type'?: "constraint"|"or_constraints"|"and_constraints"|"not_constraints"; +} + +/** + * A set of dynamic parameter constraints associated with a variant of an individual xDS resource. + * These constraints determine whether the resource matches a subscription based on the set of + * dynamic parameters in the subscription, as specified in the + * :ref:`ResourceLocator.dynamic_parameters` + * field. This allows xDS implementations (clients, servers, and caching proxies) to determine + * which variant of a resource is appropriate for a given client. + */ +export interface DynamicParameterConstraints__Output { + /** + * A single constraint to evaluate. + */ + 'constraint'?: (_envoy_service_discovery_v3_DynamicParameterConstraints_SingleConstraint__Output | null); + /** + * A list of constraints that match if any one constraint in the list + * matches. + */ + 'or_constraints'?: (_envoy_service_discovery_v3_DynamicParameterConstraints_ConstraintList__Output | null); + /** + * A list of constraints that must all match. + */ + 'and_constraints'?: (_envoy_service_discovery_v3_DynamicParameterConstraints_ConstraintList__Output | null); + /** + * The inverse (NOT) of a set of constraints. + */ + 'not_constraints'?: (_envoy_service_discovery_v3_DynamicParameterConstraints__Output | null); + 'type': "constraint"|"or_constraints"|"and_constraints"|"not_constraints"; +} diff --git a/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/Resource.ts b/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/Resource.ts index 0e5897ab4..6bef71ff8 100644 --- a/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/Resource.ts +++ b/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/Resource.ts @@ -2,6 +2,8 @@ import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../../google/protobuf/Any'; import type { Duration as _google_protobuf_Duration, Duration__Output as _google_protobuf_Duration__Output } from '../../../../google/protobuf/Duration'; +import type { ResourceName as _envoy_service_discovery_v3_ResourceName, ResourceName__Output as _envoy_service_discovery_v3_ResourceName__Output } from '../../../../envoy/service/discovery/v3/ResourceName'; +import type { Metadata as _envoy_config_core_v3_Metadata, Metadata__Output as _envoy_config_core_v3_Metadata__Output } from '../../../../envoy/config/core/v3/Metadata'; /** * Cache control properties for the resource. @@ -30,7 +32,7 @@ export interface _envoy_service_discovery_v3_Resource_CacheControl__Output { } /** - * [#next-free-field: 8] + * [#next-free-field: 10] */ export interface Resource { /** @@ -44,6 +46,7 @@ export interface Resource { 'resource'?: (_google_protobuf_Any | null); /** * The resource's name, to distinguish it from others of the same type of resource. + * Only one of ``name`` or ``resource_name`` may be set. */ 'name'?: (string); /** @@ -71,10 +74,22 @@ export interface Resource { * [#not-implemented-hide:] */ 'cache_control'?: (_envoy_service_discovery_v3_Resource_CacheControl | null); + /** + * Alternative to the ``name`` field, to be used when the server supports + * multiple variants of the named resource that are differentiated by + * dynamic parameter constraints. + * Only one of ``name`` or ``resource_name`` may be set. + */ + 'resource_name'?: (_envoy_service_discovery_v3_ResourceName | null); + /** + * The Metadata field can be used to provide additional information for the resource. + * E.g. the trace data for debugging. + */ + 'metadata'?: (_envoy_config_core_v3_Metadata | null); } /** - * [#next-free-field: 8] + * [#next-free-field: 10] */ export interface Resource__Output { /** @@ -88,6 +103,7 @@ export interface Resource__Output { 'resource': (_google_protobuf_Any__Output | null); /** * The resource's name, to distinguish it from others of the same type of resource. + * Only one of ``name`` or ``resource_name`` may be set. */ 'name': (string); /** @@ -115,4 +131,16 @@ export interface Resource__Output { * [#not-implemented-hide:] */ 'cache_control': (_envoy_service_discovery_v3_Resource_CacheControl__Output | null); + /** + * Alternative to the ``name`` field, to be used when the server supports + * multiple variants of the named resource that are differentiated by + * dynamic parameter constraints. + * Only one of ``name`` or ``resource_name`` may be set. + */ + 'resource_name': (_envoy_service_discovery_v3_ResourceName__Output | null); + /** + * The Metadata field can be used to provide additional information for the resource. + * E.g. the trace data for debugging. + */ + 'metadata': (_envoy_config_core_v3_Metadata__Output | null); } diff --git a/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/ResourceLocator.ts b/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/ResourceLocator.ts new file mode 100644 index 000000000..c37dc68d3 --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/ResourceLocator.ts @@ -0,0 +1,34 @@ +// Original file: deps/envoy-api/envoy/service/discovery/v3/discovery.proto + + +/** + * Specifies a resource to be subscribed to. + */ +export interface ResourceLocator { + /** + * The resource name to subscribe to. + */ + 'name'?: (string); + /** + * A set of dynamic parameters used to match against the dynamic parameter + * constraints on the resource. This allows clients to select between + * multiple variants of the same resource. + */ + 'dynamic_parameters'?: ({[key: string]: string}); +} + +/** + * Specifies a resource to be subscribed to. + */ +export interface ResourceLocator__Output { + /** + * The resource name to subscribe to. + */ + 'name': (string); + /** + * A set of dynamic parameters used to match against the dynamic parameter + * constraints on the resource. This allows clients to select between + * multiple variants of the same resource. + */ + 'dynamic_parameters': ({[key: string]: string}); +} diff --git a/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/ResourceName.ts b/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/ResourceName.ts new file mode 100644 index 000000000..da7f4fd8a --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/service/discovery/v3/ResourceName.ts @@ -0,0 +1,33 @@ +// Original file: deps/envoy-api/envoy/service/discovery/v3/discovery.proto + +import type { DynamicParameterConstraints as _envoy_service_discovery_v3_DynamicParameterConstraints, DynamicParameterConstraints__Output as _envoy_service_discovery_v3_DynamicParameterConstraints__Output } from '../../../../envoy/service/discovery/v3/DynamicParameterConstraints'; + +/** + * Specifies a concrete resource name. + */ +export interface ResourceName { + /** + * The name of the resource. + */ + 'name'?: (string); + /** + * Dynamic parameter constraints associated with this resource. To be used by client-side caches + * (including xDS proxies) when matching subscribed resource locators. + */ + 'dynamic_parameter_constraints'?: (_envoy_service_discovery_v3_DynamicParameterConstraints | null); +} + +/** + * Specifies a concrete resource name. + */ +export interface ResourceName__Output { + /** + * The name of the resource. + */ + 'name': (string); + /** + * Dynamic parameter constraints associated with this resource. To be used by client-side caches + * (including xDS proxies) when matching subscribed resource locators. + */ + 'dynamic_parameter_constraints': (_envoy_service_discovery_v3_DynamicParameterConstraints__Output | null); +} diff --git a/packages/grpc-js-xds/src/generated/envoy/service/load_stats/v3/LoadStatsResponse.ts b/packages/grpc-js-xds/src/generated/envoy/service/load_stats/v3/LoadStatsResponse.ts index 40f561870..6429d18c8 100644 --- a/packages/grpc-js-xds/src/generated/envoy/service/load_stats/v3/LoadStatsResponse.ts +++ b/packages/grpc-js-xds/src/generated/envoy/service/load_stats/v3/LoadStatsResponse.ts @@ -9,22 +9,22 @@ import type { Duration as _google_protobuf_Duration, Duration__Output as _google export interface LoadStatsResponse { /** * Clusters to report stats for. - * Not populated if *send_all_clusters* is true. + * Not populated if ``send_all_clusters`` is true. */ 'clusters'?: (string)[]; /** * The minimum interval of time to collect stats over. This is only a minimum for two reasons: * * 1. There may be some delay from when the timer fires until stats sampling occurs. - * 2. For clusters that were already feature in the previous *LoadStatsResponse*, any traffic - * that is observed in between the corresponding previous *LoadStatsRequest* and this - * *LoadStatsResponse* will also be accumulated and billed to the cluster. This avoids a period + * 2. For clusters that were already feature in the previous ``LoadStatsResponse``, any traffic + * that is observed in between the corresponding previous ``LoadStatsRequest`` and this + * ``LoadStatsResponse`` will also be accumulated and billed to the cluster. This avoids a period * of inobservability that might otherwise exists between the messages. New clusters are not * subject to this consideration. */ 'load_reporting_interval'?: (_google_protobuf_Duration | null); /** - * Set to *true* if the management server supports endpoint granularity + * Set to ``true`` if the management server supports endpoint granularity * report. */ 'report_endpoint_granularity'?: (boolean); @@ -43,22 +43,22 @@ export interface LoadStatsResponse { export interface LoadStatsResponse__Output { /** * Clusters to report stats for. - * Not populated if *send_all_clusters* is true. + * Not populated if ``send_all_clusters`` is true. */ 'clusters': (string)[]; /** * The minimum interval of time to collect stats over. This is only a minimum for two reasons: * * 1. There may be some delay from when the timer fires until stats sampling occurs. - * 2. For clusters that were already feature in the previous *LoadStatsResponse*, any traffic - * that is observed in between the corresponding previous *LoadStatsRequest* and this - * *LoadStatsResponse* will also be accumulated and billed to the cluster. This avoids a period + * 2. For clusters that were already feature in the previous ``LoadStatsResponse``, any traffic + * that is observed in between the corresponding previous ``LoadStatsRequest`` and this + * ``LoadStatsResponse`` will also be accumulated and billed to the cluster. This avoids a period * of inobservability that might otherwise exists between the messages. New clusters are not * subject to this consideration. */ 'load_reporting_interval': (_google_protobuf_Duration__Output | null); /** - * Set to *true* if the management server supports endpoint granularity + * Set to ``true`` if the management server supports endpoint granularity * report. */ 'report_endpoint_granularity': (boolean); diff --git a/packages/grpc-js-xds/src/generated/envoy/type/http/v3/PathTransformation.ts b/packages/grpc-js-xds/src/generated/envoy/type/http/v3/PathTransformation.ts index 4dc10efcb..c8aca7e3f 100644 --- a/packages/grpc-js-xds/src/generated/envoy/type/http/v3/PathTransformation.ts +++ b/packages/grpc-js-xds/src/generated/envoy/type/http/v3/PathTransformation.ts @@ -3,10 +3,10 @@ /** * Determines if adjacent slashes are merged into one. A common use case is for a request path - * header. Using this option in `:ref: PathNormalizationOptions - * ` - * will allow incoming requests with path `//dir///file` to match against route with `prefix` - * match set to `/dir`. When using for header transformations, note that slash merging is not + * header. Using this option in ``:ref: PathNormalizationOptions + * `` + * will allow incoming requests with path ``//dir///file`` to match against route with ``prefix`` + * match set to ``/dir``. When using for header transformations, note that slash merging is not * part of `HTTP spec `_ and is provided for convenience. */ export interface _envoy_type_http_v3_PathTransformation_Operation_MergeSlashes { @@ -14,10 +14,10 @@ export interface _envoy_type_http_v3_PathTransformation_Operation_MergeSlashes { /** * Determines if adjacent slashes are merged into one. A common use case is for a request path - * header. Using this option in `:ref: PathNormalizationOptions - * ` - * will allow incoming requests with path `//dir///file` to match against route with `prefix` - * match set to `/dir`. When using for header transformations, note that slash merging is not + * header. Using this option in ``:ref: PathNormalizationOptions + * `` + * will allow incoming requests with path ``//dir///file`` to match against route with ``prefix`` + * match set to ``/dir``. When using for header transformations, note that slash merging is not * part of `HTTP spec `_ and is provided for convenience. */ export interface _envoy_type_http_v3_PathTransformation_Operation_MergeSlashes__Output { diff --git a/packages/grpc-js-xds/src/generated/envoy/type/matcher/v3/RegexMatcher.ts b/packages/grpc-js-xds/src/generated/envoy/type/matcher/v3/RegexMatcher.ts index 1addb1730..c83f8b473 100644 --- a/packages/grpc-js-xds/src/generated/envoy/type/matcher/v3/RegexMatcher.ts +++ b/packages/grpc-js-xds/src/generated/envoy/type/matcher/v3/RegexMatcher.ts @@ -7,14 +7,14 @@ import type { UInt32Value as _google_protobuf_UInt32Value, UInt32Value__Output a * the documented `syntax `_. The engine is designed * to complete execution in linear time as well as limit the amount of memory used. * - * Envoy supports program size checking via runtime. The runtime keys `re2.max_program_size.error_level` - * and `re2.max_program_size.warn_level` can be set to integers as the maximum program size or + * Envoy supports program size checking via runtime. The runtime keys ``re2.max_program_size.error_level`` + * and ``re2.max_program_size.warn_level`` can be set to integers as the maximum program size or * complexity that a compiled regex can have before an exception is thrown or a warning is - * logged, respectively. `re2.max_program_size.error_level` defaults to 100, and - * `re2.max_program_size.warn_level` has no default if unset (will not check/log a warning). + * logged, respectively. ``re2.max_program_size.error_level`` defaults to 100, and + * ``re2.max_program_size.warn_level`` has no default if unset (will not check/log a warning). * - * Envoy emits two stats for tracking the program size of regexes: the histogram `re2.program_size`, - * which records the program size, and the counter `re2.exceeded_warn_level`, which is incremented + * Envoy emits two stats for tracking the program size of regexes: the histogram ``re2.program_size``, + * which records the program size, and the counter ``re2.exceeded_warn_level``, which is incremented * each time the program size exceeds the warn level threshold. */ export interface _envoy_type_matcher_v3_RegexMatcher_GoogleRE2 { @@ -26,6 +26,11 @@ export interface _envoy_type_matcher_v3_RegexMatcher_GoogleRE2 { * * This field is deprecated; regexp validation should be performed on the management server * instead of being done by each individual client. + * + * .. note:: + * + * Although this field is deprecated, the program size will still be checked against the + * global ``re2.max_program_size.error_level`` runtime value. */ 'max_program_size'?: (_google_protobuf_UInt32Value | null); } @@ -35,14 +40,14 @@ export interface _envoy_type_matcher_v3_RegexMatcher_GoogleRE2 { * the documented `syntax `_. The engine is designed * to complete execution in linear time as well as limit the amount of memory used. * - * Envoy supports program size checking via runtime. The runtime keys `re2.max_program_size.error_level` - * and `re2.max_program_size.warn_level` can be set to integers as the maximum program size or + * Envoy supports program size checking via runtime. The runtime keys ``re2.max_program_size.error_level`` + * and ``re2.max_program_size.warn_level`` can be set to integers as the maximum program size or * complexity that a compiled regex can have before an exception is thrown or a warning is - * logged, respectively. `re2.max_program_size.error_level` defaults to 100, and - * `re2.max_program_size.warn_level` has no default if unset (will not check/log a warning). + * logged, respectively. ``re2.max_program_size.error_level`` defaults to 100, and + * ``re2.max_program_size.warn_level`` has no default if unset (will not check/log a warning). * - * Envoy emits two stats for tracking the program size of regexes: the histogram `re2.program_size`, - * which records the program size, and the counter `re2.exceeded_warn_level`, which is incremented + * Envoy emits two stats for tracking the program size of regexes: the histogram ``re2.program_size``, + * which records the program size, and the counter ``re2.exceeded_warn_level``, which is incremented * each time the program size exceeds the warn level threshold. */ export interface _envoy_type_matcher_v3_RegexMatcher_GoogleRE2__Output { @@ -54,6 +59,11 @@ export interface _envoy_type_matcher_v3_RegexMatcher_GoogleRE2__Output { * * This field is deprecated; regexp validation should be performed on the management server * instead of being done by each individual client. + * + * .. note:: + * + * Although this field is deprecated, the program size will still be checked against the + * global ``re2.max_program_size.error_level`` runtime value. */ 'max_program_size': (_google_protobuf_UInt32Value__Output | null); } @@ -67,7 +77,8 @@ export interface RegexMatcher { */ 'google_re2'?: (_envoy_type_matcher_v3_RegexMatcher_GoogleRE2 | null); /** - * The regex match string. The string must be supported by the configured engine. + * The regex match string. The string must be supported by the configured engine. The regex is matched + * against the full string, not as a partial match. */ 'regex'?: (string); 'engine_type'?: "google_re2"; @@ -82,7 +93,8 @@ export interface RegexMatcher__Output { */ 'google_re2'?: (_envoy_type_matcher_v3_RegexMatcher_GoogleRE2__Output | null); /** - * The regex match string. The string must be supported by the configured engine. + * The regex match string. The string must be supported by the configured engine. The regex is matched + * against the full string, not as a partial match. */ 'regex': (string); 'engine_type': "google_re2"; diff --git a/packages/grpc-js-xds/src/generated/envoy/type/matcher/v3/StringMatcher.ts b/packages/grpc-js-xds/src/generated/envoy/type/matcher/v3/StringMatcher.ts index 7440746f1..181d59d54 100644 --- a/packages/grpc-js-xds/src/generated/envoy/type/matcher/v3/StringMatcher.ts +++ b/packages/grpc-js-xds/src/generated/envoy/type/matcher/v3/StringMatcher.ts @@ -12,7 +12,7 @@ export interface StringMatcher { * * Examples: * - * * *abc* only matches the value *abc*. + * * ``abc`` only matches the value ``abc``. */ 'exact'?: (string); /** @@ -21,7 +21,7 @@ export interface StringMatcher { * * Examples: * - * * *abc* matches the value *abc.xyz* + * * ``abc`` matches the value ``abc.xyz`` */ 'prefix'?: (string); /** @@ -30,7 +30,7 @@ export interface StringMatcher { * * Examples: * - * * *abc* matches the value *xyz.abc* + * * ``abc`` matches the value ``xyz.abc`` */ 'suffix'?: (string); /** @@ -40,7 +40,7 @@ export interface StringMatcher { /** * If true, indicates the exact/prefix/suffix/contains matching should be case insensitive. This * has no effect for the safe_regex match. - * For example, the matcher *data* will match both input string *Data* and *data* if set to true. + * For example, the matcher ``data`` will match both input string ``Data`` and ``data`` if set to true. */ 'ignore_case'?: (boolean); /** @@ -49,7 +49,7 @@ export interface StringMatcher { * * Examples: * - * * *abc* matches the value *xyz.abc.def* + * * ``abc`` matches the value ``xyz.abc.def`` */ 'contains'?: (string); 'match_pattern'?: "exact"|"prefix"|"suffix"|"safe_regex"|"contains"; @@ -65,7 +65,7 @@ export interface StringMatcher__Output { * * Examples: * - * * *abc* only matches the value *abc*. + * * ``abc`` only matches the value ``abc``. */ 'exact'?: (string); /** @@ -74,7 +74,7 @@ export interface StringMatcher__Output { * * Examples: * - * * *abc* matches the value *abc.xyz* + * * ``abc`` matches the value ``abc.xyz`` */ 'prefix'?: (string); /** @@ -83,7 +83,7 @@ export interface StringMatcher__Output { * * Examples: * - * * *abc* matches the value *xyz.abc* + * * ``abc`` matches the value ``xyz.abc`` */ 'suffix'?: (string); /** @@ -93,7 +93,7 @@ export interface StringMatcher__Output { /** * If true, indicates the exact/prefix/suffix/contains matching should be case insensitive. This * has no effect for the safe_regex match. - * For example, the matcher *data* will match both input string *Data* and *data* if set to true. + * For example, the matcher ``data`` will match both input string ``Data`` and ``data`` if set to true. */ 'ignore_case': (boolean); /** @@ -102,7 +102,7 @@ export interface StringMatcher__Output { * * Examples: * - * * *abc* matches the value *xyz.abc.def* + * * ``abc`` matches the value ``xyz.abc.def`` */ 'contains'?: (string); 'match_pattern': "exact"|"prefix"|"suffix"|"safe_regex"|"contains"; diff --git a/packages/grpc-js-xds/src/generated/envoy/type/matcher/v3/StructMatcher.ts b/packages/grpc-js-xds/src/generated/envoy/type/matcher/v3/StructMatcher.ts index 141806489..22afe24a9 100644 --- a/packages/grpc-js-xds/src/generated/envoy/type/matcher/v3/StructMatcher.ts +++ b/packages/grpc-js-xds/src/generated/envoy/type/matcher/v3/StructMatcher.ts @@ -26,7 +26,7 @@ export interface _envoy_type_matcher_v3_StructMatcher_PathSegment__Output { /** * StructMatcher provides a general interface to check if a given value is matched in - * google.protobuf.Struct. It uses `path` to retrieve the value + * google.protobuf.Struct. It uses ``path`` to retrieve the value * from the struct and then check if it's matched to the specified value. * * For example, for the following Struct: @@ -90,7 +90,7 @@ export interface StructMatcher { /** * StructMatcher provides a general interface to check if a given value is matched in - * google.protobuf.Struct. It uses `path` to retrieve the value + * google.protobuf.Struct. It uses ``path`` to retrieve the value * from the struct and then check if it's matched to the specified value. * * For example, for the following Struct: diff --git a/packages/grpc-js-xds/src/generated/envoy/type/metadata/v3/MetadataKey.ts b/packages/grpc-js-xds/src/generated/envoy/type/metadata/v3/MetadataKey.ts index 50b6690d3..bc81233fc 100644 --- a/packages/grpc-js-xds/src/generated/envoy/type/metadata/v3/MetadataKey.ts +++ b/packages/grpc-js-xds/src/generated/envoy/type/metadata/v3/MetadataKey.ts @@ -26,7 +26,7 @@ export interface _envoy_type_metadata_v3_MetadataKey_PathSegment__Output { } /** - * MetadataKey provides a general interface using `key` and `path` to retrieve value from + * MetadataKey provides a general interface using ``key`` and ``path`` to retrieve value from * :ref:`Metadata `. * * For example, for the following Metadata: @@ -67,7 +67,7 @@ export interface MetadataKey { } /** - * MetadataKey provides a general interface using `key` and `path` to retrieve value from + * MetadataKey provides a general interface using ``key`` and ``path`` to retrieve value from * :ref:`Metadata `. * * For example, for the following Metadata: diff --git a/packages/grpc-js-xds/src/generated/fault.ts b/packages/grpc-js-xds/src/generated/fault.ts index 896382e50..4ec7ed078 100644 --- a/packages/grpc-js-xds/src/generated/fault.ts +++ b/packages/grpc-js-xds/src/generated/fault.ts @@ -14,21 +14,16 @@ export interface ProtoGrpcType { core: { v3: { Address: MessageTypeDefinition - AggregatedConfigSource: MessageTypeDefinition - ApiConfigSource: MessageTypeDefinition - ApiVersion: EnumTypeDefinition AsyncDataSource: MessageTypeDefinition BackoffStrategy: MessageTypeDefinition BindConfig: MessageTypeDefinition BuildVersion: MessageTypeDefinition CidrRange: MessageTypeDefinition - ConfigSource: MessageTypeDefinition ControlPlane: MessageTypeDefinition DataSource: MessageTypeDefinition EnvoyInternalAddress: MessageTypeDefinition Extension: MessageTypeDefinition - ExtensionConfigSource: MessageTypeDefinition - GrpcService: MessageTypeDefinition + ExtraSourceAddress: MessageTypeDefinition HeaderMap: MessageTypeDefinition HeaderValue: MessageTypeDefinition HeaderValueOption: MessageTypeDefinition @@ -38,8 +33,8 @@ export interface ProtoGrpcType { Node: MessageTypeDefinition Pipe: MessageTypeDefinition ProxyProtocolConfig: MessageTypeDefinition + ProxyProtocolPassThroughTLVs: MessageTypeDefinition QueryParameter: MessageTypeDefinition - RateLimitSettings: MessageTypeDefinition RemoteDataSource: MessageTypeDefinition RequestMethod: EnumTypeDefinition RetryPolicy: MessageTypeDefinition @@ -49,9 +44,9 @@ export interface ProtoGrpcType { RuntimeFractionalPercent: MessageTypeDefinition RuntimePercent: MessageTypeDefinition RuntimeUInt32: MessageTypeDefinition - SelfConfigSource: MessageTypeDefinition SocketAddress: MessageTypeDefinition SocketOption: MessageTypeDefinition + SocketOptionsOverride: MessageTypeDefinition TcpKeepalive: MessageTypeDefinition TrafficDirection: EnumTypeDefinition TransportSocket: MessageTypeDefinition @@ -61,6 +56,7 @@ export interface ProtoGrpcType { } route: { v3: { + ClusterSpecifierPlugin: MessageTypeDefinition CorsPolicy: MessageTypeDefinition Decorator: MessageTypeDefinition DirectResponseAction: MessageTypeDefinition @@ -76,6 +72,7 @@ export interface ProtoGrpcType { RetryPolicy: MessageTypeDefinition Route: MessageTypeDefinition RouteAction: MessageTypeDefinition + RouteList: MessageTypeDefinition RouteMatch: MessageTypeDefinition Tracing: MessageTypeDefinition VirtualCluster: MessageTypeDefinition @@ -146,7 +143,6 @@ export interface ProtoGrpcType { DescriptorProto: MessageTypeDefinition DoubleValue: MessageTypeDefinition Duration: MessageTypeDefinition - Empty: MessageTypeDefinition EnumDescriptorProto: MessageTypeDefinition EnumOptions: MessageTypeDefinition EnumValueDescriptorProto: MessageTypeDefinition @@ -227,8 +223,18 @@ export interface ProtoGrpcType { } core: { v3: { - Authority: MessageTypeDefinition ContextParams: MessageTypeDefinition + TypedExtensionConfig: MessageTypeDefinition + } + } + type: { + matcher: { + v3: { + ListStringMatcher: MessageTypeDefinition + Matcher: MessageTypeDefinition + RegexMatcher: MessageTypeDefinition + StringMatcher: MessageTypeDefinition + } } } } diff --git a/packages/grpc-js-xds/src/generated/google/protobuf/MethodOptions.ts b/packages/grpc-js-xds/src/generated/google/protobuf/MethodOptions.ts index 5f81f0dd9..e47fd756c 100644 --- a/packages/grpc-js-xds/src/generated/google/protobuf/MethodOptions.ts +++ b/packages/grpc-js-xds/src/generated/google/protobuf/MethodOptions.ts @@ -1,16 +1,13 @@ // Original file: null import type { UninterpretedOption as _google_protobuf_UninterpretedOption, UninterpretedOption__Output as _google_protobuf_UninterpretedOption__Output } from '../../google/protobuf/UninterpretedOption'; -import type { HttpRule as _google_api_HttpRule, HttpRule__Output as _google_api_HttpRule__Output } from '../../google/api/HttpRule'; export interface MethodOptions { 'deprecated'?: (boolean); 'uninterpretedOption'?: (_google_protobuf_UninterpretedOption)[]; - '.google.api.http'?: (_google_api_HttpRule | null); } export interface MethodOptions__Output { 'deprecated': (boolean); 'uninterpretedOption': (_google_protobuf_UninterpretedOption__Output)[]; - '.google.api.http': (_google_api_HttpRule__Output | null); } diff --git a/packages/grpc-js-xds/src/generated/http_connection_manager.ts b/packages/grpc-js-xds/src/generated/http_connection_manager.ts index 137dcd45a..e0e06f904 100644 --- a/packages/grpc-js-xds/src/generated/http_connection_manager.ts +++ b/packages/grpc-js-xds/src/generated/http_connection_manager.ts @@ -21,6 +21,7 @@ export interface ProtoGrpcType { ExtensionFilter: MessageTypeDefinition GrpcStatusFilter: MessageTypeDefinition HeaderFilter: MessageTypeDefinition + LogTypeFilter: MessageTypeDefinition MetadataFilter: MessageTypeDefinition NotHealthCheckFilter: MessageTypeDefinition OrFilter: MessageTypeDefinition @@ -48,6 +49,7 @@ export interface ProtoGrpcType { EnvoyInternalAddress: MessageTypeDefinition Extension: MessageTypeDefinition ExtensionConfigSource: MessageTypeDefinition + ExtraSourceAddress: MessageTypeDefinition GrpcProtocolOptions: MessageTypeDefinition GrpcService: MessageTypeDefinition HeaderMap: MessageTypeDefinition @@ -62,9 +64,12 @@ export interface ProtoGrpcType { Locality: MessageTypeDefinition Metadata: MessageTypeDefinition Node: MessageTypeDefinition + PathConfigSource: MessageTypeDefinition Pipe: MessageTypeDefinition ProxyProtocolConfig: MessageTypeDefinition + ProxyProtocolPassThroughTLVs: MessageTypeDefinition QueryParameter: MessageTypeDefinition + QuicKeepAliveSettings: MessageTypeDefinition QuicProtocolOptions: MessageTypeDefinition RateLimitSettings: MessageTypeDefinition RemoteDataSource: MessageTypeDefinition @@ -80,6 +85,7 @@ export interface ProtoGrpcType { SelfConfigSource: MessageTypeDefinition SocketAddress: MessageTypeDefinition SocketOption: MessageTypeDefinition + SocketOptionsOverride: MessageTypeDefinition SubstitutionFormatString: MessageTypeDefinition TcpKeepalive: MessageTypeDefinition TcpProtocolOptions: MessageTypeDefinition @@ -109,6 +115,7 @@ export interface ProtoGrpcType { Route: MessageTypeDefinition RouteAction: MessageTypeDefinition RouteConfiguration: MessageTypeDefinition + RouteList: MessageTypeDefinition RouteMatch: MessageTypeDefinition ScopedRouteConfiguration: MessageTypeDefinition Tracing: MessageTypeDefinition @@ -124,6 +131,21 @@ export interface ProtoGrpcType { } } } + data: { + accesslog: { + v3: { + AccessLogCommon: MessageTypeDefinition + AccessLogType: EnumTypeDefinition + ConnectionProperties: MessageTypeDefinition + HTTPAccessLogEntry: MessageTypeDefinition + HTTPRequestProperties: MessageTypeDefinition + HTTPResponseProperties: MessageTypeDefinition + ResponseFlags: MessageTypeDefinition + TCPAccessLogEntry: MessageTypeDefinition + TLSProperties: MessageTypeDefinition + } + } + } extensions: { filters: { network: { @@ -275,6 +297,17 @@ export interface ProtoGrpcType { v3: { Authority: MessageTypeDefinition ContextParams: MessageTypeDefinition + TypedExtensionConfig: MessageTypeDefinition + } + } + type: { + matcher: { + v3: { + ListStringMatcher: MessageTypeDefinition + Matcher: MessageTypeDefinition + RegexMatcher: MessageTypeDefinition + StringMatcher: MessageTypeDefinition + } } } } diff --git a/packages/grpc-js-xds/src/generated/listener.ts b/packages/grpc-js-xds/src/generated/listener.ts index b92353ab1..4ffc4712d 100644 --- a/packages/grpc-js-xds/src/generated/listener.ts +++ b/packages/grpc-js-xds/src/generated/listener.ts @@ -21,6 +21,7 @@ export interface ProtoGrpcType { ExtensionFilter: MessageTypeDefinition GrpcStatusFilter: MessageTypeDefinition HeaderFilter: MessageTypeDefinition + LogTypeFilter: MessageTypeDefinition MetadataFilter: MessageTypeDefinition NotHealthCheckFilter: MessageTypeDefinition OrFilter: MessageTypeDefinition @@ -48,6 +49,7 @@ export interface ProtoGrpcType { EnvoyInternalAddress: MessageTypeDefinition Extension: MessageTypeDefinition ExtensionConfigSource: MessageTypeDefinition + ExtraSourceAddress: MessageTypeDefinition GrpcProtocolOptions: MessageTypeDefinition GrpcService: MessageTypeDefinition HeaderMap: MessageTypeDefinition @@ -62,9 +64,12 @@ export interface ProtoGrpcType { Locality: MessageTypeDefinition Metadata: MessageTypeDefinition Node: MessageTypeDefinition + PathConfigSource: MessageTypeDefinition Pipe: MessageTypeDefinition ProxyProtocolConfig: MessageTypeDefinition + ProxyProtocolPassThroughTLVs: MessageTypeDefinition QueryParameter: MessageTypeDefinition + QuicKeepAliveSettings: MessageTypeDefinition QuicProtocolOptions: MessageTypeDefinition RateLimitSettings: MessageTypeDefinition RemoteDataSource: MessageTypeDefinition @@ -80,6 +85,7 @@ export interface ProtoGrpcType { SelfConfigSource: MessageTypeDefinition SocketAddress: MessageTypeDefinition SocketOption: MessageTypeDefinition + SocketOptionsOverride: MessageTypeDefinition TcpKeepalive: MessageTypeDefinition TcpProtocolOptions: MessageTypeDefinition TrafficDirection: EnumTypeDefinition @@ -93,7 +99,9 @@ export interface ProtoGrpcType { listener: { v3: { ActiveRawUdpListenerConfig: MessageTypeDefinition + AdditionalAddress: MessageTypeDefinition ApiListener: MessageTypeDefinition + ApiListenerManager: MessageTypeDefinition Filter: MessageTypeDefinition FilterChain: MessageTypeDefinition FilterChainMatch: MessageTypeDefinition @@ -101,12 +109,15 @@ export interface ProtoGrpcType { ListenerCollection: MessageTypeDefinition ListenerFilter: MessageTypeDefinition ListenerFilterChainMatchPredicate: MessageTypeDefinition + ListenerManager: MessageTypeDefinition QuicProtocolOptions: MessageTypeDefinition UdpListenerConfig: MessageTypeDefinition + ValidationListenerManager: MessageTypeDefinition } } route: { v3: { + ClusterSpecifierPlugin: MessageTypeDefinition CorsPolicy: MessageTypeDefinition Decorator: MessageTypeDefinition DirectResponseAction: MessageTypeDefinition @@ -122,6 +133,7 @@ export interface ProtoGrpcType { RetryPolicy: MessageTypeDefinition Route: MessageTypeDefinition RouteAction: MessageTypeDefinition + RouteList: MessageTypeDefinition RouteMatch: MessageTypeDefinition Tracing: MessageTypeDefinition VirtualCluster: MessageTypeDefinition @@ -130,6 +142,21 @@ export interface ProtoGrpcType { } } } + data: { + accesslog: { + v3: { + AccessLogCommon: MessageTypeDefinition + AccessLogType: EnumTypeDefinition + ConnectionProperties: MessageTypeDefinition + HTTPAccessLogEntry: MessageTypeDefinition + HTTPRequestProperties: MessageTypeDefinition + HTTPResponseProperties: MessageTypeDefinition + ResponseFlags: MessageTypeDefinition + TCPAccessLogEntry: MessageTypeDefinition + TLSProperties: MessageTypeDefinition + } + } + } type: { matcher: { v3: { @@ -258,6 +285,17 @@ export interface ProtoGrpcType { CollectionEntry: MessageTypeDefinition ContextParams: MessageTypeDefinition ResourceLocator: MessageTypeDefinition + TypedExtensionConfig: MessageTypeDefinition + } + } + type: { + matcher: { + v3: { + ListStringMatcher: MessageTypeDefinition + Matcher: MessageTypeDefinition + RegexMatcher: MessageTypeDefinition + StringMatcher: MessageTypeDefinition + } } } } diff --git a/packages/grpc-js-xds/src/generated/lrs.ts b/packages/grpc-js-xds/src/generated/lrs.ts index e57d6c249..d49f1123c 100644 --- a/packages/grpc-js-xds/src/generated/lrs.ts +++ b/packages/grpc-js-xds/src/generated/lrs.ts @@ -24,6 +24,7 @@ export interface ProtoGrpcType { DataSource: MessageTypeDefinition EnvoyInternalAddress: MessageTypeDefinition Extension: MessageTypeDefinition + ExtraSourceAddress: MessageTypeDefinition HeaderMap: MessageTypeDefinition HeaderValue: MessageTypeDefinition HeaderValueOption: MessageTypeDefinition @@ -44,6 +45,7 @@ export interface ProtoGrpcType { RuntimeUInt32: MessageTypeDefinition SocketAddress: MessageTypeDefinition SocketOption: MessageTypeDefinition + SocketOptionsOverride: MessageTypeDefinition TcpKeepalive: MessageTypeDefinition TrafficDirection: EnumTypeDefinition TransportSocket: MessageTypeDefinition diff --git a/packages/grpc-js-xds/src/generated/route.ts b/packages/grpc-js-xds/src/generated/route.ts index d6485bcd7..25552e612 100644 --- a/packages/grpc-js-xds/src/generated/route.ts +++ b/packages/grpc-js-xds/src/generated/route.ts @@ -28,6 +28,7 @@ export interface ProtoGrpcType { EnvoyInternalAddress: MessageTypeDefinition Extension: MessageTypeDefinition ExtensionConfigSource: MessageTypeDefinition + ExtraSourceAddress: MessageTypeDefinition GrpcService: MessageTypeDefinition HeaderMap: MessageTypeDefinition HeaderValue: MessageTypeDefinition @@ -36,8 +37,10 @@ export interface ProtoGrpcType { Locality: MessageTypeDefinition Metadata: MessageTypeDefinition Node: MessageTypeDefinition + PathConfigSource: MessageTypeDefinition Pipe: MessageTypeDefinition ProxyProtocolConfig: MessageTypeDefinition + ProxyProtocolPassThroughTLVs: MessageTypeDefinition QueryParameter: MessageTypeDefinition RateLimitSettings: MessageTypeDefinition RemoteDataSource: MessageTypeDefinition @@ -52,6 +55,7 @@ export interface ProtoGrpcType { SelfConfigSource: MessageTypeDefinition SocketAddress: MessageTypeDefinition SocketOption: MessageTypeDefinition + SocketOptionsOverride: MessageTypeDefinition TcpKeepalive: MessageTypeDefinition TrafficDirection: EnumTypeDefinition TransportSocket: MessageTypeDefinition @@ -78,6 +82,7 @@ export interface ProtoGrpcType { Route: MessageTypeDefinition RouteAction: MessageTypeDefinition RouteConfiguration: MessageTypeDefinition + RouteList: MessageTypeDefinition RouteMatch: MessageTypeDefinition Tracing: MessageTypeDefinition Vhds: MessageTypeDefinition @@ -212,6 +217,17 @@ export interface ProtoGrpcType { v3: { Authority: MessageTypeDefinition ContextParams: MessageTypeDefinition + TypedExtensionConfig: MessageTypeDefinition + } + } + type: { + matcher: { + v3: { + ListStringMatcher: MessageTypeDefinition + Matcher: MessageTypeDefinition + RegexMatcher: MessageTypeDefinition + StringMatcher: MessageTypeDefinition + } } } } diff --git a/packages/grpc-js-xds/src/generated/wrr_locality.ts b/packages/grpc-js-xds/src/generated/wrr_locality.ts new file mode 100644 index 000000000..e0275ef9a --- /dev/null +++ b/packages/grpc-js-xds/src/generated/wrr_locality.ts @@ -0,0 +1,231 @@ +import type * as grpc from '@grpc/grpc-js'; +import type { EnumTypeDefinition, MessageTypeDefinition } from '@grpc/proto-loader'; + + +type SubtypeConstructor any, Subtype> = { + new(...args: ConstructorParameters): Subtype; +}; + +export interface ProtoGrpcType { + envoy: { + annotations: { + } + config: { + cluster: { + v3: { + CircuitBreakers: MessageTypeDefinition + Cluster: MessageTypeDefinition + ClusterCollection: MessageTypeDefinition + Filter: MessageTypeDefinition + LoadBalancingPolicy: MessageTypeDefinition + OutlierDetection: MessageTypeDefinition + TrackClusterStats: MessageTypeDefinition + UpstreamConnectionOptions: MessageTypeDefinition + } + } + core: { + v3: { + Address: MessageTypeDefinition + AggregatedConfigSource: MessageTypeDefinition + AlternateProtocolsCacheOptions: MessageTypeDefinition + ApiConfigSource: MessageTypeDefinition + ApiVersion: EnumTypeDefinition + AsyncDataSource: MessageTypeDefinition + BackoffStrategy: MessageTypeDefinition + BindConfig: MessageTypeDefinition + BuildVersion: MessageTypeDefinition + CidrRange: MessageTypeDefinition + ConfigSource: MessageTypeDefinition + ControlPlane: MessageTypeDefinition + DataSource: MessageTypeDefinition + DnsResolutionConfig: MessageTypeDefinition + DnsResolverOptions: MessageTypeDefinition + EnvoyInternalAddress: MessageTypeDefinition + EventServiceConfig: MessageTypeDefinition + Extension: MessageTypeDefinition + ExtensionConfigSource: MessageTypeDefinition + ExtraSourceAddress: MessageTypeDefinition + GrpcProtocolOptions: MessageTypeDefinition + GrpcService: MessageTypeDefinition + HeaderMap: MessageTypeDefinition + HeaderValue: MessageTypeDefinition + HeaderValueOption: MessageTypeDefinition + HealthCheck: MessageTypeDefinition + HealthStatus: EnumTypeDefinition + HealthStatusSet: MessageTypeDefinition + Http1ProtocolOptions: MessageTypeDefinition + Http2ProtocolOptions: MessageTypeDefinition + Http3ProtocolOptions: MessageTypeDefinition + HttpProtocolOptions: MessageTypeDefinition + HttpUri: MessageTypeDefinition + KeepaliveSettings: MessageTypeDefinition + Locality: MessageTypeDefinition + Metadata: MessageTypeDefinition + Node: MessageTypeDefinition + PathConfigSource: MessageTypeDefinition + Pipe: MessageTypeDefinition + QueryParameter: MessageTypeDefinition + QuicKeepAliveSettings: MessageTypeDefinition + QuicProtocolOptions: MessageTypeDefinition + RateLimitSettings: MessageTypeDefinition + RemoteDataSource: MessageTypeDefinition + RequestMethod: EnumTypeDefinition + RetryPolicy: MessageTypeDefinition + RoutingPriority: EnumTypeDefinition + RuntimeDouble: MessageTypeDefinition + RuntimeFeatureFlag: MessageTypeDefinition + RuntimeFractionalPercent: MessageTypeDefinition + RuntimePercent: MessageTypeDefinition + RuntimeUInt32: MessageTypeDefinition + SchemeHeaderTransformation: MessageTypeDefinition + SelfConfigSource: MessageTypeDefinition + SocketAddress: MessageTypeDefinition + SocketOption: MessageTypeDefinition + SocketOptionsOverride: MessageTypeDefinition + TcpKeepalive: MessageTypeDefinition + TcpProtocolOptions: MessageTypeDefinition + TrafficDirection: EnumTypeDefinition + TransportSocket: MessageTypeDefinition + TypedExtensionConfig: MessageTypeDefinition + UpstreamHttpProtocolOptions: MessageTypeDefinition + WatchedDirectory: MessageTypeDefinition + } + } + endpoint: { + v3: { + ClusterLoadAssignment: MessageTypeDefinition + Endpoint: MessageTypeDefinition + LbEndpoint: MessageTypeDefinition + LedsClusterLocalityConfig: MessageTypeDefinition + LocalityLbEndpoints: MessageTypeDefinition + } + } + } + extensions: { + load_balancing_policies: { + wrr_locality: { + v3: { + WrrLocality: MessageTypeDefinition + } + } + } + } + type: { + matcher: { + v3: { + ListStringMatcher: MessageTypeDefinition + RegexMatchAndSubstitute: MessageTypeDefinition + RegexMatcher: MessageTypeDefinition + StringMatcher: MessageTypeDefinition + } + } + v3: { + CodecClientType: EnumTypeDefinition + DoubleRange: MessageTypeDefinition + FractionalPercent: MessageTypeDefinition + Int32Range: MessageTypeDefinition + Int64Range: MessageTypeDefinition + Percent: MessageTypeDefinition + SemanticVersion: MessageTypeDefinition + } + } + } + google: { + protobuf: { + Any: MessageTypeDefinition + BoolValue: MessageTypeDefinition + BytesValue: MessageTypeDefinition + DescriptorProto: MessageTypeDefinition + DoubleValue: MessageTypeDefinition + Duration: MessageTypeDefinition + Empty: MessageTypeDefinition + EnumDescriptorProto: MessageTypeDefinition + EnumOptions: MessageTypeDefinition + EnumValueDescriptorProto: MessageTypeDefinition + EnumValueOptions: MessageTypeDefinition + FieldDescriptorProto: MessageTypeDefinition + FieldOptions: MessageTypeDefinition + FileDescriptorProto: MessageTypeDefinition + FileDescriptorSet: MessageTypeDefinition + FileOptions: MessageTypeDefinition + FloatValue: MessageTypeDefinition + GeneratedCodeInfo: MessageTypeDefinition + Int32Value: MessageTypeDefinition + Int64Value: MessageTypeDefinition + ListValue: MessageTypeDefinition + MessageOptions: MessageTypeDefinition + MethodDescriptorProto: MessageTypeDefinition + MethodOptions: MessageTypeDefinition + NullValue: EnumTypeDefinition + OneofDescriptorProto: MessageTypeDefinition + OneofOptions: MessageTypeDefinition + ServiceDescriptorProto: MessageTypeDefinition + ServiceOptions: MessageTypeDefinition + SourceCodeInfo: MessageTypeDefinition + StringValue: MessageTypeDefinition + Struct: MessageTypeDefinition + Timestamp: MessageTypeDefinition + UInt32Value: MessageTypeDefinition + UInt64Value: MessageTypeDefinition + UninterpretedOption: MessageTypeDefinition + Value: MessageTypeDefinition + } + } + udpa: { + annotations: { + FieldMigrateAnnotation: MessageTypeDefinition + FieldSecurityAnnotation: MessageTypeDefinition + FileMigrateAnnotation: MessageTypeDefinition + MigrateAnnotation: MessageTypeDefinition + PackageVersionStatus: EnumTypeDefinition + StatusAnnotation: MessageTypeDefinition + VersioningAnnotation: MessageTypeDefinition + } + } + validate: { + AnyRules: MessageTypeDefinition + BoolRules: MessageTypeDefinition + BytesRules: MessageTypeDefinition + DoubleRules: MessageTypeDefinition + DurationRules: MessageTypeDefinition + EnumRules: MessageTypeDefinition + FieldRules: MessageTypeDefinition + Fixed32Rules: MessageTypeDefinition + Fixed64Rules: MessageTypeDefinition + FloatRules: MessageTypeDefinition + Int32Rules: MessageTypeDefinition + Int64Rules: MessageTypeDefinition + KnownRegex: EnumTypeDefinition + MapRules: MessageTypeDefinition + MessageRules: MessageTypeDefinition + RepeatedRules: MessageTypeDefinition + SFixed32Rules: MessageTypeDefinition + SFixed64Rules: MessageTypeDefinition + SInt32Rules: MessageTypeDefinition + SInt64Rules: MessageTypeDefinition + StringRules: MessageTypeDefinition + TimestampRules: MessageTypeDefinition + UInt32Rules: MessageTypeDefinition + UInt64Rules: MessageTypeDefinition + } + xds: { + annotations: { + v3: { + FieldStatusAnnotation: MessageTypeDefinition + FileStatusAnnotation: MessageTypeDefinition + MessageStatusAnnotation: MessageTypeDefinition + PackageVersionStatus: EnumTypeDefinition + StatusAnnotation: MessageTypeDefinition + } + } + core: { + v3: { + Authority: MessageTypeDefinition + CollectionEntry: MessageTypeDefinition + ContextParams: MessageTypeDefinition + ResourceLocator: MessageTypeDefinition + } + } + } +} + diff --git a/packages/grpc-js-xds/src/generated/xds/core/v3/TypedExtensionConfig.ts b/packages/grpc-js-xds/src/generated/xds/core/v3/TypedExtensionConfig.ts new file mode 100644 index 000000000..b6ce18c0a --- /dev/null +++ b/packages/grpc-js-xds/src/generated/xds/core/v3/TypedExtensionConfig.ts @@ -0,0 +1,43 @@ +// Original file: deps/xds/xds/core/v3/extension.proto + +import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../google/protobuf/Any'; + +/** + * Message type for extension configuration. + */ +export interface TypedExtensionConfig { + /** + * The name of an extension. This is not used to select the extension, instead + * it serves the role of an opaque identifier. + */ + 'name'?: (string); + /** + * The typed config for the extension. The type URL will be used to identify + * the extension. In the case that the type URL is *xds.type.v3.TypedStruct* + * (or, for historical reasons, *udpa.type.v1.TypedStruct*), the inner type + * URL of *TypedStruct* will be utilized. See the + * :ref:`extension configuration overview + * ` for further details. + */ + 'typed_config'?: (_google_protobuf_Any | null); +} + +/** + * Message type for extension configuration. + */ +export interface TypedExtensionConfig__Output { + /** + * The name of an extension. This is not used to select the extension, instead + * it serves the role of an opaque identifier. + */ + 'name': (string); + /** + * The typed config for the extension. The type URL will be used to identify + * the extension. In the case that the type URL is *xds.type.v3.TypedStruct* + * (or, for historical reasons, *udpa.type.v1.TypedStruct*), the inner type + * URL of *TypedStruct* will be utilized. See the + * :ref:`extension configuration overview + * ` for further details. + */ + 'typed_config': (_google_protobuf_Any__Output | null); +} diff --git a/packages/grpc-js-xds/src/generated/xds/type/matcher/v3/ListStringMatcher.ts b/packages/grpc-js-xds/src/generated/xds/type/matcher/v3/ListStringMatcher.ts new file mode 100644 index 000000000..e839f292b --- /dev/null +++ b/packages/grpc-js-xds/src/generated/xds/type/matcher/v3/ListStringMatcher.ts @@ -0,0 +1,17 @@ +// Original file: deps/xds/xds/type/matcher/v3/string.proto + +import type { StringMatcher as _xds_type_matcher_v3_StringMatcher, StringMatcher__Output as _xds_type_matcher_v3_StringMatcher__Output } from '../../../../xds/type/matcher/v3/StringMatcher'; + +/** + * Specifies a list of ways to match a string. + */ +export interface ListStringMatcher { + 'patterns'?: (_xds_type_matcher_v3_StringMatcher)[]; +} + +/** + * Specifies a list of ways to match a string. + */ +export interface ListStringMatcher__Output { + 'patterns': (_xds_type_matcher_v3_StringMatcher__Output)[]; +} diff --git a/packages/grpc-js-xds/src/generated/xds/type/matcher/v3/Matcher.ts b/packages/grpc-js-xds/src/generated/xds/type/matcher/v3/Matcher.ts new file mode 100644 index 000000000..be93c0f16 --- /dev/null +++ b/packages/grpc-js-xds/src/generated/xds/type/matcher/v3/Matcher.ts @@ -0,0 +1,307 @@ +// Original file: deps/xds/xds/type/matcher/v3/matcher.proto + +import type { Matcher as _xds_type_matcher_v3_Matcher, Matcher__Output as _xds_type_matcher_v3_Matcher__Output } from '../../../../xds/type/matcher/v3/Matcher'; +import type { TypedExtensionConfig as _xds_core_v3_TypedExtensionConfig, TypedExtensionConfig__Output as _xds_core_v3_TypedExtensionConfig__Output } from '../../../../xds/core/v3/TypedExtensionConfig'; +import type { StringMatcher as _xds_type_matcher_v3_StringMatcher, StringMatcher__Output as _xds_type_matcher_v3_StringMatcher__Output } from '../../../../xds/type/matcher/v3/StringMatcher'; + +/** + * An individual matcher. + */ +export interface _xds_type_matcher_v3_Matcher_MatcherList_FieldMatcher { + /** + * Determines if the match succeeds. + */ + 'predicate'?: (_xds_type_matcher_v3_Matcher_MatcherList_Predicate | null); + /** + * What to do if the match succeeds. + */ + 'on_match'?: (_xds_type_matcher_v3_Matcher_OnMatch | null); +} + +/** + * An individual matcher. + */ +export interface _xds_type_matcher_v3_Matcher_MatcherList_FieldMatcher__Output { + /** + * Determines if the match succeeds. + */ + 'predicate': (_xds_type_matcher_v3_Matcher_MatcherList_Predicate__Output | null); + /** + * What to do if the match succeeds. + */ + 'on_match': (_xds_type_matcher_v3_Matcher_OnMatch__Output | null); +} + +/** + * A map of configured matchers. Used to allow using a map within a oneof. + */ +export interface _xds_type_matcher_v3_Matcher_MatcherTree_MatchMap { + 'map'?: ({[key: string]: _xds_type_matcher_v3_Matcher_OnMatch}); +} + +/** + * A map of configured matchers. Used to allow using a map within a oneof. + */ +export interface _xds_type_matcher_v3_Matcher_MatcherTree_MatchMap__Output { + 'map': ({[key: string]: _xds_type_matcher_v3_Matcher_OnMatch__Output}); +} + +/** + * A linear list of field matchers. + * The field matchers are evaluated in order, and the first match + * wins. + */ +export interface _xds_type_matcher_v3_Matcher_MatcherList { + /** + * A list of matchers. First match wins. + */ + 'matchers'?: (_xds_type_matcher_v3_Matcher_MatcherList_FieldMatcher)[]; +} + +/** + * A linear list of field matchers. + * The field matchers are evaluated in order, and the first match + * wins. + */ +export interface _xds_type_matcher_v3_Matcher_MatcherList__Output { + /** + * A list of matchers. First match wins. + */ + 'matchers': (_xds_type_matcher_v3_Matcher_MatcherList_FieldMatcher__Output)[]; +} + +export interface _xds_type_matcher_v3_Matcher_MatcherTree { + /** + * Protocol-specific specification of input field to match on. + */ + 'input'?: (_xds_core_v3_TypedExtensionConfig | null); + 'exact_match_map'?: (_xds_type_matcher_v3_Matcher_MatcherTree_MatchMap | null); + /** + * Longest matching prefix wins. + */ + 'prefix_match_map'?: (_xds_type_matcher_v3_Matcher_MatcherTree_MatchMap | null); + /** + * Extension for custom matching logic. + */ + 'custom_match'?: (_xds_core_v3_TypedExtensionConfig | null); + /** + * Exact or prefix match maps in which to look up the input value. + * If the lookup succeeds, the match is considered successful, and + * the corresponding OnMatch is used. + */ + 'tree_type'?: "exact_match_map"|"prefix_match_map"|"custom_match"; +} + +export interface _xds_type_matcher_v3_Matcher_MatcherTree__Output { + /** + * Protocol-specific specification of input field to match on. + */ + 'input': (_xds_core_v3_TypedExtensionConfig__Output | null); + 'exact_match_map'?: (_xds_type_matcher_v3_Matcher_MatcherTree_MatchMap__Output | null); + /** + * Longest matching prefix wins. + */ + 'prefix_match_map'?: (_xds_type_matcher_v3_Matcher_MatcherTree_MatchMap__Output | null); + /** + * Extension for custom matching logic. + */ + 'custom_match'?: (_xds_core_v3_TypedExtensionConfig__Output | null); + /** + * Exact or prefix match maps in which to look up the input value. + * If the lookup succeeds, the match is considered successful, and + * the corresponding OnMatch is used. + */ + 'tree_type': "exact_match_map"|"prefix_match_map"|"custom_match"; +} + +/** + * What to do if a match is successful. + */ +export interface _xds_type_matcher_v3_Matcher_OnMatch { + /** + * Nested matcher to evaluate. + * If the nested matcher does not match and does not specify + * on_no_match, then this matcher is considered not to have + * matched, even if a predicate at this level or above returned + * true. + */ + 'matcher'?: (_xds_type_matcher_v3_Matcher | null); + /** + * Protocol-specific action to take. + */ + 'action'?: (_xds_core_v3_TypedExtensionConfig | null); + 'on_match'?: "matcher"|"action"; +} + +/** + * What to do if a match is successful. + */ +export interface _xds_type_matcher_v3_Matcher_OnMatch__Output { + /** + * Nested matcher to evaluate. + * If the nested matcher does not match and does not specify + * on_no_match, then this matcher is considered not to have + * matched, even if a predicate at this level or above returned + * true. + */ + 'matcher'?: (_xds_type_matcher_v3_Matcher__Output | null); + /** + * Protocol-specific action to take. + */ + 'action'?: (_xds_core_v3_TypedExtensionConfig__Output | null); + 'on_match': "matcher"|"action"; +} + +/** + * Predicate to determine if a match is successful. + */ +export interface _xds_type_matcher_v3_Matcher_MatcherList_Predicate { + /** + * A single predicate to evaluate. + */ + 'single_predicate'?: (_xds_type_matcher_v3_Matcher_MatcherList_Predicate_SinglePredicate | null); + /** + * A list of predicates to be OR-ed together. + */ + 'or_matcher'?: (_xds_type_matcher_v3_Matcher_MatcherList_Predicate_PredicateList | null); + /** + * A list of predicates to be AND-ed together. + */ + 'and_matcher'?: (_xds_type_matcher_v3_Matcher_MatcherList_Predicate_PredicateList | null); + /** + * The invert of a predicate + */ + 'not_matcher'?: (_xds_type_matcher_v3_Matcher_MatcherList_Predicate | null); + 'match_type'?: "single_predicate"|"or_matcher"|"and_matcher"|"not_matcher"; +} + +/** + * Predicate to determine if a match is successful. + */ +export interface _xds_type_matcher_v3_Matcher_MatcherList_Predicate__Output { + /** + * A single predicate to evaluate. + */ + 'single_predicate'?: (_xds_type_matcher_v3_Matcher_MatcherList_Predicate_SinglePredicate__Output | null); + /** + * A list of predicates to be OR-ed together. + */ + 'or_matcher'?: (_xds_type_matcher_v3_Matcher_MatcherList_Predicate_PredicateList__Output | null); + /** + * A list of predicates to be AND-ed together. + */ + 'and_matcher'?: (_xds_type_matcher_v3_Matcher_MatcherList_Predicate_PredicateList__Output | null); + /** + * The invert of a predicate + */ + 'not_matcher'?: (_xds_type_matcher_v3_Matcher_MatcherList_Predicate__Output | null); + 'match_type': "single_predicate"|"or_matcher"|"and_matcher"|"not_matcher"; +} + +/** + * A list of two or more matchers. Used to allow using a list within a oneof. + */ +export interface _xds_type_matcher_v3_Matcher_MatcherList_Predicate_PredicateList { + 'predicate'?: (_xds_type_matcher_v3_Matcher_MatcherList_Predicate)[]; +} + +/** + * A list of two or more matchers. Used to allow using a list within a oneof. + */ +export interface _xds_type_matcher_v3_Matcher_MatcherList_Predicate_PredicateList__Output { + 'predicate': (_xds_type_matcher_v3_Matcher_MatcherList_Predicate__Output)[]; +} + +/** + * Predicate for a single input field. + */ +export interface _xds_type_matcher_v3_Matcher_MatcherList_Predicate_SinglePredicate { + /** + * Protocol-specific specification of input field to match on. + * [#extension-category: envoy.matching.common_inputs] + */ + 'input'?: (_xds_core_v3_TypedExtensionConfig | null); + /** + * Built-in string matcher. + */ + 'value_match'?: (_xds_type_matcher_v3_StringMatcher | null); + /** + * Extension for custom matching logic. + * [#extension-category: envoy.matching.input_matchers] + */ + 'custom_match'?: (_xds_core_v3_TypedExtensionConfig | null); + 'matcher'?: "value_match"|"custom_match"; +} + +/** + * Predicate for a single input field. + */ +export interface _xds_type_matcher_v3_Matcher_MatcherList_Predicate_SinglePredicate__Output { + /** + * Protocol-specific specification of input field to match on. + * [#extension-category: envoy.matching.common_inputs] + */ + 'input': (_xds_core_v3_TypedExtensionConfig__Output | null); + /** + * Built-in string matcher. + */ + 'value_match'?: (_xds_type_matcher_v3_StringMatcher__Output | null); + /** + * Extension for custom matching logic. + * [#extension-category: envoy.matching.input_matchers] + */ + 'custom_match'?: (_xds_core_v3_TypedExtensionConfig__Output | null); + 'matcher': "value_match"|"custom_match"; +} + +/** + * A matcher, which may traverse a matching tree in order to result in a match action. + * During matching, the tree will be traversed until a match is found, or if no match + * is found the action specified by the most specific on_no_match will be evaluated. + * As an on_no_match might result in another matching tree being evaluated, this process + * might repeat several times until the final OnMatch (or no match) is decided. + */ +export interface Matcher { + /** + * A linear list of matchers to evaluate. + */ + 'matcher_list'?: (_xds_type_matcher_v3_Matcher_MatcherList | null); + /** + * A match tree to evaluate. + */ + 'matcher_tree'?: (_xds_type_matcher_v3_Matcher_MatcherTree | null); + /** + * Optional OnMatch to use if the matcher failed. + * If specified, the OnMatch is used, and the matcher is considered + * to have matched. + * If not specified, the matcher is considered not to have matched. + */ + 'on_no_match'?: (_xds_type_matcher_v3_Matcher_OnMatch | null); + 'matcher_type'?: "matcher_list"|"matcher_tree"; +} + +/** + * A matcher, which may traverse a matching tree in order to result in a match action. + * During matching, the tree will be traversed until a match is found, or if no match + * is found the action specified by the most specific on_no_match will be evaluated. + * As an on_no_match might result in another matching tree being evaluated, this process + * might repeat several times until the final OnMatch (or no match) is decided. + */ +export interface Matcher__Output { + /** + * A linear list of matchers to evaluate. + */ + 'matcher_list'?: (_xds_type_matcher_v3_Matcher_MatcherList__Output | null); + /** + * A match tree to evaluate. + */ + 'matcher_tree'?: (_xds_type_matcher_v3_Matcher_MatcherTree__Output | null); + /** + * Optional OnMatch to use if the matcher failed. + * If specified, the OnMatch is used, and the matcher is considered + * to have matched. + * If not specified, the matcher is considered not to have matched. + */ + 'on_no_match': (_xds_type_matcher_v3_Matcher_OnMatch__Output | null); + 'matcher_type': "matcher_list"|"matcher_tree"; +} diff --git a/packages/grpc-js-xds/src/generated/xds/type/matcher/v3/RegexMatcher.ts b/packages/grpc-js-xds/src/generated/xds/type/matcher/v3/RegexMatcher.ts new file mode 100644 index 000000000..575051041 --- /dev/null +++ b/packages/grpc-js-xds/src/generated/xds/type/matcher/v3/RegexMatcher.ts @@ -0,0 +1,80 @@ +// Original file: deps/xds/xds/type/matcher/v3/regex.proto + + +/** + * Google's `RE2 `_ regex engine. The regex + * string must adhere to the documented `syntax + * `_. The engine is designed to + * complete execution in linear time as well as limit the amount of memory + * used. + * + * Envoy supports program size checking via runtime. The runtime keys + * `re2.max_program_size.error_level` and `re2.max_program_size.warn_level` + * can be set to integers as the maximum program size or complexity that a + * compiled regex can have before an exception is thrown or a warning is + * logged, respectively. `re2.max_program_size.error_level` defaults to 100, + * and `re2.max_program_size.warn_level` has no default if unset (will not + * check/log a warning). + * + * Envoy emits two stats for tracking the program size of regexes: the + * histogram `re2.program_size`, which records the program size, and the + * counter `re2.exceeded_warn_level`, which is incremented each time the + * program size exceeds the warn level threshold. + */ +export interface _xds_type_matcher_v3_RegexMatcher_GoogleRE2 { +} + +/** + * Google's `RE2 `_ regex engine. The regex + * string must adhere to the documented `syntax + * `_. The engine is designed to + * complete execution in linear time as well as limit the amount of memory + * used. + * + * Envoy supports program size checking via runtime. The runtime keys + * `re2.max_program_size.error_level` and `re2.max_program_size.warn_level` + * can be set to integers as the maximum program size or complexity that a + * compiled regex can have before an exception is thrown or a warning is + * logged, respectively. `re2.max_program_size.error_level` defaults to 100, + * and `re2.max_program_size.warn_level` has no default if unset (will not + * check/log a warning). + * + * Envoy emits two stats for tracking the program size of regexes: the + * histogram `re2.program_size`, which records the program size, and the + * counter `re2.exceeded_warn_level`, which is incremented each time the + * program size exceeds the warn level threshold. + */ +export interface _xds_type_matcher_v3_RegexMatcher_GoogleRE2__Output { +} + +/** + * A regex matcher designed for safety when used with untrusted input. + */ +export interface RegexMatcher { + /** + * Google's RE2 regex engine. + */ + 'google_re2'?: (_xds_type_matcher_v3_RegexMatcher_GoogleRE2 | null); + /** + * The regex match string. The string must be supported by the configured + * engine. + */ + 'regex'?: (string); + 'engine_type'?: "google_re2"; +} + +/** + * A regex matcher designed for safety when used with untrusted input. + */ +export interface RegexMatcher__Output { + /** + * Google's RE2 regex engine. + */ + 'google_re2'?: (_xds_type_matcher_v3_RegexMatcher_GoogleRE2__Output | null); + /** + * The regex match string. The string must be supported by the configured + * engine. + */ + 'regex': (string); + 'engine_type': "google_re2"; +} diff --git a/packages/grpc-js-xds/src/generated/xds/type/matcher/v3/StringMatcher.ts b/packages/grpc-js-xds/src/generated/xds/type/matcher/v3/StringMatcher.ts new file mode 100644 index 000000000..af2f2f56f --- /dev/null +++ b/packages/grpc-js-xds/src/generated/xds/type/matcher/v3/StringMatcher.ts @@ -0,0 +1,109 @@ +// Original file: deps/xds/xds/type/matcher/v3/string.proto + +import type { RegexMatcher as _xds_type_matcher_v3_RegexMatcher, RegexMatcher__Output as _xds_type_matcher_v3_RegexMatcher__Output } from '../../../../xds/type/matcher/v3/RegexMatcher'; + +/** + * Specifies the way to match a string. + * [#next-free-field: 8] + */ +export interface StringMatcher { + /** + * The input string must match exactly the string specified here. + * + * Examples: + * + * * *abc* only matches the value *abc*. + */ + 'exact'?: (string); + /** + * The input string must have the prefix specified here. + * Note: empty prefix is not allowed, please use regex instead. + * + * Examples: + * + * * *abc* matches the value *abc.xyz* + */ + 'prefix'?: (string); + /** + * The input string must have the suffix specified here. + * Note: empty prefix is not allowed, please use regex instead. + * + * Examples: + * + * * *abc* matches the value *xyz.abc* + */ + 'suffix'?: (string); + /** + * The input string must match the regular expression specified here. + */ + 'safe_regex'?: (_xds_type_matcher_v3_RegexMatcher | null); + /** + * If true, indicates the exact/prefix/suffix matching should be case insensitive. This has no + * effect for the safe_regex match. + * For example, the matcher *data* will match both input string *Data* and *data* if set to true. + */ + 'ignore_case'?: (boolean); + /** + * The input string must have the substring specified here. + * Note: empty contains match is not allowed, please use regex instead. + * + * Examples: + * + * * *abc* matches the value *xyz.abc.def* + */ + 'contains'?: (string); + 'match_pattern'?: "exact"|"prefix"|"suffix"|"safe_regex"|"contains"; +} + +/** + * Specifies the way to match a string. + * [#next-free-field: 8] + */ +export interface StringMatcher__Output { + /** + * The input string must match exactly the string specified here. + * + * Examples: + * + * * *abc* only matches the value *abc*. + */ + 'exact'?: (string); + /** + * The input string must have the prefix specified here. + * Note: empty prefix is not allowed, please use regex instead. + * + * Examples: + * + * * *abc* matches the value *abc.xyz* + */ + 'prefix'?: (string); + /** + * The input string must have the suffix specified here. + * Note: empty prefix is not allowed, please use regex instead. + * + * Examples: + * + * * *abc* matches the value *xyz.abc* + */ + 'suffix'?: (string); + /** + * The input string must match the regular expression specified here. + */ + 'safe_regex'?: (_xds_type_matcher_v3_RegexMatcher__Output | null); + /** + * If true, indicates the exact/prefix/suffix matching should be case insensitive. This has no + * effect for the safe_regex match. + * For example, the matcher *data* will match both input string *Data* and *data* if set to true. + */ + 'ignore_case': (boolean); + /** + * The input string must have the substring specified here. + * Note: empty contains match is not allowed, please use regex instead. + * + * Examples: + * + * * *abc* matches the value *xyz.abc.def* + */ + 'contains'?: (string); + 'match_pattern': "exact"|"prefix"|"suffix"|"safe_regex"|"contains"; +} diff --git a/packages/grpc-js-xds/src/load-balancer-lrs.ts b/packages/grpc-js-xds/src/load-balancer-lrs.ts deleted file mode 100644 index 40c653a03..000000000 --- a/packages/grpc-js-xds/src/load-balancer-lrs.ts +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright 2020 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 { connectivityState as ConnectivityState, StatusObject, status as Status, experimental } from '@grpc/grpc-js'; -import { Locality__Output } from './generated/envoy/config/core/v3/Locality'; -import { validateXdsServerConfig, XdsServerConfig } from './xds-bootstrap'; -import { XdsClusterLocalityStats, XdsClient, getSingletonXdsClient } from './xds-client'; -import LoadBalancer = experimental.LoadBalancer; -import ChannelControlHelper = experimental.ChannelControlHelper; -import registerLoadBalancerType = experimental.registerLoadBalancerType; -import SubchannelAddress = experimental.SubchannelAddress; -import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; -import Picker = experimental.Picker; -import PickArgs = experimental.PickArgs; -import PickResultType = experimental.PickResultType; -import PickResult = experimental.PickResult; -import Filter = experimental.Filter; -import BaseFilter = experimental.BaseFilter; -import FilterFactory = experimental.FilterFactory; -import Call = experimental.CallStream; -import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; -import selectLbConfigFromList = experimental.selectLbConfigFromList; - -const TYPE_NAME = 'lrs'; - -class LrsLoadBalancingConfig implements TypedLoadBalancingConfig { - getLoadBalancerName(): string { - return TYPE_NAME; - } - toJsonObject(): object { - return { - [TYPE_NAME]: { - cluster_name: this.clusterName, - eds_service_name: this.edsServiceName, - lrs_load_reporting_server: this.lrsLoadReportingServer, - locality: this.locality, - child_policy: [this.childPolicy.toJsonObject()] - } - } - } - - constructor(private clusterName: string, private edsServiceName: string, private lrsLoadReportingServer: XdsServerConfig, private locality: Locality__Output, private childPolicy: TypedLoadBalancingConfig) {} - - getClusterName() { - return this.clusterName; - } - - getEdsServiceName() { - return this.edsServiceName; - } - - getLrsLoadReportingServer() { - return this.lrsLoadReportingServer; - } - - getLocality() { - return this.locality; - } - - getChildPolicy() { - return this.childPolicy; - } - - static createFromJson(obj: any): LrsLoadBalancingConfig { - if (!('cluster_name' in obj && typeof obj.cluster_name === 'string')) { - throw new Error('lrs config must have a string field cluster_name'); - } - if (!('eds_service_name' in obj && typeof obj.eds_service_name === 'string')) { - throw new Error('lrs config must have a string field eds_service_name'); - } - if (!('locality' in obj && obj.locality !== null && typeof obj.locality === 'object')) { - throw new Error('lrs config must have an object field locality'); - } - if ('region' in obj.locality && typeof obj.locality.region !== 'string') { - throw new Error('lrs config locality.region field must be a string if provided'); - } - if ('zone' in obj.locality && typeof obj.locality.zone !== 'string') { - throw new Error('lrs config locality.zone field must be a string if provided'); - } - if ('sub_zone' in obj.locality && typeof obj.locality.sub_zone !== 'string') { - throw new Error('lrs config locality.sub_zone field must be a string if provided'); - } - if (!('child_policy' in obj && Array.isArray(obj.child_policy))) { - throw new Error('lrs config must have a child_policy array'); - } - if (!('lrs_load_reporting_server' in obj && obj.lrs_load_reporting_server !== null && typeof obj.lrs_load_reporting_server === 'object')) { - throw new Error('lrs config must have an object field lrs_load_reporting_server'); - } - const childConfig = selectLbConfigFromList(obj.child_policy); - if (!childConfig) { - throw new Error('lrs config child_policy parsing failed'); - } - return new LrsLoadBalancingConfig(obj.cluster_name, obj.eds_service_name, validateXdsServerConfig(obj.lrs_load_reporting_server), { - region: obj.locality.region ?? '', - zone: obj.locality.zone ?? '', - sub_zone: obj.locality.sub_zone ?? '' - }, childConfig); - } -} - -/** - * Picker that delegates picking to another picker, and reports when calls - * created using those picks start and end. - */ -class LoadReportingPicker implements Picker { - constructor( - private wrappedPicker: Picker, - private localityStatsReporter: XdsClusterLocalityStats - ) {} - - pick(pickArgs: PickArgs): PickResult { - const wrappedPick = this.wrappedPicker.pick(pickArgs); - if (wrappedPick.pickResultType === PickResultType.COMPLETE) { - return { - pickResultType: PickResultType.COMPLETE, - subchannel: wrappedPick.subchannel, - status: null, - onCallStarted: () => { - wrappedPick.onCallStarted?.(); - this.localityStatsReporter.addCallStarted(); - }, - onCallEnded: status => { - wrappedPick.onCallEnded?.(status); - this.localityStatsReporter.addCallFinished(status !== Status.OK); - } - }; - } else { - return wrappedPick; - } - } -} - -/** - * "Load balancer" that delegates the actual load balancing logic to another - * LoadBalancer class and adds hooks to track when calls started using that - * LoadBalancer start and end, and uses the XdsClient to report that - * information back to the xDS server. - */ -export class LrsLoadBalancer implements LoadBalancer { - private childBalancer: ChildLoadBalancerHandler; - private localityStatsReporter: XdsClusterLocalityStats | null = null; - - constructor(private channelControlHelper: ChannelControlHelper) { - this.childBalancer = new ChildLoadBalancerHandler(experimental.createChildChannelControlHelper(channelControlHelper, { - updateState: (connectivityState: ConnectivityState, picker: Picker) => { - if (this.localityStatsReporter !== null) { - picker = new LoadReportingPicker(picker, this.localityStatsReporter); - } - channelControlHelper.updateState(connectivityState, picker); - }, - })); - } - - updateAddressList( - addressList: SubchannelAddress[], - lbConfig: TypedLoadBalancingConfig, - attributes: { [key: string]: unknown } - ): void { - if (!(lbConfig instanceof LrsLoadBalancingConfig)) { - return; - } - this.localityStatsReporter = (attributes.xdsClient as XdsClient).addClusterLocalityStats( - lbConfig.getLrsLoadReportingServer(), - lbConfig.getClusterName(), - lbConfig.getEdsServiceName(), - lbConfig.getLocality() - ); - this.childBalancer.updateAddressList(addressList, lbConfig.getChildPolicy(), attributes); - } - exitIdle(): void { - this.childBalancer.exitIdle(); - } - resetBackoff(): void { - this.childBalancer.resetBackoff(); - } - destroy(): void { - this.childBalancer.destroy(); - } - getTypeName(): string { - return TYPE_NAME; - } -} - -export function setup() { - registerLoadBalancerType(TYPE_NAME, LrsLoadBalancer, LrsLoadBalancingConfig); -} From c6797262460207c82cd74496a06b11642879c06e Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 22 Aug 2023 09:53:19 -0700 Subject: [PATCH 012/109] Add custom LB interop test support --- .../grpc-js-xds/interop/xds-interop-client.ts | 100 +++++++++++++++++- .../src/load-balancer-xds-wrr-locality.ts | 54 ++++++++++ packages/grpc-js/src/load-balancing-call.ts | 4 +- 3 files changed, 151 insertions(+), 7 deletions(-) diff --git a/packages/grpc-js-xds/interop/xds-interop-client.ts b/packages/grpc-js-xds/interop/xds-interop-client.ts index 3055b5b5d..c28e9cc28 100644 --- a/packages/grpc-js-xds/interop/xds-interop-client.ts +++ b/packages/grpc-js-xds/interop/xds-interop-client.ts @@ -30,8 +30,98 @@ import { XdsUpdateClientConfigureServiceHandlers } from './generated/grpc/testin import { Empty__Output } from './generated/grpc/testing/Empty'; import { LoadBalancerAccumulatedStatsResponse } from './generated/grpc/testing/LoadBalancerAccumulatedStatsResponse'; +import TypedLoadBalancingConfig = grpc.experimental.TypedLoadBalancingConfig; +import LoadBalancer = grpc.experimental.LoadBalancer; +import ChannelControlHelper = grpc.experimental.ChannelControlHelper; +import ChildLoadBalancerHandler = grpc.experimental.ChildLoadBalancerHandler; +import SubchannelAddress = grpc.experimental.SubchannelAddress; +import Picker = grpc.experimental.Picker; +import PickArgs = grpc.experimental.PickArgs; +import PickResult = grpc.experimental.PickResult; +import PickResultType = grpc.experimental.PickResultType; +import createChildChannelControlHelper = grpc.experimental.createChildChannelControlHelper; +import parseLoadBalancingConfig = grpc.experimental.parseLoadBalancingConfig; + grpc_xds.register(); +const LB_POLICY_NAME = 'RpcBehaviorLoadBalancer'; + +class RpcBehaviorLoadBalancingConfig implements TypedLoadBalancingConfig { + constructor(private rpcBehavior: string) {} + getLoadBalancerName(): string { + return LB_POLICY_NAME; + } + toJsonObject(): object { + return { + [LB_POLICY_NAME]: { + 'rpcBehavior': this.rpcBehavior + } + }; + } + getRpcBehavior() { + return this.rpcBehavior; + } + static createFromJson(obj: any): RpcBehaviorLoadBalancingConfig { + if (!('rpcBehavior' in obj && typeof obj.rpcBehavior === 'string')) { + throw new Error(`${LB_POLICY_NAME} parsing error: expected string field rpcBehavior`); + } + return new RpcBehaviorLoadBalancingConfig(obj.rpcBehavior); + } +} + +class RpcBehaviorPicker implements Picker { + constructor(private wrappedPicker: Picker, private rpcBehavior: string) {} + pick(pickArgs: PickArgs): PickResult { + const wrappedPick = this.wrappedPicker.pick(pickArgs); + if (wrappedPick.pickResultType === PickResultType.COMPLETE) { + pickArgs.metadata.add('rpc-behavior', this.rpcBehavior); + } + return wrappedPick; + } +} + +const RPC_BEHAVIOR_CHILD_CONFIG = parseLoadBalancingConfig({round_robin: {}}); + +/** + * Load balancer implementation for Custom LB policy test + */ +class RpcBehaviorLoadBalancer implements LoadBalancer { + private child: ChildLoadBalancerHandler; + private latestConfig: RpcBehaviorLoadBalancingConfig | null = null; + constructor(channelControlHelper: ChannelControlHelper) { + const childChannelControlHelper = createChildChannelControlHelper(channelControlHelper, { + updateState: (connectivityState, picker) => { + if (connectivityState === grpc.connectivityState.READY && this.latestConfig) { + picker = new RpcBehaviorPicker(picker, this.latestConfig.getLoadBalancerName()); + } + channelControlHelper.updateState(connectivityState, picker); + } + }); + this.child = new ChildLoadBalancerHandler(childChannelControlHelper); + } + updateAddressList(addressList: SubchannelAddress[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { + if (!(lbConfig instanceof RpcBehaviorLoadBalancingConfig)) { + return; + } + this.latestConfig = lbConfig; + this.child.updateAddressList(addressList, RPC_BEHAVIOR_CHILD_CONFIG, attributes); + } + exitIdle(): void { + this.child.exitIdle(); + } + resetBackoff(): void { + this.child.resetBackoff(); + } + destroy(): void { + this.child.destroy(); + } + getTypeName(): string { + return LB_POLICY_NAME; + } +} + +grpc.experimental.registerLoadBalancerType(LB_POLICY_NAME, RpcBehaviorLoadBalancer, RpcBehaviorLoadBalancingConfig); + const packageDefinition = protoLoader.loadSync('grpc/testing/test.proto', { keepCase: true, defaults: true, @@ -91,7 +181,7 @@ class CallSubscriber { } if (peerName in this.callsSucceededByPeer) { this.callsSucceededByPeer[peerName] += 1; - } else { + } else { this.callsSucceededByPeer[peerName] = 1; } this.callsSucceeded += 1; @@ -426,9 +516,9 @@ function main() { * channels do not share any subchannels. It does not have any * inherent function. */ console.log(`Interop client channel ${i} starting sending ${argv.qps} QPS to ${argv.server}`); - sendConstantQps(new loadedProto.grpc.testing.TestService(argv.server, grpc.credentials.createInsecure(), {'unique': i}), - argv.qps, - argv.fail_on_failed_rpcs === 'true', + sendConstantQps(new loadedProto.grpc.testing.TestService(argv.server, grpc.credentials.createInsecure(), {'unique': i}), + argv.qps, + argv.fail_on_failed_rpcs === 'true', callStatsTracker); } @@ -486,4 +576,4 @@ function main() { if (require.main === module) { main(); -} \ No newline at end of file +} diff --git a/packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts b/packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts index 757f9b93b..2a7d677b1 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts @@ -16,6 +16,7 @@ */ import { LoadBalancingConfig, experimental, logVerbosity } from "@grpc/grpc-js"; +import { loadProtosWithOptionsSync } from "@grpc/proto-loader/build/src/util"; import { WeightedTargetRaw } from "./load-balancer-weighted-target"; import { isLocalitySubchannelAddress } from "./load-balancer-priority"; import { localityToName } from "./load-balancer-xds-cluster-resolver"; @@ -26,6 +27,11 @@ import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; import SubchannelAddress = experimental.SubchannelAddress; import parseLoadBalancingConfig = experimental.parseLoadBalancingConfig; import registerLoadBalancerType = experimental.registerLoadBalancerType; +import { Any__Output } from "./generated/google/protobuf/Any"; +import { WrrLocality__Output } from "./generated/envoy/extensions/load_balancing_policies/wrr_locality/v3/WrrLocality"; +import { TypedExtensionConfig__Output } from "./generated/envoy/config/core/v3/TypedExtensionConfig"; +import { LoadBalancingPolicy__Output } from "./generated/envoy/config/cluster/v3/LoadBalancingPolicy"; +import { registerLbPolicy } from "./lb-policy-registry"; const TRACER_NAME = 'xds_wrr_locality'; @@ -107,6 +113,54 @@ class XdsWrrLocalityLoadBalancer implements LoadBalancer { } } +const WRR_LOCALITY_TYPE_URL = 'envoy.extensions.load_balancing_policies.wrr_locality.v3.WrrLocality'; + +const resourceRoot = loadProtosWithOptionsSync([ + 'xds/type/v3/typed_struct.proto', + 'udpa/type/v1/typed_struct.proto'], { + keepCase: true, + includeDirs: [ + // Paths are relative to src/build + __dirname + '/../../deps/xds/', + __dirname + '/../../deps/protoc-gen-validate' + ], + } +); + +const toObjectOptions = { + longs: String, + enums: String, + defaults: true, + oneofs: true +} + +function decodeWrrLocality(message: Any__Output): WrrLocality__Output { + const name = message.type_url.substring(message.type_url.lastIndexOf('/') + 1); + const type = resourceRoot.lookup(name); + if (type) { + const decodedMessage = (type as any).decode(message.value); + return decodedMessage.$type.toObject(decodedMessage, toObjectOptions) as WrrLocality__Output; + } else { + throw new Error(`TypedStruct parsing error: unexpected type URL ${message.type_url}`); + } +} + +function convertToLoadBalancingPolicy(protoPolicy: TypedExtensionConfig__Output, selectChildPolicy: (childPolicy: LoadBalancingPolicy__Output) => LoadBalancingConfig): LoadBalancingConfig { + if (protoPolicy.typed_config?.type_url !== WRR_LOCALITY_TYPE_URL) { + throw new Error(`WRR Locality LB policy parsing error: unexpected type URL ${protoPolicy.typed_config?.type_url}`); + } + const wrrLocalityMessage = decodeWrrLocality(protoPolicy.typed_config); + if (!wrrLocalityMessage.endpoint_picking_policy) { + throw new Error('WRR Locality LB parsing error: no endpoint_picking_policy specified'); + } + return { + [TYPE_NAME]: { + child_policy: selectChildPolicy(wrrLocalityMessage.endpoint_picking_policy) + } + }; +} + export function setup() { registerLoadBalancerType(TYPE_NAME, XdsWrrLocalityLoadBalancer, XdsWrrLocalityLoadBalancingConfig); + registerLbPolicy(WRR_LOCALITY_TYPE_URL, convertToLoadBalancingPolicy); } diff --git a/packages/grpc-js/src/load-balancing-call.ts b/packages/grpc-js/src/load-balancing-call.ts index 6e9718a7a..69dc518f8 100644 --- a/packages/grpc-js/src/load-balancing-call.ts +++ b/packages/grpc-js/src/load-balancing-call.ts @@ -114,8 +114,9 @@ export class LoadBalancingCall implements Call { throw new Error('doPick called before start'); } this.trace('Pick called'); + const finalMetadata = this.metadata.clone(); const pickResult = this.channel.doPick( - this.metadata, + finalMetadata, this.callConfig.pickInformation ); const subchannelString = pickResult.subchannel @@ -140,7 +141,6 @@ export class LoadBalancingCall implements Call { .generateMetadata({ service_url: this.serviceUrl }) .then( credsMetadata => { - const finalMetadata = this.metadata!.clone(); finalMetadata.merge(credsMetadata); if (finalMetadata.get('authorization').length > 1) { this.outputStatus( From a417e9bc3bba72ef99e0528c2b9704ade1dcf420 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 22 Aug 2023 13:49:52 -0700 Subject: [PATCH 013/109] proto-loader: Bump version to 0.7.9 --- packages/proto-loader/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/proto-loader/package.json b/packages/proto-loader/package.json index 69038b74e..c53976be6 100644 --- a/packages/proto-loader/package.json +++ b/packages/proto-loader/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/proto-loader", - "version": "0.7.9-pre.1", + "version": "0.7.9", "author": "Google Inc.", "contributors": [ { From 73260353637c422a41c229d58116aaeb2c1dbcb8 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 23 Aug 2023 09:37:47 -0700 Subject: [PATCH 014/109] Fix tests --- .../src/load-balancer-xds-cluster-impl.ts | 10 +- .../grpc-js-xds/test/test-confg-parsing.ts | 126 +++++++----------- 2 files changed, 50 insertions(+), 86 deletions(-) diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts index eb3df8c38..4050a3cf8 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts @@ -70,14 +70,10 @@ class XdsClusterImplLoadBalancingConfig implements TypedLoadBalancingConfig { cluster: this.cluster, drop_categories: this.dropCategories, child_policy: [this.childPolicy.toJsonObject()], - max_concurrent_requests: this.maxConcurrentRequests + max_concurrent_requests: this.maxConcurrentRequests, + eds_service_name: this.edsServiceName, + lrs_load_reporting_server: this.lrsLoadReportingServer, }; - if (this.edsServiceName !== undefined) { - jsonObj.eds_service_name = this.edsServiceName; - } - if (this.lrsLoadReportingServer !== undefined) { - jsonObj.lrs_load_reporting_server_name = this.lrsLoadReportingServer; - } return { [TYPE_NAME]: jsonObj }; diff --git a/packages/grpc-js-xds/test/test-confg-parsing.ts b/packages/grpc-js-xds/test/test-confg-parsing.ts index ba91bb2e1..740934ba1 100644 --- a/packages/grpc-js-xds/test/test-confg-parsing.ts +++ b/packages/grpc-js-xds/test/test-confg-parsing.ts @@ -69,33 +69,22 @@ const allTestCases: {[lbPolicyName: string]: TestCase[]} = { name: 'empty fields', input: { discovery_mechanisms: [], - locality_picking_policy: [], - endpoint_picking_policy: [] + xds_lb_policy: [] } }, { name: 'missing discovery_mechanisms', input: { - locality_picking_policy: [], - endpoint_picking_policy: [] + xds_lb_policy: [] }, error: /discovery_mechanisms/ }, { - name: 'missing locality_picking_policy', + name: 'missing xds_lb_policy', input: { - discovery_mechanisms: [], - endpoint_picking_policy: [] - }, - error: /locality_picking_policy/ - }, - { - name: 'missing endpoint_picking_policy', - input: { - discovery_mechanisms: [], - locality_picking_policy: [] + discovery_mechanisms: [] }, - error: /endpoint_picking_policy/ + error: /xds_lb_policy/ }, { name: 'discovery_mechanism: EDS', @@ -104,8 +93,7 @@ const allTestCases: {[lbPolicyName: string]: TestCase[]} = { cluster: 'abc', type: 'EDS' }], - locality_picking_policy: [], - endpoint_picking_policy: [] + xds_lb_policy: [] }, output: { discovery_mechanisms: [{ @@ -113,8 +101,7 @@ const allTestCases: {[lbPolicyName: string]: TestCase[]} = { type: 'EDS', lrs_load_reporting_server: undefined }], - locality_picking_policy: [], - endpoint_picking_policy: [] + xds_lb_policy: [] } }, { @@ -124,8 +111,7 @@ const allTestCases: {[lbPolicyName: string]: TestCase[]} = { cluster: 'abc', type: 'LOGICAL_DNS' }], - locality_picking_policy: [], - endpoint_picking_policy: [] + xds_lb_policy: [] }, output: { discovery_mechanisms: [{ @@ -133,8 +119,7 @@ const allTestCases: {[lbPolicyName: string]: TestCase[]} = { type: 'LOGICAL_DNS', lrs_load_reporting_server: undefined }], - locality_picking_policy: [], - endpoint_picking_policy: [] + xds_lb_policy: [] } }, { @@ -148,8 +133,7 @@ const allTestCases: {[lbPolicyName: string]: TestCase[]} = { dns_hostname: undefined, lrs_load_reporting_server: undefined }], - locality_picking_policy: [], - endpoint_picking_policy: [] + xds_lb_policy: [] } }, { @@ -170,8 +154,7 @@ const allTestCases: {[lbPolicyName: string]: TestCase[]} = { server_features: ['test'] } }], - locality_picking_policy: [], - endpoint_picking_policy: [] + xds_lb_policy: [] } } ], @@ -180,12 +163,30 @@ const allTestCases: {[lbPolicyName: string]: TestCase[]} = { name: 'only required fields', input: { cluster: 'abc', + eds_service_name: 'def', drop_categories: [], + lrs_load_reporting_server: { + server_uri: 'localhost:12345', + channel_creds: [{ + type: 'google_default', + config: {} + }], + server_features: ['test'] + }, child_policy: [{round_robin: {}}] }, output: { cluster: 'abc', + eds_service_name: 'def', drop_categories: [], + lrs_load_reporting_server: { + server_uri: 'localhost:12345', + channel_creds: [{ + type: 'google_default', + config: {} + }], + server_features: ['test'] + }, child_policy: [{round_robin: {}}], max_concurrent_requests: 1024 } @@ -194,40 +195,8 @@ const allTestCases: {[lbPolicyName: string]: TestCase[]} = { name: 'undefined optional fields', input: { cluster: 'abc', - drop_categories: [], - child_policy: [{round_robin: {}}], - eds_service_name: undefined, - max_concurrent_requests: undefined - }, - output: { - cluster: 'abc', - drop_categories: [], - child_policy: [{round_robin: {}}], - max_concurrent_requests: 1024 - } - }, - { - name: 'populated optional fields', - input: { - cluster: 'abc', - drop_categories: [{ - category: 'test', - requests_per_million: 100 - }], - child_policy: [{round_robin: {}}], - eds_service_name: 'def', - max_concurrent_requests: 123 - }, - } - ], - lrs: [ - { - name: 'only required fields', - input: { - cluster_name: 'abc', eds_service_name: 'def', - locality: {}, - child_policy: [{round_robin: {}}], + drop_categories: [], lrs_load_reporting_server: { server_uri: 'localhost:12345', channel_creds: [{ @@ -235,17 +204,14 @@ const allTestCases: {[lbPolicyName: string]: TestCase[]} = { config: {} }], server_features: ['test'] - } + }, + child_policy: [{round_robin: {}}], + max_concurrent_requests: undefined }, output: { - cluster_name: 'abc', + cluster: 'abc', eds_service_name: 'def', - locality: { - region: '', - zone: '', - sub_zone: '' - }, - child_policy: [{round_robin: {}}], + drop_categories: [], lrs_load_reporting_server: { server_uri: 'localhost:12345', channel_creds: [{ @@ -253,20 +219,20 @@ const allTestCases: {[lbPolicyName: string]: TestCase[]} = { config: {} }], server_features: ['test'] - } + }, + child_policy: [{round_robin: {}}], + max_concurrent_requests: 1024 } }, { name: 'populated optional fields', input: { - cluster_name: 'abc', + cluster: 'abc', eds_service_name: 'def', - locality: { - region: 'a', - zone: 'b', - sub_zone: 'c' - }, - child_policy: [{round_robin: {}}], + drop_categories: [{ + category: 'test', + requests_per_million: 100 + }], lrs_load_reporting_server: { server_uri: 'localhost:12345', channel_creds: [{ @@ -274,8 +240,10 @@ const allTestCases: {[lbPolicyName: string]: TestCase[]} = { config: {} }], server_features: ['test'] - } - } + }, + child_policy: [{round_robin: {}}], + max_concurrent_requests: 123 + }, } ], priority: [ From 9ca8302725373249ef774da502e70d5c303ffab4 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 23 Aug 2023 14:32:15 -0700 Subject: [PATCH 015/109] Add tests and fix bugs --- packages/grpc-js-xds/gulpfile.ts | 1 + .../grpc-js-xds/interop/xds-interop-client.ts | 2 +- packages/grpc-js-xds/src/index.ts | 4 + .../grpc-js-xds/src/lb-policy-registry.ts | 20 ++- .../src/lb-policy-registry/round-robin.ts | 2 +- .../src/lb-policy-registry/typed-struct.ts | 15 +- .../src/load-balancer-xds-wrr-locality.ts | 8 +- .../cluster-resource-type.ts | 11 +- packages/grpc-js-xds/test/framework.ts | 21 ++- .../test/test-custom-lb-policies.ts | 166 ++++++++++++++++++ packages/grpc-js-xds/test/xds-server.ts | 9 +- packages/grpc-js/src/experimental.ts | 3 +- 12 files changed, 239 insertions(+), 23 deletions(-) create mode 100644 packages/grpc-js-xds/test/test-custom-lb-policies.ts diff --git a/packages/grpc-js-xds/gulpfile.ts b/packages/grpc-js-xds/gulpfile.ts index f2e77b8bd..6f17a4020 100644 --- a/packages/grpc-js-xds/gulpfile.ts +++ b/packages/grpc-js-xds/gulpfile.ts @@ -62,6 +62,7 @@ const compile = checkTask(() => execNpmCommand('compile')); const runTests = checkTask(() => { process.env.GRPC_EXPERIMENTAL_XDS_FEDERATION = 'true'; + process.env.GRPC_EXPERIMENTAL_XDS_CUSTOM_LB_CONFIG = 'true'; return gulp.src(`${outDir}/test/**/*.js`) .pipe(mocha({reporter: 'mocha-jenkins-reporter', require: ['ts-node/register']})); diff --git a/packages/grpc-js-xds/interop/xds-interop-client.ts b/packages/grpc-js-xds/interop/xds-interop-client.ts index c28e9cc28..2893acad9 100644 --- a/packages/grpc-js-xds/interop/xds-interop-client.ts +++ b/packages/grpc-js-xds/interop/xds-interop-client.ts @@ -44,7 +44,7 @@ import parseLoadBalancingConfig = grpc.experimental.parseLoadBalancingConfig; grpc_xds.register(); -const LB_POLICY_NAME = 'RpcBehaviorLoadBalancer'; +const LB_POLICY_NAME = 'test.RpcBehaviorLoadBalancer'; class RpcBehaviorLoadBalancingConfig implements TypedLoadBalancingConfig { constructor(private rpcBehavior: string) {} diff --git a/packages/grpc-js-xds/src/index.ts b/packages/grpc-js-xds/src/index.ts index 319719bde..95c26a20a 100644 --- a/packages/grpc-js-xds/src/index.ts +++ b/packages/grpc-js-xds/src/index.ts @@ -26,6 +26,8 @@ import * as xds_wrr_locality from './load-balancer-xds-wrr-locality'; import * as router_filter from './http-filter/router-filter'; import * as fault_injection_filter from './http-filter/fault-injection-filter'; import * as csds from './csds'; +import * as round_robin_lb from './lb-policy-registry/round-robin'; +import * as typed_struct_lb from './lb-policy-registry/typed-struct'; /** * Register the "xds:" name scheme with the @grpc/grpc-js library. @@ -42,4 +44,6 @@ export function register() { router_filter.setup(); fault_injection_filter.setup(); csds.setup(); + round_robin_lb.setup(); + typed_struct_lb.setup(); } diff --git a/packages/grpc-js-xds/src/lb-policy-registry.ts b/packages/grpc-js-xds/src/lb-policy-registry.ts index 18d86655e..80a06323e 100644 --- a/packages/grpc-js-xds/src/lb-policy-registry.ts +++ b/packages/grpc-js-xds/src/lb-policy-registry.ts @@ -26,8 +26,13 @@ function trace(text: string) { const MAX_RECURSION_DEPTH = 16; +/** + * Parse a protoPolicy to a LoadBalancingConfig. A null return value indicates + * that parsing failed, but that it should not be treated as an error, and + * instead the next policy should be used. + */ interface ProtoLbPolicyConverter { - (protoPolicy: TypedExtensionConfig__Output, selectChildPolicy: (childPolicy: LoadBalancingPolicy__Output) => LoadBalancingConfig): LoadBalancingConfig + (protoPolicy: TypedExtensionConfig__Output, selectChildPolicy: (childPolicy: LoadBalancingPolicy__Output) => LoadBalancingConfig): LoadBalancingConfig | null; } interface RegisteredLbPolicy { @@ -41,20 +46,29 @@ export function registerLbPolicy(typeUrl: string, converter: ProtoLbPolicyConver } export function convertToLoadBalancingConfig(protoPolicy: LoadBalancingPolicy__Output, recursionDepth = 0): LoadBalancingConfig { + trace('Registry entries: [' + Object.keys(registry) + ']'); if (recursionDepth > MAX_RECURSION_DEPTH) { throw new Error(`convertToLoadBalancingConfig: Max recursion depth ${MAX_RECURSION_DEPTH} reached`); } for (const policyCandidate of protoPolicy.policies) { + trace('Attempting to parse config ' + JSON.stringify(policyCandidate)); const extensionConfig = policyCandidate.typed_extension_config; if (!extensionConfig?.typed_config) { continue; } const typeUrl = extensionConfig.typed_config.type_url; + trace('Attempting to parse config with type_url=' + typeUrl); + let parseResult: LoadBalancingConfig | null; if (typeUrl in registry) { try { - return registry[typeUrl].convertToLoadBalancingPolicy(extensionConfig, childPolicy => convertToLoadBalancingConfig(childPolicy, recursionDepth + 1)); + parseResult = registry[typeUrl].convertToLoadBalancingPolicy(extensionConfig, childPolicy => convertToLoadBalancingConfig(childPolicy, recursionDepth + 1)); } catch (e) { - throw new Error(`Error parsing ${typeUrl} LoadBalancingPolicy: ${(e as Error).message}`); + throw new Error(`Error parsing ${typeUrl} LoadBalancingPolicy named ${extensionConfig.name}: ${(e as Error).message}`); + } + if (parseResult) { + return parseResult; + } else { + continue; } } } diff --git a/packages/grpc-js-xds/src/lb-policy-registry/round-robin.ts b/packages/grpc-js-xds/src/lb-policy-registry/round-robin.ts index 9585bfde2..311c8c38c 100644 --- a/packages/grpc-js-xds/src/lb-policy-registry/round-robin.ts +++ b/packages/grpc-js-xds/src/lb-policy-registry/round-robin.ts @@ -20,7 +20,7 @@ import { LoadBalancingPolicy__Output } from "../generated/envoy/config/cluster/v import { TypedExtensionConfig__Output } from "../generated/envoy/config/core/v3/TypedExtensionConfig"; import { registerLbPolicy } from "../lb-policy-registry"; -const ROUND_ROBIN_TYPE_URL = 'envoy.extensions.load_balancing_policies.round_robin.v3.RoundRobin'; +const ROUND_ROBIN_TYPE_URL = 'type.googleapis.com/envoy.extensions.load_balancing_policies.round_robin.v3.RoundRobin'; function convertToLoadBalancingPolicy(protoPolicy: TypedExtensionConfig__Output, selectChildPolicy: (childPolicy: LoadBalancingPolicy__Output) => LoadBalancingConfig): LoadBalancingConfig { if (protoPolicy.typed_config?.type_url !== ROUND_ROBIN_TYPE_URL) { diff --git a/packages/grpc-js-xds/src/lb-policy-registry/typed-struct.ts b/packages/grpc-js-xds/src/lb-policy-registry/typed-struct.ts index 70a6c02b3..6b50d9e83 100644 --- a/packages/grpc-js-xds/src/lb-policy-registry/typed-struct.ts +++ b/packages/grpc-js-xds/src/lb-policy-registry/typed-struct.ts @@ -15,7 +15,7 @@ * */ -import { LoadBalancingConfig } from "@grpc/grpc-js"; +import { LoadBalancingConfig, experimental } from "@grpc/grpc-js"; import { LoadBalancingPolicy__Output } from "../generated/envoy/config/cluster/v3/LoadBalancingPolicy"; import { TypedExtensionConfig__Output } from "../generated/envoy/config/core/v3/TypedExtensionConfig"; import { registerLbPolicy } from "../lb-policy-registry"; @@ -25,16 +25,16 @@ import { Struct__Output } from "../generated/google/protobuf/Struct"; import { Value__Output } from "../generated/google/protobuf/Value"; import { TypedStruct__Output } from "../generated/xds/type/v3/TypedStruct"; -const XDS_TYPED_STRUCT_TYPE_URL = 'xds.type.v3.TypedStruct'; -const UDPA_TYPED_STRUCT_TYPE_URL = 'udpa.type.v1.TypedStruct'; +const XDS_TYPED_STRUCT_TYPE_URL = 'type.googleapis.com/xds.type.v3.TypedStruct'; +const UDPA_TYPED_STRUCT_TYPE_URL = 'type.googleapis.com/udpa.type.v1.TypedStruct'; const resourceRoot = loadProtosWithOptionsSync([ 'xds/type/v3/typed_struct.proto', 'udpa/type/v1/typed_struct.proto'], { keepCase: true, includeDirs: [ - // Paths are relative to src/build - __dirname + '/../../deps/xds/', + // Paths are relative to src/build/lb-policy-registry + __dirname + '/../../../deps/xds/', ], } ); @@ -90,7 +90,7 @@ function flattenStruct(struct: Struct__Output): FlatStruct { return result; } -function convertToLoadBalancingPolicy(protoPolicy: TypedExtensionConfig__Output, selectChildPolicy: (childPolicy: LoadBalancingPolicy__Output) => LoadBalancingConfig): LoadBalancingConfig { +function convertToLoadBalancingPolicy(protoPolicy: TypedExtensionConfig__Output, selectChildPolicy: (childPolicy: LoadBalancingPolicy__Output) => LoadBalancingConfig): LoadBalancingConfig | null { if (protoPolicy.typed_config?.type_url !== XDS_TYPED_STRUCT_TYPE_URL && protoPolicy.typed_config?.type_url !== UDPA_TYPED_STRUCT_TYPE_URL) { throw new Error(`Typed struct LB policy parsing error: unexpected type URL ${protoPolicy.typed_config?.type_url}`); } @@ -99,6 +99,9 @@ function convertToLoadBalancingPolicy(protoPolicy: TypedExtensionConfig__Output, throw new Error(`Typed struct LB parsing error: unexpected value ${typedStruct.value}`); } const policyName = typedStruct.type_url.substring(typedStruct.type_url.lastIndexOf('/') + 1); + if (!experimental.isLoadBalancerNameRegistered(policyName)) { + return null; + } return { [policyName]: flattenStruct(typedStruct.value) }; diff --git a/packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts b/packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts index 2a7d677b1..5faf8b8e5 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts @@ -113,14 +113,14 @@ class XdsWrrLocalityLoadBalancer implements LoadBalancer { } } -const WRR_LOCALITY_TYPE_URL = 'envoy.extensions.load_balancing_policies.wrr_locality.v3.WrrLocality'; +const WRR_LOCALITY_TYPE_URL = 'type.googleapis.com/envoy.extensions.load_balancing_policies.wrr_locality.v3.WrrLocality'; const resourceRoot = loadProtosWithOptionsSync([ - 'xds/type/v3/typed_struct.proto', - 'udpa/type/v1/typed_struct.proto'], { + 'envoy/extensions/load_balancing_policies/wrr_locality/v3/wrr_locality.proto'], { keepCase: true, includeDirs: [ // Paths are relative to src/build + __dirname + '/../../deps/envoy-api/', __dirname + '/../../deps/xds/', __dirname + '/../../deps/protoc-gen-validate' ], @@ -155,7 +155,7 @@ function convertToLoadBalancingPolicy(protoPolicy: TypedExtensionConfig__Output, } return { [TYPE_NAME]: { - child_policy: selectChildPolicy(wrrLocalityMessage.endpoint_picking_policy) + child_policy: [selectChildPolicy(wrrLocalityMessage.endpoint_picking_policy)] } }; } diff --git a/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts index 726a5d2d2..1e8ea41f8 100644 --- a/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts +++ b/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts @@ -17,7 +17,7 @@ import { CDS_TYPE_URL, CLUSTER_CONFIG_TYPE_URL, decodeSingleResource } from "../resources"; import { XdsDecodeContext, XdsDecodeResult, XdsResourceType } from "./xds-resource-type"; -import { LoadBalancingConfig, experimental } from "@grpc/grpc-js"; +import { LoadBalancingConfig, experimental, logVerbosity } from "@grpc/grpc-js"; import { XdsServerConfig } from "../xds-bootstrap"; import { Duration__Output } from "../generated/google/protobuf/Duration"; import { OutlierDetection__Output } from "../generated/envoy/config/cluster/v3/OutlierDetection"; @@ -32,6 +32,13 @@ import SuccessRateEjectionConfig = experimental.SuccessRateEjectionConfig; import FailurePercentageEjectionConfig = experimental.FailurePercentageEjectionConfig; import parseLoadBalancingConfig = experimental.parseLoadBalancingConfig; +const TRACER_NAME = 'xds_client'; + +function trace(text: string): void { + experimental.trace(logVerbosity.DEBUG, TRACER_NAME, text); +} + + export interface CdsUpdate { type: 'AGGREGATE' | 'EDS' | 'LOGICAL_DNS'; name: string; @@ -128,11 +135,13 @@ export class ClusterResourceType extends XdsResourceType { try { lbPolicyConfig = convertToLoadBalancingConfig(message.load_balancing_policy); } catch (e) { + trace('LB policy config parsing failed with error ' + e); return null; } try { parseLoadBalancingConfig(lbPolicyConfig); } catch (e) { + trace('LB policy config parsing failed with error ' + e); return null; } } else if (message.lb_policy === 'ROUND_ROBIN') { diff --git a/packages/grpc-js-xds/test/framework.ts b/packages/grpc-js-xds/test/framework.ts index f5e7bc1de..f89a4680f 100644 --- a/packages/grpc-js-xds/test/framework.ts +++ b/packages/grpc-js-xds/test/framework.ts @@ -28,6 +28,7 @@ import { CLUSTER_CONFIG_TYPE_URL, HTTP_CONNECTION_MANGER_TYPE_URL } from "../src 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"; interface Endpoint { locality: Locality; @@ -69,7 +70,7 @@ export interface FakeCluster { } export class FakeEdsCluster implements FakeCluster { - constructor(private clusterName: string, private endpointName: string, private endpoints: Endpoint[]) {} + constructor(private clusterName: string, private endpointName: string, private endpoints: Endpoint[], private loadBalancingPolicyOverride?: Any) {} getEndpointConfig(): ClusterLoadAssignment { return { @@ -79,11 +80,10 @@ export class FakeEdsCluster implements FakeCluster { } getClusterConfig(): Cluster { - return { + const result: Cluster = { name: this.clusterName, type: 'EDS', eds_cluster_config: {eds_config: {ads: {}}, service_name: this.endpointName}, - lb_policy: 'ROUND_ROBIN', lrs_server: {self: {}}, circuit_breakers: { thresholds: [ @@ -93,7 +93,22 @@ export class FakeEdsCluster implements FakeCluster { } ] } + }; + if (this.loadBalancingPolicyOverride) { + result.load_balancing_policy = { + policies: [ + { + typed_extension_config: { + 'name': 'test', + typed_config: this.loadBalancingPolicyOverride + } + } + ] + } + } else { + result.lb_policy = 'ROUND_ROBIN'; } + return result; } getAllClusterConfigs(): Cluster[] { diff --git a/packages/grpc-js-xds/test/test-custom-lb-policies.ts b/packages/grpc-js-xds/test/test-custom-lb-policies.ts new file mode 100644 index 000000000..7493a23f0 --- /dev/null +++ b/packages/grpc-js-xds/test/test-custom-lb-policies.ts @@ -0,0 +1,166 @@ +/* + * 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 { AnyExtension } from "@grpc/proto-loader"; +import { Any } from "../src/generated/google/protobuf/Any"; +import { Backend } from "./backend"; +import { XdsTestClient } from "./client"; +import { FakeEdsCluster, FakeRouteGroup } from "./framework"; +import { XdsServer } from "./xds-server"; +import * as assert from 'assert'; +import { WrrLocality } from "../src/generated/envoy/extensions/load_balancing_policies/wrr_locality/v3/WrrLocality"; +import { TypedStruct } from "../src/generated/xds/type/v3/TypedStruct"; + +describe('Custom LB policies', () => { + let xdsServer: XdsServer; + let client: XdsTestClient; + beforeEach(done => { + xdsServer = new XdsServer(); + xdsServer.startServer(error => { + done(error); + }); + }); + afterEach(() => { + client?.close(); + xdsServer?.shutdownServer(); + }); + it('Should handle round_robin', done => { + const lbPolicy: Any = { + '@type': 'type.googleapis.com/envoy.extensions.load_balancing_policies.round_robin.v3.RoundRobin' + }; + const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [new Backend()], locality:{region: 'region1'}}], lbPolicy); + const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: cluster}]); + routeGroup.startAllBackends().then(() => { + xdsServer.setEdsResource(cluster.getEndpointConfig()); + xdsServer.setCdsResource(cluster.getClusterConfig()); + xdsServer.setRdsResource(routeGroup.getRouteConfiguration()); + xdsServer.setLdsResource(routeGroup.getListener()); + xdsServer.addResponseListener((typeUrl, responseState) => { + if (responseState.state === 'NACKED') { + client.stopCalls(); + assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`); + } + }) + client = XdsTestClient.createFromServer('listener1', xdsServer); + client.sendOneCall(done); + }, reason => done(reason)); + }); + it('Should handle xds_wrr_locality with round_robin child', done => { + const lbPolicy: WrrLocality & AnyExtension = { + '@type': 'type.googleapis.com/envoy.extensions.load_balancing_policies.wrr_locality.v3.WrrLocality', + endpoint_picking_policy: { + policies: [ + { + typed_extension_config: { + name: 'child', + typed_config: { + '@type': 'type.googleapis.com/envoy.extensions.load_balancing_policies.round_robin.v3.RoundRobin' + } + } + } + ] + } + }; + const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [new Backend()], locality:{region: 'region1'}}], lbPolicy); + const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: cluster}]); + routeGroup.startAllBackends().then(() => { + xdsServer.setEdsResource(cluster.getEndpointConfig()); + xdsServer.setCdsResource(cluster.getClusterConfig()); + xdsServer.setRdsResource(routeGroup.getRouteConfiguration()); + xdsServer.setLdsResource(routeGroup.getListener()); + xdsServer.addResponseListener((typeUrl, responseState) => { + if (responseState.state === 'NACKED') { + client.stopCalls(); + assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`); + } + }) + client = XdsTestClient.createFromServer('listener1', xdsServer); + client.sendOneCall(done); + }, reason => done(reason)); + }); + it('Should handle a typed_struct policy', done => { + const lbPolicy: TypedStruct & AnyExtension = { + '@type': 'type.googleapis.com/xds.type.v3.TypedStruct', + type_url: 'round_robin', + value: { + fields: {} + } + }; + const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [new Backend()], locality:{region: 'region1'}}], lbPolicy); + const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: cluster}]); + routeGroup.startAllBackends().then(() => { + xdsServer.setEdsResource(cluster.getEndpointConfig()); + xdsServer.setCdsResource(cluster.getClusterConfig()); + xdsServer.setRdsResource(routeGroup.getRouteConfiguration()); + xdsServer.setLdsResource(routeGroup.getListener()); + xdsServer.addResponseListener((typeUrl, responseState) => { + if (responseState.state === 'NACKED') { + client.stopCalls(); + assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`); + } + }) + client = XdsTestClient.createFromServer('listener1', xdsServer); + client.sendOneCall(done); + }, reason => done(reason)); + }); + it('Should handle xds_wrr_locality with an unrecognized first child', done => { + const invalidChildPolicy: TypedStruct & AnyExtension = { + '@type': 'type.googleapis.com/xds.type.v3.TypedStruct', + type_url: 'test.ThisLoadBalancerDoesNotExist', + value: { + fields: {} + } + } + const lbPolicy: WrrLocality & AnyExtension = { + '@type': 'type.googleapis.com/envoy.extensions.load_balancing_policies.wrr_locality.v3.WrrLocality', + endpoint_picking_policy: { + policies: [ + { + typed_extension_config: { + name: 'child', + typed_config: invalidChildPolicy + } + }, + { + typed_extension_config: { + name: 'child', + typed_config: { + '@type': 'type.googleapis.com/envoy.extensions.load_balancing_policies.round_robin.v3.RoundRobin' + } + } + } + ] + } + }; + const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [new Backend()], locality:{region: 'region1'}}], lbPolicy); + const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: cluster}]); + routeGroup.startAllBackends().then(() => { + xdsServer.setEdsResource(cluster.getEndpointConfig()); + xdsServer.setCdsResource(cluster.getClusterConfig()); + xdsServer.setRdsResource(routeGroup.getRouteConfiguration()); + xdsServer.setLdsResource(routeGroup.getListener()); + xdsServer.addResponseListener((typeUrl, responseState) => { + if (responseState.state === 'NACKED') { + client.stopCalls(); + assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`); + } + }) + client = XdsTestClient.createFromServer('listener1', xdsServer); + client.sendOneCall(done); + }, reason => done(reason)); + }); +}); diff --git a/packages/grpc-js-xds/test/xds-server.ts b/packages/grpc-js-xds/test/xds-server.ts index c9b829642..6f021c176 100644 --- a/packages/grpc-js-xds/test/xds-server.ts +++ b/packages/grpc-js-xds/test/xds-server.ts @@ -36,12 +36,15 @@ const loadedProtos = loadPackageDefinition(loadSync( [ 'envoy/service/discovery/v3/ads.proto', 'envoy/service/load_stats/v3/lrs.proto', - 'envoy/config/listener/v3/listener.proto', + 'envoy/config/listener/v3/listener.proto', 'envoy/config/route/v3/route.proto', 'envoy/config/cluster/v3/cluster.proto', 'envoy/config/endpoint/v3/endpoint.proto', 'envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto', - 'envoy/extensions/clusters/aggregate/v3/cluster.proto' + 'envoy/extensions/clusters/aggregate/v3/cluster.proto', + 'envoy/extensions/load_balancing_policies/round_robin/v3/round_robin.proto', + 'envoy/extensions/load_balancing_policies/wrr_locality/v3/wrr_locality.proto', + 'xds/type/v3/typed_struct.proto' ], { keepCase: true, @@ -319,7 +322,7 @@ export class XdsServer { callback(error, port); }); } - + shutdownServer() { this.server?.forceShutdown(); } diff --git a/packages/grpc-js/src/experimental.ts b/packages/grpc-js/src/experimental.ts index e4bd164ec..42fd577ca 100644 --- a/packages/grpc-js/src/experimental.ts +++ b/packages/grpc-js/src/experimental.ts @@ -16,7 +16,8 @@ export { createChildChannelControlHelper, registerLoadBalancerType, selectLbConfigFromList, - parseLoadBalancingConfig + parseLoadBalancingConfig, + isLoadBalancerNameRegistered } from './load-balancer'; export { SubchannelAddress, From fa26f4f70f80979661429d5a9e44d07ffe4b6890 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 23 Aug 2023 14:36:49 -0700 Subject: [PATCH 016/109] Add spec links --- packages/grpc-js-xds/src/lb-policy-registry.ts | 2 ++ packages/grpc-js-xds/src/lb-policy-registry/round-robin.ts | 2 ++ packages/grpc-js-xds/src/lb-policy-registry/typed-struct.ts | 2 ++ packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts | 2 ++ 4 files changed, 8 insertions(+) diff --git a/packages/grpc-js-xds/src/lb-policy-registry.ts b/packages/grpc-js-xds/src/lb-policy-registry.ts index 80a06323e..f7aa74254 100644 --- a/packages/grpc-js-xds/src/lb-policy-registry.ts +++ b/packages/grpc-js-xds/src/lb-policy-registry.ts @@ -15,6 +15,8 @@ * */ +// https://github.com/grpc/proposal/blob/master/A52-xds-custom-lb-policies.md + import { LoadBalancingConfig, experimental, logVerbosity } from "@grpc/grpc-js"; import { LoadBalancingPolicy__Output } from "./generated/envoy/config/cluster/v3/LoadBalancingPolicy"; import { TypedExtensionConfig__Output } from "./generated/envoy/config/core/v3/TypedExtensionConfig"; diff --git a/packages/grpc-js-xds/src/lb-policy-registry/round-robin.ts b/packages/grpc-js-xds/src/lb-policy-registry/round-robin.ts index 311c8c38c..b43b1c9e3 100644 --- a/packages/grpc-js-xds/src/lb-policy-registry/round-robin.ts +++ b/packages/grpc-js-xds/src/lb-policy-registry/round-robin.ts @@ -15,6 +15,8 @@ * */ +// https://github.com/grpc/proposal/blob/master/A52-xds-custom-lb-policies.md + import { LoadBalancingConfig } from "@grpc/grpc-js"; import { LoadBalancingPolicy__Output } from "../generated/envoy/config/cluster/v3/LoadBalancingPolicy"; import { TypedExtensionConfig__Output } from "../generated/envoy/config/core/v3/TypedExtensionConfig"; diff --git a/packages/grpc-js-xds/src/lb-policy-registry/typed-struct.ts b/packages/grpc-js-xds/src/lb-policy-registry/typed-struct.ts index 6b50d9e83..9ae0b32ee 100644 --- a/packages/grpc-js-xds/src/lb-policy-registry/typed-struct.ts +++ b/packages/grpc-js-xds/src/lb-policy-registry/typed-struct.ts @@ -15,6 +15,8 @@ * */ +// https://github.com/grpc/proposal/blob/master/A52-xds-custom-lb-policies.md + import { LoadBalancingConfig, experimental } from "@grpc/grpc-js"; import { LoadBalancingPolicy__Output } from "../generated/envoy/config/cluster/v3/LoadBalancingPolicy"; import { TypedExtensionConfig__Output } from "../generated/envoy/config/core/v3/TypedExtensionConfig"; diff --git a/packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts b/packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts index 5faf8b8e5..8636937fd 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts @@ -15,6 +15,8 @@ * */ +// https://github.com/grpc/proposal/blob/master/A52-xds-custom-lb-policies.md + import { LoadBalancingConfig, experimental, logVerbosity } from "@grpc/grpc-js"; import { loadProtosWithOptionsSync } from "@grpc/proto-loader/build/src/util"; import { WeightedTargetRaw } from "./load-balancer-weighted-target"; From c8b5d3119b87dc5e4b0d3468aac8fea98768bb37 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 23 Aug 2023 16:13:00 -0700 Subject: [PATCH 017/109] Fix missing proto file references --- packages/grpc-js-xds/package.json | 2 ++ packages/grpc-js-xds/src/lb-policy-registry/typed-struct.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/packages/grpc-js-xds/package.json b/packages/grpc-js-xds/package.json index 3c61b7383..b43304cea 100644 --- a/packages/grpc-js-xds/package.json +++ b/packages/grpc-js-xds/package.json @@ -59,6 +59,7 @@ "build/src/**/*.{js,d.ts,js.map}", "deps/envoy-api/envoy/admin/v3/**/*.proto", "deps/envoy-api/envoy/config/**/*.proto", + "deps/envoy-api/envoy/data/**/*.proto", "deps/envoy-api/envoy/service/**/*.proto", "deps/envoy-api/envoy/type/**/*.proto", "deps/envoy-api/envoy/annotations/**/*.proto", @@ -66,6 +67,7 @@ "deps/googleapis/google/api/**/*.proto", "deps/googleapis/google/protobuf/**/*.proto", "deps/googleapis/google/rpc/**/*.proto", + "deps/protoc-gen-validate/**/*.proto", "deps/xds/udpa/annotations/**/*.proto", "deps/xds/udpa/type/**/*.proto", "deps/xds/xds/annotations/**/*.proto", diff --git a/packages/grpc-js-xds/src/lb-policy-registry/typed-struct.ts b/packages/grpc-js-xds/src/lb-policy-registry/typed-struct.ts index 9ae0b32ee..b310782d7 100644 --- a/packages/grpc-js-xds/src/lb-policy-registry/typed-struct.ts +++ b/packages/grpc-js-xds/src/lb-policy-registry/typed-struct.ts @@ -37,6 +37,7 @@ const resourceRoot = loadProtosWithOptionsSync([ includeDirs: [ // Paths are relative to src/build/lb-policy-registry __dirname + '/../../../deps/xds/', + __dirname + '/../../../deps/protoc-gen-validate' ], } ); From 91631ba11c8837c9dc01c889716da604ade3b104 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 24 Aug 2023 10:02:30 -0700 Subject: [PATCH 018/109] Update XdsClusterImpl LB policy to accept unset LRS config --- .../src/load-balancer-xds-cluster-impl.ts | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts index 4050a3cf8..926c7a699 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts @@ -79,7 +79,7 @@ class XdsClusterImplLoadBalancingConfig implements TypedLoadBalancingConfig { }; } - constructor(private cluster: string, private dropCategories: DropCategory[], private childPolicy: TypedLoadBalancingConfig, private edsServiceName: string, private lrsLoadReportingServer: XdsServerConfig, maxConcurrentRequests?: number) { + constructor(private cluster: string, private dropCategories: DropCategory[], private childPolicy: TypedLoadBalancingConfig, private edsServiceName: string, private lrsLoadReportingServer?: XdsServerConfig, maxConcurrentRequests?: number) { this.maxConcurrentRequests = maxConcurrentRequests ?? DEFAULT_MAX_CONCURRENT_REQUESTS; } @@ -127,7 +127,11 @@ class XdsClusterImplLoadBalancingConfig implements TypedLoadBalancingConfig { if (!childConfig) { throw new Error('xds_cluster_impl config child_policy parsing failed'); } - return new XdsClusterImplLoadBalancingConfig(obj.cluster, obj.drop_categories.map(validateDropCategory), childConfig, obj.eds_service_name, validateXdsServerConfig(obj.lrs_load_reporting_server), obj.max_concurrent_requests); + let lrsServer: XdsServerConfig | undefined = undefined; + if (obj.lrs_load_reporting_server) { + lrsServer = validateXdsServerConfig(obj.lrs_load_reporting_server) + } + return new XdsClusterImplLoadBalancingConfig(obj.cluster, obj.drop_categories.map(validateDropCategory), childConfig, obj.eds_service_name, lrsServer, obj.max_concurrent_requests); } } @@ -156,7 +160,7 @@ class CallCounterMap { const callCounterMap = new CallCounterMap(); class LocalitySubchannelWrapper extends BaseSubchannelWrapper implements SubchannelInterface { - constructor(child: SubchannelInterface, private statsObject: XdsClusterLocalityStats) { + constructor(child: SubchannelInterface, private statsObject: XdsClusterLocalityStats | null) { super(child); } @@ -210,12 +214,12 @@ class XdsClusterImplPicker implements Picker { subchannel: pickSubchannel?.getWrappedSubchannel() ?? null, onCallStarted: () => { originalPick.onCallStarted?.(); - pickSubchannel?.getStatsObject().addCallStarted(); + pickSubchannel?.getStatsObject()?.addCallStarted(); callCounterMap.startCall(this.callCounterMapKey); }, onCallEnded: status => { originalPick.onCallEnded?.(status); - pickSubchannel?.getStatsObject().addCallFinished(status !== Status.OK) + pickSubchannel?.getStatsObject()?.addCallFinished(status !== Status.OK) callCounterMap.endCall(this.callCounterMapKey); } }; @@ -253,12 +257,16 @@ class XdsClusterImplBalancer implements LoadBalancer { } const locality = (subchannelAddress as LocalitySubchannelAddress).locality ?? ''; const wrapperChild = channelControlHelper.createSubchannel(subchannelAddress, subchannelArgs); - const statsObj = this.xdsClient.addClusterLocalityStats( - this.latestConfig.getLrsLoadReportingServer(), - this.latestConfig.getCluster(), - this.latestConfig.getEdsServiceName(), - locality - ); + const lrsServer = this.latestConfig.getLrsLoadReportingServer(); + let statsObj: XdsClusterLocalityStats | null = null; + if (lrsServer) { + statsObj = this.xdsClient.addClusterLocalityStats( + lrsServer, + this.latestConfig.getCluster(), + this.latestConfig.getEdsServiceName(), + locality + ); + } return new LocalitySubchannelWrapper(wrapperChild, statsObj); }, updateState: (connectivityState, originalPicker) => { From d1f0d9f80d333fb64b99921c66fe177d02a418ae Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 24 Aug 2023 13:38:56 -0700 Subject: [PATCH 019/109] grpc-js-xds: interop: add custom_lb test, reformat test list --- packages/grpc-js-xds/scripts/xds_k8s_lb.sh | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh b/packages/grpc-js-xds/scripts/xds_k8s_lb.sh index 729fb9293..a1e24ff1f 100755 --- a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh +++ b/packages/grpc-js-xds/scripts/xds_k8s_lb.sh @@ -165,7 +165,16 @@ main() { # Run tests cd "${TEST_DRIVER_FULL_DIR}" local failed_tests=0 - test_suites=("baseline_test" "api_listener_test" "change_backend_service_test" "failover_test" "remove_neg_test" "round_robin_test" "outlier_detection_test") + test_suites=( + "api_listener_test" + "baseline_test" + "change_backend_service_test" + "custom_lb_test" + "failover_test" + "outlier_detection_test" + "remove_neg_test" + "round_robin_test" + ) for test in "${test_suites[@]}"; do run_test $test || (( ++failed_tests )) done From 04ef12518dafd0ec0c22755fde3528be3886adb3 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 25 Aug 2023 10:19:01 -0700 Subject: [PATCH 020/109] Add custom LB test from interop test, fix a bug --- .../grpc-js-xds/interop/xds-interop-client.ts | 2 +- packages/grpc-js-xds/test/backend.ts | 16 ++- .../test/test-custom-lb-policies.ts | 135 ++++++++++++++++++ 3 files changed, 150 insertions(+), 3 deletions(-) diff --git a/packages/grpc-js-xds/interop/xds-interop-client.ts b/packages/grpc-js-xds/interop/xds-interop-client.ts index 2893acad9..f9034ed8e 100644 --- a/packages/grpc-js-xds/interop/xds-interop-client.ts +++ b/packages/grpc-js-xds/interop/xds-interop-client.ts @@ -92,7 +92,7 @@ class RpcBehaviorLoadBalancer implements LoadBalancer { const childChannelControlHelper = createChildChannelControlHelper(channelControlHelper, { updateState: (connectivityState, picker) => { if (connectivityState === grpc.connectivityState.READY && this.latestConfig) { - picker = new RpcBehaviorPicker(picker, this.latestConfig.getLoadBalancerName()); + picker = new RpcBehaviorPicker(picker, this.latestConfig.getRpcBehavior()); } channelControlHelper.updateState(connectivityState, picker); } diff --git a/packages/grpc-js-xds/test/backend.ts b/packages/grpc-js-xds/test/backend.ts index 3f20f1f39..01474284b 100644 --- a/packages/grpc-js-xds/test/backend.ts +++ b/packages/grpc-js-xds/test/backend.ts @@ -48,6 +48,18 @@ export class Backend { Echo(call: ServerUnaryCall, callback: sendUnaryData) { // call.request.params is currently ignored this.addCall(); + for (const behaviorEntry of call.metadata.get('rpc-behavior')) { + if (typeof behaviorEntry !== 'string') { + continue; + } + for (const behavior of behaviorEntry.split(',')) { + if (behavior.startsWith('error-code-')) { + const errorCode = Number(behavior.substring('error-code-'.length)); + callback({code: errorCode, details: 'rpc-behavior error code'}); + return; + } + } + } callback(null, {message: call.request.message}); } @@ -87,7 +99,7 @@ export class Backend { }); }); } - + getPort(): number { if (this.port === null) { throw new Error('Port not set. Backend not yet started.'); @@ -125,4 +137,4 @@ export class Backend { }); }); } -} \ No newline at end of file +} diff --git a/packages/grpc-js-xds/test/test-custom-lb-policies.ts b/packages/grpc-js-xds/test/test-custom-lb-policies.ts index 7493a23f0..16cca3a8a 100644 --- a/packages/grpc-js-xds/test/test-custom-lb-policies.ts +++ b/packages/grpc-js-xds/test/test-custom-lb-policies.ts @@ -24,6 +24,98 @@ import { XdsServer } from "./xds-server"; import * as assert from 'assert'; import { WrrLocality } from "../src/generated/envoy/extensions/load_balancing_policies/wrr_locality/v3/WrrLocality"; import { TypedStruct } from "../src/generated/xds/type/v3/TypedStruct"; +import { connectivityState, experimental, logVerbosity } from "@grpc/grpc-js"; + +import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; +import LoadBalancer = experimental.LoadBalancer; +import ChannelControlHelper = experimental.ChannelControlHelper; +import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; +import SubchannelAddress = experimental.SubchannelAddress; +import Picker = experimental.Picker; +import PickArgs = experimental.PickArgs; +import PickResult = experimental.PickResult; +import PickResultType = experimental.PickResultType; +import createChildChannelControlHelper = experimental.createChildChannelControlHelper; +import parseLoadBalancingConfig = experimental.parseLoadBalancingConfig; +import registerLoadBalancerType = experimental.registerLoadBalancerType; + +const LB_POLICY_NAME = 'test.RpcBehaviorLoadBalancer'; + +class RpcBehaviorLoadBalancingConfig implements TypedLoadBalancingConfig { + constructor(private rpcBehavior: string) {} + getLoadBalancerName(): string { + return LB_POLICY_NAME; + } + toJsonObject(): object { + return { + [LB_POLICY_NAME]: { + 'rpcBehavior': this.rpcBehavior + } + }; + } + getRpcBehavior() { + return this.rpcBehavior; + } + static createFromJson(obj: any): RpcBehaviorLoadBalancingConfig { + if (!('rpcBehavior' in obj && typeof obj.rpcBehavior === 'string')) { + throw new Error(`${LB_POLICY_NAME} parsing error: expected string field rpcBehavior`); + } + return new RpcBehaviorLoadBalancingConfig(obj.rpcBehavior); + } +} + +class RpcBehaviorPicker implements Picker { + constructor(private wrappedPicker: Picker, private rpcBehavior: string) {} + pick(pickArgs: PickArgs): PickResult { + const wrappedPick = this.wrappedPicker.pick(pickArgs); + if (wrappedPick.pickResultType === PickResultType.COMPLETE) { + pickArgs.metadata.add('rpc-behavior', this.rpcBehavior); + } + return wrappedPick; + } +} + +const RPC_BEHAVIOR_CHILD_CONFIG = parseLoadBalancingConfig({round_robin: {}}); + +/** + * Load balancer implementation for Custom LB policy test + */ +class RpcBehaviorLoadBalancer implements LoadBalancer { + private child: ChildLoadBalancerHandler; + private latestConfig: RpcBehaviorLoadBalancingConfig | null = null; + constructor(channelControlHelper: ChannelControlHelper) { + const childChannelControlHelper = createChildChannelControlHelper(channelControlHelper, { + updateState: (state, picker) => { + if (state === connectivityState.READY && this.latestConfig) { + picker = new RpcBehaviorPicker(picker, this.latestConfig.getRpcBehavior()); + } + channelControlHelper.updateState(state, picker); + } + }); + this.child = new ChildLoadBalancerHandler(childChannelControlHelper); + } + updateAddressList(addressList: SubchannelAddress[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { + if (!(lbConfig instanceof RpcBehaviorLoadBalancingConfig)) { + return; + } + this.latestConfig = lbConfig; + this.child.updateAddressList(addressList, RPC_BEHAVIOR_CHILD_CONFIG, attributes); + } + exitIdle(): void { + this.child.exitIdle(); + } + resetBackoff(): void { + this.child.resetBackoff(); + } + destroy(): void { + this.child.destroy(); + } + getTypeName(): string { + return LB_POLICY_NAME; + } +} + +registerLoadBalancerType(LB_POLICY_NAME, RpcBehaviorLoadBalancer, RpcBehaviorLoadBalancingConfig); describe('Custom LB policies', () => { let xdsServer: XdsServer; @@ -163,4 +255,47 @@ describe('Custom LB policies', () => { client.sendOneCall(done); }, reason => done(reason)); }); + it('Should handle a custom LB policy', done => { + const childPolicy: TypedStruct & AnyExtension = { + '@type': 'type.googleapis.com/xds.type.v3.TypedStruct', + type_url: 'test.RpcBehaviorLoadBalancer', + value: { + fields: { + rpcBehavior: {stringValue: 'error-code-15'} + } + } + }; + const lbPolicy: WrrLocality & AnyExtension = { + '@type': 'type.googleapis.com/envoy.extensions.load_balancing_policies.wrr_locality.v3.WrrLocality', + endpoint_picking_policy: { + policies: [ + { + typed_extension_config: { + name: 'child', + typed_config: childPolicy + } + } + ] + } + }; + const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [new Backend()], locality:{region: 'region1'}}], lbPolicy); + const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: cluster}]); + routeGroup.startAllBackends().then(() => { + xdsServer.setEdsResource(cluster.getEndpointConfig()); + xdsServer.setCdsResource(cluster.getClusterConfig()); + xdsServer.setRdsResource(routeGroup.getRouteConfiguration()); + xdsServer.setLdsResource(routeGroup.getListener()); + xdsServer.addResponseListener((typeUrl, responseState) => { + if (responseState.state === 'NACKED') { + client.stopCalls(); + assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`); + } + }) + client = XdsTestClient.createFromServer('listener1', xdsServer); + client.sendOneCall(error => { + assert.strictEqual(error?.code, 15); + done(); + }); + }, reason => done(reason)); + }) }); From 613c9144d9c4ad691dcb2923b234bd0247b61d8e Mon Sep 17 00:00:00 2001 From: gusumuzhe Date: Tue, 29 Aug 2023 17:39:38 +0800 Subject: [PATCH 021/109] fix: pick first load balancer call doPick infinite --- packages/grpc-js/src/load-balancer-pick-first.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js/src/load-balancer-pick-first.ts b/packages/grpc-js/src/load-balancer-pick-first.ts index 8635482ce..1d9233d9c 100644 --- a/packages/grpc-js/src/load-balancer-pick-first.ts +++ b/packages/grpc-js/src/load-balancer-pick-first.ts @@ -315,7 +315,7 @@ export class PickFirstLoadBalancer implements LoadBalancer { } private pickSubchannel(subchannel: SubchannelInterface) { - if (subchannel === this.currentPick) { + if (this.currentPick && subchannel.realSubchannelEquals(this.currentPick)) { return; } trace('Pick subchannel with address ' + subchannel.getAddress()); From 49b7c6af34cb7454ed0d4a85c3654db97a3cb6c4 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 30 Aug 2023 14:46:08 -0700 Subject: [PATCH 022/109] grpc-js: Make pick_first the universal leaf policy, switch to endpoint lists --- packages/grpc-js/src/experimental.ts | 6 +- .../src/load-balancer-child-handler.ts | 8 +- .../src/load-balancer-outlier-detection.ts | 287 +++++++++++------- .../grpc-js/src/load-balancer-pick-first.ts | 148 +++++++-- .../grpc-js/src/load-balancer-round-robin.ts | 145 ++++----- packages/grpc-js/src/load-balancer.ts | 20 +- packages/grpc-js/src/picker.ts | 17 +- packages/grpc-js/src/resolver-dns.ts | 51 +--- packages/grpc-js/src/resolver-ip.ts | 10 +- packages/grpc-js/src/resolver-uds.ts | 8 +- packages/grpc-js/src/resolver.ts | 4 +- .../grpc-js/src/resolving-load-balancer.ts | 8 +- packages/grpc-js/src/server.ts | 5 +- packages/grpc-js/src/subchannel-address.ts | 36 +++ packages/grpc-js/src/subchannel-interface.ts | 42 ++- packages/grpc-js/src/subchannel.ts | 12 + packages/grpc-js/test/common.ts | 10 +- packages/grpc-js/test/test-pick-first.ts | 115 ++++--- packages/grpc-js/test/test-resolver.ts | 240 +++++---------- 19 files changed, 668 insertions(+), 504 deletions(-) diff --git a/packages/grpc-js/src/experimental.ts b/packages/grpc-js/src/experimental.ts index 42fd577ca..0c7bc75e1 100644 --- a/packages/grpc-js/src/experimental.ts +++ b/packages/grpc-js/src/experimental.ts @@ -17,11 +17,15 @@ export { registerLoadBalancerType, selectLbConfigFromList, parseLoadBalancingConfig, - isLoadBalancerNameRegistered + isLoadBalancerNameRegistered, } from './load-balancer'; +export { LeafLoadBalancer } from './load-balancer-pick-first'; export { SubchannelAddress, subchannelAddressToString, + Endpoint, + endpointToString, + endpointHasAddress, } from './subchannel-address'; export { ChildLoadBalancerHandler } from './load-balancer-child-handler'; export { diff --git a/packages/grpc-js/src/load-balancer-child-handler.ts b/packages/grpc-js/src/load-balancer-child-handler.ts index b23f19263..11bfac211 100644 --- a/packages/grpc-js/src/load-balancer-child-handler.ts +++ b/packages/grpc-js/src/load-balancer-child-handler.ts @@ -21,7 +21,7 @@ import { TypedLoadBalancingConfig, createLoadBalancer, } from './load-balancer'; -import { SubchannelAddress } from './subchannel-address'; +import { Endpoint, SubchannelAddress } from './subchannel-address'; import { ChannelOptions } from './channel-options'; import { ConnectivityState } from './connectivity-state'; import { Picker } from './picker'; @@ -95,12 +95,12 @@ export class ChildLoadBalancerHandler implements LoadBalancer { /** * Prerequisites: lbConfig !== null and lbConfig.name is registered - * @param addressList + * @param endpointList * @param lbConfig * @param attributes */ updateAddressList( - addressList: SubchannelAddress[], + endpointList: Endpoint[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown } ): void { @@ -131,7 +131,7 @@ export class ChildLoadBalancerHandler implements LoadBalancer { } } this.latestConfig = lbConfig; - childToUpdate.updateAddressList(addressList, lbConfig, attributes); + childToUpdate.updateAddressList(endpointList, lbConfig, attributes); } exitIdle(): void { if (this.currentChild) { diff --git a/packages/grpc-js/src/load-balancer-outlier-detection.ts b/packages/grpc-js/src/load-balancer-outlier-detection.ts index 63a1d8f1d..b0c648efd 100644 --- a/packages/grpc-js/src/load-balancer-outlier-detection.ts +++ b/packages/grpc-js/src/load-balancer-outlier-detection.ts @@ -32,12 +32,14 @@ import { import { ChildLoadBalancerHandler } from './load-balancer-child-handler'; import { PickArgs, Picker, PickResult, PickResultType } from './picker'; import { + Endpoint, SubchannelAddress, - subchannelAddressToString, + endpointHasAddress, + endpointToString, + subchannelAddressEqual, } from './subchannel-address'; import { BaseSubchannelWrapper, - ConnectivityStateListener, SubchannelInterface, } from './subchannel-interface'; import * as logging from './logging'; @@ -107,7 +109,11 @@ function validateFieldType( expectedType: TypeofValues, objectName?: string ) { - if (fieldName in obj && obj[fieldName] !== undefined && typeof obj[fieldName] !== expectedType) { + if ( + fieldName in obj && + obj[fieldName] !== undefined && + typeof obj[fieldName] !== expectedType + ) { const fullFieldName = objectName ? `${objectName}.${fieldName}` : fieldName; throw new Error( `outlier detection config ${fullFieldName} parse error: expected ${expectedType}, got ${typeof obj[ @@ -149,7 +155,11 @@ function validatePositiveDuration( function validatePercentage(obj: any, fieldName: string, objectName?: string) { const fullFieldName = objectName ? `${objectName}.${fieldName}` : fieldName; validateFieldType(obj, fieldName, 'number', objectName); - if (fieldName in obj && obj[fieldName] !== undefined && !(obj[fieldName] >= 0 && obj[fieldName] <= 100)) { + if ( + fieldName in obj && + obj[fieldName] !== undefined && + !(obj[fieldName] >= 0 && obj[fieldName] <= 100) + ) { throw new Error( `outlier detection config ${fullFieldName} parse error: value out of range for percentage (0-100)` ); @@ -175,9 +185,7 @@ export class OutlierDetectionLoadBalancingConfig failurePercentageEjection: Partial | null, private readonly childPolicy: TypedLoadBalancingConfig ) { - if ( - childPolicy.getLoadBalancerName() === 'pick_first' - ) { + if (childPolicy.getLoadBalancerName() === 'pick_first') { throw new Error( 'outlier_detection LB policy cannot have a pick_first child policy' ); @@ -207,9 +215,10 @@ export class OutlierDetectionLoadBalancingConfig max_ejection_time: msToDuration(this.maxEjectionTimeMs), max_ejection_percent: this.maxEjectionPercent, success_rate_ejection: this.successRateEjection ?? undefined, - failure_percentage_ejection: this.failurePercentageEjection ?? undefined, - child_policy: [this.childPolicy.toJsonObject()] - } + failure_percentage_ejection: + this.failurePercentageEjection ?? undefined, + child_policy: [this.childPolicy.toJsonObject()], + }, }; } @@ -240,7 +249,10 @@ export class OutlierDetectionLoadBalancingConfig validatePositiveDuration(obj, 'base_ejection_time'); validatePositiveDuration(obj, 'max_ejection_time'); validatePercentage(obj, 'max_ejection_percent'); - if ('success_rate_ejection' in obj && obj.success_rate_ejection !== undefined) { + if ( + 'success_rate_ejection' in obj && + obj.success_rate_ejection !== undefined + ) { if (typeof obj.success_rate_ejection !== 'object') { throw new Error( 'outlier detection config success_rate_ejection must be an object' @@ -270,7 +282,10 @@ export class OutlierDetectionLoadBalancingConfig 'success_rate_ejection' ); } - if ('failure_percentage_ejection' in obj && obj.failure_percentage_ejection !== undefined) { + if ( + 'failure_percentage_ejection' in obj && + obj.failure_percentage_ejection !== undefined + ) { if (typeof obj.failure_percentage_ejection !== 'object') { throw new Error( 'outlier detection config failure_percentage_ejection must be an object' @@ -305,7 +320,9 @@ export class OutlierDetectionLoadBalancingConfig } const childPolicy = selectLbConfigFromList(obj.child_policy); if (!childPolicy) { - throw new Error('outlier detection config child_policy: no valid recognized policy found'); + throw new Error( + 'outlier detection config child_policy: no valid recognized policy found' + ); } return new OutlierDetectionLoadBalancingConfig( @@ -324,55 +341,12 @@ class OutlierDetectionSubchannelWrapper extends BaseSubchannelWrapper implements SubchannelInterface { - private childSubchannelState: ConnectivityState; - private stateListeners: ConnectivityStateListener[] = []; - private ejected = false; private refCount = 0; constructor( childSubchannel: SubchannelInterface, private mapEntry?: MapEntry ) { super(childSubchannel); - this.childSubchannelState = childSubchannel.getConnectivityState(); - childSubchannel.addConnectivityStateListener( - (subchannel, previousState, newState, keepaliveTime) => { - this.childSubchannelState = newState; - if (!this.ejected) { - for (const listener of this.stateListeners) { - listener(this, previousState, newState, keepaliveTime); - } - } - } - ); - } - - getConnectivityState(): ConnectivityState { - if (this.ejected) { - return ConnectivityState.TRANSIENT_FAILURE; - } else { - return this.childSubchannelState; - } - } - - /** - * Add a listener function to be called whenever the wrapper's - * connectivity state changes. - * @param listener - */ - addConnectivityStateListener(listener: ConnectivityStateListener) { - this.stateListeners.push(listener); - } - - /** - * Remove a listener previously added with `addConnectivityStateListener` - * @param listener A reference to a function previously passed to - * `addConnectivityStateListener` - */ - removeConnectivityStateListener(listener: ConnectivityStateListener) { - const listenerIndex = this.stateListeners.indexOf(listener); - if (listenerIndex > -1) { - this.stateListeners.splice(listenerIndex, 1); - } } ref() { @@ -394,27 +368,11 @@ class OutlierDetectionSubchannelWrapper } eject() { - this.ejected = true; - for (const listener of this.stateListeners) { - listener( - this, - this.childSubchannelState, - ConnectivityState.TRANSIENT_FAILURE, - -1 - ); - } + this.setHealthy(false); } uneject() { - this.ejected = false; - for (const listener of this.stateListeners) { - listener( - this, - ConnectivityState.TRANSIENT_FAILURE, - this.childSubchannelState, - -1 - ); - } + this.setHealthy(true); } getMapEntry(): MapEntry | undefined { @@ -459,13 +417,6 @@ class CallCounter { } } -interface MapEntry { - counter: CallCounter; - currentEjectionTimestamp: Date | null; - ejectionTimeMultiplier: number; - subchannelWrappers: OutlierDetectionSubchannelWrapper[]; -} - class OutlierDetectionPicker implements Picker { constructor(private wrappedPicker: Picker, private countCalls: boolean) {} pick(pickArgs: PickArgs): PickResult { @@ -503,9 +454,133 @@ class OutlierDetectionPicker implements Picker { } } +interface MapEntry { + counter: CallCounter; + currentEjectionTimestamp: Date | null; + ejectionTimeMultiplier: number; + subchannelWrappers: OutlierDetectionSubchannelWrapper[]; +} + +interface EndpointMapEntry { + key: Endpoint; + value: MapEntry; +} + +function endpointEqualUnordered( + endpoint1: Endpoint, + endpoint2: Endpoint +): boolean { + if (endpoint1.addresses.length !== endpoint2.addresses.length) { + return false; + } + for (const address1 of endpoint1.addresses) { + let matchFound = false; + for (const address2 of endpoint2.addresses) { + if (subchannelAddressEqual(address1, address2)) { + matchFound = true; + break; + } + } + if (!matchFound) { + return false; + } + } + return true; +} + +class EndpointMap { + private map: Set = new Set(); + + get size() { + return this.map.size; + } + + getForSubchannelAddress(address: SubchannelAddress): MapEntry | undefined { + for (const entry of this.map) { + if (endpointHasAddress(entry.key, address)) { + return entry.value; + } + } + return undefined; + } + + /** + * Delete any entries in this map with keys that are not in endpoints + * @param endpoints + */ + deleteMissing(endpoints: Endpoint[]) { + for (const entry of this.map) { + let foundEntry = false; + for (const endpoint of endpoints) { + if (endpointEqualUnordered(endpoint, entry.key)) { + foundEntry = true; + } + } + if (!foundEntry) { + this.map.delete(entry); + } + } + } + + get(endpoint: Endpoint): MapEntry | undefined { + for (const entry of this.map) { + if (endpointEqualUnordered(endpoint, entry.key)) { + return entry.value; + } + } + return undefined; + } + + set(endpoint: Endpoint, mapEntry: MapEntry) { + for (const entry of this.map) { + if (endpointEqualUnordered(endpoint, entry.key)) { + entry.value = mapEntry; + return; + } + } + this.map.add({ key: endpoint, value: mapEntry }); + } + + delete(endpoint: Endpoint) { + for (const entry of this.map) { + if (endpointEqualUnordered(endpoint, entry.key)) { + this.map.delete(entry); + return; + } + } + } + + has(endpoint: Endpoint): boolean { + for (const entry of this.map) { + if (endpointEqualUnordered(endpoint, entry.key)) { + return true; + } + } + return false; + } + + *keys(): IterableIterator { + for (const entry of this.map) { + yield entry.key; + } + } + + *values(): IterableIterator { + for (const entry of this.map) { + yield entry.value; + } + } + + *entries(): IterableIterator<[Endpoint, MapEntry]> { + for (const entry of this.map) { + yield [entry.key, entry.value]; + } + } +} + export class OutlierDetectionLoadBalancer implements LoadBalancer { private childBalancer: ChildLoadBalancerHandler; - private addressMap: Map = new Map(); + private entryMap = new EndpointMap(); private latestConfig: OutlierDetectionLoadBalancingConfig | null = null; private ejectionTimer: NodeJS.Timeout; private timerStartTime: Date | null = null; @@ -521,9 +596,8 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { subchannelAddress, subchannelArgs ); - const mapEntry = this.addressMap.get( - subchannelAddressToString(subchannelAddress) - ); + const mapEntry = + this.entryMap.getForSubchannelAddress(subchannelAddress); const subchannelWrapper = new OutlierDetectionSubchannelWrapper( originalSubchannel, mapEntry @@ -561,12 +635,12 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { private getCurrentEjectionPercent() { let ejectionCount = 0; - for (const mapEntry of this.addressMap.values()) { + for (const mapEntry of this.entryMap.values()) { if (mapEntry.currentEjectionTimestamp !== null) { ejectionCount += 1; } } - return (ejectionCount * 100) / this.addressMap.size; + return (ejectionCount * 100) / this.entryMap.size; } private runSuccessRateCheck(ejectionTimestamp: Date) { @@ -582,12 +656,12 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { const targetRequestVolume = successRateConfig.request_volume; let addresesWithTargetVolume = 0; const successRates: number[] = []; - for (const [address, mapEntry] of this.addressMap) { + for (const [endpoint, mapEntry] of this.entryMap.entries()) { const successes = mapEntry.counter.getLastSuccesses(); const failures = mapEntry.counter.getLastFailures(); trace( 'Stats for ' + - address + + endpointToString(endpoint) + ': successes=' + successes + ' failures=' + @@ -631,7 +705,7 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { ); // Step 3 - for (const [address, mapEntry] of this.addressMap.entries()) { + for (const [address, mapEntry] of this.entryMap.entries()) { // Step 3.i if ( this.getCurrentEjectionPercent() >= @@ -683,7 +757,7 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { ); // Step 1 let addressesWithTargetVolume = 0; - for (const mapEntry of this.addressMap.values()) { + for (const mapEntry of this.entryMap.values()) { const successes = mapEntry.counter.getLastSuccesses(); const failures = mapEntry.counter.getLastFailures(); if (successes + failures >= failurePercentageConfig.request_volume) { @@ -695,7 +769,7 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { } // Step 2 - for (const [address, mapEntry] of this.addressMap.entries()) { + for (const [address, mapEntry] of this.entryMap.entries()) { // Step 2.i if ( this.getCurrentEjectionPercent() >= @@ -746,7 +820,7 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { } private switchAllBuckets() { - for (const mapEntry of this.addressMap.values()) { + for (const mapEntry of this.entryMap.values()) { mapEntry.counter.switchBuckets(); } } @@ -771,7 +845,7 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { this.runSuccessRateCheck(ejectionTimestamp); this.runFailurePercentageCheck(ejectionTimestamp); - for (const [address, mapEntry] of this.addressMap.entries()) { + for (const [address, mapEntry] of this.entryMap.entries()) { if (mapEntry.currentEjectionTimestamp === null) { if (mapEntry.ejectionTimeMultiplier > 0) { mapEntry.ejectionTimeMultiplier -= 1; @@ -798,21 +872,17 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { } updateAddressList( - addressList: SubchannelAddress[], + endpointList: Endpoint[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown } ): void { if (!(lbConfig instanceof OutlierDetectionLoadBalancingConfig)) { return; } - const subchannelAddresses = new Set(); - for (const address of addressList) { - subchannelAddresses.add(subchannelAddressToString(address)); - } - for (const address of subchannelAddresses) { - if (!this.addressMap.has(address)) { - trace('Adding map entry for ' + address); - this.addressMap.set(address, { + for (const endpoint of endpointList) { + if (!this.entryMap.has(endpoint)) { + trace('Adding map entry for ' + endpointToString(endpoint)); + this.entryMap.set(endpoint, { counter: new CallCounter(), currentEjectionTimestamp: null, ejectionTimeMultiplier: 0, @@ -820,14 +890,9 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { }); } } - for (const key of this.addressMap.keys()) { - if (!subchannelAddresses.has(key)) { - trace('Removing map entry for ' + key); - this.addressMap.delete(key); - } - } + this.entryMap.deleteMissing(endpointList); const childPolicy = lbConfig.getChildPolicy(); - this.childBalancer.updateAddressList(addressList, childPolicy, attributes); + this.childBalancer.updateAddressList(endpointList, childPolicy, attributes); if ( lbConfig.getSuccessRateEjectionConfig() || @@ -850,7 +915,7 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { trace('Counting disabled. Cancelling timer.'); this.timerStartTime = null; clearTimeout(this.ejectionTimer); - for (const mapEntry of this.addressMap.values()) { + for (const mapEntry of this.entryMap.values()) { this.uneject(mapEntry); mapEntry.ejectionTimeMultiplier = 0; } diff --git a/packages/grpc-js/src/load-balancer-pick-first.ts b/packages/grpc-js/src/load-balancer-pick-first.ts index 8635482ce..7d3948216 100644 --- a/packages/grpc-js/src/load-balancer-pick-first.ts +++ b/packages/grpc-js/src/load-balancer-pick-first.ts @@ -21,6 +21,7 @@ import { TypedLoadBalancingConfig, registerDefaultLoadBalancerType, registerLoadBalancerType, + createChildChannelControlHelper, } from './load-balancer'; import { ConnectivityState } from './connectivity-state'; import { @@ -31,13 +32,16 @@ import { PickResultType, UnavailablePicker, } from './picker'; -import { SubchannelAddress } from './subchannel-address'; +import { Endpoint, SubchannelAddress } from './subchannel-address'; import * as logging from './logging'; import { LogVerbosity } from './constants'; import { SubchannelInterface, ConnectivityStateListener, + HealthListener, } from './subchannel-interface'; +import { isTcpSubchannelAddress } from './subchannel-address'; +import { isIPv6 } from 'net'; const TRACER_NAME = 'pick_first'; @@ -125,6 +129,39 @@ export function shuffled(list: T[]): T[] { return result; } +/** + * Interleave addresses in addressList by family in accordance with RFC-8304 section 4 + * @param addressList + * @returns + */ +function interleaveAddressFamilies( + addressList: SubchannelAddress[] +): SubchannelAddress[] { + const result: SubchannelAddress[] = []; + const ipv6Addresses: SubchannelAddress[] = []; + const ipv4Addresses: SubchannelAddress[] = []; + const ipv6First = + isTcpSubchannelAddress(addressList[0]) && isIPv6(addressList[0].host); + for (const address of addressList) { + if (isTcpSubchannelAddress(address) && isIPv6(address.host)) { + ipv6Addresses.push(address); + } else { + ipv4Addresses.push(address); + } + } + const firstList = ipv6First ? ipv6Addresses : ipv4Addresses; + const secondList = ipv6First ? ipv4Addresses : ipv6Addresses; + for (let i = 0; i < Math.max(firstList.length, secondList.length); i++) { + if (i < firstList.length) { + result.push(firstList[i]); + } + if (i < secondList.length) { + result.push(secondList[i]); + } + } + return result; +} + export class PickFirstLoadBalancer implements LoadBalancer { /** * The list of subchannels this load balancer is currently attempting to @@ -157,6 +194,9 @@ export class PickFirstLoadBalancer implements LoadBalancer { ) => { this.onSubchannelStateUpdate(subchannel, previousState, newState); }; + + private pickedSubchannelHealthListener: HealthListener = () => + this.calculateAndReportNewState(); /** * Timer reference for the timer tracking when to start */ @@ -179,7 +219,10 @@ export class PickFirstLoadBalancer implements LoadBalancer { * @param channelControlHelper `ChannelControlHelper` instance provided by * this load balancer's owner. */ - constructor(private readonly channelControlHelper: ChannelControlHelper) { + constructor( + private readonly channelControlHelper: ChannelControlHelper, + private reportHealthStatus = false + ) { this.connectionDelayTimeout = setTimeout(() => {}, 0); clearTimeout(this.connectionDelayTimeout); } @@ -190,10 +233,19 @@ export class PickFirstLoadBalancer implements LoadBalancer { private calculateAndReportNewState() { if (this.currentPick) { - this.updateState( - ConnectivityState.READY, - new PickFirstPicker(this.currentPick) - ); + if (this.reportHealthStatus && !this.currentPick.isHealthy()) { + this.updateState( + ConnectivityState.TRANSIENT_FAILURE, + new UnavailablePicker({ + details: `Picked subchannel ${this.currentPick.getAddress()} is unhealthy`, + }) + ); + } else { + this.updateState( + ConnectivityState.READY, + new PickFirstPicker(this.currentPick) + ); + } } else if (this.children.length === 0) { this.updateState(ConnectivityState.IDLE, new QueuePicker(this)); } else { @@ -235,6 +287,11 @@ export class PickFirstLoadBalancer implements LoadBalancer { this.channelControlHelper.removeChannelzChild( currentPick.getChannelzRef() ); + if (this.reportHealthStatus) { + currentPick.removeHealthStateWatcher( + this.pickedSubchannelHealthListener + ); + } } } @@ -306,7 +363,7 @@ export class PickFirstLoadBalancer implements LoadBalancer { this.children[subchannelIndex].subchannel.getAddress() ); process.nextTick(() => { - this.children[subchannelIndex].subchannel.startConnecting(); + this.children[subchannelIndex]?.subchannel.startConnecting(); }); } this.connectionDelayTimeout = setTimeout(() => { @@ -320,17 +377,12 @@ export class PickFirstLoadBalancer implements LoadBalancer { } trace('Pick subchannel with address ' + subchannel.getAddress()); this.stickyTransientFailureMode = false; - if (this.currentPick !== null) { - this.currentPick.unref(); - this.channelControlHelper.removeChannelzChild( - this.currentPick.getChannelzRef() - ); - this.currentPick.removeConnectivityStateListener( - this.subchannelStateListener - ); - } + this.removeCurrentPick(); this.currentPick = subchannel; subchannel.ref(); + if (this.reportHealthStatus) { + subchannel.addHealthStateWatcher(this.pickedSubchannelHealthListener); + } this.channelControlHelper.addChannelzChild(subchannel.getChannelzRef()); this.resetSubchannelList(); clearTimeout(this.connectionDelayTimeout); @@ -373,7 +425,7 @@ export class PickFirstLoadBalancer implements LoadBalancer { } updateAddressList( - addressList: SubchannelAddress[], + endpointList: Endpoint[], lbConfig: TypedLoadBalancingConfig ): void { if (!(lbConfig instanceof PickFirstLoadBalancingConfig)) { @@ -383,8 +435,15 @@ export class PickFirstLoadBalancer implements LoadBalancer { * previous update, to minimize churn. Now the DNS resolver is * rate-limited, so that is less of a concern. */ if (lbConfig.getShuffleAddressList()) { - addressList = shuffled(addressList); + endpointList = shuffled(endpointList); } + const rawAddressList = ([] as SubchannelAddress[]).concat( + ...endpointList.map(endpoint => endpoint.addresses) + ); + if (rawAddressList.length === 0) { + throw new Error('No addresses in endpoint list passed to pick_first'); + } + const addressList = interleaveAddressFamilies(rawAddressList); const newChildrenList = addressList.map(address => ({ subchannel: this.channelControlHelper.createSubchannel(address, {}), hasReportedTransientFailure: false, @@ -438,6 +497,59 @@ export class PickFirstLoadBalancer implements LoadBalancer { } } +const LEAF_CONFIG = new PickFirstLoadBalancingConfig(false); + +/** + * This class handles the leaf load balancing operations for a single endpoint. + * It is a thin wrapper around a PickFirstLoadBalancer with a different API + * that more closely reflects how it will be used as a leaf balancer. + */ +export class LeafLoadBalancer { + private pickFirstBalancer: PickFirstLoadBalancer; + private latestState: ConnectivityState = ConnectivityState.IDLE; + private latestPicker: Picker; + constructor( + private endpoint: Endpoint, + channelControlHelper: ChannelControlHelper + ) { + const childChannelControlHelper = createChildChannelControlHelper( + channelControlHelper, + { + updateState: (connectivityState, picker) => { + this.latestState = connectivityState; + this.latestPicker = picker; + channelControlHelper.updateState(connectivityState, picker); + }, + } + ); + this.pickFirstBalancer = new PickFirstLoadBalancer( + childChannelControlHelper, + /* reportHealthStatus= */ true + ); + this.latestPicker = new QueuePicker(this.pickFirstBalancer); + } + + startConnecting() { + this.pickFirstBalancer.updateAddressList([this.endpoint], LEAF_CONFIG); + } + + getConnectivityState() { + return this.latestState; + } + + getPicker() { + return this.latestPicker; + } + + getEndpoint() { + return this.endpoint; + } + + destroy() { + this.pickFirstBalancer.destroy(); + } +} + export function setup(): void { registerLoadBalancerType( TYPE_NAME, diff --git a/packages/grpc-js/src/load-balancer-round-robin.ts b/packages/grpc-js/src/load-balancer-round-robin.ts index a611cfd64..986181a84 100644 --- a/packages/grpc-js/src/load-balancer-round-robin.ts +++ b/packages/grpc-js/src/load-balancer-round-robin.ts @@ -20,26 +20,24 @@ import { ChannelControlHelper, TypedLoadBalancingConfig, registerLoadBalancerType, + createChildChannelControlHelper, } from './load-balancer'; import { ConnectivityState } from './connectivity-state'; import { QueuePicker, Picker, PickArgs, - CompletePickResult, - PickResultType, UnavailablePicker, + PickResult, } from './picker'; -import { - SubchannelAddress, - subchannelAddressToString, -} from './subchannel-address'; import * as logging from './logging'; import { LogVerbosity } from './constants'; import { - ConnectivityStateListener, - SubchannelInterface, -} from './subchannel-interface'; + Endpoint, + endpointEqual, + endpointToString, +} from './subchannel-address'; +import { LeafLoadBalancer } from './load-balancer-pick-first'; const TRACER_NAME = 'round_robin'; @@ -70,20 +68,14 @@ class RoundRobinLoadBalancingConfig implements TypedLoadBalancingConfig { class RoundRobinPicker implements Picker { constructor( - private readonly subchannelList: SubchannelInterface[], + private readonly children: { endpoint: Endpoint; picker: Picker }[], private nextIndex = 0 ) {} - pick(pickArgs: PickArgs): CompletePickResult { - const pickedSubchannel = this.subchannelList[this.nextIndex]; - this.nextIndex = (this.nextIndex + 1) % this.subchannelList.length; - return { - pickResultType: PickResultType.COMPLETE, - subchannel: pickedSubchannel, - status: null, - onCallStarted: null, - onCallEnded: null, - }; + pick(pickArgs: PickArgs): PickResult { + const childPicker = this.children[this.nextIndex].picker; + this.nextIndex = (this.nextIndex + 1) % this.children.length; + return childPicker.pick(pickArgs); } /** @@ -91,54 +83,51 @@ class RoundRobinPicker implements Picker { * balancer implementation to preserve this part of the picker state if * possible when a subchannel connects or disconnects. */ - peekNextSubchannel(): SubchannelInterface { - return this.subchannelList[this.nextIndex]; + peekNextEndpoint(): Endpoint { + return this.children[this.nextIndex].endpoint; } } export class RoundRobinLoadBalancer implements LoadBalancer { - private subchannels: SubchannelInterface[] = []; + private children: LeafLoadBalancer[] = []; private currentState: ConnectivityState = ConnectivityState.IDLE; - private subchannelStateListener: ConnectivityStateListener; - private currentReadyPicker: RoundRobinPicker | null = null; + private updatesPaused = false; + + private childChannelControlHelper: ChannelControlHelper; + constructor(private readonly channelControlHelper: ChannelControlHelper) { - this.subchannelStateListener = ( - subchannel: SubchannelInterface, - previousState: ConnectivityState, - newState: ConnectivityState - ) => { - this.calculateAndUpdateState(); - - if ( - newState === ConnectivityState.TRANSIENT_FAILURE || - newState === ConnectivityState.IDLE - ) { - this.channelControlHelper.requestReresolution(); - subchannel.startConnecting(); + this.childChannelControlHelper = createChildChannelControlHelper( + channelControlHelper, + { + updateState: (connectivityState, picker) => { + this.calculateAndUpdateState(); + }, } - }; + ); } - private countSubchannelsWithState(state: ConnectivityState) { - return this.subchannels.filter( - subchannel => subchannel.getConnectivityState() === state - ).length; + private countChildrenWithState(state: ConnectivityState) { + return this.children.filter(child => child.getConnectivityState() === state) + .length; } private calculateAndUpdateState() { - if (this.countSubchannelsWithState(ConnectivityState.READY) > 0) { - const readySubchannels = this.subchannels.filter( - subchannel => - subchannel.getConnectivityState() === ConnectivityState.READY + if (this.updatesPaused) { + return; + } + if (this.countChildrenWithState(ConnectivityState.READY) > 0) { + const readyChildren = this.children.filter( + child => child.getConnectivityState() === ConnectivityState.READY ); let index = 0; if (this.currentReadyPicker !== null) { - index = readySubchannels.indexOf( - this.currentReadyPicker.peekNextSubchannel() + const nextPickedEndpoint = this.currentReadyPicker.peekNextEndpoint(); + index = readyChildren.findIndex(child => + endpointEqual(child.getEndpoint(), nextPickedEndpoint) ); if (index < 0) { index = 0; @@ -146,14 +135,18 @@ export class RoundRobinLoadBalancer implements LoadBalancer { } this.updateState( ConnectivityState.READY, - new RoundRobinPicker(readySubchannels, index) + new RoundRobinPicker( + readyChildren.map(child => ({ + endpoint: child.getEndpoint(), + picker: child.getPicker(), + })), + index + ) ); - } else if ( - this.countSubchannelsWithState(ConnectivityState.CONNECTING) > 0 - ) { + } else if (this.countChildrenWithState(ConnectivityState.CONNECTING) > 0) { this.updateState(ConnectivityState.CONNECTING, new QueuePicker(this)); } else if ( - this.countSubchannelsWithState(ConnectivityState.TRANSIENT_FAILURE) > 0 + this.countChildrenWithState(ConnectivityState.TRANSIENT_FAILURE) > 0 ) { this.updateState( ConnectivityState.TRANSIENT_FAILURE, @@ -180,51 +173,35 @@ export class RoundRobinLoadBalancer implements LoadBalancer { } private resetSubchannelList() { - for (const subchannel of this.subchannels) { - subchannel.removeConnectivityStateListener(this.subchannelStateListener); - subchannel.unref(); - this.channelControlHelper.removeChannelzChild( - subchannel.getChannelzRef() - ); + for (const child of this.children) { + child.destroy(); } - this.subchannels = []; } updateAddressList( - addressList: SubchannelAddress[], + endpointList: Endpoint[], lbConfig: TypedLoadBalancingConfig ): void { this.resetSubchannelList(); - trace( - 'Connect to address list ' + - addressList.map(address => subchannelAddressToString(address)) - ); - this.subchannels = addressList.map(address => - this.channelControlHelper.createSubchannel(address, {}) + trace('Connect to endpoint list ' + endpointList.map(endpointToString)); + this.updatesPaused = true; + this.children = endpointList.map( + endpoint => new LeafLoadBalancer(endpoint, this.childChannelControlHelper) ); - for (const subchannel of this.subchannels) { - subchannel.ref(); - subchannel.addConnectivityStateListener(this.subchannelStateListener); - this.channelControlHelper.addChannelzChild(subchannel.getChannelzRef()); - const subchannelState = subchannel.getConnectivityState(); - if ( - subchannelState === ConnectivityState.IDLE || - subchannelState === ConnectivityState.TRANSIENT_FAILURE - ) { - subchannel.startConnecting(); - } + for (const child of this.children) { + child.startConnecting(); } + this.updatesPaused = false; this.calculateAndUpdateState(); } exitIdle(): void { - for (const subchannel of this.subchannels) { - subchannel.startConnecting(); - } + /* The round_robin LB policy is only in the IDLE state if it has no + * addresses to try to connect to and it has no picked subchannel. + * In that case, there is no meaningful action that can be taken here. */ } resetBackoff(): void { - /* The pick first load balancer does not have a connection backoff, so this - * does nothing */ + // This LB policy has no backoff to reset } destroy(): void { this.resetSubchannelList(); diff --git a/packages/grpc-js/src/load-balancer.ts b/packages/grpc-js/src/load-balancer.ts index d5d69543f..1145fdc90 100644 --- a/packages/grpc-js/src/load-balancer.ts +++ b/packages/grpc-js/src/load-balancer.ts @@ -16,7 +16,7 @@ */ import { ChannelOptions } from './channel-options'; -import { SubchannelAddress } from './subchannel-address'; +import { Endpoint, SubchannelAddress } from './subchannel-address'; import { ConnectivityState } from './connectivity-state'; import { Picker } from './picker'; import { ChannelRef, SubchannelRef } from './channelz'; @@ -95,12 +95,12 @@ export interface LoadBalancer { * The load balancer will start establishing connections with the new list, * but will continue using any existing connections until the new connections * are established - * @param addressList The new list of addresses to connect to + * @param endpointList The new list of addresses to connect to * @param lbConfig The load balancing config object from the service config, * if one was provided */ updateAddressList( - addressList: SubchannelAddress[], + endpointList: Endpoint[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown } ): void; @@ -185,7 +185,9 @@ export function isLoadBalancerNameRegistered(typeName: string): boolean { return typeName in registeredLoadBalancerTypes; } -export function parseLoadBalancingConfig(rawConfig: LoadBalancingConfig): TypedLoadBalancingConfig { +export function parseLoadBalancingConfig( + rawConfig: LoadBalancingConfig +): TypedLoadBalancingConfig { const keys = Object.keys(rawConfig); if (keys.length !== 1) { throw new Error( @@ -210,7 +212,9 @@ export function getDefaultConfig() { if (!defaultLoadBalancerType) { throw new Error('No default load balancer type registered'); } - return new registeredLoadBalancerTypes[defaultLoadBalancerType]!.LoadBalancingConfig(); + return new registeredLoadBalancerTypes[ + defaultLoadBalancerType + ]!.LoadBalancingConfig(); } export function selectLbConfigFromList( @@ -221,7 +225,11 @@ export function selectLbConfigFromList( try { return parseLoadBalancingConfig(config); } catch (e) { - log(LogVerbosity.DEBUG, 'Config parsing failed with error', (e as Error).message); + log( + LogVerbosity.DEBUG, + 'Config parsing failed with error', + (e as Error).message + ); continue; } } diff --git a/packages/grpc-js/src/picker.ts b/packages/grpc-js/src/picker.ts index d95eca21b..6474269f7 100644 --- a/packages/grpc-js/src/picker.ts +++ b/packages/grpc-js/src/picker.ts @@ -97,16 +97,13 @@ export interface Picker { */ export class UnavailablePicker implements Picker { private status: StatusObject; - constructor(status?: StatusObject) { - if (status !== undefined) { - this.status = status; - } else { - this.status = { - code: Status.UNAVAILABLE, - details: 'No connection established', - metadata: new Metadata(), - }; - } + constructor(status?: Partial) { + this.status = { + code: Status.UNAVAILABLE, + details: 'No connection established', + metadata: new Metadata(), + ...status, + }; } pick(pickArgs: PickArgs): TransientFailurePickResult { return { diff --git a/packages/grpc-js/src/resolver-dns.ts b/packages/grpc-js/src/resolver-dns.ts index c40cb8ec5..0956b460c 100644 --- a/packages/grpc-js/src/resolver-dns.ts +++ b/packages/grpc-js/src/resolver-dns.ts @@ -28,7 +28,7 @@ import { StatusObject } from './call-interface'; import { Metadata } from './metadata'; import * as logging from './logging'; import { LogVerbosity } from './constants'; -import { SubchannelAddress, TcpSubchannelAddress } from './subchannel-address'; +import { Endpoint, TcpSubchannelAddress } from './subchannel-address'; import { GrpcUri, uriToString, splitHostPort } from './uri-parser'; import { isIPv6, isIPv4 } from 'net'; import { ChannelOptions } from './channel-options'; @@ -50,35 +50,11 @@ const DEFAULT_MIN_TIME_BETWEEN_RESOLUTIONS_MS = 30_000; const resolveTxtPromise = util.promisify(dns.resolveTxt); const dnsLookupPromise = util.promisify(dns.lookup); -/** - * Merge any number of arrays into a single alternating array - * @param arrays - */ -function mergeArrays(...arrays: T[][]): T[] { - const result: T[] = []; - for ( - let i = 0; - i < - Math.max.apply( - null, - arrays.map(array => array.length) - ); - i++ - ) { - for (const array of arrays) { - if (i < array.length) { - result.push(array[i]); - } - } - } - return result; -} - /** * Resolver implementation that handles DNS names and IP addresses. */ class DnsResolver implements Resolver { - private readonly ipResult: SubchannelAddress[] | null; + private readonly ipResult: Endpoint[] | null; private readonly dnsHostname: string | null; private readonly port: number | null; /** @@ -89,7 +65,7 @@ class DnsResolver implements Resolver { private readonly minTimeBetweenResolutionsMs: number; private pendingLookupPromise: Promise | null = null; private pendingTxtPromise: Promise | null = null; - private latestLookupResult: TcpSubchannelAddress[] | null = null; + private latestLookupResult: Endpoint[] | null = null; private latestServiceConfig: ServiceConfig | null = null; private latestServiceConfigError: StatusObject | null = null; private percentage: number; @@ -114,8 +90,12 @@ class DnsResolver implements Resolver { if (isIPv4(hostPort.host) || isIPv6(hostPort.host)) { this.ipResult = [ { - host: hostPort.host, - port: hostPort.port ?? DEFAULT_PORT, + addresses: [ + { + host: hostPort.host, + port: hostPort.port ?? DEFAULT_PORT, + }, + ], }, ]; this.dnsHostname = null; @@ -213,18 +193,15 @@ class DnsResolver implements Resolver { this.pendingLookupPromise = null; this.backoff.reset(); this.backoff.stop(); - const ip4Addresses: dns.LookupAddress[] = addressList.filter( - addr => addr.family === 4 - ); - const ip6Addresses: dns.LookupAddress[] = addressList.filter( - addr => addr.family === 6 - ); - this.latestLookupResult = mergeArrays(ip6Addresses, ip4Addresses).map( + const subchannelAddresses: TcpSubchannelAddress[] = addressList.map( addr => ({ host: addr.address, port: +this.port! }) ); + this.latestLookupResult = subchannelAddresses.map(address => ({ + addresses: [address], + })); const allAddressesString: string = '[' + - this.latestLookupResult + subchannelAddresses .map(addr => addr.host + ':' + addr.port) .join(',') + ']'; diff --git a/packages/grpc-js/src/resolver-ip.ts b/packages/grpc-js/src/resolver-ip.ts index 0704131e1..cda35d3b9 100644 --- a/packages/grpc-js/src/resolver-ip.ts +++ b/packages/grpc-js/src/resolver-ip.ts @@ -20,7 +20,7 @@ import { ChannelOptions } from './channel-options'; import { LogVerbosity, Status } from './constants'; import { Metadata } from './metadata'; import { registerResolver, Resolver, ResolverListener } from './resolver'; -import { SubchannelAddress } from './subchannel-address'; +import { Endpoint, SubchannelAddress } from './subchannel-address'; import { GrpcUri, splitHostPort, uriToString } from './uri-parser'; import * as logging from './logging'; @@ -39,7 +39,7 @@ const IPV6_SCHEME = 'ipv6'; const DEFAULT_PORT = 443; class IpResolver implements Resolver { - private addresses: SubchannelAddress[] = []; + private endpoints: Endpoint[] = []; private error: StatusObject | null = null; constructor( target: GrpcUri, @@ -83,8 +83,8 @@ class IpResolver implements Resolver { port: hostPort.port ?? DEFAULT_PORT, }); } - this.addresses = addresses; - trace('Parsed ' + target.scheme + ' address list ' + this.addresses); + this.endpoints = addresses.map(address => ({ addresses: [address] })); + trace('Parsed ' + target.scheme + ' address list ' + addresses); } updateResolution(): void { process.nextTick(() => { @@ -92,7 +92,7 @@ class IpResolver implements Resolver { this.listener.onError(this.error); } else { this.listener.onSuccessfulResolution( - this.addresses, + this.endpoints, null, null, null, diff --git a/packages/grpc-js/src/resolver-uds.ts b/packages/grpc-js/src/resolver-uds.ts index 24095ec29..4fa1944b1 100644 --- a/packages/grpc-js/src/resolver-uds.ts +++ b/packages/grpc-js/src/resolver-uds.ts @@ -15,12 +15,12 @@ */ import { Resolver, ResolverListener, registerResolver } from './resolver'; -import { SubchannelAddress } from './subchannel-address'; +import { Endpoint } from './subchannel-address'; import { GrpcUri } from './uri-parser'; import { ChannelOptions } from './channel-options'; class UdsResolver implements Resolver { - private addresses: SubchannelAddress[] = []; + private endpoints: Endpoint[] = []; constructor( target: GrpcUri, private listener: ResolverListener, @@ -32,12 +32,12 @@ class UdsResolver implements Resolver { } else { path = target.path; } - this.addresses = [{ path }]; + this.endpoints = [{ addresses: [{ path }] }]; } updateResolution(): void { process.nextTick( this.listener.onSuccessfulResolution, - this.addresses, + this.endpoints, null, null, null, diff --git a/packages/grpc-js/src/resolver.ts b/packages/grpc-js/src/resolver.ts index 435086255..4dfa8d133 100644 --- a/packages/grpc-js/src/resolver.ts +++ b/packages/grpc-js/src/resolver.ts @@ -17,7 +17,7 @@ import { MethodConfig, ServiceConfig } from './service-config'; import { StatusObject } from './call-interface'; -import { SubchannelAddress } from './subchannel-address'; +import { Endpoint } from './subchannel-address'; import { GrpcUri, uriToString } from './uri-parser'; import { ChannelOptions } from './channel-options'; import { Metadata } from './metadata'; @@ -55,7 +55,7 @@ export interface ResolverListener { * service configuration was invalid */ onSuccessfulResolution( - addressList: SubchannelAddress[], + addressList: Endpoint[], serviceConfig: ServiceConfig | null, serviceConfigError: StatusObject | null, configSelector: ConfigSelector | null, diff --git a/packages/grpc-js/src/resolving-load-balancer.ts b/packages/grpc-js/src/resolving-load-balancer.ts index e2b2c1fa5..22808dc32 100644 --- a/packages/grpc-js/src/resolving-load-balancer.ts +++ b/packages/grpc-js/src/resolving-load-balancer.ts @@ -32,7 +32,7 @@ import { StatusObject } from './call-interface'; import { Metadata } from './metadata'; import * as logging from './logging'; import { LogVerbosity } from './constants'; -import { SubchannelAddress } from './subchannel-address'; +import { Endpoint } from './subchannel-address'; import { GrpcUri, uriToString } from './uri-parser'; import { ChildLoadBalancerHandler } from './load-balancer-child-handler'; import { ChannelOptions } from './channel-options'; @@ -177,7 +177,7 @@ export class ResolvingLoadBalancer implements LoadBalancer { target, { onSuccessfulResolution: ( - addressList: SubchannelAddress[], + endpointList: Endpoint[], serviceConfig: ServiceConfig | null, serviceConfigError: ServiceError | null, configSelector: ConfigSelector | null, @@ -226,7 +226,7 @@ export class ResolvingLoadBalancer implements LoadBalancer { return; } this.childLoadBalancer.updateAddressList( - addressList, + endpointList, loadBalancingConfig, attributes ); @@ -307,7 +307,7 @@ export class ResolvingLoadBalancer implements LoadBalancer { } updateAddressList( - addressList: SubchannelAddress[], + endpointList: Endpoint[], lbConfig: TypedLoadBalancingConfig | null ): never { throw new Error('updateAddressList not supported on ResolvingLoadBalancer'); diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index c9308ca62..4099f1350 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -631,12 +631,15 @@ export class Server { const resolverListener: ResolverListener = { onSuccessfulResolution: ( - addressList, + endpointList, serviceConfig, serviceConfigError ) => { // We only want one resolution result. Discard all future results resolverListener.onSuccessfulResolution = () => {}; + const addressList = ([] as SubchannelAddress[]).concat( + ...endpointList.map(endpoint => endpoint.addresses) + ); if (addressList.length === 0) { deferredCallback( new Error(`No addresses resolved for port ${port}`), diff --git a/packages/grpc-js/src/subchannel-address.ts b/packages/grpc-js/src/subchannel-address.ts index 1ab88f45d..36fd99ea6 100644 --- a/packages/grpc-js/src/subchannel-address.ts +++ b/packages/grpc-js/src/subchannel-address.ts @@ -86,3 +86,39 @@ export function stringToSubchannelAddress( }; } } + +export interface Endpoint { + addresses: SubchannelAddress[]; +} + +export function endpointEqual(endpoint1: Endpoint, endpoint2: Endpoint) { + if (endpoint1.addresses.length !== endpoint2.addresses.length) { + return false; + } + for (let i = 0; i < endpoint1.addresses.length; i++) { + if ( + !subchannelAddressEqual(endpoint1.addresses[i], endpoint2.addresses[i]) + ) { + return false; + } + } + return true; +} + +export function endpointToString(endpoint: Endpoint): string { + return ( + '[' + endpoint.addresses.map(subchannelAddressToString).join(', ') + ']' + ); +} + +export function endpointHasAddress( + endpoint: Endpoint, + expectedAddress: SubchannelAddress +): boolean { + for (const address of endpoint.addresses) { + if (subchannelAddressEqual(address, expectedAddress)) { + return true; + } + } + return false; +} diff --git a/packages/grpc-js/src/subchannel-interface.ts b/packages/grpc-js/src/subchannel-interface.ts index 9b947ad32..c7fdca4fd 100644 --- a/packages/grpc-js/src/subchannel-interface.ts +++ b/packages/grpc-js/src/subchannel-interface.ts @@ -26,6 +26,8 @@ export type ConnectivityStateListener = ( keepaliveTime: number ) => void; +export type HealthListener = (healthy: boolean) => void; + /** * This is an interface for load balancing policies to use to interact with * subchannels. This allows load balancing policies to wrap and unwrap @@ -45,6 +47,9 @@ export interface SubchannelInterface { ref(): void; unref(): void; getChannelzRef(): SubchannelRef; + isHealthy(): boolean; + addHealthStateWatcher(listener: HealthListener): void; + removeHealthStateWatcher(listener: HealthListener): void; /** * If this is a wrapper, return the wrapped subchannel, otherwise return this */ @@ -58,7 +63,23 @@ export interface SubchannelInterface { } export abstract class BaseSubchannelWrapper implements SubchannelInterface { - constructor(protected child: SubchannelInterface) {} + private healthy = true; + private healthListeners: Set = new Set(); + constructor(protected child: SubchannelInterface) { + child.addHealthStateWatcher(childHealthy => { + /* A change to the child health state only affects this wrapper's overall + * health state if this wrapper is reporting healthy. */ + if (this.healthy) { + this.updateHealthListeners(); + } + }); + } + + private updateHealthListeners(): void { + for (const listener of this.healthListeners) { + listener(this.isHealthy()); + } + } getConnectivityState(): ConnectivityState { return this.child.getConnectivityState(); @@ -87,6 +108,25 @@ export abstract class BaseSubchannelWrapper implements SubchannelInterface { getChannelzRef(): SubchannelRef { return this.child.getChannelzRef(); } + isHealthy(): boolean { + return this.healthy && this.child.isHealthy(); + } + addHealthStateWatcher(listener: HealthListener): void { + this.healthListeners.add(listener); + } + removeHealthStateWatcher(listener: HealthListener): void { + this.healthListeners.delete(listener); + } + protected setHealthy(healthy: boolean): void { + if (healthy !== this.healthy) { + this.healthy = healthy; + /* A change to this wrapper's health state only affects the overall + * reported health state if the child is healthy. */ + if (this.child.isHealthy()) { + this.updateHealthListeners(); + } + } + } getRealSubchannel(): Subchannel { return this.child.getRealSubchannel(); } diff --git a/packages/grpc-js/src/subchannel.ts b/packages/grpc-js/src/subchannel.ts index 6fad9500a..cf49ced77 100644 --- a/packages/grpc-js/src/subchannel.ts +++ b/packages/grpc-js/src/subchannel.ts @@ -461,6 +461,18 @@ export class Subchannel { return this.channelzRef; } + isHealthy(): boolean { + return true; + } + + addHealthStateWatcher(listener: (healthy: boolean) => void): void { + // Do nothing with the listener + } + + removeHealthStateWatcher(listener: (healthy: boolean) => void): void { + // Do nothing with the listener + } + getRealSubchannel(): this { return this; } diff --git a/packages/grpc-js/test/common.ts b/packages/grpc-js/test/common.ts index d15a9d5ed..20352fb2f 100644 --- a/packages/grpc-js/test/common.ts +++ b/packages/grpc-js/test/common.ts @@ -27,7 +27,10 @@ import { loadPackageDefinition, } from '../src/make-client'; import { readFileSync } from 'fs'; -import { SubchannelInterface } from '../src/subchannel-interface'; +import { + HealthListener, + SubchannelInterface, +} from '../src/subchannel-interface'; import { SubchannelRef } from '../src/channelz'; import { Subchannel } from '../src/subchannel'; import { ConnectivityState } from '../src/connectivity-state'; @@ -198,6 +201,11 @@ export class MockSubchannel implements SubchannelInterface { realSubchannelEquals(other: grpc.experimental.SubchannelInterface): boolean { return this === other; } + isHealthy(): boolean { + return true; + } + addHealthStateWatcher(listener: HealthListener): void {} + removeHealthStateWatcher(listener: HealthListener): void {} } export { assert2 }; diff --git a/packages/grpc-js/test/test-pick-first.ts b/packages/grpc-js/test/test-pick-first.ts index e9e9e5601..7e862de58 100644 --- a/packages/grpc-js/test/test-pick-first.ts +++ b/packages/grpc-js/test/test-pick-first.ts @@ -29,10 +29,7 @@ import { } from '../src/load-balancer-pick-first'; import { Metadata } from '../src/metadata'; import { Picker } from '../src/picker'; -import { - SubchannelAddress, - subchannelAddressToString, -} from '../src/subchannel-address'; +import { Endpoint, subchannelAddressToString } from '../src/subchannel-address'; import { MockSubchannel, TestClient, TestServer } from './common'; function updateStateCallBackForExpectedStateSequence( @@ -120,7 +117,10 @@ describe('pick_first load balancing policy', () => { } ); const pickFirst = new PickFirstLoadBalancer(channelControlHelper); - pickFirst.updateAddressList([{ host: 'localhost', port: 1 }], config); + pickFirst.updateAddressList( + [{ addresses: [{ host: 'localhost', port: 1 }] }], + config + ); process.nextTick(() => { subchannels[0].transitionToState(ConnectivityState.READY); }); @@ -144,7 +144,10 @@ describe('pick_first load balancing policy', () => { } ); const pickFirst = new PickFirstLoadBalancer(channelControlHelper); - pickFirst.updateAddressList([{ host: 'localhost', port: 1 }], config); + pickFirst.updateAddressList( + [{ addresses: [{ host: 'localhost', port: 1 }] }], + config + ); }); it('Should stay CONNECTING if only some subchannels fail to connect', done => { const channelControlHelper = createChildChannelControlHelper( @@ -159,8 +162,8 @@ describe('pick_first load balancing policy', () => { const pickFirst = new PickFirstLoadBalancer(channelControlHelper); pickFirst.updateAddressList( [ - { host: 'localhost', port: 1 }, - { host: 'localhost', port: 2 }, + { addresses: [{ host: 'localhost', port: 1 }] }, + { addresses: [{ host: 'localhost', port: 2 }] }, ], config ); @@ -181,8 +184,8 @@ describe('pick_first load balancing policy', () => { const pickFirst = new PickFirstLoadBalancer(channelControlHelper); pickFirst.updateAddressList( [ - { host: 'localhost', port: 1 }, - { host: 'localhost', port: 2 }, + { addresses: [{ host: 'localhost', port: 1 }] }, + { addresses: [{ host: 'localhost', port: 2 }] }, ], config ); @@ -206,8 +209,8 @@ describe('pick_first load balancing policy', () => { const pickFirst = new PickFirstLoadBalancer(channelControlHelper); pickFirst.updateAddressList( [ - { host: 'localhost', port: 1 }, - { host: 'localhost', port: 2 }, + { addresses: [{ host: 'localhost', port: 1 }] }, + { addresses: [{ host: 'localhost', port: 2 }] }, ], config ); @@ -245,8 +248,8 @@ describe('pick_first load balancing policy', () => { const pickFirst = new PickFirstLoadBalancer(channelControlHelper); pickFirst.updateAddressList( [ - { host: 'localhost', port: 1 }, - { host: 'localhost', port: 2 }, + { addresses: [{ host: 'localhost', port: 1 }] }, + { addresses: [{ host: 'localhost', port: 2 }] }, ], config ); @@ -272,8 +275,8 @@ describe('pick_first load balancing policy', () => { const pickFirst = new PickFirstLoadBalancer(channelControlHelper); pickFirst.updateAddressList( [ - { host: 'localhost', port: 1 }, - { host: 'localhost', port: 2 }, + { addresses: [{ host: 'localhost', port: 1 }] }, + { addresses: [{ host: 'localhost', port: 2 }] }, ], config ); @@ -303,8 +306,8 @@ describe('pick_first load balancing policy', () => { const pickFirst = new PickFirstLoadBalancer(channelControlHelper); pickFirst.updateAddressList( [ - { host: 'localhost', port: 1 }, - { host: 'localhost', port: 2 }, + { addresses: [{ host: 'localhost', port: 1 }] }, + { addresses: [{ host: 'localhost', port: 2 }] }, ], config ); @@ -312,8 +315,8 @@ describe('pick_first load balancing policy', () => { currentStartState = ConnectivityState.CONNECTING; pickFirst.updateAddressList( [ - { host: 'localhost', port: 3 }, - { host: 'localhost', port: 4 }, + { addresses: [{ host: 'localhost', port: 1 }] }, + { addresses: [{ host: 'localhost', port: 2 }] }, ], config ); @@ -341,14 +344,17 @@ describe('pick_first load balancing policy', () => { const pickFirst = new PickFirstLoadBalancer(channelControlHelper); pickFirst.updateAddressList( [ - { host: 'localhost', port: 1 }, - { host: 'localhost', port: 2 }, + { addresses: [{ host: 'localhost', port: 1 }] }, + { addresses: [{ host: 'localhost', port: 2 }] }, ], config ); process.nextTick(() => { currentStartState = ConnectivityState.READY; - pickFirst.updateAddressList([{ host: 'localhost', port: 3 }], config); + pickFirst.updateAddressList( + [{ addresses: [{ host: 'localhost', port: 3 }] }], + config + ); }); }); it('Should transition from READY to IDLE if the connected subchannel disconnects', done => { @@ -371,7 +377,10 @@ describe('pick_first load balancing policy', () => { } ); const pickFirst = new PickFirstLoadBalancer(channelControlHelper); - pickFirst.updateAddressList([{ host: 'localhost', port: 1 }], config); + pickFirst.updateAddressList( + [{ addresses: [{ host: 'localhost', port: 1 }] }], + config + ); process.nextTick(() => { subchannels[0].transitionToState(ConnectivityState.IDLE); }); @@ -396,10 +405,16 @@ describe('pick_first load balancing policy', () => { } ); const pickFirst = new PickFirstLoadBalancer(channelControlHelper); - pickFirst.updateAddressList([{ host: 'localhost', port: 1 }], config); + pickFirst.updateAddressList( + [{ addresses: [{ host: 'localhost', port: 1 }] }], + config + ); process.nextTick(() => { currentStartState = ConnectivityState.IDLE; - pickFirst.updateAddressList([{ host: 'localhost', port: 2 }], config); + pickFirst.updateAddressList( + [{ addresses: [{ host: 'localhost', port: 2 }] }], + config + ); process.nextTick(() => { subchannels[0].transitionToState(ConnectivityState.IDLE); }); @@ -425,10 +440,16 @@ describe('pick_first load balancing policy', () => { } ); const pickFirst = new PickFirstLoadBalancer(channelControlHelper); - pickFirst.updateAddressList([{ host: 'localhost', port: 1 }], config); + pickFirst.updateAddressList( + [{ addresses: [{ host: 'localhost', port: 1 }] }], + config + ); process.nextTick(() => { currentStartState = ConnectivityState.TRANSIENT_FAILURE; - pickFirst.updateAddressList([{ host: 'localhost', port: 2 }], config); + pickFirst.updateAddressList( + [{ addresses: [{ host: 'localhost', port: 2 }] }], + config + ); process.nextTick(() => { subchannels[0].transitionToState(ConnectivityState.IDLE); }); @@ -454,9 +475,15 @@ describe('pick_first load balancing policy', () => { } ); const pickFirst = new PickFirstLoadBalancer(channelControlHelper); - pickFirst.updateAddressList([{ host: 'localhost', port: 1 }], config); + pickFirst.updateAddressList( + [{ addresses: [{ host: 'localhost', port: 1 }] }], + config + ); process.nextTick(() => { - pickFirst.updateAddressList([{ host: 'localhost', port: 2 }], config); + pickFirst.updateAddressList( + [{ addresses: [{ host: 'localhost', port: 2 }] }], + config + ); process.nextTick(() => { subchannels[0].transitionToState(ConnectivityState.IDLE); }); @@ -490,24 +517,24 @@ describe('pick_first load balancing policy', () => { }, } ); - const addresses: SubchannelAddress[] = []; + const endpoints: Endpoint[] = []; for (let i = 0; i < 10; i++) { - addresses.push({ host: 'localhost', port: i + 1 }); + endpoints.push({ addresses: [{ host: 'localhost', port: i + 1 }] }); } const pickFirst = new PickFirstLoadBalancer(channelControlHelper); /* Pick from 10 subchannels 5 times, with address randomization enabled, * and verify that at least two different subchannels are picked. The * probability choosing the same address every time is 1/10,000, which * I am considering an acceptable flake rate */ - pickFirst.updateAddressList(addresses, shuffleConfig); + pickFirst.updateAddressList(endpoints, shuffleConfig); process.nextTick(() => { - pickFirst.updateAddressList(addresses, shuffleConfig); + pickFirst.updateAddressList(endpoints, shuffleConfig); process.nextTick(() => { - pickFirst.updateAddressList(addresses, shuffleConfig); + pickFirst.updateAddressList(endpoints, shuffleConfig); process.nextTick(() => { - pickFirst.updateAddressList(addresses, shuffleConfig); + pickFirst.updateAddressList(endpoints, shuffleConfig); process.nextTick(() => { - pickFirst.updateAddressList(addresses, shuffleConfig); + pickFirst.updateAddressList(endpoints, shuffleConfig); process.nextTick(() => { assert(pickedSubchannels.size > 1); done(); @@ -546,20 +573,20 @@ describe('pick_first load balancing policy', () => { }, } ); - const addresses: SubchannelAddress[] = []; + const endpoints: Endpoint[] = []; for (let i = 0; i < 10; i++) { - addresses.push({ host: 'localhost', port: i + 1 }); + endpoints.push({ addresses: [{ host: 'localhost', port: i + 1 }] }); } const pickFirst = new PickFirstLoadBalancer(channelControlHelper); - pickFirst.updateAddressList(addresses, config); + pickFirst.updateAddressList(endpoints, config); process.nextTick(() => { - pickFirst.updateAddressList(addresses, config); + pickFirst.updateAddressList(endpoints, config); process.nextTick(() => { - pickFirst.updateAddressList(addresses, config); + pickFirst.updateAddressList(endpoints, config); process.nextTick(() => { - pickFirst.updateAddressList(addresses, config); + pickFirst.updateAddressList(endpoints, config); process.nextTick(() => { - pickFirst.updateAddressList(addresses, config); + pickFirst.updateAddressList(endpoints, config); process.nextTick(() => { assert(pickedSubchannels.size === 1); done(); diff --git a/packages/grpc-js/test/test-resolver.ts b/packages/grpc-js/test/test-resolver.ts index 98d74823b..c88367285 100644 --- a/packages/grpc-js/test/test-resolver.ts +++ b/packages/grpc-js/test/test-resolver.ts @@ -25,12 +25,27 @@ import * as resolver_ip from '../src/resolver-ip'; import { ServiceConfig } from '../src/service-config'; import { StatusObject } from '../src/call-interface'; import { + Endpoint, SubchannelAddress, - isTcpSubchannelAddress, - subchannelAddressToString, + endpointToString, + subchannelAddressEqual, } from '../src/subchannel-address'; import { parseUri, GrpcUri } from '../src/uri-parser'; +function hasMatchingAddress( + endpointList: Endpoint[], + expectedAddress: SubchannelAddress +): boolean { + for (const endpoint of endpointList) { + for (const address of endpoint.addresses) { + if (subchannelAddressEqual(address, expectedAddress)) { + return true; + } + } + } + return false; +} + describe('Name Resolver', () => { before(() => { resolver_dns.setup(); @@ -46,27 +61,17 @@ describe('Name Resolver', () => { )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( - addressList: SubchannelAddress[], + endpointList: Endpoint[], serviceConfig: ServiceConfig | null, serviceConfigError: StatusObject | null ) => { // Only handle the first resolution result listener.onSuccessfulResolution = () => {}; assert( - addressList.some( - addr => - isTcpSubchannelAddress(addr) && - addr.host === '127.0.0.1' && - addr.port === 50051 - ) + hasMatchingAddress(endpointList, { host: '127.0.0.1', port: 50051 }) ); assert( - addressList.some( - addr => - isTcpSubchannelAddress(addr) && - addr.host === '::1' && - addr.port === 50051 - ) + hasMatchingAddress(endpointList, { host: '::1', port: 50051 }) ); done(); }, @@ -83,28 +88,16 @@ describe('Name Resolver', () => { )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( - addressList: SubchannelAddress[], + endpointList: Endpoint[], serviceConfig: ServiceConfig | null, serviceConfigError: StatusObject | null ) => { // Only handle the first resolution result listener.onSuccessfulResolution = () => {}; assert( - addressList.some( - addr => - isTcpSubchannelAddress(addr) && - addr.host === '127.0.0.1' && - addr.port === 443 - ) - ); - assert( - addressList.some( - addr => - isTcpSubchannelAddress(addr) && - addr.host === '::1' && - addr.port === 443 - ) + hasMatchingAddress(endpointList, { host: '127.0.0.1', port: 443 }) ); + assert(hasMatchingAddress(endpointList, { host: '::1', port: 443 })); done(); }, onError: (error: StatusObject) => { @@ -118,19 +111,14 @@ describe('Name Resolver', () => { const target = resolverManager.mapUriDefaultScheme(parseUri('1.2.3.4')!)!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( - addressList: SubchannelAddress[], + endpointList: Endpoint[], serviceConfig: ServiceConfig | null, serviceConfigError: StatusObject | null ) => { // Only handle the first resolution result listener.onSuccessfulResolution = () => {}; assert( - addressList.some( - addr => - isTcpSubchannelAddress(addr) && - addr.host === '1.2.3.4' && - addr.port === 443 - ) + hasMatchingAddress(endpointList, { host: '1.2.3.4', port: 443 }) ); done(); }, @@ -145,20 +133,13 @@ describe('Name Resolver', () => { const target = resolverManager.mapUriDefaultScheme(parseUri('::1')!)!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( - addressList: SubchannelAddress[], + endpointList: Endpoint[], serviceConfig: ServiceConfig | null, serviceConfigError: StatusObject | null ) => { // Only handle the first resolution result listener.onSuccessfulResolution = () => {}; - assert( - addressList.some( - addr => - isTcpSubchannelAddress(addr) && - addr.host === '::1' && - addr.port === 443 - ) - ); + assert(hasMatchingAddress(endpointList, { host: '::1', port: 443 })); done(); }, onError: (error: StatusObject) => { @@ -174,19 +155,14 @@ describe('Name Resolver', () => { )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( - addressList: SubchannelAddress[], + endpointList: Endpoint[], serviceConfig: ServiceConfig | null, serviceConfigError: StatusObject | null ) => { // Only handle the first resolution result listener.onSuccessfulResolution = () => {}; assert( - addressList.some( - addr => - isTcpSubchannelAddress(addr) && - addr.host === '::1' && - addr.port === 50051 - ) + hasMatchingAddress(endpointList, { host: '::1', port: 50051 }) ); done(); }, @@ -203,13 +179,13 @@ describe('Name Resolver', () => { )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( - addressList: SubchannelAddress[], + endpointList: Endpoint[], serviceConfig: ServiceConfig | null, serviceConfigError: StatusObject | null ) => { // Only handle the first resolution result listener.onSuccessfulResolution = () => {}; - assert(addressList.length > 0); + assert(endpointList.length > 0); done(); }, onError: (error: StatusObject) => { @@ -227,7 +203,7 @@ describe('Name Resolver', () => { )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( - addressList: SubchannelAddress[], + endpointList: Endpoint[], serviceConfig: ServiceConfig | null, serviceConfigError: StatusObject | null ) => { @@ -253,7 +229,7 @@ describe('Name Resolver', () => { let count = 0; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( - addressList: SubchannelAddress[], + endpointList: Endpoint[], serviceConfig: ServiceConfig | null, serviceConfigError: StatusObject | null ) => { @@ -290,21 +266,16 @@ describe('Name Resolver', () => { )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( - addressList: SubchannelAddress[], + endpointList: Endpoint[], serviceConfig: ServiceConfig | null, serviceConfigError: StatusObject | null ) => { // Only handle the first resolution result listener.onSuccessfulResolution = () => {}; assert( - addressList.some( - addr => - isTcpSubchannelAddress(addr) && - addr.host === '127.0.0.1' && - addr.port === 443 - ), - `None of [${addressList.map(addr => - subchannelAddressToString(addr) + hasMatchingAddress(endpointList, { host: '127.0.0.1', port: 443 }), + `None of [${endpointList.map(addr => + endpointToString(addr) )}] matched '127.0.0.1:443'` ); done(); @@ -324,20 +295,13 @@ describe('Name Resolver', () => { )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( - addressList: SubchannelAddress[], + endpointList: Endpoint[], serviceConfig: ServiceConfig | null, serviceConfigError: StatusObject | null ) => { // Only handle the first resolution result listener.onSuccessfulResolution = () => {}; - assert( - addressList.some( - addr => - isTcpSubchannelAddress(addr) && - addr.host === '::1' && - addr.port === 443 - ) - ); + assert(hasMatchingAddress(endpointList, { host: '::1', port: 443 })); done(); }, onError: (error: StatusObject) => { @@ -356,21 +320,16 @@ describe('Name Resolver', () => { )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( - addressList: SubchannelAddress[], + endpointList: Endpoint[], serviceConfig: ServiceConfig | null, serviceConfigError: StatusObject | null ) => { // Only handle the first resolution result listener.onSuccessfulResolution = () => {}; assert( - addressList.some( - addr => - isTcpSubchannelAddress(addr) && - addr.host === '127.0.0.1' && - addr.port === 443 - ), - `None of [${addressList.map(addr => - subchannelAddressToString(addr) + hasMatchingAddress(endpointList, { host: '127.0.0.1', port: 443 }), + `None of [${endpointList.map(addr => + endpointToString(addr) )}] matched '127.0.0.1:443'` ); /* TODO(murgatroid99): check for IPv6 result, once we can get that @@ -392,13 +351,13 @@ describe('Name Resolver', () => { )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( - addressList: SubchannelAddress[], + endpointList: Endpoint[], serviceConfig: ServiceConfig | null, serviceConfigError: StatusObject | null ) => { // Only handle the first resolution result listener.onSuccessfulResolution = () => {}; - assert(addressList.length > 0); + assert(endpointList.length > 0); done(); }, onError: (error: StatusObject) => { @@ -422,11 +381,11 @@ describe('Name Resolver', () => { )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( - addressList: SubchannelAddress[], + endpointList: Endpoint[], serviceConfig: ServiceConfig | null, serviceConfigError: StatusObject | null ) => { - assert(addressList.length > 0); + assert(endpointList.length > 0); completeCount += 1; if (completeCount === 2) { // Only handle the first resolution result @@ -452,25 +411,15 @@ describe('Name Resolver', () => { target, { onSuccessfulResolution: ( - addressList: SubchannelAddress[], + endpointList: Endpoint[], serviceConfig: ServiceConfig | null, serviceConfigError: StatusObject | null ) => { assert( - addressList.some( - addr => - isTcpSubchannelAddress(addr) && - addr.host === '127.0.0.1' && - addr.port === 443 - ) + hasMatchingAddress(endpointList, { host: '127.0.0.1', port: 443 }) ); assert( - addressList.some( - addr => - isTcpSubchannelAddress(addr) && - addr.host === '::1' && - addr.port === 443 - ) + hasMatchingAddress(endpointList, { host: '::1', port: 443 }) ); resultCount += 1; if (resultCount === 1) { @@ -498,7 +447,7 @@ describe('Name Resolver', () => { target, { onSuccessfulResolution: ( - addressList: SubchannelAddress[], + endpointList: Endpoint[], serviceConfig: ServiceConfig | null, serviceConfigError: StatusObject | null ) => { @@ -527,17 +476,13 @@ describe('Name Resolver', () => { )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( - addressList: SubchannelAddress[], + endpointList: Endpoint[], serviceConfig: ServiceConfig | null, serviceConfigError: StatusObject | null ) => { // Only handle the first resolution result listener.onSuccessfulResolution = () => {}; - assert( - addressList.some( - addr => !isTcpSubchannelAddress(addr) && addr.path === 'socket' - ) - ); + assert(hasMatchingAddress(endpointList, { path: 'socket' })); done(); }, onError: (error: StatusObject) => { @@ -553,18 +498,13 @@ describe('Name Resolver', () => { )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( - addressList: SubchannelAddress[], + endpointList: Endpoint[], serviceConfig: ServiceConfig | null, serviceConfigError: StatusObject | null ) => { // Only handle the first resolution result listener.onSuccessfulResolution = () => {}; - assert( - addressList.some( - addr => - !isTcpSubchannelAddress(addr) && addr.path === '/tmp/socket' - ) - ); + assert(hasMatchingAddress(endpointList, { path: '/tmp/socket' })); done(); }, onError: (error: StatusObject) => { @@ -582,19 +522,14 @@ describe('Name Resolver', () => { )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( - addressList: SubchannelAddress[], + endpointList: Endpoint[], serviceConfig: ServiceConfig | null, serviceConfigError: StatusObject | null ) => { // Only handle the first resolution result listener.onSuccessfulResolution = () => {}; assert( - addressList.some( - addr => - isTcpSubchannelAddress(addr) && - addr.host === '127.0.0.1' && - addr.port === 443 - ) + hasMatchingAddress(endpointList, { host: '127.0.0.1', port: 443 }) ); done(); }, @@ -611,19 +546,14 @@ describe('Name Resolver', () => { )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( - addressList: SubchannelAddress[], + endpointList: Endpoint[], serviceConfig: ServiceConfig | null, serviceConfigError: StatusObject | null ) => { // Only handle the first resolution result listener.onSuccessfulResolution = () => {}; assert( - addressList.some( - addr => - isTcpSubchannelAddress(addr) && - addr.host === '127.0.0.1' && - addr.port === 50051 - ) + hasMatchingAddress(endpointList, { host: '127.0.0.1', port: 50051 }) ); done(); }, @@ -640,27 +570,17 @@ describe('Name Resolver', () => { )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( - addressList: SubchannelAddress[], + endpointList: Endpoint[], serviceConfig: ServiceConfig | null, serviceConfigError: StatusObject | null ) => { // Only handle the first resolution result listener.onSuccessfulResolution = () => {}; assert( - addressList.some( - addr => - isTcpSubchannelAddress(addr) && - addr.host === '127.0.0.1' && - addr.port === 50051 - ) + hasMatchingAddress(endpointList, { host: '127.0.0.1', port: 50051 }) ); assert( - addressList.some( - addr => - isTcpSubchannelAddress(addr) && - addr.host === '127.0.0.1' && - addr.port === 50052 - ) + hasMatchingAddress(endpointList, { host: '127.0.0.1', port: 50052 }) ); done(); }, @@ -677,20 +597,13 @@ describe('Name Resolver', () => { )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( - addressList: SubchannelAddress[], + endpointList: Endpoint[], serviceConfig: ServiceConfig | null, serviceConfigError: StatusObject | null ) => { // Only handle the first resolution result listener.onSuccessfulResolution = () => {}; - assert( - addressList.some( - addr => - isTcpSubchannelAddress(addr) && - addr.host === '::1' && - addr.port === 443 - ) - ); + assert(hasMatchingAddress(endpointList, { host: '::1', port: 443 })); done(); }, onError: (error: StatusObject) => { @@ -706,19 +619,14 @@ describe('Name Resolver', () => { )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( - addressList: SubchannelAddress[], + endpointList: Endpoint[], serviceConfig: ServiceConfig | null, serviceConfigError: StatusObject | null ) => { // Only handle the first resolution result listener.onSuccessfulResolution = () => {}; assert( - addressList.some( - addr => - isTcpSubchannelAddress(addr) && - addr.host === '::1' && - addr.port === 50051 - ) + hasMatchingAddress(endpointList, { host: '::1', port: 50051 }) ); done(); }, @@ -735,27 +643,17 @@ describe('Name Resolver', () => { )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( - addressList: SubchannelAddress[], + endpointList: Endpoint[], serviceConfig: ServiceConfig | null, serviceConfigError: StatusObject | null ) => { // Only handle the first resolution result listener.onSuccessfulResolution = () => {}; assert( - addressList.some( - addr => - isTcpSubchannelAddress(addr) && - addr.host === '::1' && - addr.port === 50051 - ) + hasMatchingAddress(endpointList, { host: '::1', port: 50051 }) ); assert( - addressList.some( - addr => - isTcpSubchannelAddress(addr) && - addr.host === '::1' && - addr.port === 50052 - ) + hasMatchingAddress(endpointList, { host: '::1', port: 50052 }) ); done(); }, From e919aa7aa375385bba9661496e67f9ca243bf032 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 30 Aug 2023 14:47:06 -0700 Subject: [PATCH 023/109] grpc-js-xds: Update LB policies to handle grpc-js changes --- .../grpc-js-xds/interop/xds-interop-client.ts | 6 +-- packages/grpc-js-xds/src/load-balancer-cds.ts | 11 +---- .../grpc-js-xds/src/load-balancer-priority.ts | 44 +++++++++---------- .../src/load-balancer-weighted-target.ts | 30 ++++++------- .../src/load-balancer-xds-cluster-impl.ts | 27 +++++++++--- .../src/load-balancer-xds-cluster-manager.ts | 12 ++--- .../src/load-balancer-xds-cluster-resolver.ts | 40 +++++++++-------- .../src/load-balancer-xds-wrr-locality.ts | 12 ++--- .../test/test-custom-lb-policies.ts | 6 +-- 9 files changed, 99 insertions(+), 89 deletions(-) diff --git a/packages/grpc-js-xds/interop/xds-interop-client.ts b/packages/grpc-js-xds/interop/xds-interop-client.ts index f9034ed8e..dc70034f9 100644 --- a/packages/grpc-js-xds/interop/xds-interop-client.ts +++ b/packages/grpc-js-xds/interop/xds-interop-client.ts @@ -34,7 +34,7 @@ import TypedLoadBalancingConfig = grpc.experimental.TypedLoadBalancingConfig; import LoadBalancer = grpc.experimental.LoadBalancer; import ChannelControlHelper = grpc.experimental.ChannelControlHelper; import ChildLoadBalancerHandler = grpc.experimental.ChildLoadBalancerHandler; -import SubchannelAddress = grpc.experimental.SubchannelAddress; +import Endpoint = grpc.experimental.Endpoint; import Picker = grpc.experimental.Picker; import PickArgs = grpc.experimental.PickArgs; import PickResult = grpc.experimental.PickResult; @@ -99,12 +99,12 @@ class RpcBehaviorLoadBalancer implements LoadBalancer { }); this.child = new ChildLoadBalancerHandler(childChannelControlHelper); } - updateAddressList(addressList: SubchannelAddress[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { + updateAddressList(endpointList: Endpoint[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { if (!(lbConfig instanceof RpcBehaviorLoadBalancingConfig)) { return; } this.latestConfig = lbConfig; - this.child.updateAddressList(addressList, RPC_BEHAVIOR_CHILD_CONFIG, attributes); + this.child.updateAddressList(endpointList, RPC_BEHAVIOR_CHILD_CONFIG, attributes); } exitIdle(): void { this.child.exitIdle(); diff --git a/packages/grpc-js-xds/src/load-balancer-cds.ts b/packages/grpc-js-xds/src/load-balancer-cds.ts index 647ab2b97..1e4f63b4b 100644 --- a/packages/grpc-js-xds/src/load-balancer-cds.ts +++ b/packages/grpc-js-xds/src/load-balancer-cds.ts @@ -18,23 +18,16 @@ import { connectivityState, status, Metadata, logVerbosity, experimental, LoadBalancingConfig } from '@grpc/grpc-js'; import { getSingletonXdsClient, Watcher, XdsClient } from './xds-client'; import { Cluster__Output } from './generated/envoy/config/cluster/v3/Cluster'; -import SubchannelAddress = experimental.SubchannelAddress; +import Endpoint = experimental.Endpoint; import UnavailablePicker = experimental.UnavailablePicker; import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; import LoadBalancer = experimental.LoadBalancer; import ChannelControlHelper = experimental.ChannelControlHelper; import registerLoadBalancerType = experimental.registerLoadBalancerType; import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; -import SuccessRateEjectionConfig = experimental.SuccessRateEjectionConfig; -import FailurePercentageEjectionConfig = experimental.FailurePercentageEjectionConfig; import QueuePicker = experimental.QueuePicker; -import OutlierDetectionRawConfig = experimental.OutlierDetectionRawConfig; import parseLoadBalancingConfig = experimental.parseLoadBalancingConfig; -import { OutlierDetection__Output } from './generated/envoy/config/cluster/v3/OutlierDetection'; -import { Duration__Output } from './generated/google/protobuf/Duration'; -import { EXPERIMENTAL_OUTLIER_DETECTION } from './environment'; import { DiscoveryMechanism, XdsClusterResolverChildPolicyHandler } from './load-balancer-xds-cluster-resolver'; -import { CLUSTER_CONFIG_TYPE_URL, decodeSingleResource } from './resources'; import { CdsUpdate, ClusterResourceType } from './xds-resource-type/cluster-resource-type'; const TRACER_NAME = 'cds_balancer'; @@ -258,7 +251,7 @@ export class CdsLoadBalancer implements LoadBalancer { } updateAddressList( - addressList: SubchannelAddress[], + endpointList: Endpoint[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown } ): void { diff --git a/packages/grpc-js-xds/src/load-balancer-priority.ts b/packages/grpc-js-xds/src/load-balancer-priority.ts index 4a3e41a11..ba4fd2cfa 100644 --- a/packages/grpc-js-xds/src/load-balancer-priority.ts +++ b/packages/grpc-js-xds/src/load-balancer-priority.ts @@ -19,8 +19,8 @@ import { connectivityState as ConnectivityState, status as Status, Metadata, log import LoadBalancer = experimental.LoadBalancer; import ChannelControlHelper = experimental.ChannelControlHelper; import registerLoadBalancerType = experimental.registerLoadBalancerType; -import SubchannelAddress = experimental.SubchannelAddress; -import subchannelAddressToString = experimental.subchannelAddressToString; +import Endpoint = experimental.Endpoint; +import endpointToString = experimental.endpointToString; import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; import Picker = experimental.Picker; import QueuePicker = experimental.QueuePicker; @@ -40,16 +40,16 @@ const TYPE_NAME = 'priority'; const DEFAULT_FAILOVER_TIME_MS = 10_000; const DEFAULT_RETENTION_INTERVAL_MS = 15 * 60 * 1000; -export type LocalitySubchannelAddress = SubchannelAddress & { +export interface LocalityEndpoint extends Endpoint { localityPath: string[]; locality: Locality__Output; weight: number; }; -export function isLocalitySubchannelAddress( - address: SubchannelAddress -): address is LocalitySubchannelAddress { - return Array.isArray((address as LocalitySubchannelAddress).localityPath); +export function isLocalityEndpoint( + address: Endpoint +): address is LocalityEndpoint { + return Array.isArray((address as LocalityEndpoint).localityPath); } /** @@ -138,7 +138,7 @@ class PriorityLoadBalancingConfig implements TypedLoadBalancingConfig { interface PriorityChildBalancer { updateAddressList( - addressList: SubchannelAddress[], + endpointList: Endpoint[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown } ): void; @@ -154,7 +154,7 @@ interface PriorityChildBalancer { } interface UpdateArgs { - subchannelAddress: SubchannelAddress[]; + subchannelAddress: Endpoint[]; lbConfig: TypedLoadBalancingConfig; ignoreReresolutionRequests: boolean; } @@ -218,11 +218,11 @@ export class PriorityLoadBalancer implements LoadBalancer { } updateAddressList( - addressList: SubchannelAddress[], + endpointList: Endpoint[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown } ): void { - this.childBalancer.updateAddressList(addressList, lbConfig, attributes); + this.childBalancer.updateAddressList(endpointList, lbConfig, attributes); } exitIdle() { @@ -412,7 +412,7 @@ export class PriorityLoadBalancer implements LoadBalancer { } updateAddressList( - addressList: SubchannelAddress[], + endpointList: Endpoint[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown } ): void { @@ -425,23 +425,23 @@ export class PriorityLoadBalancer implements LoadBalancer { * which child it belongs to. So we bucket those addresses by that first * element, and pass along the rest of the localityPath for that child * to use. */ - const childAddressMap: Map = new Map< + const childAddressMap: Map = new Map< string, - LocalitySubchannelAddress[] + LocalityEndpoint[] >(); - for (const address of addressList) { - if (!isLocalitySubchannelAddress(address)) { + for (const endpoint of endpointList) { + if (!isLocalityEndpoint(endpoint)) { // Reject address that cannot be prioritized return; } - if (address.localityPath.length < 1) { + if (endpoint.localityPath.length < 1) { // Reject address that cannot be prioritized return; } - const childName = address.localityPath[0]; - const childAddress: LocalitySubchannelAddress = { - ...address, - localityPath: address.localityPath.slice(1), + const childName = endpoint.localityPath[0]; + const childAddress: LocalityEndpoint = { + ...endpoint, + localityPath: endpoint.localityPath.slice(1), }; let childAddressList = childAddressMap.get(childName); if (childAddressList === undefined) { @@ -458,7 +458,7 @@ export class PriorityLoadBalancer implements LoadBalancer { * update all existing children with their new configs */ for (const [childName, childConfig] of lbConfig.getChildren()) { const childAddresses = childAddressMap.get(childName) ?? []; - trace('Assigning child ' + childName + ' address list ' + childAddresses.map(address => '(' + subchannelAddressToString(address) + ' path=' + address.localityPath + ')')) + trace('Assigning child ' + childName + ' endpoint list ' + childAddresses.map(endpoint => '(' + endpointToString(endpoint) + ' path=' + endpoint.localityPath + ')')) this.latestUpdates.set(childName, { subchannelAddress: childAddresses, lbConfig: childConfig.config, diff --git a/packages/grpc-js-xds/src/load-balancer-weighted-target.ts b/packages/grpc-js-xds/src/load-balancer-weighted-target.ts index 231f3b179..16874e01b 100644 --- a/packages/grpc-js-xds/src/load-balancer-weighted-target.ts +++ b/packages/grpc-js-xds/src/load-balancer-weighted-target.ts @@ -16,7 +16,7 @@ */ import { connectivityState as ConnectivityState, status as Status, Metadata, logVerbosity, experimental, LoadBalancingConfig } from "@grpc/grpc-js"; -import { isLocalitySubchannelAddress, LocalitySubchannelAddress } from "./load-balancer-priority"; +import { isLocalityEndpoint, LocalityEndpoint } from "./load-balancer-priority"; import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; import LoadBalancer = experimental.LoadBalancer; import ChannelControlHelper = experimental.ChannelControlHelper; @@ -27,8 +27,8 @@ import PickResult = experimental.PickResult; import PickArgs = experimental.PickArgs; import QueuePicker = experimental.QueuePicker; import UnavailablePicker = experimental.UnavailablePicker; -import SubchannelAddress = experimental.SubchannelAddress; -import subchannelAddressToString = experimental.subchannelAddressToString; +import Endpoint = experimental.Endpoint; +import endpointToString = experimental.endpointToString; import selectLbConfigFromList = experimental.selectLbConfigFromList; const TRACER_NAME = 'weighted_target'; @@ -154,7 +154,7 @@ class WeightedTargetPicker implements Picker { } interface WeightedChild { - updateAddressList(addressList: SubchannelAddress[], lbConfig: WeightedTarget, attributes: { [key: string]: unknown; }): void; + updateAddressList(endpointList: Endpoint[], lbConfig: WeightedTarget, attributes: { [key: string]: unknown; }): void; exitIdle(): void; resetBackoff(): void; destroy(): void; @@ -190,9 +190,9 @@ export class WeightedTargetLoadBalancer implements LoadBalancer { this.parent.maybeUpdateState(); } - updateAddressList(addressList: SubchannelAddress[], lbConfig: WeightedTarget, attributes: { [key: string]: unknown; }): void { + updateAddressList(endpointList: Endpoint[], lbConfig: WeightedTarget, attributes: { [key: string]: unknown; }): void { this.weight = lbConfig.weight; - this.childBalancer.updateAddressList(addressList, lbConfig.child_policy, attributes); + this.childBalancer.updateAddressList(endpointList, lbConfig.child_policy, attributes); } exitIdle(): void { this.childBalancer.exitIdle(); @@ -319,7 +319,7 @@ export class WeightedTargetLoadBalancer implements LoadBalancer { this.channelControlHelper.updateState(connectivityState, picker); } - updateAddressList(addressList: SubchannelAddress[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { + updateAddressList(addressList: Endpoint[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { if (!(lbConfig instanceof WeightedTargetLoadBalancingConfig)) { // Reject a config of the wrong type trace('Discarding address list update with unrecognized config ' + JSON.stringify(lbConfig.toJsonObject(), undefined, 2)); @@ -330,9 +330,9 @@ export class WeightedTargetLoadBalancer implements LoadBalancer { * which child it belongs to. So we bucket those addresses by that first * element, and pass along the rest of the localityPath for that child * to use. */ - const childAddressMap = new Map(); + const childEndpointMap = new Map(); for (const address of addressList) { - if (!isLocalitySubchannelAddress(address)) { + if (!isLocalityEndpoint(address)) { // Reject address that cannot be associated with targets return; } @@ -341,14 +341,14 @@ export class WeightedTargetLoadBalancer implements LoadBalancer { return; } const childName = address.localityPath[0]; - const childAddress: LocalitySubchannelAddress = { + const childAddress: LocalityEndpoint = { ...address, localityPath: address.localityPath.slice(1), }; - let childAddressList = childAddressMap.get(childName); + let childAddressList = childEndpointMap.get(childName); if (childAddressList === undefined) { childAddressList = []; - childAddressMap.set(childName, childAddressList); + childEndpointMap.set(childName, childAddressList); } childAddressList.push(childAddress); } @@ -363,9 +363,9 @@ export class WeightedTargetLoadBalancer implements LoadBalancer { } else { target.maybeReactivate(); } - const targetAddresses = childAddressMap.get(targetName) ?? []; - trace('Assigning target ' + targetName + ' address list ' + targetAddresses.map(address => '(' + subchannelAddressToString(address) + ' path=' + address.localityPath + ')')); - target.updateAddressList(targetAddresses, targetConfig, attributes); + const targetEndpoints = childEndpointMap.get(targetName) ?? []; + trace('Assigning target ' + targetName + ' address list ' + targetEndpoints.map(endpoint => '(' + endpointToString(endpoint) + ' path=' + endpoint.localityPath + ')')); + target.updateAddressList(targetEndpoints, targetConfig, attributes); } // Deactivate targets that are not in the new config diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts index 926c7a699..81e365624 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts @@ -18,11 +18,13 @@ import { experimental, logVerbosity, status as Status, Metadata, connectivityState } from "@grpc/grpc-js"; import { validateXdsServerConfig, XdsServerConfig } from "./xds-bootstrap"; import { getSingletonXdsClient, XdsClient, XdsClusterDropStats, XdsClusterLocalityStats } from "./xds-client"; -import { LocalitySubchannelAddress } from "./load-balancer-priority"; +import { LocalityEndpoint } from "./load-balancer-priority"; import LoadBalancer = experimental.LoadBalancer; import registerLoadBalancerType = experimental.registerLoadBalancerType; -import SubchannelAddress = experimental.SubchannelAddress; +import Endpoint = experimental.Endpoint; +import endpointHasAddress = experimental.endpointHasAddress; +import subchannelAddressToString = experimental.subchannelAddressToString; import Picker = experimental.Picker; import PickArgs = experimental.PickArgs; import PickResult = experimental.PickResult; @@ -34,6 +36,7 @@ import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; import selectLbConfigFromList = experimental.selectLbConfigFromList; import SubchannelInterface = experimental.SubchannelInterface; import BaseSubchannelWrapper = experimental.BaseSubchannelWrapper; +import { Locality__Output } from "./generated/envoy/config/core/v3/Locality"; const TRACER_NAME = 'xds_cluster_impl'; @@ -245,6 +248,7 @@ function getCallCounterMapKey(cluster: string, edsServiceName?: string): string class XdsClusterImplBalancer implements LoadBalancer { private childBalancer: ChildLoadBalancerHandler; + private lastestEndpointList: Endpoint[] | null = null; private latestConfig: XdsClusterImplLoadBalancingConfig | null = null; private clusterDropStats: XdsClusterDropStats | null = null; private xdsClient: XdsClient | null = null; @@ -252,11 +256,20 @@ class XdsClusterImplBalancer implements LoadBalancer { constructor(private readonly channelControlHelper: ChannelControlHelper) { this.childBalancer = new ChildLoadBalancerHandler(createChildChannelControlHelper(channelControlHelper, { createSubchannel: (subchannelAddress, subchannelArgs) => { - if (!this.xdsClient || !this.latestConfig) { + if (!this.xdsClient || !this.latestConfig || !this.lastestEndpointList) { throw new Error('xds_cluster_impl: invalid state: createSubchannel called with xdsClient or latestConfig not populated'); } - const locality = (subchannelAddress as LocalitySubchannelAddress).locality ?? ''; const wrapperChild = channelControlHelper.createSubchannel(subchannelAddress, subchannelArgs); + let locality: Locality__Output | null = null; + for (const endpoint of this.lastestEndpointList) { + if (endpointHasAddress(endpoint, subchannelAddress)) { + locality = (endpoint as LocalityEndpoint).locality; + } + } + if (locality === null) { + trace('Not reporting load for address ' + subchannelAddressToString(subchannelAddress) + ' because it has unknown locality.'); + return wrapperChild; + } const lrsServer = this.latestConfig.getLrsLoadReportingServer(); let statsObj: XdsClusterLocalityStats | null = null; if (lrsServer) { @@ -279,15 +292,15 @@ class XdsClusterImplBalancer implements LoadBalancer { } })); } - updateAddressList(addressList: SubchannelAddress[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { + updateAddressList(endpointList: Endpoint[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { if (!(lbConfig instanceof XdsClusterImplLoadBalancingConfig)) { trace('Discarding address list update with unrecognized config ' + JSON.stringify(lbConfig.toJsonObject(), undefined, 2)); return; } trace('Received update with config: ' + JSON.stringify(lbConfig, undefined, 2)); + this.lastestEndpointList = endpointList; this.latestConfig = lbConfig; this.xdsClient = attributes.xdsClient as XdsClient; - if (lbConfig.getLrsLoadReportingServer()) { this.clusterDropStats = this.xdsClient.addClusterDropStats( lbConfig.getLrsLoadReportingServer()!, @@ -296,7 +309,7 @@ class XdsClusterImplBalancer implements LoadBalancer { ); } - this.childBalancer.updateAddressList(addressList, lbConfig.getChildPolicy(), attributes); + this.childBalancer.updateAddressList(endpointList, lbConfig.getChildPolicy(), attributes); } exitIdle(): void { this.childBalancer.exitIdle(); diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-manager.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-manager.ts index ce3207dfd..a1855d142 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-manager.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-manager.ts @@ -25,7 +25,7 @@ import PickArgs = experimental.PickArgs; import PickResultType = experimental.PickResultType; import UnavailablePicker = experimental.UnavailablePicker; import QueuePicker = experimental.QueuePicker; -import SubchannelAddress = experimental.SubchannelAddress; +import Endpoint = experimental.Endpoint; import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; import ChannelControlHelper = experimental.ChannelControlHelper; import selectLbConfigFromList = experimental.selectLbConfigFromList; @@ -111,7 +111,7 @@ class XdsClusterManagerPicker implements Picker { } interface XdsClusterManagerChild { - updateAddressList(addressList: SubchannelAddress[], childConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void; + updateAddressList(endpointList: Endpoint[], childConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void; exitIdle(): void; resetBackoff(): void; destroy(): void; @@ -142,8 +142,8 @@ class XdsClusterManager implements LoadBalancer { this.picker = picker; this.parent.maybeUpdateState(); } - updateAddressList(addressList: SubchannelAddress[], childConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { - this.childBalancer.updateAddressList(addressList, childConfig, attributes); + updateAddressList(endpointList: Endpoint[], childConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { + this.childBalancer.updateAddressList(endpointList, childConfig, attributes); } exitIdle(): void { this.childBalancer.exitIdle(); @@ -235,7 +235,7 @@ class XdsClusterManager implements LoadBalancer { this.channelControlHelper.updateState(connectivityState, picker); } - updateAddressList(addressList: SubchannelAddress[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { + updateAddressList(endpointList: Endpoint[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { if (!(lbConfig instanceof XdsClusterManagerLoadBalancingConfig)) { // Reject a config of the wrong type trace('Discarding address list update with unrecognized config ' + JSON.stringify(lbConfig.toJsonObject(), undefined, 2)); @@ -259,7 +259,7 @@ class XdsClusterManager implements LoadBalancer { for (const [name, childConfig] of configChildren.entries()) { if (!this.children.has(name)) { const newChild = new this.XdsClusterManagerChildImpl(this, name); - newChild.updateAddressList(addressList, childConfig, attributes); + newChild.updateAddressList(endpointList, childConfig, attributes); this.children.set(name, newChild); } } diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts index 6c11c52bf..752919c98 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts @@ -20,7 +20,7 @@ import { registerLoadBalancerType } from "@grpc/grpc-js/build/src/load-balancer" import { EXPERIMENTAL_OUTLIER_DETECTION } from "./environment"; import { Locality__Output } from "./generated/envoy/config/core/v3/Locality"; import { ClusterLoadAssignment__Output } from "./generated/envoy/config/endpoint/v3/ClusterLoadAssignment"; -import { LocalitySubchannelAddress, PriorityChildRaw } from "./load-balancer-priority"; +import { LocalityEndpoint, PriorityChildRaw } from "./load-balancer-priority"; import { getSingletonXdsClient, Watcher, XdsClient } from "./xds-client"; import { DropCategory } from "./load-balancer-xds-cluster-impl"; @@ -28,11 +28,13 @@ import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; import LoadBalancer = experimental.LoadBalancer; import Resolver = experimental.Resolver; import SubchannelAddress = experimental.SubchannelAddress; +import Endpoint = experimental.Endpoint; import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; import createResolver = experimental.createResolver; import ChannelControlHelper = experimental.ChannelControlHelper; import OutlierDetectionRawConfig = experimental.OutlierDetectionRawConfig; import subchannelAddressToString = experimental.subchannelAddressToString; +import endpointToString = experimental.endpointToString; import selectLbConfigFromList = experimental.selectLbConfigFromList; import parseLoadBalancingConfig = experimental.parseLoadBalancingConfig; import UnavailablePicker = experimental.UnavailablePicker; @@ -116,7 +118,7 @@ class XdsClusterResolverLoadBalancingConfig implements TypedLoadBalancingConfig interface LocalityEntry { locality: Locality__Output; weight: number; - addresses: SubchannelAddress[]; + endpoints: Endpoint[]; } interface PriorityEntry { @@ -164,18 +166,20 @@ function getEdsPriorities(edsUpdate: ClusterLoadAssignment__Output): PriorityEnt if (!endpoint.load_balancing_weight) { continue; } - const addresses: SubchannelAddress[] = endpoint.lb_endpoints.filter(lbEndpoint => lbEndpoint.health_status === 'UNKNOWN' || lbEndpoint.health_status === 'HEALTHY').map( + const endpoints: Endpoint[] = endpoint.lb_endpoints.filter(lbEndpoint => lbEndpoint.health_status === 'UNKNOWN' || lbEndpoint.health_status === 'HEALTHY').map( (lbEndpoint) => { /* The validator in the XdsClient class ensures that each endpoint has * a socket_address with an IP address and a port_value. */ const socketAddress = lbEndpoint.endpoint!.address!.socket_address!; return { - host: socketAddress.address!, - port: socketAddress.port_value!, + addresses: [{ + host: socketAddress.address!, + port: socketAddress.port_value!, + }] }; } ); - if (addresses.length === 0) { + if (endpoints.length === 0) { continue; } let priorityEntry: PriorityEntry; @@ -190,7 +194,7 @@ function getEdsPriorities(edsUpdate: ClusterLoadAssignment__Output): PriorityEnt } priorityEntry.localities.push({ locality: endpoint.locality!, - addresses: addresses, + endpoints: endpoints, weight: endpoint.load_balancing_weight.value }); } @@ -198,7 +202,7 @@ function getEdsPriorities(edsUpdate: ClusterLoadAssignment__Output): PriorityEnt return result.filter(priority => priority); } -function getDnsPriorities(addresses: SubchannelAddress[]): PriorityEntry[] { +function getDnsPriorities(endpoints: Endpoint[]): PriorityEntry[] { return [{ localities: [{ locality: { @@ -207,7 +211,7 @@ function getDnsPriorities(addresses: SubchannelAddress[]): PriorityEntry[] { sub_zone: '' }, weight: 1, - addresses: addresses + endpoints: endpoints }], dropCategories: [] }]; @@ -249,7 +253,7 @@ export class XdsClusterResolver implements LoadBalancer { } const fullPriorityList: string[] = []; const priorityChildren: {[name: string]: PriorityChildRaw} = {}; - const addressList: LocalitySubchannelAddress[] = []; + const endpointList: LocalityEndpoint[] = []; const edsChildPolicy = this.latestConfig.getXdsLbPolicy(); for (const entry of this.discoveryMechanismList) { const newPriorityNames: string[] = []; @@ -291,15 +295,15 @@ export class XdsClusterResolver implements LoadBalancer { newPriorityNames[priority] = newPriorityName; for (const localityObj of priorityEntry.localities) { - for (const address of localityObj.addresses) { - addressList.push({ + for (const endpoint of localityObj.endpoints) { + endpointList.push({ localityPath: [ newPriorityName, localityToName(localityObj.locality), ], locality: localityObj.locality, weight: localityObj.weight, - ...address, + ...endpoint }); } newLocalityPriorities.set(localityToName(localityObj.locality), priority); @@ -349,16 +353,16 @@ export class XdsClusterResolver implements LoadBalancer { this.channelControlHelper.updateState(connectivityState.TRANSIENT_FAILURE, new UnavailablePicker({code: status.UNAVAILABLE, details: `LB policy config parsing failed with error ${(e as Error).message}`, metadata: new Metadata()})); return; } - trace('Child update addresses: ' + addressList.map(address => '(' + subchannelAddressToString(address) + ' path=' + address.localityPath + ')')); + trace('Child update addresses: ' + endpointList.map(endpoint => '(' + endpointToString(endpoint) + ' path=' + endpoint.localityPath + ')')); trace('Child update priority config: ' + JSON.stringify(childConfig, undefined, 2)); this.childBalancer.updateAddressList( - addressList, + endpointList, typedChildConfig, this.latestAttributes ); } - updateAddressList(addressList: SubchannelAddress[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { + updateAddressList(addressList: Endpoint[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { if (!(lbConfig instanceof XdsClusterResolverLoadBalancingConfig)) { trace('Discarding address list update with unrecognized config ' + JSON.stringify(lbConfig, undefined, 2)); return; @@ -399,8 +403,8 @@ export class XdsClusterResolver implements LoadBalancer { } } else { const resolver = createResolver({scheme: 'dns', path: mechanism.dns_hostname!}, { - onSuccessfulResolution: addressList => { - mechanismEntry.latestUpdate = getDnsPriorities(addressList); + onSuccessfulResolution: endpointList => { + mechanismEntry.latestUpdate = getDnsPriorities(endpointList); this.maybeUpdateChild(); }, onError: error => { diff --git a/packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts b/packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts index 8636937fd..3ef789395 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts @@ -20,13 +20,13 @@ import { LoadBalancingConfig, experimental, logVerbosity } from "@grpc/grpc-js"; import { loadProtosWithOptionsSync } from "@grpc/proto-loader/build/src/util"; import { WeightedTargetRaw } from "./load-balancer-weighted-target"; -import { isLocalitySubchannelAddress } from "./load-balancer-priority"; +import { isLocalityEndpoint } from "./load-balancer-priority"; import { localityToName } from "./load-balancer-xds-cluster-resolver"; import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; import LoadBalancer = experimental.LoadBalancer; import ChannelControlHelper = experimental.ChannelControlHelper; import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; -import SubchannelAddress = experimental.SubchannelAddress; +import Endpoint = experimental.Endpoint; import parseLoadBalancingConfig = experimental.parseLoadBalancingConfig; import registerLoadBalancerType = experimental.registerLoadBalancerType; import { Any__Output } from "./generated/google/protobuf/Any"; @@ -76,14 +76,14 @@ class XdsWrrLocalityLoadBalancer implements LoadBalancer { constructor(private readonly channelControlHelper: ChannelControlHelper) { this.childBalancer = new ChildLoadBalancerHandler(channelControlHelper); } - updateAddressList(addressList: SubchannelAddress[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { + updateAddressList(endpointList: Endpoint[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { if (!(lbConfig instanceof XdsWrrLocalityLoadBalancingConfig)) { trace('Discarding address list update with unrecognized config ' + JSON.stringify(lbConfig, undefined, 2)); return; } const targets: {[localityName: string]: WeightedTargetRaw} = {}; - for (const address of addressList) { - if (!isLocalitySubchannelAddress(address)) { + for (const address of endpointList) { + if (!isLocalityEndpoint(address)) { return; } const localityName = localityToName(address.locality); @@ -99,7 +99,7 @@ class XdsWrrLocalityLoadBalancer implements LoadBalancer { targets: targets } }; - this.childBalancer.updateAddressList(addressList, parseLoadBalancingConfig(childConfig), attributes); + this.childBalancer.updateAddressList(endpointList, parseLoadBalancingConfig(childConfig), attributes); } exitIdle(): void { this.childBalancer.exitIdle(); diff --git a/packages/grpc-js-xds/test/test-custom-lb-policies.ts b/packages/grpc-js-xds/test/test-custom-lb-policies.ts index 16cca3a8a..6994bf392 100644 --- a/packages/grpc-js-xds/test/test-custom-lb-policies.ts +++ b/packages/grpc-js-xds/test/test-custom-lb-policies.ts @@ -30,7 +30,7 @@ import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; import LoadBalancer = experimental.LoadBalancer; import ChannelControlHelper = experimental.ChannelControlHelper; import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; -import SubchannelAddress = experimental.SubchannelAddress; +import Endpoint = experimental.Endpoint; import Picker = experimental.Picker; import PickArgs = experimental.PickArgs; import PickResult = experimental.PickResult; @@ -94,12 +94,12 @@ class RpcBehaviorLoadBalancer implements LoadBalancer { }); this.child = new ChildLoadBalancerHandler(childChannelControlHelper); } - updateAddressList(addressList: SubchannelAddress[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { + updateAddressList(endpointList: Endpoint[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { if (!(lbConfig instanceof RpcBehaviorLoadBalancingConfig)) { return; } this.latestConfig = lbConfig; - this.child.updateAddressList(addressList, RPC_BEHAVIOR_CHILD_CONFIG, attributes); + this.child.updateAddressList(endpointList, RPC_BEHAVIOR_CHILD_CONFIG, attributes); } exitIdle(): void { this.child.exitIdle(); From 3ff8b674bb6360f686636c33478a0c9ba4feadb7 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 30 Aug 2023 14:57:52 -0700 Subject: [PATCH 024/109] Export HealthListener type in experimental --- packages/grpc-js/src/experimental.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/grpc-js/src/experimental.ts b/packages/grpc-js/src/experimental.ts index 0c7bc75e1..870fbe285 100644 --- a/packages/grpc-js/src/experimental.ts +++ b/packages/grpc-js/src/experimental.ts @@ -44,6 +44,7 @@ export { SubchannelInterface, BaseSubchannelWrapper, ConnectivityStateListener, + HealthListener, } from './subchannel-interface'; export { OutlierDetectionRawConfig, From 266af4c19f766ba304213b009b7131b8efd36f4f Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 30 Aug 2023 15:16:25 -0700 Subject: [PATCH 025/109] Add pick_first tests --- packages/grpc-js/test/test-pick-first.ts | 48 ++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/packages/grpc-js/test/test-pick-first.ts b/packages/grpc-js/test/test-pick-first.ts index 7e862de58..8ab4f4d40 100644 --- a/packages/grpc-js/test/test-pick-first.ts +++ b/packages/grpc-js/test/test-pick-first.ts @@ -125,6 +125,54 @@ describe('pick_first load balancing policy', () => { subchannels[0].transitionToState(ConnectivityState.READY); }); }); + it('Should report READY when a subchannel other than the first connects', done => { + const channelControlHelper = createChildChannelControlHelper( + baseChannelControlHelper, + { + updateState: updateStateCallBackForExpectedStateSequence( + [ConnectivityState.CONNECTING, ConnectivityState.READY], + done + ), + } + ); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + pickFirst.updateAddressList( + [ + { addresses: [{ host: 'localhost', port: 1 }] }, + { addresses: [{ host: 'localhost', port: 2 }] }, + ], + config + ); + process.nextTick(() => { + subchannels[1].transitionToState(ConnectivityState.READY); + }); + }); + it('Should report READY when a subchannel other than the first in the same endpoint connects', done => { + const channelControlHelper = createChildChannelControlHelper( + baseChannelControlHelper, + { + updateState: updateStateCallBackForExpectedStateSequence( + [ConnectivityState.CONNECTING, ConnectivityState.READY], + done + ), + } + ); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + pickFirst.updateAddressList( + [ + { + addresses: [ + { host: 'localhost', port: 1 }, + { host: 'localhost', port: 2 }, + ], + }, + ], + config + ); + process.nextTick(() => { + subchannels[1].transitionToState(ConnectivityState.READY); + }); + }); it('Should report READY when updated with a subchannel that is already READY', done => { const channelControlHelper = createChildChannelControlHelper( baseChannelControlHelper, From 00e1ac46a8db0e5a6d98f0f688ade51c96c07ce5 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 6 Sep 2023 10:31:53 -0700 Subject: [PATCH 026/109] grpc-js: Pass channel options to LoadBalancer constructors --- .../grpc-js-xds/interop/xds-interop-client.ts | 4 +- packages/grpc-js-xds/src/load-balancer-cds.ts | 6 +-- .../grpc-js-xds/src/load-balancer-priority.ts | 6 +-- .../src/load-balancer-weighted-target.ts | 6 +-- .../src/load-balancer-xds-cluster-impl.ts | 6 +-- .../src/load-balancer-xds-cluster-manager.ts | 6 +-- .../src/load-balancer-xds-cluster-resolver.ts | 6 +-- .../src/load-balancer-xds-wrr-locality.ts | 6 +-- .../test/test-custom-lb-policies.ts | 6 +-- .../src/load-balancer-child-handler.ts | 7 ++- .../src/load-balancer-outlier-detection.ts | 8 ++- .../grpc-js/src/load-balancer-pick-first.ts | 14 ++++-- .../grpc-js/src/load-balancer-round-robin.ts | 13 ++++- packages/grpc-js/src/load-balancer.ts | 11 +++-- .../grpc-js/src/resolving-load-balancer.ts | 49 ++++++++++--------- packages/grpc-js/test/test-pick-first.ts | 34 ++++++------- 16 files changed, 110 insertions(+), 78 deletions(-) diff --git a/packages/grpc-js-xds/interop/xds-interop-client.ts b/packages/grpc-js-xds/interop/xds-interop-client.ts index dc70034f9..f26278abb 100644 --- a/packages/grpc-js-xds/interop/xds-interop-client.ts +++ b/packages/grpc-js-xds/interop/xds-interop-client.ts @@ -88,7 +88,7 @@ const RPC_BEHAVIOR_CHILD_CONFIG = parseLoadBalancingConfig({round_robin: {}}); class RpcBehaviorLoadBalancer implements LoadBalancer { private child: ChildLoadBalancerHandler; private latestConfig: RpcBehaviorLoadBalancingConfig | null = null; - constructor(channelControlHelper: ChannelControlHelper) { + constructor(channelControlHelper: ChannelControlHelper, options: grpc.ChannelOptions) { const childChannelControlHelper = createChildChannelControlHelper(channelControlHelper, { updateState: (connectivityState, picker) => { if (connectivityState === grpc.connectivityState.READY && this.latestConfig) { @@ -97,7 +97,7 @@ class RpcBehaviorLoadBalancer implements LoadBalancer { channelControlHelper.updateState(connectivityState, picker); } }); - this.child = new ChildLoadBalancerHandler(childChannelControlHelper); + this.child = new ChildLoadBalancerHandler(childChannelControlHelper, options); } updateAddressList(endpointList: Endpoint[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { if (!(lbConfig instanceof RpcBehaviorLoadBalancingConfig)) { diff --git a/packages/grpc-js-xds/src/load-balancer-cds.ts b/packages/grpc-js-xds/src/load-balancer-cds.ts index 1e4f63b4b..bebca6fe3 100644 --- a/packages/grpc-js-xds/src/load-balancer-cds.ts +++ b/packages/grpc-js-xds/src/load-balancer-cds.ts @@ -15,7 +15,7 @@ * */ -import { connectivityState, status, Metadata, logVerbosity, experimental, LoadBalancingConfig } from '@grpc/grpc-js'; +import { connectivityState, status, Metadata, logVerbosity, experimental, LoadBalancingConfig, ChannelOptions } from '@grpc/grpc-js'; import { getSingletonXdsClient, Watcher, XdsClient } from './xds-client'; import { Cluster__Output } from './generated/envoy/config/cluster/v3/Cluster'; import Endpoint = experimental.Endpoint; @@ -155,8 +155,8 @@ export class CdsLoadBalancer implements LoadBalancer { private updatedChild = false; - constructor(private readonly channelControlHelper: ChannelControlHelper) { - this.childBalancer = new XdsClusterResolverChildPolicyHandler(channelControlHelper); + constructor(private readonly channelControlHelper: ChannelControlHelper, options: ChannelOptions) { + this.childBalancer = new XdsClusterResolverChildPolicyHandler(channelControlHelper, options); } private reportError(errorMessage: string) { diff --git a/packages/grpc-js-xds/src/load-balancer-priority.ts b/packages/grpc-js-xds/src/load-balancer-priority.ts index ba4fd2cfa..4a4acb477 100644 --- a/packages/grpc-js-xds/src/load-balancer-priority.ts +++ b/packages/grpc-js-xds/src/load-balancer-priority.ts @@ -15,7 +15,7 @@ * */ -import { connectivityState as ConnectivityState, status as Status, Metadata, logVerbosity as LogVerbosity, experimental, LoadBalancingConfig } from '@grpc/grpc-js'; +import { connectivityState as ConnectivityState, status as Status, Metadata, logVerbosity as LogVerbosity, experimental, LoadBalancingConfig, ChannelOptions } from '@grpc/grpc-js'; import LoadBalancer = experimental.LoadBalancer; import ChannelControlHelper = experimental.ChannelControlHelper; import registerLoadBalancerType = experimental.registerLoadBalancerType; @@ -180,7 +180,7 @@ export class PriorityLoadBalancer implements LoadBalancer { this.parent.channelControlHelper.requestReresolution(); } } - })); + }), parent.options); this.picker = new QueuePicker(this.childBalancer); this.startFailoverTimer(); } @@ -306,7 +306,7 @@ export class PriorityLoadBalancer implements LoadBalancer { private updatesPaused = false; - constructor(private channelControlHelper: ChannelControlHelper) {} + constructor(private channelControlHelper: ChannelControlHelper, private options: ChannelOptions) {} private updateState(state: ConnectivityState, picker: Picker) { trace( diff --git a/packages/grpc-js-xds/src/load-balancer-weighted-target.ts b/packages/grpc-js-xds/src/load-balancer-weighted-target.ts index 16874e01b..89192b622 100644 --- a/packages/grpc-js-xds/src/load-balancer-weighted-target.ts +++ b/packages/grpc-js-xds/src/load-balancer-weighted-target.ts @@ -15,7 +15,7 @@ * */ -import { connectivityState as ConnectivityState, status as Status, Metadata, logVerbosity, experimental, LoadBalancingConfig } from "@grpc/grpc-js"; +import { connectivityState as ConnectivityState, status as Status, Metadata, logVerbosity, experimental, LoadBalancingConfig, ChannelOptions } from "@grpc/grpc-js"; import { isLocalityEndpoint, LocalityEndpoint } from "./load-balancer-priority"; import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; import LoadBalancer = experimental.LoadBalancer; @@ -178,7 +178,7 @@ export class WeightedTargetLoadBalancer implements LoadBalancer { updateState: (connectivityState: ConnectivityState, picker: Picker) => { this.updateState(connectivityState, picker); }, - })); + }), parent.options); this.picker = new QueuePicker(this.childBalancer); } @@ -243,7 +243,7 @@ export class WeightedTargetLoadBalancer implements LoadBalancer { private targetList: string[] = []; private updatesPaused = false; - constructor(private channelControlHelper: ChannelControlHelper) {} + constructor(private channelControlHelper: ChannelControlHelper, private options: ChannelOptions) {} private maybeUpdateState() { if (!this.updatesPaused) { diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts index 81e365624..f163b6fe2 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts @@ -15,7 +15,7 @@ * */ -import { experimental, logVerbosity, status as Status, Metadata, connectivityState } from "@grpc/grpc-js"; +import { experimental, logVerbosity, status as Status, Metadata, connectivityState, ChannelOptions } from "@grpc/grpc-js"; import { validateXdsServerConfig, XdsServerConfig } from "./xds-bootstrap"; import { getSingletonXdsClient, XdsClient, XdsClusterDropStats, XdsClusterLocalityStats } from "./xds-client"; import { LocalityEndpoint } from "./load-balancer-priority"; @@ -253,7 +253,7 @@ class XdsClusterImplBalancer implements LoadBalancer { private clusterDropStats: XdsClusterDropStats | null = null; private xdsClient: XdsClient | null = null; - constructor(private readonly channelControlHelper: ChannelControlHelper) { + constructor(private readonly channelControlHelper: ChannelControlHelper, options: ChannelOptions) { this.childBalancer = new ChildLoadBalancerHandler(createChildChannelControlHelper(channelControlHelper, { createSubchannel: (subchannelAddress, subchannelArgs) => { if (!this.xdsClient || !this.latestConfig || !this.lastestEndpointList) { @@ -290,7 +290,7 @@ class XdsClusterImplBalancer implements LoadBalancer { channelControlHelper.updateState(connectivityState, picker); } } - })); + }), options); } updateAddressList(endpointList: Endpoint[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { if (!(lbConfig instanceof XdsClusterImplLoadBalancingConfig)) { diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-manager.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-manager.ts index a1855d142..3560d8481 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-manager.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-manager.ts @@ -15,7 +15,7 @@ * */ -import { connectivityState as ConnectivityState, status as Status, experimental, logVerbosity, Metadata, status } from "@grpc/grpc-js/"; +import { connectivityState as ConnectivityState, status as Status, experimental, logVerbosity, Metadata, status, ChannelOptions } from "@grpc/grpc-js/"; import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; import LoadBalancer = experimental.LoadBalancer; @@ -131,7 +131,7 @@ class XdsClusterManager implements LoadBalancer { updateState: (connectivityState: ConnectivityState, picker: Picker) => { this.updateState(connectivityState, picker); }, - })); + }), parent.options); this.picker = new QueuePicker(this.childBalancer); } @@ -167,7 +167,7 @@ class XdsClusterManager implements LoadBalancer { // Shutdown is a placeholder value that will never appear in normal operation. private currentState: ConnectivityState = ConnectivityState.SHUTDOWN; private updatesPaused = false; - constructor(private channelControlHelper: ChannelControlHelper) {} + constructor(private channelControlHelper: ChannelControlHelper, private options: ChannelOptions) {} private maybeUpdateState() { if (!this.updatesPaused) { diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts index 752919c98..c4bf984f7 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts @@ -15,7 +15,7 @@ * */ -import { LoadBalancingConfig, Metadata, connectivityState, experimental, logVerbosity, status } from "@grpc/grpc-js"; +import { ChannelOptions, LoadBalancingConfig, Metadata, connectivityState, experimental, logVerbosity, status } from "@grpc/grpc-js"; import { registerLoadBalancerType } from "@grpc/grpc-js/build/src/load-balancer"; import { EXPERIMENTAL_OUTLIER_DETECTION } from "./environment"; import { Locality__Output } from "./generated/envoy/config/core/v3/Locality"; @@ -232,14 +232,14 @@ export class XdsClusterResolver implements LoadBalancer { private xdsClient: XdsClient | null = null; private childBalancer: ChildLoadBalancerHandler; - constructor(private readonly channelControlHelper: ChannelControlHelper) { + constructor(private readonly channelControlHelper: ChannelControlHelper, options: ChannelOptions) { this.childBalancer = new ChildLoadBalancerHandler(experimental.createChildChannelControlHelper(channelControlHelper, { requestReresolution: () => { for (const entry of this.discoveryMechanismList) { entry.resolver?.updateResolution(); } } - })); + }), options); } private maybeUpdateChild() { diff --git a/packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts b/packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts index 3ef789395..f3fbcd513 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts @@ -17,7 +17,7 @@ // https://github.com/grpc/proposal/blob/master/A52-xds-custom-lb-policies.md -import { LoadBalancingConfig, experimental, logVerbosity } from "@grpc/grpc-js"; +import { ChannelOptions, LoadBalancingConfig, experimental, logVerbosity } from "@grpc/grpc-js"; import { loadProtosWithOptionsSync } from "@grpc/proto-loader/build/src/util"; import { WeightedTargetRaw } from "./load-balancer-weighted-target"; import { isLocalityEndpoint } from "./load-balancer-priority"; @@ -73,8 +73,8 @@ class XdsWrrLocalityLoadBalancingConfig implements TypedLoadBalancingConfig { class XdsWrrLocalityLoadBalancer implements LoadBalancer { private childBalancer: ChildLoadBalancerHandler; - constructor(private readonly channelControlHelper: ChannelControlHelper) { - this.childBalancer = new ChildLoadBalancerHandler(channelControlHelper); + constructor(private readonly channelControlHelper: ChannelControlHelper, options: ChannelOptions) { + this.childBalancer = new ChildLoadBalancerHandler(channelControlHelper, options); } updateAddressList(endpointList: Endpoint[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { if (!(lbConfig instanceof XdsWrrLocalityLoadBalancingConfig)) { diff --git a/packages/grpc-js-xds/test/test-custom-lb-policies.ts b/packages/grpc-js-xds/test/test-custom-lb-policies.ts index 6994bf392..6da6fecc8 100644 --- a/packages/grpc-js-xds/test/test-custom-lb-policies.ts +++ b/packages/grpc-js-xds/test/test-custom-lb-policies.ts @@ -24,7 +24,7 @@ import { XdsServer } from "./xds-server"; import * as assert from 'assert'; import { WrrLocality } from "../src/generated/envoy/extensions/load_balancing_policies/wrr_locality/v3/WrrLocality"; import { TypedStruct } from "../src/generated/xds/type/v3/TypedStruct"; -import { connectivityState, experimental, logVerbosity } from "@grpc/grpc-js"; +import { ChannelOptions, connectivityState, experimental, logVerbosity } from "@grpc/grpc-js"; import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; import LoadBalancer = experimental.LoadBalancer; @@ -83,7 +83,7 @@ const RPC_BEHAVIOR_CHILD_CONFIG = parseLoadBalancingConfig({round_robin: {}}); class RpcBehaviorLoadBalancer implements LoadBalancer { private child: ChildLoadBalancerHandler; private latestConfig: RpcBehaviorLoadBalancingConfig | null = null; - constructor(channelControlHelper: ChannelControlHelper) { + constructor(channelControlHelper: ChannelControlHelper, options: ChannelOptions) { const childChannelControlHelper = createChildChannelControlHelper(channelControlHelper, { updateState: (state, picker) => { if (state === connectivityState.READY && this.latestConfig) { @@ -92,7 +92,7 @@ class RpcBehaviorLoadBalancer implements LoadBalancer { channelControlHelper.updateState(state, picker); } }); - this.child = new ChildLoadBalancerHandler(childChannelControlHelper); + this.child = new ChildLoadBalancerHandler(childChannelControlHelper, options); } updateAddressList(endpointList: Endpoint[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { if (!(lbConfig instanceof RpcBehaviorLoadBalancingConfig)) { diff --git a/packages/grpc-js/src/load-balancer-child-handler.ts b/packages/grpc-js/src/load-balancer-child-handler.ts index 11bfac211..a29d6c92b 100644 --- a/packages/grpc-js/src/load-balancer-child-handler.ts +++ b/packages/grpc-js/src/load-balancer-child-handler.ts @@ -84,7 +84,10 @@ export class ChildLoadBalancerHandler implements LoadBalancer { } }; - constructor(private readonly channelControlHelper: ChannelControlHelper) {} + constructor( + private readonly channelControlHelper: ChannelControlHelper, + private readonly options: ChannelOptions + ) {} protected configUpdateRequiresNewPolicyInstance( oldConfig: TypedLoadBalancingConfig, @@ -111,7 +114,7 @@ export class ChildLoadBalancerHandler implements LoadBalancer { this.configUpdateRequiresNewPolicyInstance(this.latestConfig, lbConfig) ) { const newHelper = new this.ChildPolicyHelper(this); - const newChild = createLoadBalancer(lbConfig, newHelper)!; + const newChild = createLoadBalancer(lbConfig, newHelper, this.options)!; newHelper.setChild(newChild); if (this.currentChild === null) { this.currentChild = newChild; diff --git a/packages/grpc-js/src/load-balancer-outlier-detection.ts b/packages/grpc-js/src/load-balancer-outlier-detection.ts index b0c648efd..8e0513611 100644 --- a/packages/grpc-js/src/load-balancer-outlier-detection.ts +++ b/packages/grpc-js/src/load-balancer-outlier-detection.ts @@ -585,7 +585,10 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { private ejectionTimer: NodeJS.Timeout; private timerStartTime: Date | null = null; - constructor(channelControlHelper: ChannelControlHelper) { + constructor( + channelControlHelper: ChannelControlHelper, + options: ChannelOptions + ) { this.childBalancer = new ChildLoadBalancerHandler( createChildChannelControlHelper(channelControlHelper, { createSubchannel: ( @@ -619,7 +622,8 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { channelControlHelper.updateState(connectivityState, picker); } }, - }) + }), + options ); this.ejectionTimer = setInterval(() => {}, 0); clearInterval(this.ejectionTimer); diff --git a/packages/grpc-js/src/load-balancer-pick-first.ts b/packages/grpc-js/src/load-balancer-pick-first.ts index 2cf108675..9af662662 100644 --- a/packages/grpc-js/src/load-balancer-pick-first.ts +++ b/packages/grpc-js/src/load-balancer-pick-first.ts @@ -42,6 +42,7 @@ import { } from './subchannel-interface'; import { isTcpSubchannelAddress } from './subchannel-address'; import { isIPv6 } from 'net'; +import { ChannelOptions } from './channel-options'; const TRACER_NAME = 'pick_first'; @@ -162,6 +163,9 @@ function interleaveAddressFamilies( return result; } +const REPORT_HEALTH_STATUS_OPTION_NAME = + 'grpc-node.internal.pick-first.report_health_status'; + export class PickFirstLoadBalancer implements LoadBalancer { /** * The list of subchannels this load balancer is currently attempting to @@ -212,6 +216,8 @@ export class PickFirstLoadBalancer implements LoadBalancer { */ private stickyTransientFailureMode = false; + private reportHealthStatus: boolean; + /** * Load balancer that attempts to connect to each backend in the address list * in order, and picks the first one that connects, using it for every @@ -221,10 +227,11 @@ export class PickFirstLoadBalancer implements LoadBalancer { */ constructor( private readonly channelControlHelper: ChannelControlHelper, - private reportHealthStatus = false + options: ChannelOptions ) { this.connectionDelayTimeout = setTimeout(() => {}, 0); clearTimeout(this.connectionDelayTimeout); + this.reportHealthStatus = options[REPORT_HEALTH_STATUS_OPTION_NAME]; } private allChildrenHaveReportedTF(): boolean { @@ -510,7 +517,8 @@ export class LeafLoadBalancer { private latestPicker: Picker; constructor( private endpoint: Endpoint, - channelControlHelper: ChannelControlHelper + channelControlHelper: ChannelControlHelper, + options: ChannelOptions ) { const childChannelControlHelper = createChildChannelControlHelper( channelControlHelper, @@ -524,7 +532,7 @@ export class LeafLoadBalancer { ); this.pickFirstBalancer = new PickFirstLoadBalancer( childChannelControlHelper, - /* reportHealthStatus= */ true + { ...options, [REPORT_HEALTH_STATUS_OPTION_NAME]: true } ); this.latestPicker = new QueuePicker(this.pickFirstBalancer); } diff --git a/packages/grpc-js/src/load-balancer-round-robin.ts b/packages/grpc-js/src/load-balancer-round-robin.ts index 986181a84..9f093596a 100644 --- a/packages/grpc-js/src/load-balancer-round-robin.ts +++ b/packages/grpc-js/src/load-balancer-round-robin.ts @@ -38,6 +38,7 @@ import { endpointToString, } from './subchannel-address'; import { LeafLoadBalancer } from './load-balancer-pick-first'; +import { ChannelOptions } from './channel-options'; const TRACER_NAME = 'round_robin'; @@ -99,7 +100,10 @@ export class RoundRobinLoadBalancer implements LoadBalancer { private childChannelControlHelper: ChannelControlHelper; - constructor(private readonly channelControlHelper: ChannelControlHelper) { + constructor( + private readonly channelControlHelper: ChannelControlHelper, + private readonly options: ChannelOptions + ) { this.childChannelControlHelper = createChildChannelControlHelper( channelControlHelper, { @@ -186,7 +190,12 @@ export class RoundRobinLoadBalancer implements LoadBalancer { trace('Connect to endpoint list ' + endpointList.map(endpointToString)); this.updatesPaused = true; this.children = endpointList.map( - endpoint => new LeafLoadBalancer(endpoint, this.childChannelControlHelper) + endpoint => + new LeafLoadBalancer( + endpoint, + this.childChannelControlHelper, + this.options + ) ); for (const child of this.children) { child.startConnecting(); diff --git a/packages/grpc-js/src/load-balancer.ts b/packages/grpc-js/src/load-balancer.ts index 1145fdc90..f8071317a 100644 --- a/packages/grpc-js/src/load-balancer.ts +++ b/packages/grpc-js/src/load-balancer.ts @@ -128,7 +128,10 @@ export interface LoadBalancer { } export interface LoadBalancerConstructor { - new (channelControlHelper: ChannelControlHelper): LoadBalancer; + new ( + channelControlHelper: ChannelControlHelper, + options: ChannelOptions + ): LoadBalancer; } export interface TypedLoadBalancingConfig { @@ -169,12 +172,14 @@ export function registerDefaultLoadBalancerType(typeName: string) { export function createLoadBalancer( config: TypedLoadBalancingConfig, - channelControlHelper: ChannelControlHelper + channelControlHelper: ChannelControlHelper, + options: ChannelOptions ): LoadBalancer | null { const typeName = config.getLoadBalancerName(); if (typeName in registeredLoadBalancerTypes) { return new registeredLoadBalancerTypes[typeName].LoadBalancer( - channelControlHelper + channelControlHelper, + options ); } else { return null; diff --git a/packages/grpc-js/src/resolving-load-balancer.ts b/packages/grpc-js/src/resolving-load-balancer.ts index 22808dc32..df480400a 100644 --- a/packages/grpc-js/src/resolving-load-balancer.ts +++ b/packages/grpc-js/src/resolving-load-balancer.ts @@ -149,30 +149,33 @@ export class ResolvingLoadBalancer implements LoadBalancer { }; } this.updateState(ConnectivityState.IDLE, new QueuePicker(this)); - this.childLoadBalancer = new ChildLoadBalancerHandler({ - createSubchannel: - channelControlHelper.createSubchannel.bind(channelControlHelper), - requestReresolution: () => { - /* If the backoffTimeout is running, we're still backing off from - * making resolve requests, so we shouldn't make another one here. - * In that case, the backoff timer callback will call - * updateResolution */ - if (this.backoffTimeout.isRunning()) { - this.continueResolving = true; - } else { - this.updateResolution(); - } - }, - updateState: (newState: ConnectivityState, picker: Picker) => { - this.latestChildState = newState; - this.latestChildPicker = picker; - this.updateState(newState, picker); + this.childLoadBalancer = new ChildLoadBalancerHandler( + { + createSubchannel: + channelControlHelper.createSubchannel.bind(channelControlHelper), + requestReresolution: () => { + /* If the backoffTimeout is running, we're still backing off from + * making resolve requests, so we shouldn't make another one here. + * In that case, the backoff timer callback will call + * updateResolution */ + if (this.backoffTimeout.isRunning()) { + this.continueResolving = true; + } else { + this.updateResolution(); + } + }, + updateState: (newState: ConnectivityState, picker: Picker) => { + this.latestChildState = newState; + this.latestChildPicker = picker; + this.updateState(newState, picker); + }, + addChannelzChild: + channelControlHelper.addChannelzChild.bind(channelControlHelper), + removeChannelzChild: + channelControlHelper.removeChannelzChild.bind(channelControlHelper), }, - addChannelzChild: - channelControlHelper.addChannelzChild.bind(channelControlHelper), - removeChannelzChild: - channelControlHelper.removeChannelzChild.bind(channelControlHelper), - }); + channelOptions + ); this.innerResolver = createResolver( target, { diff --git a/packages/grpc-js/test/test-pick-first.ts b/packages/grpc-js/test/test-pick-first.ts index 8ab4f4d40..18dbf9d2d 100644 --- a/packages/grpc-js/test/test-pick-first.ts +++ b/packages/grpc-js/test/test-pick-first.ts @@ -116,7 +116,7 @@ describe('pick_first load balancing policy', () => { ), } ); - const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper, {}); pickFirst.updateAddressList( [{ addresses: [{ host: 'localhost', port: 1 }] }], config @@ -135,7 +135,7 @@ describe('pick_first load balancing policy', () => { ), } ); - const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper, {}); pickFirst.updateAddressList( [ { addresses: [{ host: 'localhost', port: 1 }] }, @@ -157,7 +157,7 @@ describe('pick_first load balancing policy', () => { ), } ); - const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper, {}); pickFirst.updateAddressList( [ { @@ -191,7 +191,7 @@ describe('pick_first load balancing policy', () => { ), } ); - const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper, {}); pickFirst.updateAddressList( [{ addresses: [{ host: 'localhost', port: 1 }] }], config @@ -207,7 +207,7 @@ describe('pick_first load balancing policy', () => { ), } ); - const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper, {}); pickFirst.updateAddressList( [ { addresses: [{ host: 'localhost', port: 1 }] }, @@ -229,7 +229,7 @@ describe('pick_first load balancing policy', () => { ), } ); - const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper, {}); pickFirst.updateAddressList( [ { addresses: [{ host: 'localhost', port: 1 }] }, @@ -254,7 +254,7 @@ describe('pick_first load balancing policy', () => { ), } ); - const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper, {}); pickFirst.updateAddressList( [ { addresses: [{ host: 'localhost', port: 1 }] }, @@ -293,7 +293,7 @@ describe('pick_first load balancing policy', () => { ), } ); - const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper, {}); pickFirst.updateAddressList( [ { addresses: [{ host: 'localhost', port: 1 }] }, @@ -320,7 +320,7 @@ describe('pick_first load balancing policy', () => { ), } ); - const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper, {}); pickFirst.updateAddressList( [ { addresses: [{ host: 'localhost', port: 1 }] }, @@ -351,7 +351,7 @@ describe('pick_first load balancing policy', () => { ), } ); - const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper, {}); pickFirst.updateAddressList( [ { addresses: [{ host: 'localhost', port: 1 }] }, @@ -389,7 +389,7 @@ describe('pick_first load balancing policy', () => { ), } ); - const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper, {}); pickFirst.updateAddressList( [ { addresses: [{ host: 'localhost', port: 1 }] }, @@ -424,7 +424,7 @@ describe('pick_first load balancing policy', () => { ), } ); - const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper, {}); pickFirst.updateAddressList( [{ addresses: [{ host: 'localhost', port: 1 }] }], config @@ -452,7 +452,7 @@ describe('pick_first load balancing policy', () => { ), } ); - const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper, {}); pickFirst.updateAddressList( [{ addresses: [{ host: 'localhost', port: 1 }] }], config @@ -487,7 +487,7 @@ describe('pick_first load balancing policy', () => { ), } ); - const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper, {}); pickFirst.updateAddressList( [{ addresses: [{ host: 'localhost', port: 1 }] }], config @@ -522,7 +522,7 @@ describe('pick_first load balancing policy', () => { ), } ); - const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper, {}); pickFirst.updateAddressList( [{ addresses: [{ host: 'localhost', port: 1 }] }], config @@ -569,7 +569,7 @@ describe('pick_first load balancing policy', () => { for (let i = 0; i < 10; i++) { endpoints.push({ addresses: [{ host: 'localhost', port: i + 1 }] }); } - const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper, {}); /* Pick from 10 subchannels 5 times, with address randomization enabled, * and verify that at least two different subchannels are picked. The * probability choosing the same address every time is 1/10,000, which @@ -625,7 +625,7 @@ describe('pick_first load balancing policy', () => { for (let i = 0; i < 10; i++) { endpoints.push({ addresses: [{ host: 'localhost', port: i + 1 }] }); } - const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper, {}); pickFirst.updateAddressList(endpoints, config); process.nextTick(() => { pickFirst.updateAddressList(endpoints, config); From 3096f22ba6333f33a4e1299733345b496876f951 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 7 Sep 2023 17:12:58 -0700 Subject: [PATCH 027/109] grpc-js-xds: Add xxhash-wasm dependency, generate ring_hash code --- packages/grpc-js-xds/package.json | 7 +- .../common/v3/ConsistentHashingLbConfig.ts | 67 +++++++ .../common/v3/LocalityLbConfig.ts | 101 ++++++++++ .../common/v3/SlowStartConfig.ts | 71 ++++++++ .../ring_hash/v3/RingHash.ts | 163 +++++++++++++++++ .../generated/google/protobuf/FieldOptions.ts | 5 - .../grpc-js-xds/src/generated/ring_hash.ts | 172 ++++++++++++++++++ 7 files changed, 578 insertions(+), 8 deletions(-) create mode 100644 packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/common/v3/ConsistentHashingLbConfig.ts create mode 100644 packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/common/v3/LocalityLbConfig.ts create mode 100644 packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/common/v3/SlowStartConfig.ts create mode 100644 packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/ring_hash/v3/RingHash.ts create mode 100644 packages/grpc-js-xds/src/generated/ring_hash.ts diff --git a/packages/grpc-js-xds/package.json b/packages/grpc-js-xds/package.json index b43304cea..c240bfee5 100644 --- a/packages/grpc-js-xds/package.json +++ b/packages/grpc-js-xds/package.json @@ -12,7 +12,7 @@ "prepare": "npm run compile", "pretest": "npm run compile", "posttest": "npm run check", - "generate-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --includeDirs deps/envoy-api/ deps/xds/ deps/googleapis/ deps/protoc-gen-validate/ -O src/generated/ --grpcLib @grpc/grpc-js envoy/service/discovery/v3/ads.proto envoy/service/load_stats/v3/lrs.proto envoy/config/listener/v3/listener.proto envoy/config/route/v3/route.proto envoy/config/cluster/v3/cluster.proto envoy/config/endpoint/v3/endpoint.proto envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto udpa/type/v1/typed_struct.proto xds/type/v3/typed_struct.proto envoy/extensions/filters/http/fault/v3/fault.proto envoy/service/status/v3/csds.proto envoy/extensions/load_balancing_policies/wrr_locality/v3/wrr_locality.proto", + "generate-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --includeDirs deps/envoy-api/ deps/xds/ deps/googleapis/ deps/protoc-gen-validate/ -O src/generated/ --grpcLib @grpc/grpc-js envoy/service/discovery/v3/ads.proto envoy/service/load_stats/v3/lrs.proto envoy/config/listener/v3/listener.proto envoy/config/route/v3/route.proto envoy/config/cluster/v3/cluster.proto envoy/config/endpoint/v3/endpoint.proto envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto udpa/type/v1/typed_struct.proto xds/type/v3/typed_struct.proto envoy/extensions/filters/http/fault/v3/fault.proto envoy/service/status/v3/csds.proto envoy/extensions/load_balancing_policies/wrr_locality/v3/wrr_locality.proto envoy/extensions/load_balancing_policies/ring_hash/v3/ring_hash.proto", "generate-interop-types": "proto-loader-gen-types --keep-case --longs String --enums String --defaults --oneofs --json --includeComments --includeDirs proto/ -O interop/generated --grpcLib @grpc/grpc-js grpc/testing/test.proto", "generate-test-types": "proto-loader-gen-types --keep-case --longs String --enums String --defaults --oneofs --json --includeComments --includeDirs proto/ -O test/generated --grpcLib @grpc/grpc-js grpc/testing/echo.proto" }, @@ -39,14 +39,15 @@ "@types/node": "^13.11.1", "@types/yargs": "^15.0.5", "gts": "^2.0.2", - "typescript": "^3.8.3", + "typescript": "^4.9.5", "yargs": "^15.4.1" }, "dependencies": { "@grpc/proto-loader": "^0.6.0", "google-auth-library": "^7.0.2", "re2-wasm": "^1.0.1", - "vscode-uri": "^3.0.7" + "vscode-uri": "^3.0.7", + "xxhash-wasm": "^1.0.2" }, "peerDependencies": { "@grpc/grpc-js": "~1.8.0" diff --git a/packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/common/v3/ConsistentHashingLbConfig.ts b/packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/common/v3/ConsistentHashingLbConfig.ts new file mode 100644 index 000000000..c216720f1 --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/common/v3/ConsistentHashingLbConfig.ts @@ -0,0 +1,67 @@ +// Original file: deps/envoy-api/envoy/extensions/load_balancing_policies/common/v3/common.proto + +import type { UInt32Value as _google_protobuf_UInt32Value, UInt32Value__Output as _google_protobuf_UInt32Value__Output } from '../../../../../google/protobuf/UInt32Value'; + +/** + * Common Configuration for all consistent hashing load balancers (MaglevLb, RingHashLb, etc.) + */ +export interface ConsistentHashingLbConfig { + /** + * If set to ``true``, the cluster will use hostname instead of the resolved + * address as the key to consistently hash to an upstream host. Only valid for StrictDNS clusters with hostnames which resolve to a single IP address. + */ + 'use_hostname_for_hashing'?: (boolean); + /** + * Configures percentage of average cluster load to bound per upstream host. For example, with a value of 150 + * no upstream host will get a load more than 1.5 times the average load of all the hosts in the cluster. + * If not specified, the load is not bounded for any upstream host. Typical value for this parameter is between 120 and 200. + * Minimum is 100. + * + * Applies to both Ring Hash and Maglev load balancers. + * + * This is implemented based on the method described in the paper https://arxiv.org/abs/1608.01350. For the specified + * ``hash_balance_factor``, requests to any upstream host are capped at ``hash_balance_factor/100`` times the average number of requests + * across the cluster. When a request arrives for an upstream host that is currently serving at its max capacity, linear probing + * is used to identify an eligible host. Further, the linear probe is implemented using a random jump in hosts ring/table to identify + * the eligible host (this technique is as described in the paper https://arxiv.org/abs/1908.08762 - the random jump avoids the + * cascading overflow effect when choosing the next host in the ring/table). + * + * If weights are specified on the hosts, they are respected. + * + * This is an O(N) algorithm, unlike other load balancers. Using a lower ``hash_balance_factor`` results in more hosts + * being probed, so use a higher value if you require better performance. + */ + 'hash_balance_factor'?: (_google_protobuf_UInt32Value | null); +} + +/** + * Common Configuration for all consistent hashing load balancers (MaglevLb, RingHashLb, etc.) + */ +export interface ConsistentHashingLbConfig__Output { + /** + * If set to ``true``, the cluster will use hostname instead of the resolved + * address as the key to consistently hash to an upstream host. Only valid for StrictDNS clusters with hostnames which resolve to a single IP address. + */ + 'use_hostname_for_hashing': (boolean); + /** + * Configures percentage of average cluster load to bound per upstream host. For example, with a value of 150 + * no upstream host will get a load more than 1.5 times the average load of all the hosts in the cluster. + * If not specified, the load is not bounded for any upstream host. Typical value for this parameter is between 120 and 200. + * Minimum is 100. + * + * Applies to both Ring Hash and Maglev load balancers. + * + * This is implemented based on the method described in the paper https://arxiv.org/abs/1608.01350. For the specified + * ``hash_balance_factor``, requests to any upstream host are capped at ``hash_balance_factor/100`` times the average number of requests + * across the cluster. When a request arrives for an upstream host that is currently serving at its max capacity, linear probing + * is used to identify an eligible host. Further, the linear probe is implemented using a random jump in hosts ring/table to identify + * the eligible host (this technique is as described in the paper https://arxiv.org/abs/1908.08762 - the random jump avoids the + * cascading overflow effect when choosing the next host in the ring/table). + * + * If weights are specified on the hosts, they are respected. + * + * This is an O(N) algorithm, unlike other load balancers. Using a lower ``hash_balance_factor`` results in more hosts + * being probed, so use a higher value if you require better performance. + */ + 'hash_balance_factor': (_google_protobuf_UInt32Value__Output | null); +} diff --git a/packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/common/v3/LocalityLbConfig.ts b/packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/common/v3/LocalityLbConfig.ts new file mode 100644 index 000000000..d6fecd36b --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/common/v3/LocalityLbConfig.ts @@ -0,0 +1,101 @@ +// Original file: deps/envoy-api/envoy/extensions/load_balancing_policies/common/v3/common.proto + +import type { Percent as _envoy_type_v3_Percent, Percent__Output as _envoy_type_v3_Percent__Output } from '../../../../../envoy/type/v3/Percent'; +import type { UInt64Value as _google_protobuf_UInt64Value, UInt64Value__Output as _google_protobuf_UInt64Value__Output } from '../../../../../google/protobuf/UInt64Value'; +import type { Long } from '@grpc/proto-loader'; + +/** + * Configuration for :ref:`locality weighted load balancing + * ` + */ +export interface _envoy_extensions_load_balancing_policies_common_v3_LocalityLbConfig_LocalityWeightedLbConfig { +} + +/** + * Configuration for :ref:`locality weighted load balancing + * ` + */ +export interface _envoy_extensions_load_balancing_policies_common_v3_LocalityLbConfig_LocalityWeightedLbConfig__Output { +} + +/** + * Configuration for :ref:`zone aware routing + * `. + */ +export interface _envoy_extensions_load_balancing_policies_common_v3_LocalityLbConfig_ZoneAwareLbConfig { + /** + * Configures percentage of requests that will be considered for zone aware routing + * if zone aware routing is configured. If not specified, the default is 100%. + * * :ref:`runtime values `. + * * :ref:`Zone aware routing support `. + */ + 'routing_enabled'?: (_envoy_type_v3_Percent | null); + /** + * Configures minimum upstream cluster size required for zone aware routing + * If upstream cluster size is less than specified, zone aware routing is not performed + * even if zone aware routing is configured. If not specified, the default is 6. + * * :ref:`runtime values `. + * * :ref:`Zone aware routing support `. + */ + 'min_cluster_size'?: (_google_protobuf_UInt64Value | null); + /** + * If set to true, Envoy will not consider any hosts when the cluster is in :ref:`panic + * mode`. Instead, the cluster will fail all + * requests as if all hosts are unhealthy. This can help avoid potentially overwhelming a + * failing service. + */ + 'fail_traffic_on_panic'?: (boolean); +} + +/** + * Configuration for :ref:`zone aware routing + * `. + */ +export interface _envoy_extensions_load_balancing_policies_common_v3_LocalityLbConfig_ZoneAwareLbConfig__Output { + /** + * Configures percentage of requests that will be considered for zone aware routing + * if zone aware routing is configured. If not specified, the default is 100%. + * * :ref:`runtime values `. + * * :ref:`Zone aware routing support `. + */ + 'routing_enabled': (_envoy_type_v3_Percent__Output | null); + /** + * Configures minimum upstream cluster size required for zone aware routing + * If upstream cluster size is less than specified, zone aware routing is not performed + * even if zone aware routing is configured. If not specified, the default is 6. + * * :ref:`runtime values `. + * * :ref:`Zone aware routing support `. + */ + 'min_cluster_size': (_google_protobuf_UInt64Value__Output | null); + /** + * If set to true, Envoy will not consider any hosts when the cluster is in :ref:`panic + * mode`. Instead, the cluster will fail all + * requests as if all hosts are unhealthy. This can help avoid potentially overwhelming a + * failing service. + */ + 'fail_traffic_on_panic': (boolean); +} + +export interface LocalityLbConfig { + /** + * Configuration for local zone aware load balancing. + */ + 'zone_aware_lb_config'?: (_envoy_extensions_load_balancing_policies_common_v3_LocalityLbConfig_ZoneAwareLbConfig | null); + /** + * Enable locality weighted load balancing. + */ + 'locality_weighted_lb_config'?: (_envoy_extensions_load_balancing_policies_common_v3_LocalityLbConfig_LocalityWeightedLbConfig | null); + 'locality_config_specifier'?: "zone_aware_lb_config"|"locality_weighted_lb_config"; +} + +export interface LocalityLbConfig__Output { + /** + * Configuration for local zone aware load balancing. + */ + 'zone_aware_lb_config'?: (_envoy_extensions_load_balancing_policies_common_v3_LocalityLbConfig_ZoneAwareLbConfig__Output | null); + /** + * Enable locality weighted load balancing. + */ + 'locality_weighted_lb_config'?: (_envoy_extensions_load_balancing_policies_common_v3_LocalityLbConfig_LocalityWeightedLbConfig__Output | null); + 'locality_config_specifier': "zone_aware_lb_config"|"locality_weighted_lb_config"; +} diff --git a/packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/common/v3/SlowStartConfig.ts b/packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/common/v3/SlowStartConfig.ts new file mode 100644 index 000000000..bc222ff89 --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/common/v3/SlowStartConfig.ts @@ -0,0 +1,71 @@ +// Original file: deps/envoy-api/envoy/extensions/load_balancing_policies/common/v3/common.proto + +import type { Duration as _google_protobuf_Duration, Duration__Output as _google_protobuf_Duration__Output } from '../../../../../google/protobuf/Duration'; +import type { RuntimeDouble as _envoy_config_core_v3_RuntimeDouble, RuntimeDouble__Output as _envoy_config_core_v3_RuntimeDouble__Output } from '../../../../../envoy/config/core/v3/RuntimeDouble'; +import type { Percent as _envoy_type_v3_Percent, Percent__Output as _envoy_type_v3_Percent__Output } from '../../../../../envoy/type/v3/Percent'; + +/** + * Configuration for :ref:`slow start mode `. + */ +export interface SlowStartConfig { + /** + * Represents the size of slow start window. + * If set, the newly created host remains in slow start mode starting from its creation time + * for the duration of slow start window. + */ + 'slow_start_window'?: (_google_protobuf_Duration | null); + /** + * This parameter controls the speed of traffic increase over the slow start window. Defaults to 1.0, + * so that endpoint would get linearly increasing amount of traffic. + * When increasing the value for this parameter, the speed of traffic ramp-up increases non-linearly. + * The value of aggression parameter should be greater than 0.0. + * By tuning the parameter, is possible to achieve polynomial or exponential shape of ramp-up curve. + * + * During slow start window, effective weight of an endpoint would be scaled with time factor and aggression: + * ``new_weight = weight * max(min_weight_percent, time_factor ^ (1 / aggression))``, + * where ``time_factor=(time_since_start_seconds / slow_start_time_seconds)``. + * + * As time progresses, more and more traffic would be sent to endpoint, which is in slow start window. + * Once host exits slow start, time_factor and aggression no longer affect its weight. + */ + 'aggression'?: (_envoy_config_core_v3_RuntimeDouble | null); + /** + * Configures the minimum percentage of origin weight that avoids too small new weight, + * which may cause endpoints in slow start mode receive no traffic in slow start window. + * If not specified, the default is 10%. + */ + 'min_weight_percent'?: (_envoy_type_v3_Percent | null); +} + +/** + * Configuration for :ref:`slow start mode `. + */ +export interface SlowStartConfig__Output { + /** + * Represents the size of slow start window. + * If set, the newly created host remains in slow start mode starting from its creation time + * for the duration of slow start window. + */ + 'slow_start_window': (_google_protobuf_Duration__Output | null); + /** + * This parameter controls the speed of traffic increase over the slow start window. Defaults to 1.0, + * so that endpoint would get linearly increasing amount of traffic. + * When increasing the value for this parameter, the speed of traffic ramp-up increases non-linearly. + * The value of aggression parameter should be greater than 0.0. + * By tuning the parameter, is possible to achieve polynomial or exponential shape of ramp-up curve. + * + * During slow start window, effective weight of an endpoint would be scaled with time factor and aggression: + * ``new_weight = weight * max(min_weight_percent, time_factor ^ (1 / aggression))``, + * where ``time_factor=(time_since_start_seconds / slow_start_time_seconds)``. + * + * As time progresses, more and more traffic would be sent to endpoint, which is in slow start window. + * Once host exits slow start, time_factor and aggression no longer affect its weight. + */ + 'aggression': (_envoy_config_core_v3_RuntimeDouble__Output | null); + /** + * Configures the minimum percentage of origin weight that avoids too small new weight, + * which may cause endpoints in slow start mode receive no traffic in slow start window. + * If not specified, the default is 10%. + */ + 'min_weight_percent': (_envoy_type_v3_Percent__Output | null); +} diff --git a/packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/ring_hash/v3/RingHash.ts b/packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/ring_hash/v3/RingHash.ts new file mode 100644 index 000000000..4e2a73031 --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/ring_hash/v3/RingHash.ts @@ -0,0 +1,163 @@ +// Original file: deps/envoy-api/envoy/extensions/load_balancing_policies/ring_hash/v3/ring_hash.proto + +import type { UInt64Value as _google_protobuf_UInt64Value, UInt64Value__Output as _google_protobuf_UInt64Value__Output } from '../../../../../google/protobuf/UInt64Value'; +import type { UInt32Value as _google_protobuf_UInt32Value, UInt32Value__Output as _google_protobuf_UInt32Value__Output } from '../../../../../google/protobuf/UInt32Value'; +import type { ConsistentHashingLbConfig as _envoy_extensions_load_balancing_policies_common_v3_ConsistentHashingLbConfig, ConsistentHashingLbConfig__Output as _envoy_extensions_load_balancing_policies_common_v3_ConsistentHashingLbConfig__Output } from '../../../../../envoy/extensions/load_balancing_policies/common/v3/ConsistentHashingLbConfig'; +import type { _envoy_extensions_load_balancing_policies_common_v3_LocalityLbConfig_LocalityWeightedLbConfig, _envoy_extensions_load_balancing_policies_common_v3_LocalityLbConfig_LocalityWeightedLbConfig__Output } from '../../../../../envoy/extensions/load_balancing_policies/common/v3/LocalityLbConfig'; +import type { Long } from '@grpc/proto-loader'; + +// Original file: deps/envoy-api/envoy/extensions/load_balancing_policies/ring_hash/v3/ring_hash.proto + +/** + * The hash function used to hash hosts onto the ketama ring. + */ +export enum _envoy_extensions_load_balancing_policies_ring_hash_v3_RingHash_HashFunction { + /** + * Currently defaults to XX_HASH. + */ + DEFAULT_HASH = 0, + /** + * Use `xxHash `_. + */ + XX_HASH = 1, + /** + * Use `MurmurHash2 `_, this is compatible with + * std:hash in GNU libstdc++ 3.4.20 or above. This is typically the case when compiled + * on Linux and not macOS. + */ + MURMUR_HASH_2 = 2, +} + +/** + * This configuration allows the built-in RING_HASH LB policy to be configured via the LB policy + * extension point. See the :ref:`load balancing architecture overview + * ` for more information. + * [#next-free-field: 8] + */ +export interface RingHash { + /** + * The hash function used to hash hosts onto the ketama ring. The value defaults to + * :ref:`XX_HASH`. + */ + 'hash_function'?: (_envoy_extensions_load_balancing_policies_ring_hash_v3_RingHash_HashFunction | keyof typeof _envoy_extensions_load_balancing_policies_ring_hash_v3_RingHash_HashFunction); + /** + * Minimum hash ring size. The larger the ring is (that is, the more hashes there are for each + * provided host) the better the request distribution will reflect the desired weights. Defaults + * to 1024 entries, and limited to 8M entries. See also + * :ref:`maximum_ring_size`. + */ + 'minimum_ring_size'?: (_google_protobuf_UInt64Value | null); + /** + * Maximum hash ring size. Defaults to 8M entries, and limited to 8M entries, but can be lowered + * to further constrain resource use. See also + * :ref:`minimum_ring_size`. + */ + 'maximum_ring_size'?: (_google_protobuf_UInt64Value | null); + /** + * If set to `true`, the cluster will use hostname instead of the resolved + * address as the key to consistently hash to an upstream host. Only valid for StrictDNS clusters with hostnames which resolve to a single IP address. + * + * ..note:: + * This is deprecated and please use :ref:`consistent_hashing_lb_config + * ` instead. + */ + 'use_hostname_for_hashing'?: (boolean); + /** + * Configures percentage of average cluster load to bound per upstream host. For example, with a value of 150 + * no upstream host will get a load more than 1.5 times the average load of all the hosts in the cluster. + * If not specified, the load is not bounded for any upstream host. Typical value for this parameter is between 120 and 200. + * Minimum is 100. + * + * This is implemented based on the method described in the paper https://arxiv.org/abs/1608.01350. For the specified + * `hash_balance_factor`, requests to any upstream host are capped at `hash_balance_factor/100` times the average number of requests + * across the cluster. When a request arrives for an upstream host that is currently serving at its max capacity, linear probing + * is used to identify an eligible host. Further, the linear probe is implemented using a random jump in hosts ring/table to identify + * the eligible host (this technique is as described in the paper https://arxiv.org/abs/1908.08762 - the random jump avoids the + * cascading overflow effect when choosing the next host in the ring/table). + * + * If weights are specified on the hosts, they are respected. + * + * This is an O(N) algorithm, unlike other load balancers. Using a lower `hash_balance_factor` results in more hosts + * being probed, so use a higher value if you require better performance. + * + * ..note:: + * This is deprecated and please use :ref:`consistent_hashing_lb_config + * ` instead. + */ + 'hash_balance_factor'?: (_google_protobuf_UInt32Value | null); + /** + * Common configuration for hashing-based load balancing policies. + */ + 'consistent_hashing_lb_config'?: (_envoy_extensions_load_balancing_policies_common_v3_ConsistentHashingLbConfig | null); + /** + * Enable locality weighted load balancing for ring hash lb explicitly. + */ + 'locality_weighted_lb_config'?: (_envoy_extensions_load_balancing_policies_common_v3_LocalityLbConfig_LocalityWeightedLbConfig | null); +} + +/** + * This configuration allows the built-in RING_HASH LB policy to be configured via the LB policy + * extension point. See the :ref:`load balancing architecture overview + * ` for more information. + * [#next-free-field: 8] + */ +export interface RingHash__Output { + /** + * The hash function used to hash hosts onto the ketama ring. The value defaults to + * :ref:`XX_HASH`. + */ + 'hash_function': (keyof typeof _envoy_extensions_load_balancing_policies_ring_hash_v3_RingHash_HashFunction); + /** + * Minimum hash ring size. The larger the ring is (that is, the more hashes there are for each + * provided host) the better the request distribution will reflect the desired weights. Defaults + * to 1024 entries, and limited to 8M entries. See also + * :ref:`maximum_ring_size`. + */ + 'minimum_ring_size': (_google_protobuf_UInt64Value__Output | null); + /** + * Maximum hash ring size. Defaults to 8M entries, and limited to 8M entries, but can be lowered + * to further constrain resource use. See also + * :ref:`minimum_ring_size`. + */ + 'maximum_ring_size': (_google_protobuf_UInt64Value__Output | null); + /** + * If set to `true`, the cluster will use hostname instead of the resolved + * address as the key to consistently hash to an upstream host. Only valid for StrictDNS clusters with hostnames which resolve to a single IP address. + * + * ..note:: + * This is deprecated and please use :ref:`consistent_hashing_lb_config + * ` instead. + */ + 'use_hostname_for_hashing': (boolean); + /** + * Configures percentage of average cluster load to bound per upstream host. For example, with a value of 150 + * no upstream host will get a load more than 1.5 times the average load of all the hosts in the cluster. + * If not specified, the load is not bounded for any upstream host. Typical value for this parameter is between 120 and 200. + * Minimum is 100. + * + * This is implemented based on the method described in the paper https://arxiv.org/abs/1608.01350. For the specified + * `hash_balance_factor`, requests to any upstream host are capped at `hash_balance_factor/100` times the average number of requests + * across the cluster. When a request arrives for an upstream host that is currently serving at its max capacity, linear probing + * is used to identify an eligible host. Further, the linear probe is implemented using a random jump in hosts ring/table to identify + * the eligible host (this technique is as described in the paper https://arxiv.org/abs/1908.08762 - the random jump avoids the + * cascading overflow effect when choosing the next host in the ring/table). + * + * If weights are specified on the hosts, they are respected. + * + * This is an O(N) algorithm, unlike other load balancers. Using a lower `hash_balance_factor` results in more hosts + * being probed, so use a higher value if you require better performance. + * + * ..note:: + * This is deprecated and please use :ref:`consistent_hashing_lb_config + * ` instead. + */ + 'hash_balance_factor': (_google_protobuf_UInt32Value__Output | null); + /** + * Common configuration for hashing-based load balancing policies. + */ + 'consistent_hashing_lb_config': (_envoy_extensions_load_balancing_policies_common_v3_ConsistentHashingLbConfig__Output | null); + /** + * Enable locality weighted load balancing for ring hash lb explicitly. + */ + 'locality_weighted_lb_config': (_envoy_extensions_load_balancing_policies_common_v3_LocalityLbConfig_LocalityWeightedLbConfig__Output | null); +} diff --git a/packages/grpc-js-xds/src/generated/google/protobuf/FieldOptions.ts b/packages/grpc-js-xds/src/generated/google/protobuf/FieldOptions.ts index d62db88d0..f59acfbfe 100644 --- a/packages/grpc-js-xds/src/generated/google/protobuf/FieldOptions.ts +++ b/packages/grpc-js-xds/src/generated/google/protobuf/FieldOptions.ts @@ -2,7 +2,6 @@ import type { UninterpretedOption as _google_protobuf_UninterpretedOption, UninterpretedOption__Output as _google_protobuf_UninterpretedOption__Output } from '../../google/protobuf/UninterpretedOption'; import type { FieldRules as _validate_FieldRules, FieldRules__Output as _validate_FieldRules__Output } from '../../validate/FieldRules'; -import type { FieldSecurityAnnotation as _udpa_annotations_FieldSecurityAnnotation, FieldSecurityAnnotation__Output as _udpa_annotations_FieldSecurityAnnotation__Output } from '../../udpa/annotations/FieldSecurityAnnotation'; import type { FieldMigrateAnnotation as _udpa_annotations_FieldMigrateAnnotation, FieldMigrateAnnotation__Output as _udpa_annotations_FieldMigrateAnnotation__Output } from '../../udpa/annotations/FieldMigrateAnnotation'; import type { FieldStatusAnnotation as _xds_annotations_v3_FieldStatusAnnotation, FieldStatusAnnotation__Output as _xds_annotations_v3_FieldStatusAnnotation__Output } from '../../xds/annotations/v3/FieldStatusAnnotation'; @@ -31,8 +30,6 @@ export interface FieldOptions { 'weak'?: (boolean); 'uninterpretedOption'?: (_google_protobuf_UninterpretedOption)[]; '.validate.rules'?: (_validate_FieldRules | null); - '.udpa.annotations.security'?: (_udpa_annotations_FieldSecurityAnnotation | null); - '.udpa.annotations.sensitive'?: (boolean); '.envoy.annotations.deprecated_at_minor_version'?: (string); '.udpa.annotations.field_migrate'?: (_udpa_annotations_FieldMigrateAnnotation | null); '.envoy.annotations.disallowed_by_default'?: (boolean); @@ -48,8 +45,6 @@ export interface FieldOptions__Output { 'weak': (boolean); 'uninterpretedOption': (_google_protobuf_UninterpretedOption__Output)[]; '.validate.rules': (_validate_FieldRules__Output | null); - '.udpa.annotations.security': (_udpa_annotations_FieldSecurityAnnotation__Output | null); - '.udpa.annotations.sensitive': (boolean); '.envoy.annotations.deprecated_at_minor_version': (string); '.udpa.annotations.field_migrate': (_udpa_annotations_FieldMigrateAnnotation__Output | null); '.envoy.annotations.disallowed_by_default': (boolean); diff --git a/packages/grpc-js-xds/src/generated/ring_hash.ts b/packages/grpc-js-xds/src/generated/ring_hash.ts new file mode 100644 index 000000000..d298067df --- /dev/null +++ b/packages/grpc-js-xds/src/generated/ring_hash.ts @@ -0,0 +1,172 @@ +import type * as grpc from '@grpc/grpc-js'; +import type { EnumTypeDefinition, MessageTypeDefinition } from '@grpc/proto-loader'; + + +type SubtypeConstructor any, Subtype> = { + new(...args: ConstructorParameters): Subtype; +}; + +export interface ProtoGrpcType { + envoy: { + annotations: { + } + config: { + core: { + v3: { + Address: MessageTypeDefinition + AsyncDataSource: MessageTypeDefinition + BackoffStrategy: MessageTypeDefinition + BindConfig: MessageTypeDefinition + BuildVersion: MessageTypeDefinition + CidrRange: MessageTypeDefinition + ControlPlane: MessageTypeDefinition + DataSource: MessageTypeDefinition + EnvoyInternalAddress: MessageTypeDefinition + Extension: MessageTypeDefinition + ExtraSourceAddress: MessageTypeDefinition + HeaderMap: MessageTypeDefinition + HeaderValue: MessageTypeDefinition + HeaderValueOption: MessageTypeDefinition + HttpUri: MessageTypeDefinition + Locality: MessageTypeDefinition + Metadata: MessageTypeDefinition + Node: MessageTypeDefinition + Pipe: MessageTypeDefinition + QueryParameter: MessageTypeDefinition + RemoteDataSource: MessageTypeDefinition + RequestMethod: EnumTypeDefinition + RetryPolicy: MessageTypeDefinition + RoutingPriority: EnumTypeDefinition + RuntimeDouble: MessageTypeDefinition + RuntimeFeatureFlag: MessageTypeDefinition + RuntimeFractionalPercent: MessageTypeDefinition + RuntimePercent: MessageTypeDefinition + RuntimeUInt32: MessageTypeDefinition + SocketAddress: MessageTypeDefinition + SocketOption: MessageTypeDefinition + SocketOptionsOverride: MessageTypeDefinition + TcpKeepalive: MessageTypeDefinition + TrafficDirection: EnumTypeDefinition + TransportSocket: MessageTypeDefinition + WatchedDirectory: MessageTypeDefinition + } + } + } + extensions: { + load_balancing_policies: { + common: { + v3: { + ConsistentHashingLbConfig: MessageTypeDefinition + LocalityLbConfig: MessageTypeDefinition + SlowStartConfig: MessageTypeDefinition + } + } + ring_hash: { + v3: { + RingHash: MessageTypeDefinition + } + } + } + } + type: { + v3: { + FractionalPercent: MessageTypeDefinition + Percent: MessageTypeDefinition + SemanticVersion: MessageTypeDefinition + } + } + } + google: { + protobuf: { + Any: MessageTypeDefinition + BoolValue: MessageTypeDefinition + BytesValue: MessageTypeDefinition + DescriptorProto: MessageTypeDefinition + DoubleValue: MessageTypeDefinition + Duration: MessageTypeDefinition + EnumDescriptorProto: MessageTypeDefinition + EnumOptions: MessageTypeDefinition + EnumValueDescriptorProto: MessageTypeDefinition + EnumValueOptions: MessageTypeDefinition + FieldDescriptorProto: MessageTypeDefinition + FieldOptions: MessageTypeDefinition + FileDescriptorProto: MessageTypeDefinition + FileDescriptorSet: MessageTypeDefinition + FileOptions: MessageTypeDefinition + FloatValue: MessageTypeDefinition + GeneratedCodeInfo: MessageTypeDefinition + Int32Value: MessageTypeDefinition + Int64Value: MessageTypeDefinition + ListValue: MessageTypeDefinition + MessageOptions: MessageTypeDefinition + MethodDescriptorProto: MessageTypeDefinition + MethodOptions: MessageTypeDefinition + NullValue: EnumTypeDefinition + OneofDescriptorProto: MessageTypeDefinition + OneofOptions: MessageTypeDefinition + ServiceDescriptorProto: MessageTypeDefinition + ServiceOptions: MessageTypeDefinition + SourceCodeInfo: MessageTypeDefinition + StringValue: MessageTypeDefinition + Struct: MessageTypeDefinition + Timestamp: MessageTypeDefinition + UInt32Value: MessageTypeDefinition + UInt64Value: MessageTypeDefinition + UninterpretedOption: MessageTypeDefinition + Value: MessageTypeDefinition + } + } + udpa: { + annotations: { + FieldMigrateAnnotation: MessageTypeDefinition + FileMigrateAnnotation: MessageTypeDefinition + MigrateAnnotation: MessageTypeDefinition + PackageVersionStatus: EnumTypeDefinition + StatusAnnotation: MessageTypeDefinition + VersioningAnnotation: MessageTypeDefinition + } + } + validate: { + AnyRules: MessageTypeDefinition + BoolRules: MessageTypeDefinition + BytesRules: MessageTypeDefinition + DoubleRules: MessageTypeDefinition + DurationRules: MessageTypeDefinition + EnumRules: MessageTypeDefinition + FieldRules: MessageTypeDefinition + Fixed32Rules: MessageTypeDefinition + Fixed64Rules: MessageTypeDefinition + FloatRules: MessageTypeDefinition + Int32Rules: MessageTypeDefinition + Int64Rules: MessageTypeDefinition + KnownRegex: EnumTypeDefinition + MapRules: MessageTypeDefinition + MessageRules: MessageTypeDefinition + RepeatedRules: MessageTypeDefinition + SFixed32Rules: MessageTypeDefinition + SFixed64Rules: MessageTypeDefinition + SInt32Rules: MessageTypeDefinition + SInt64Rules: MessageTypeDefinition + StringRules: MessageTypeDefinition + TimestampRules: MessageTypeDefinition + UInt32Rules: MessageTypeDefinition + UInt64Rules: MessageTypeDefinition + } + xds: { + annotations: { + v3: { + FieldStatusAnnotation: MessageTypeDefinition + FileStatusAnnotation: MessageTypeDefinition + MessageStatusAnnotation: MessageTypeDefinition + PackageVersionStatus: EnumTypeDefinition + StatusAnnotation: MessageTypeDefinition + } + } + core: { + v3: { + ContextParams: MessageTypeDefinition + } + } + } +} + From 3a43cba3a3a5afb5c0bbfc621922ee023f977e5e Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 7 Sep 2023 17:14:39 -0700 Subject: [PATCH 028/109] grpc-js-xds: Implement ring_hash LB policy --- packages/grpc-js-xds/gulpfile.ts | 1 + packages/grpc-js-xds/src/environment.ts | 1 + packages/grpc-js-xds/src/http-filter.ts | 4 +- packages/grpc-js-xds/src/index.ts | 2 + .../grpc-js-xds/src/load-balancer-priority.ts | 21 +- .../src/load-balancer-ring-hash.ts | 507 ++++++++++++++++++ .../src/load-balancer-xds-cluster-manager.ts | 30 +- .../src/load-balancer-xds-cluster-resolver.ts | 29 +- .../src/load-balancer-xds-wrr-locality.ts | 2 +- packages/grpc-js-xds/src/matcher.ts | 4 +- packages/grpc-js-xds/src/resolver-xds.ts | 44 +- packages/grpc-js-xds/src/route-action.ts | 92 +++- packages/grpc-js-xds/src/xds-bootstrap.ts | 6 +- packages/grpc-js-xds/src/xds-client.ts | 4 +- .../cluster-resource-type.ts | 23 +- packages/grpc-js-xds/src/xxhash.ts | 31 ++ packages/grpc-js-xds/test/framework.ts | 27 +- .../grpc-js-xds/test/test-confg-parsing.ts | 31 ++ packages/grpc-js-xds/test/test-ring-hash.ts | 108 ++++ packages/grpc-js-xds/test/xds-server.ts | 1 + packages/grpc-js-xds/tsconfig.json | 4 +- packages/grpc-js/src/channel-options.ts | 2 + packages/grpc-js/src/experimental.ts | 1 + packages/grpc-js/src/internal-channel.ts | 11 +- .../src/load-balancer-outlier-detection.ts | 122 +---- .../grpc-js/src/load-balancer-pick-first.ts | 13 + packages/grpc-js/src/picker.ts | 36 +- packages/grpc-js/src/resolver.ts | 2 +- .../grpc-js/src/resolving-load-balancer.ts | 2 +- packages/grpc-js/src/subchannel-address.ts | 124 +++++ 30 files changed, 1081 insertions(+), 204 deletions(-) create mode 100644 packages/grpc-js-xds/src/load-balancer-ring-hash.ts create mode 100644 packages/grpc-js-xds/src/xxhash.ts create mode 100644 packages/grpc-js-xds/test/test-ring-hash.ts diff --git a/packages/grpc-js-xds/gulpfile.ts b/packages/grpc-js-xds/gulpfile.ts index 6f17a4020..2bb43a7f1 100644 --- a/packages/grpc-js-xds/gulpfile.ts +++ b/packages/grpc-js-xds/gulpfile.ts @@ -63,6 +63,7 @@ const compile = checkTask(() => execNpmCommand('compile')); const runTests = checkTask(() => { process.env.GRPC_EXPERIMENTAL_XDS_FEDERATION = 'true'; process.env.GRPC_EXPERIMENTAL_XDS_CUSTOM_LB_CONFIG = 'true'; + process.env.GRPC_XDS_EXPERIMENTAL_ENABLE_RING_HASH = 'true'; return gulp.src(`${outDir}/test/**/*.js`) .pipe(mocha({reporter: 'mocha-jenkins-reporter', require: ['ts-node/register']})); diff --git a/packages/grpc-js-xds/src/environment.ts b/packages/grpc-js-xds/src/environment.ts index 530bb256c..858903111 100644 --- a/packages/grpc-js-xds/src/environment.ts +++ b/packages/grpc-js-xds/src/environment.ts @@ -20,3 +20,4 @@ export const EXPERIMENTAL_OUTLIER_DETECTION = (process.env.GRPC_EXPERIMENTAL_ENA export const EXPERIMENTAL_RETRY = (process.env.GRPC_XDS_EXPERIMENTAL_ENABLE_RETRY ?? 'true') === 'true'; export const EXPERIMENTAL_FEDERATION = (process.env.GRPC_EXPERIMENTAL_XDS_FEDERATION ?? 'false') === 'true'; export const EXPERIMENTAL_CUSTOM_LB_CONFIG = (process.env.GRPC_EXPERIMENTAL_XDS_CUSTOM_LB_CONFIG ?? 'false') === 'true'; +export const EXPERIMENTAL_RING_HASH = (process.env.GRPC_XDS_EXPERIMENTAL_ENABLE_RING_HASH ?? 'false') === 'true'; diff --git a/packages/grpc-js-xds/src/http-filter.ts b/packages/grpc-js-xds/src/http-filter.ts index 29ce5958f..f8da5b828 100644 --- a/packages/grpc-js-xds/src/http-filter.ts +++ b/packages/grpc-js-xds/src/http-filter.ts @@ -116,7 +116,7 @@ export function validateTopLevelFilter(httpFilter: HttpFilter__Output): boolean try { typeUrl = getTopLevelFilterUrl(encodedConfig); } catch (e) { - trace(httpFilter.name + ' validation failed with error ' + e.message); + trace(httpFilter.name + ' validation failed with error ' + (e as Error).message); return false; } const registryEntry = FILTER_REGISTRY.get(typeUrl); @@ -243,4 +243,4 @@ export function createHttpFilter(config: HttpFilterConfig, overrideConfig?: Http } else { return null; } -} \ No newline at end of file +} diff --git a/packages/grpc-js-xds/src/index.ts b/packages/grpc-js-xds/src/index.ts index 95c26a20a..70aa5bef2 100644 --- a/packages/grpc-js-xds/src/index.ts +++ b/packages/grpc-js-xds/src/index.ts @@ -23,6 +23,7 @@ import * as load_balancer_priority from './load-balancer-priority'; import * as load_balancer_weighted_target from './load-balancer-weighted-target'; import * as load_balancer_xds_cluster_manager from './load-balancer-xds-cluster-manager'; import * as xds_wrr_locality from './load-balancer-xds-wrr-locality'; +import * as ring_hash from './load-balancer-ring-hash'; import * as router_filter from './http-filter/router-filter'; import * as fault_injection_filter from './http-filter/fault-injection-filter'; import * as csds from './csds'; @@ -41,6 +42,7 @@ export function register() { load_balancer_weighted_target.setup(); load_balancer_xds_cluster_manager.setup(); xds_wrr_locality.setup(); + ring_hash.setup(); router_filter.setup(); fault_injection_filter.setup(); csds.setup(); diff --git a/packages/grpc-js-xds/src/load-balancer-priority.ts b/packages/grpc-js-xds/src/load-balancer-priority.ts index 4a4acb477..54e01fa84 100644 --- a/packages/grpc-js-xds/src/load-balancer-priority.ts +++ b/packages/grpc-js-xds/src/load-balancer-priority.ts @@ -41,9 +41,26 @@ const DEFAULT_FAILOVER_TIME_MS = 10_000; const DEFAULT_RETENTION_INTERVAL_MS = 15 * 60 * 1000; export interface LocalityEndpoint extends Endpoint { + /** + * A sequence of strings that determines how to divide endpoints up in priority and + * weighted_target. + */ localityPath: string[]; + /** + * The locality this endpoint is in. Used in wrr_locality and xds_cluster_impl. + */ locality: Locality__Output; - weight: number; + /** + * The load balancing weight for the entire locality that contains this + * endpoint. Used in xds_wrr_locality. + */ + localityWeight: number; + /** + * The overall load balancing weight for this endpoint, calculated as the + * product of the load balancing weight for this endpoint within its locality + * and the load balancing weight of the locality. Used in ring_hash. + */ + endpointWeight: number; }; export function isLocalityEndpoint( @@ -317,7 +334,7 @@ export class PriorityLoadBalancer implements LoadBalancer { * so that when the picker calls exitIdle, that in turn calls exitIdle on * the PriorityChildImpl, which will start the failover timer. */ if (state === ConnectivityState.IDLE) { - picker = new QueuePicker(this); + picker = new QueuePicker(this, picker); } this.channelControlHelper.updateState(state, picker); } diff --git a/packages/grpc-js-xds/src/load-balancer-ring-hash.ts b/packages/grpc-js-xds/src/load-balancer-ring-hash.ts new file mode 100644 index 000000000..a124d3b88 --- /dev/null +++ b/packages/grpc-js-xds/src/load-balancer-ring-hash.ts @@ -0,0 +1,507 @@ +/* + * 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 { experimental, logVerbosity, connectivityState, status, Metadata, ChannelOptions, LoadBalancingConfig } from '@grpc/grpc-js'; +import { isLocalityEndpoint } from './load-balancer-priority'; +import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; +import LeafLoadBalancer = experimental.LeafLoadBalancer; +import Endpoint = experimental.Endpoint; +import Picker = experimental.Picker; +import PickArgs = experimental.PickArgs; +import PickResult = experimental.PickResult; +import PickResultType = experimental.PickResultType; +import LoadBalancer = experimental.LoadBalancer; +import ChannelControlHelper = experimental.ChannelControlHelper; +import createChildChannelControlHelper = experimental.createChildChannelControlHelper; +import UnavailablePicker = experimental.UnavailablePicker; +import subchannelAddressToString = experimental.subchannelAddressToString; +import registerLoadBalancerType = experimental.registerLoadBalancerType; +import EndpointMap = experimental.EndpointMap; +import { loadXxhashApi, xxhashApi } from './xxhash'; +import { EXPERIMENTAL_RING_HASH } from './environment'; +import { loadProtosWithOptionsSync } from '@grpc/proto-loader/build/src/util'; +import { RingHash__Output } from './generated/envoy/extensions/load_balancing_policies/ring_hash/v3/RingHash'; +import { Any__Output } from './generated/google/protobuf/Any'; +import { TypedExtensionConfig__Output } from './generated/envoy/config/core/v3/TypedExtensionConfig'; +import { LoadBalancingPolicy__Output } from './generated/envoy/config/cluster/v3/LoadBalancingPolicy'; +import { registerLbPolicy } from './lb-policy-registry'; + +const TRACER_NAME = 'ring_hash'; + +function trace(text: string): void { + experimental.trace(logVerbosity.DEBUG, TRACER_NAME, text); +} + +const TYPE_NAME = 'ring_hash'; + +const DEFAULT_MIN_RING_SIZE = 1024; +const DEFAULT_MAX_RING_SIZE = 4096; +const ABSOLUTE_MAX_RING_SIZE = 8_388_608; +const DEFAULT_RING_SIZE_CAP = 4096; + +class RingHashLoadBalancingConfig implements TypedLoadBalancingConfig { + private minRingSize: number; + private maxRingSize: number; + constructor(minRingSize?: number, maxRingSize?: number) { + this.minRingSize = Math.min( + minRingSize ?? DEFAULT_MIN_RING_SIZE, + ABSOLUTE_MAX_RING_SIZE + ); + this.maxRingSize = Math.min( + maxRingSize ?? DEFAULT_MAX_RING_SIZE, + ABSOLUTE_MAX_RING_SIZE + ); + } + getLoadBalancerName(): string { + return TYPE_NAME; + } + toJsonObject(): object { + return { + [TYPE_NAME]: { + min_ring_size: this.minRingSize, + max_ring_size: this.maxRingSize, + } + }; + } + getMinRingSize() { + return this.minRingSize; + } + getMaxRingSize() { + return this.maxRingSize; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static createFromJson(obj: any): TypedLoadBalancingConfig { + if ('min_ring_size' in obj) { + if (typeof obj.min_ring_size === 'number') { + if (obj.min_ring_size > ABSOLUTE_MAX_RING_SIZE) { + throw new Error(`ring_hash config field min_ring_size exceeds the cap of ${ABSOLUTE_MAX_RING_SIZE}: ${obj.min_ring_size}`); + } + } else { + throw new Error( + 'ring_hash config field min_ring_size must be a number if provided' + ); + } + } + if ('max_ring_size' in obj) { + if (typeof obj.max_ring_size === 'number') { + if (obj.max_ring_size > ABSOLUTE_MAX_RING_SIZE) { + throw new Error(`ring_hash config field max_ring_size exceeds the cap of ${ABSOLUTE_MAX_RING_SIZE}: ${obj.max_ring_size}`); + } + } else { + throw new Error( + 'ring_hash config field max_ring_size must be a number if provided' + ); + } + } + return new RingHashLoadBalancingConfig( + obj.min_ring_size, + obj.max_ring_size + ); + } +} + +interface RingEntry { + leafBalancer: LeafLoadBalancer; + hash: bigint; +} + +interface EndpointWeight { + endpoint: Endpoint; + weight: number; + normalizedWeight: number; +} + +class RingHashPicker implements Picker { + constructor(private ring: RingEntry[]) {} + /** + * Find the least index in the ring with a hash greater than or equal to the + * hash parameter, or 0 if no such index exists. + * @param hash + */ + private findIndexForHash(hash: bigint): number { + // Binary search to find the target index + let low = 0; + let high = this.ring.length; + let index = 0; + while (low <= high) { + /* Commonly in binary search, this operation can overflow and result in + * the wrong value. However, in this case the ring size is absolutely + * limtied to 1<<23, so low+high < MAX_SAFE_INTEGER */ + index = Math.floor((low + high) / 2); + if (index === this.ring.length) { + index = 0; + break; + } + const midval = this.ring[index].hash; + const midval1 = index === 0 ? 0n : this.ring[index - 1].hash; + if (hash <= midval && hash > midval1) { + break; + } + if (midval < hash) { + low = index + 1; + } else { + high = index - 1; + } + if (low > high) { + index = 0; + break; + } + } + return index; + } + pick(pickArgs: PickArgs): PickResult { + trace('Pick called. Hash=' + pickArgs.extraPickInfo.hash); + const firstIndex = this.findIndexForHash( + BigInt(pickArgs.extraPickInfo.hash) + ); + for (let i = 0; i < this.ring.length; i++) { + const index = (firstIndex + i) % this.ring.length; + const entryState = this.ring[index].leafBalancer.getConnectivityState(); + if (entryState === connectivityState.READY) { + return this.ring[index].leafBalancer.getPicker().pick(pickArgs); + } + if (entryState === connectivityState.IDLE) { + this.ring[index].leafBalancer.startConnecting(); + return { + pickResultType: PickResultType.QUEUE, + subchannel: null, + status: null, + onCallStarted: null, + onCallEnded: null, + }; + } + if (entryState === connectivityState.CONNECTING) { + return { + pickResultType: PickResultType.QUEUE, + subchannel: null, + status: null, + onCallStarted: null, + onCallEnded: null, + }; + } + } + return { + pickResultType: PickResultType.TRANSIENT_FAILURE, + status: { + code: status.UNAVAILABLE, + details: + 'ring_hash: invalid state: all child balancers in TRANSIENT_FAILURE', + metadata: new Metadata(), + }, + subchannel: null, + onCallStarted: null, + onCallEnded: null, + }; + } +} + +class RingHashLoadBalancer implements LoadBalancer { + /** + * Tracks endpoint repetition across address updates, to use an appropriate + * existing leaf load balancer for the same endpoint when possible. + */ + private leafMap = new EndpointMap(); + /** + * Tracks endpoints from a single address update, with their associated + * weights aggregated from all weights associated with that endpoint in that + * update. + */ + private leafWeightMap = new EndpointMap(); + private childChannelControlHelper: ChannelControlHelper; + private updatesPaused = false; + private currentState: connectivityState = connectivityState.IDLE; + private ring: RingEntry[] = []; + private ringHashSizeCap = DEFAULT_RING_SIZE_CAP; + constructor(private channelControlHelper: ChannelControlHelper, private options: ChannelOptions) { + this.childChannelControlHelper = createChildChannelControlHelper( + channelControlHelper, + { + updateState: (state, picker) => { + this.calculateAndUpdateState(); + /* If this LB policy is in the TRANSIENT_FAILURE state, requests will + * not trigger new connections, so we need to explicitly try connecting + * to other endpoints that are currently IDLE to try to eventually + * connect to something. */ + if ( + state === connectivityState.TRANSIENT_FAILURE && + this.currentState === connectivityState.TRANSIENT_FAILURE + ) { + for (const leaf of this.leafMap.values()) { + const leafState = leaf.getConnectivityState(); + if (leafState === connectivityState.CONNECTING) { + break; + } + if (leafState === connectivityState.IDLE) { + leaf.startConnecting(); + break; + } + } + } + }, + } + ); + if (options['grpc.lb.ring_hash.ring_size_cap'] !== undefined) { + this.ringHashSizeCap = options['grpc.lb.ring_hash.ring_size_cap']; + } + } + + private calculateAndUpdateState() { + if (this.updatesPaused) { + return; + } + const stateCounts = { + [connectivityState.READY]: 0, + [connectivityState.TRANSIENT_FAILURE]: 0, + [connectivityState.CONNECTING]: 0, + [connectivityState.IDLE]: 0, + [connectivityState.SHUTDOWN]: 0, + }; + for (const leaf of this.leafMap.values()) { + stateCounts[leaf.getConnectivityState()] += 1; + } + if (stateCounts[connectivityState.READY] > 0) { + this.updateState(connectivityState.READY, new RingHashPicker(this.ring)); + // REPORT READY + } else if (stateCounts[connectivityState.TRANSIENT_FAILURE] > 1) { + this.updateState( + connectivityState.TRANSIENT_FAILURE, + new UnavailablePicker() + ); + } else if (stateCounts[connectivityState.CONNECTING] > 0) { + this.updateState( + connectivityState.CONNECTING, + new RingHashPicker(this.ring) + ); + } else if ( + stateCounts[connectivityState.TRANSIENT_FAILURE] > 0 && + this.leafMap.size > 1 + ) { + this.updateState( + connectivityState.CONNECTING, + new RingHashPicker(this.ring) + ); + } else if (stateCounts[connectivityState.IDLE] > 0) { + this.updateState(connectivityState.IDLE, new RingHashPicker(this.ring)); + } else { + this.updateState( + connectivityState.TRANSIENT_FAILURE, + new UnavailablePicker() + ); + } + } + + private updateState(newState: connectivityState, picker: Picker) { + trace( + connectivityState[this.currentState] + + ' -> ' + + connectivityState[newState] + ); + this.currentState = newState; + this.channelControlHelper.updateState(newState, picker); + } + + private constructRing( + endpointList: Endpoint[], + config: RingHashLoadBalancingConfig + ) { + this.ring = []; + const endpointWeights: EndpointWeight[] = []; + let weightSum = 0; + for (const endpoint of endpointList) { + const weight = this.leafWeightMap.get(endpoint) ?? 1; + endpointWeights.push({ endpoint, weight, normalizedWeight: 0 }); + weightSum += weight; + } + /* The normalized weights sum to 1, with some small potential error due to + * the limitation of floating point precision. */ + let minNormalizedWeight = 1; + for (const endpointWeight of endpointWeights) { + endpointWeight.normalizedWeight = endpointWeight.weight / weightSum; + minNormalizedWeight = Math.min( + endpointWeight.normalizedWeight, + minNormalizedWeight + ); + } + const minRingSize = Math.min(config.getMinRingSize(), this.ringHashSizeCap); + const maxRingSize = Math.min(config.getMaxRingSize(), this.ringHashSizeCap); + /* Calculate a scale factor that meets the following conditions: + * 1. The result is between minRingSize and maxRingSize, inclusive + * 2. The smallest normalized weight is scaled to a whole number, if it + * does not violate the previous condition. + * The size of the ring is ceil(scale) + */ + const scale = Math.min( + Math.ceil(minNormalizedWeight * minRingSize) / minNormalizedWeight, + maxRingSize + ); + trace('Creating a ring with size ' + Math.ceil(scale)); + /* For each endpoint, create a number of entries proportional to its + * weight, such that the total number of entries is equal to ceil(scale). + */ + let currentHashes = 0; + let targetHashes = 0; + for (const endpointWeight of endpointWeights) { + const addressString = subchannelAddressToString( + endpointWeight.endpoint.addresses[0] + ); + targetHashes += scale * endpointWeight.normalizedWeight; + const leafBalancer = this.leafMap.get(endpointWeight.endpoint); + if (!leafBalancer) { + throw new Error( + 'ring_hash: Invalid state: endpoint found in leafWeightMap but not in leafMap' + ); + } + let count = 0; + while (currentHashes < targetHashes) { + const hashKey = `${addressString}_${count}`; + const hash = xxhashApi!.h64(hashKey, 0n); + this.ring.push({ hash, leafBalancer }); + currentHashes++; + count++; + } + } + /* The ring is sorted by the hash so that it can be efficiently searched + * for a hash that is closest to any arbitrary hash. */ + this.ring.sort((a, b) => { + if (a.hash > b.hash) { + return 1; + } else if (a.hash < b.hash) { + return -1; + } else { + return 0; + } + }); + } + + updateAddressList( + endpointList: Endpoint[], + lbConfig: TypedLoadBalancingConfig, + attributes: { [key: string]: unknown } + ): void { + if (!(lbConfig instanceof RingHashLoadBalancingConfig)) { + trace('Discarding address update with unrecognized config ' + JSON.stringify(lbConfig.toJsonObject(), undefined, 2)); + return; + } + trace('Received update with config ' + JSON.stringify(lbConfig.toJsonObject(), undefined, 2)); + this.updatesPaused = true; + this.leafWeightMap.clear(); + const dedupedEndpointList: Endpoint[] = []; + for (const endpoint of endpointList) { + const leafBalancer = this.leafMap.get(endpoint); + if (leafBalancer) { + leafBalancer.updateEndpoint(endpoint); + } else { + this.leafMap.set( + endpoint, + new LeafLoadBalancer(endpoint, this.childChannelControlHelper, this.options) + ); + } + const weight = this.leafWeightMap.get(endpoint); + if (weight === undefined) { + dedupedEndpointList.push(endpoint); + } + this.leafWeightMap.set(endpoint, (weight ?? 0) + (isLocalityEndpoint(endpoint) ? endpoint.endpointWeight : 1)); + } + const removedLeaves = this.leafMap.deleteMissing(endpointList); + for (const leaf of removedLeaves) { + leaf.destroy(); + } + loadXxhashApi().then(() => { + this.constructRing(dedupedEndpointList, lbConfig); + this.updatesPaused = false; + this.calculateAndUpdateState(); + }); + } + exitIdle(): void { + /* This operation does not make sense here. We don't want to make the whole + * balancer exit idle, and instead propagate that to individual chlidren as + * relevant. */ + } + resetBackoff(): void { + // There is no backoff to reset here + } + destroy(): void { + this.ring = []; + for (const child of this.leafMap.values()) { + child.destroy(); + } + this.leafMap.clear(); + this.leafWeightMap.clear(); + } + getTypeName(): string { + return TYPE_NAME; + } +} + +const RING_HASH_TYPE_URL = 'type.googleapis.com/envoy.extensions.load_balancing_policies.ring_hash.v3.RingHash'; + +const resourceRoot = loadProtosWithOptionsSync([ + 'envoy/extensions/load_balancing_policies/ring_hash/v3/ring_hash.proto'], { + keepCase: true, + includeDirs: [ + // Paths are relative to src/build + __dirname + '/../../deps/envoy-api/', + __dirname + '/../../deps/xds/', + __dirname + '/../../deps/protoc-gen-validate' + ], + } +); + +const toObjectOptions = { + longs: String, + enums: String, + defaults: true, + oneofs: true +} + +function decodeRingHash(message: Any__Output): RingHash__Output { + const name = message.type_url.substring(message.type_url.lastIndexOf('/') + 1); + const type = resourceRoot.lookup(name); + if (type) { + const decodedMessage = (type as any).decode(message.value); + return decodedMessage.$type.toObject(decodedMessage, toObjectOptions) as RingHash__Output; + } else { + throw new Error(`TypedStruct parsing error: unexpected type URL ${message.type_url}`); + } +} + +function convertToLoadBalancingPolicy(protoPolicy: TypedExtensionConfig__Output, selectChildPolicy: (childPolicy: LoadBalancingPolicy__Output) => LoadBalancingConfig): LoadBalancingConfig { + if (protoPolicy.typed_config?.type_url !== RING_HASH_TYPE_URL) { + throw new Error(`Ring Hash LB policy parsing error: unexpected type URL ${protoPolicy.typed_config?.type_url}`); + } + const ringHashMessage = decodeRingHash(protoPolicy.typed_config); + if (ringHashMessage.hash_function !== 'XX_HASH') { + throw new Error(`Ring Hash LB policy parsing error: unexpected hash function ${ringHashMessage.hash_function}`); + } + return { + [TYPE_NAME]: { + min_ring_size: ringHashMessage.minimum_ring_size?.value ?? 1024, + max_ring_size: ringHashMessage.maximum_ring_size?.value ?? 8_388_608 + } + }; +} + +export function setup() { + if (EXPERIMENTAL_RING_HASH) { + registerLoadBalancerType( + TYPE_NAME, + RingHashLoadBalancer, + RingHashLoadBalancingConfig + ); + registerLbPolicy(RING_HASH_TYPE_URL, convertToLoadBalancingPolicy); + } +} diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-manager.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-manager.ts index 3560d8481..99059dcac 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-manager.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-manager.ts @@ -204,35 +204,7 @@ class XdsClusterManager implements LoadBalancer { } else { connectivityState = ConnectivityState.TRANSIENT_FAILURE; } - /* For each of the states CONNECTING, IDLE, and TRANSIENT_FAILURE, there is - * exactly one corresponding picker, so if the state is one of those and - * that does not change, no new information is provided by passing the - * new state upward. */ - if (connectivityState === this.currentState && connectivityState !== ConnectivityState.READY) { - return; - } - let picker: Picker; - - switch (connectivityState) { - case ConnectivityState.READY: - picker = new XdsClusterManagerPicker(pickerMap); - break; - case ConnectivityState.CONNECTING: - case ConnectivityState.IDLE: - picker = new QueuePicker(this); - break; - default: - picker = new UnavailablePicker({ - code: Status.UNAVAILABLE, - details: 'xds_cluster_manager: all children report state TRANSIENT_FAILURE', - metadata: new Metadata() - }); - } - trace( - 'Transitioning to ' + - ConnectivityState[connectivityState] - ); - this.channelControlHelper.updateState(connectivityState, picker); + this.channelControlHelper.updateState(connectivityState, new XdsClusterManagerPicker(pickerMap)); } updateAddressList(endpointList: Endpoint[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts index c4bf984f7..29c0b6f31 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts @@ -115,10 +115,15 @@ class XdsClusterResolverLoadBalancingConfig implements TypedLoadBalancingConfig } } +interface WeightedEndpoint { + endpoint: Endpoint; + weight: number; +} + interface LocalityEntry { locality: Locality__Output; weight: number; - endpoints: Endpoint[]; + endpoints: WeightedEndpoint[]; } interface PriorityEntry { @@ -166,16 +171,19 @@ function getEdsPriorities(edsUpdate: ClusterLoadAssignment__Output): PriorityEnt if (!endpoint.load_balancing_weight) { continue; } - const endpoints: Endpoint[] = endpoint.lb_endpoints.filter(lbEndpoint => lbEndpoint.health_status === 'UNKNOWN' || lbEndpoint.health_status === 'HEALTHY').map( + const endpoints: WeightedEndpoint[] = endpoint.lb_endpoints.filter(lbEndpoint => lbEndpoint.health_status === 'UNKNOWN' || lbEndpoint.health_status === 'HEALTHY').map( (lbEndpoint) => { /* The validator in the XdsClient class ensures that each endpoint has * a socket_address with an IP address and a port_value. */ const socketAddress = lbEndpoint.endpoint!.address!.socket_address!; return { - addresses: [{ - host: socketAddress.address!, - port: socketAddress.port_value!, - }] + endpoint: { + addresses: [{ + host: socketAddress.address!, + port: socketAddress.port_value!, + }] + }, + weight: lbEndpoint.load_balancing_weight?.value ?? 1 }; } ); @@ -211,7 +219,7 @@ function getDnsPriorities(endpoints: Endpoint[]): PriorityEntry[] { sub_zone: '' }, weight: 1, - endpoints: endpoints + endpoints: endpoints.map(endpoint => ({endpoint: endpoint, weight: 1})) }], dropCategories: [] }]; @@ -295,15 +303,16 @@ export class XdsClusterResolver implements LoadBalancer { newPriorityNames[priority] = newPriorityName; for (const localityObj of priorityEntry.localities) { - for (const endpoint of localityObj.endpoints) { + for (const weightedEndpoint of localityObj.endpoints) { endpointList.push({ localityPath: [ newPriorityName, localityToName(localityObj.locality), ], locality: localityObj.locality, - weight: localityObj.weight, - ...endpoint + localityWeight: localityObj.weight, + endpointWeight: localityObj.weight * weightedEndpoint.weight, + ...weightedEndpoint.endpoint }); } newLocalityPriorities.set(localityToName(localityObj.locality), priority); diff --git a/packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts b/packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts index f3fbcd513..3fb57d6e2 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-wrr-locality.ts @@ -90,7 +90,7 @@ class XdsWrrLocalityLoadBalancer implements LoadBalancer { if (!(localityName in targets)) { targets[localityName] = { child_policy: lbConfig.getChildPolicy(), - weight: address.weight + weight: address.localityWeight }; } } diff --git a/packages/grpc-js-xds/src/matcher.ts b/packages/grpc-js-xds/src/matcher.ts index 148df7f85..b657d32c9 100644 --- a/packages/grpc-js-xds/src/matcher.ts +++ b/packages/grpc-js-xds/src/matcher.ts @@ -71,7 +71,7 @@ export class SafeRegexValueMatcher implements ValueMatcher { const numberRegex = new RE2(/^-?\d+$/u); export class RangeValueMatcher implements ValueMatcher { - constructor(private start: BigInt, private end: BigInt) {} + constructor(private start: bigint, private end: bigint) {} apply(value: string) { if (!numberRegex.test(value)) { @@ -264,4 +264,4 @@ export class FullMatcher implements Matcher { headers: ${this.headerMatchers.map(matcher => matcher.toString()).join('\n\t')} fraction: ${this.fraction ? fractionToString(this.fraction): 'none'}`; } -} \ No newline at end of file +} diff --git a/packages/grpc-js-xds/src/resolver-xds.ts b/packages/grpc-js-xds/src/resolver-xds.ts index cd830d521..3acdd2329 100644 --- a/packages/grpc-js-xds/src/resolver-xds.ts +++ b/packages/grpc-js-xds/src/resolver-xds.ts @@ -34,18 +34,19 @@ import { HeaderMatcher__Output } from './generated/envoy/config/route/v3/HeaderM import ConfigSelector = experimental.ConfigSelector; import { ContainsValueMatcher, ExactValueMatcher, FullMatcher, HeaderMatcher, Matcher, PathExactValueMatcher, PathPrefixValueMatcher, PathSafeRegexValueMatcher, PrefixValueMatcher, PresentValueMatcher, RangeValueMatcher, RejectValueMatcher, SafeRegexValueMatcher, SuffixValueMatcher, ValueMatcher } from './matcher'; import { envoyFractionToFraction, Fraction } from "./fraction"; -import { RouteAction, SingleClusterRouteAction, WeightedCluster, WeightedClusterRouteAction } from './route-action'; +import { HashPolicy, RouteAction, SingleClusterRouteAction, WeightedCluster, WeightedClusterRouteAction } from './route-action'; import { decodeSingleResource, HTTP_CONNECTION_MANGER_TYPE_URL } from './resources'; import Duration = experimental.Duration; import { Duration__Output } from './generated/google/protobuf/Duration'; import { createHttpFilter, HttpFilterConfig, parseOverrideFilterConfig, parseTopLevelFilterConfig } from './http-filter'; -import { EXPERIMENTAL_FAULT_INJECTION, EXPERIMENTAL_FEDERATION, EXPERIMENTAL_RETRY } from './environment'; +import { EXPERIMENTAL_FAULT_INJECTION, EXPERIMENTAL_FEDERATION, EXPERIMENTAL_RETRY, EXPERIMENTAL_RING_HASH } from './environment'; import Filter = experimental.Filter; import FilterFactory = experimental.FilterFactory; import { BootstrapInfo, loadBootstrapInfo, validateBootstrapConfig } from './xds-bootstrap'; import { ListenerResourceType } from './xds-resource-type/listener-resource-type'; import { RouteConfigurationResourceType } from './xds-resource-type/route-config-resource-type'; import { protoDurationToDuration } from './duration'; +import { loadXxhashApi } from './xxhash'; const TRACER_NAME = 'xds_resolver'; @@ -381,7 +382,11 @@ class XdsResolver implements Resolver { } } - private handleRouteConfig(routeConfig: RouteConfiguration__Output) { + private async handleRouteConfig(routeConfig: RouteConfiguration__Output) { + /* We need to load the xxhash API before this function finishes, because + * it is invoked in the config selector, which can be called immediately + * after this function returns. */ + await loadXxhashApi(); this.latestRouteConfig = routeConfig; /* Select the virtual host using the default authority override if it * exists, and the channel target otherwise. */ @@ -456,6 +461,26 @@ class XdsResolver implements Resolver { } } } + const hashPolicies: HashPolicy[] = []; + if (EXPERIMENTAL_RING_HASH) { + for (const routeHashPolicy of route.route!.hash_policy) { + if (routeHashPolicy.policy_specifier === 'header') { + const headerPolicy = routeHashPolicy.header!; + hashPolicies.push({ + type: 'HEADER', + terminal: routeHashPolicy.terminal, + headerName: headerPolicy.header_name, + regex: headerPolicy.regex_rewrite?.pattern ? new RE2(headerPolicy.regex_rewrite.pattern.regex, 'ug') : undefined, + regexSubstitution: headerPolicy.regex_rewrite?.substitution + }); + } else if (routeHashPolicy.policy_specifier === 'filter_state' && routeHashPolicy.filter_state!.key === 'io.grpc.channel_id') { + hashPolicies.push({ + type: 'CHANNEL_ID', + terminal: routeHashPolicy.terminal + }); + } + } + } switch (route.route!.cluster_specifier) { case 'cluster_header': continue; @@ -483,7 +508,7 @@ class XdsResolver implements Resolver { } } } - routeAction = new SingleClusterRouteAction(cluster, {name: [], timeout: timeout, retryPolicy: retryPolicy}, extraFilterFactories); + routeAction = new SingleClusterRouteAction(cluster, {name: [], timeout: timeout, retryPolicy: retryPolicy}, extraFilterFactories, hashPolicies); break; } case 'weighted_clusters': { @@ -525,7 +550,7 @@ class XdsResolver implements Resolver { } weightedClusters.push({name: clusterWeight.name, weight: clusterWeight.weight?.value ?? 0, dynamicFilterFactories: extraFilterFactories}); } - routeAction = new WeightedClusterRouteAction(weightedClusters, route.route!.weighted_clusters!.total_weight?.value ?? 100, {name: [], timeout: timeout, retryPolicy: retryPolicy}); + routeAction = new WeightedClusterRouteAction(weightedClusters, route.route!.weighted_clusters!.total_weight?.value ?? 100, {name: [], timeout: timeout, retryPolicy: retryPolicy}, hashPolicies); break; } default: @@ -554,7 +579,7 @@ class XdsResolver implements Resolver { this.clusterRefcounts.set(name, {inLastConfig: true, refCount: 0}); } } - const configSelector: ConfigSelector = (methodName, metadata) => { + const configSelector: ConfigSelector = (methodName, metadata, channelId) => { for (const {matcher, action} of matchList) { if (matcher.apply(methodName, metadata)) { const clusterResult = action.getCluster(); @@ -562,10 +587,11 @@ class XdsResolver implements Resolver { const onCommitted = () => { this.unrefCluster(clusterResult.name); } + const hash = action.getHash(metadata, channelId); return { methodConfig: clusterResult.methodConfig, onCommitted: onCommitted, - pickInformation: {cluster: clusterResult.name}, + pickInformation: {cluster: clusterResult.name, hash: `${hash}`}, status: status.OK, dynamicFilterFactories: clusterResult.dynamicFilterFactories }; @@ -573,8 +599,8 @@ class XdsResolver implements Resolver { } return { methodConfig: {name: []}, - // cluster won't be used here, but it's set because of some TypeScript weirdness - pickInformation: {cluster: ''}, + // These fields won't be used here, but they're set because of some TypeScript weirdness + pickInformation: {cluster: '', hash: ''}, status: status.UNAVAILABLE, dynamicFilterFactories: [] }; diff --git a/packages/grpc-js-xds/src/route-action.ts b/packages/grpc-js-xds/src/route-action.ts index 83530f1b4..2f87fee96 100644 --- a/packages/grpc-js-xds/src/route-action.ts +++ b/packages/grpc-js-xds/src/route-action.ts @@ -14,10 +14,12 @@ * limitations under the License. */ -import { MethodConfig, experimental } from '@grpc/grpc-js'; +import { Metadata, MethodConfig, experimental } from '@grpc/grpc-js'; import Duration = experimental.Duration; import Filter = experimental.Filter; import FilterFactory = experimental.FilterFactory; +import { RE2 } from 're2-wasm'; +import { xxhashApi } from './xxhash'; export interface ClusterResult { name: string; @@ -28,6 +30,7 @@ export interface ClusterResult { export interface RouteAction { toString(): string; getCluster(): ClusterResult; + getHash(metadata: Metadata, channelId: number): bigint; } function durationToLogString(duration: Duration) { @@ -39,8 +42,83 @@ function durationToLogString(duration: Duration) { } } +export interface HashPolicy { + type: 'HEADER' | 'CHANNEL_ID'; + terminal: boolean; + headerName?: string; + regex?: RE2; + regexSubstitution?: string; +} + +/** + * Must be called only after xxhash.loadXxhashApi() resolves. + * @param hashPolicies + * @param metadata + * @param channelId + */ +function getHash(hashPolicies: HashPolicy[], metadata: Metadata, channelId: number): bigint { + let hash: bigint | null = null; + for (const policy of hashPolicies) { + let newHash: bigint | null = null; + switch (policy.type) { + case 'CHANNEL_ID': + newHash = xxhashApi!.h64(`${channelId}`, 0n); + break; + case 'HEADER': { + if (!policy.headerName) { + break; + } + if (policy.headerName.endsWith('-bin')) { + break; + } + let headerString: string; + if (policy.headerName === 'content-type') { + headerString = 'application/grpc'; + } else { + const headerValues = metadata.get(policy.headerName); + if (headerValues.length === 0) { + break; + } + headerString = headerValues.join(','); + } + let rewrittenHeaderString = headerString; + if (policy.regex && policy.regexSubstitution) { + /* The JS string replace method uses $-prefixed patterns to produce + * other strings. See + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_the_replacement + * RE2-based regex substitutions use \n where n is a number to refer + * to capture group n, and they otherwise have no special replacement + * patterns. See + * https://github.com/envoyproxy/envoy/blob/2443032526cf6e50d63d35770df9473dd0460fc0/api/envoy/type/matcher/v3/regex.proto#L79-L87 + * We convert an RE2 regex substitution into a string substitution by + * first replacing each "$" with "$$" (which produces "$" in the + * output), and then replace each "\n" for any whole number n with + * "$n". */ + const regexSubstitution = policy.regexSubstitution.replace(/\$/g, '$$$$').replace(/\\(\d+)/g, '$$$1'); + rewrittenHeaderString = headerString.replace(policy.regex, regexSubstitution); + } + newHash = xxhashApi!.h64(rewrittenHeaderString, 0n); + break; + } + } + if (hash === null) { + hash = newHash; + } else if (newHash !== null) { + hash = ((hash << 1n) | (hash >> 63n)) ^ newHash; + } + if (policy.terminal && hash !== null) { + break; + } + } + if (hash === null) { + return xxhashApi!.h64(`${Math.random()}`, 0n); + } else { + return hash; + } +} + export class SingleClusterRouteAction implements RouteAction { - constructor(private cluster: string, private methodConfig: MethodConfig, private extraFilterFactories: FilterFactory[]) {} + constructor(private cluster: string, private methodConfig: MethodConfig, private extraFilterFactories: FilterFactory[], private hashPolicies: HashPolicy[]) {} getCluster() { return { @@ -50,6 +128,10 @@ export class SingleClusterRouteAction implements RouteAction { }; } + getHash(metadata: Metadata, channelId: number): bigint { + return getHash(this.hashPolicies, metadata, channelId); + } + toString() { return 'SingleCluster(' + this.cluster + ', ' + JSON.stringify(this.methodConfig) + ')'; } @@ -72,7 +154,7 @@ export class WeightedClusterRouteAction implements RouteAction { * The weighted cluster choices represented as a CDF */ private clusterChoices: ClusterChoice[]; - constructor(private clusters: WeightedCluster[], private totalWeight: number, private methodConfig: MethodConfig) { + constructor(private clusters: WeightedCluster[], private totalWeight: number, private methodConfig: MethodConfig, private hashPolicies: HashPolicy[]) { this.clusterChoices = []; let lastNumerator = 0; for (const clusterWeight of clusters) { @@ -96,6 +178,10 @@ export class WeightedClusterRouteAction implements RouteAction { return {name: '', methodConfig: this.methodConfig, dynamicFilterFactories: []}; } + getHash(metadata: Metadata, channelId: number): bigint { + return getHash(this.hashPolicies, metadata, channelId); + } + toString() { const clusterListString = this.clusters.map(({name, weight}) => '(' + name + ':' + weight + ')').join(', ') return 'WeightedCluster(' + clusterListString + ', ' + JSON.stringify(this.methodConfig) + ')'; diff --git a/packages/grpc-js-xds/src/xds-bootstrap.ts b/packages/grpc-js-xds/src/xds-bootstrap.ts index fccd3edcf..536439dd0 100644 --- a/packages/grpc-js-xds/src/xds-bootstrap.ts +++ b/packages/grpc-js-xds/src/xds-bootstrap.ts @@ -357,14 +357,14 @@ export function loadBootstrapInfo(): BootstrapInfo { try { rawBootstrap = fs.readFileSync(bootstrapPath, { encoding: 'utf8'}); } catch (e) { - throw new Error(`Failed to read xDS bootstrap file from path ${bootstrapPath} with error ${e.message}`); + throw new Error(`Failed to read xDS bootstrap file from path ${bootstrapPath} with error ${(e as Error).message}`); } try { const parsedFile = JSON.parse(rawBootstrap); loadedBootstrapInfo = validateBootstrapConfig(parsedFile); return loadedBootstrapInfo; } catch (e) { - throw new Error(`Failed to parse xDS bootstrap file at path ${bootstrapPath} with error ${e.message}`) + throw new Error(`Failed to parse xDS bootstrap file at path ${bootstrapPath} with error ${(e as Error).message}`) } } @@ -383,7 +383,7 @@ export function loadBootstrapInfo(): BootstrapInfo { loadedBootstrapInfo = validateBootstrapConfig(parsedConfig); } catch (e) { throw new Error( - `Failed to parse xDS bootstrap config from environment variable GRPC_XDS_BOOTSTRAP_CONFIG with error ${e.message}` + `Failed to parse xDS bootstrap config from environment variable GRPC_XDS_BOOTSTRAP_CONFIG with error ${(e as Error).message}` ); } diff --git a/packages/grpc-js-xds/src/xds-client.ts b/packages/grpc-js-xds/src/xds-client.ts index 464e26596..baabf2e76 100644 --- a/packages/grpc-js-xds/src/xds-client.ts +++ b/packages/grpc-js-xds/src/xds-client.ts @@ -208,14 +208,14 @@ class AdsResponseParser { try { decodeResult = this.result.type.decode(decodeContext, resource); } catch (e) { - this.result.errors.push(`${errorPrefix} ${e.message}`); + this.result.errors.push(`${errorPrefix} ${(e as Error).message}`); return; } let parsedName: XdsResourceName; try { parsedName = parseXdsResourceName(decodeResult.name, this.result.type!.getTypeUrl()); } catch (e) { - this.result.errors.push(`${errorPrefix} ${e.message}`); + this.result.errors.push(`${errorPrefix} ${(e as Error).message}`); return; } this.adsCallState.typeStates.get(this.result.type!)?.subscribedResources.get(parsedName.authority)?.get(parsedName.key)?.markSeen(); diff --git a/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts index 1e8ea41f8..c13a91d7e 100644 --- a/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts +++ b/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts @@ -21,7 +21,7 @@ import { LoadBalancingConfig, experimental, logVerbosity } from "@grpc/grpc-js"; import { XdsServerConfig } from "../xds-bootstrap"; import { Duration__Output } from "../generated/google/protobuf/Duration"; import { OutlierDetection__Output } from "../generated/envoy/config/cluster/v3/OutlierDetection"; -import { EXPERIMENTAL_CUSTOM_LB_CONFIG, EXPERIMENTAL_OUTLIER_DETECTION } from "../environment"; +import { EXPERIMENTAL_CUSTOM_LB_CONFIG, EXPERIMENTAL_OUTLIER_DETECTION, EXPERIMENTAL_RING_HASH } from "../environment"; import { Cluster__Output } from "../generated/envoy/config/cluster/v3/Cluster"; import { UInt32Value__Output } from "../generated/google/protobuf/UInt32Value"; import { Any__Output } from "../generated/google/protobuf/Any"; @@ -150,6 +150,27 @@ export class ClusterResourceType extends XdsResourceType { child_policy: [{round_robin: {}}] } }; + } else if(EXPERIMENTAL_RING_HASH && message.lb_policy === 'RING_HASH') { + if (!message.ring_hash_lb_config) { + return null; + } + if (message.ring_hash_lb_config.hash_function !== 'XX_HASH') { + return null; + } + const minRingSize = message.ring_hash_lb_config.minimum_ring_size ? Number(message.ring_hash_lb_config.minimum_ring_size.value) : 1024; + if (minRingSize > 8_388_608) { + return null; + } + const maxRingSize = message.ring_hash_lb_config.maximum_ring_size ? Number(message.ring_hash_lb_config.maximum_ring_size.value) : 8_388_608; + if (maxRingSize > 8_388_608) { + return null; + } + lbPolicyConfig = { + ring_hash: { + min_ring_size: minRingSize, + max_ring_size: maxRingSize + } + }; } else { return null; } diff --git a/packages/grpc-js-xds/src/xxhash.ts b/packages/grpc-js-xds/src/xxhash.ts new file mode 100644 index 000000000..63f68af2b --- /dev/null +++ b/packages/grpc-js-xds/src/xxhash.ts @@ -0,0 +1,31 @@ +/* + * 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. + * + */ + +/* The simpler `import xxhash from 'xxhash-wasm';` doesn't compile correctly + * to CommonJS require calls for some reason, so we use this import to get + * the type, and then an explicit require call to get the actual value. */ +import xxhashImport from 'xxhash-wasm'; +const xxhash: typeof xxhashImport = require('xxhash-wasm'); + +export let xxhashApi: Awaited> | null = null; + +export async function loadXxhashApi() { + if (!xxhashApi) { + xxhashApi = await xxhash(); + } + return xxhashApi; +} diff --git a/packages/grpc-js-xds/test/framework.ts b/packages/grpc-js-xds/test/framework.ts index f89a4680f..d38437a10 100644 --- a/packages/grpc-js-xds/test/framework.ts +++ b/packages/grpc-js-xds/test/framework.ts @@ -70,7 +70,7 @@ export interface FakeCluster { } export class FakeEdsCluster implements FakeCluster { - constructor(private clusterName: string, private endpointName: string, private endpoints: Endpoint[], private loadBalancingPolicyOverride?: Any) {} + constructor(private clusterName: string, private endpointName: string, private endpoints: Endpoint[], private loadBalancingPolicyOverride?: Any | 'RING_HASH') {} getEndpointConfig(): ClusterLoadAssignment { return { @@ -94,7 +94,12 @@ export class FakeEdsCluster implements FakeCluster { ] } }; - if (this.loadBalancingPolicyOverride) { + if (this.loadBalancingPolicyOverride === 'RING_HASH') { + result.lb_policy = 'RING_HASH'; + result.ring_hash_lb_config = { + hash_function: 'XX_HASH' + }; + } else if (this.loadBalancingPolicyOverride) { result.load_balancing_policy = { policies: [ { @@ -257,8 +262,14 @@ function createRouteConfig(route: FakeRoute): Route { prefix: '' }, route: { - cluster: route.cluster.getName() - } + cluster: route.cluster.getName(), + // Default to consistent hash + hash_policy: [{ + filter_state: { + key: 'io.grpc.channel_id' + } + }] + }, }; } else { return { @@ -271,7 +282,13 @@ function createRouteConfig(route: FakeRoute): Route { name: clusterWeight.cluster.getName(), weight: {value: clusterWeight.weight} })) - } + }, + // Default to consistent hash + hash_policy: [{ + filter_state: { + key: 'io.grpc.channel_id' + } + }] } } } diff --git a/packages/grpc-js-xds/test/test-confg-parsing.ts b/packages/grpc-js-xds/test/test-confg-parsing.ts index 740934ba1..f2065805f 100644 --- a/packages/grpc-js-xds/test/test-confg-parsing.ts +++ b/packages/grpc-js-xds/test/test-confg-parsing.ts @@ -311,6 +311,37 @@ const allTestCases: {[lbPolicyName: string]: TestCase[]} = { } } } + ], + ring_hash: [ + { + name: 'empty config', + input: {}, + output: { + min_ring_size: 1024, + max_ring_size: 4096 + } + }, + { + name: 'populated config', + input: { + min_ring_size: 2048, + max_ring_size: 8192 + } + }, + { + name: 'min_ring_size too large', + input: { + min_ring_size: 8_388_609 + }, + error: /min_ring_size/ + }, + { + name: 'max_ring_size too large', + input: { + max_ring_size: 8_388_609 + }, + error: /max_ring_size/ + } ] } diff --git a/packages/grpc-js-xds/test/test-ring-hash.ts b/packages/grpc-js-xds/test/test-ring-hash.ts new file mode 100644 index 000000000..0a9e893ef --- /dev/null +++ b/packages/grpc-js-xds/test/test-ring-hash.ts @@ -0,0 +1,108 @@ +/* + * 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 { Backend } from "./backend"; +import { XdsTestClient } from "./client"; +import { FakeEdsCluster, FakeRouteGroup } from "./framework"; +import { XdsServer } from "./xds-server"; + +import { register } from "../src"; +import assert = require("assert"); +import { Any } from "../src/generated/google/protobuf/Any"; +import { AnyExtension } from "@grpc/proto-loader"; +import { RingHash } from "../src/generated/envoy/extensions/load_balancing_policies/ring_hash/v3/RingHash"; + +register(); + +describe('Ring hash LB policy', () => { + let xdsServer: XdsServer; + let client: XdsTestClient; + beforeEach(done => { + xdsServer = new XdsServer(); + xdsServer.startServer(error => { + done(error); + }); + }); + afterEach(() => { + client?.close(); + xdsServer?.shutdownServer(); + }); + it('Should route requests to the single backend with the old lbPolicy field', done => { + const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [new Backend()], locality:{region: 'region1'}}], 'RING_HASH'); + const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: cluster}]); + routeGroup.startAllBackends().then(() => { + xdsServer.setEdsResource(cluster.getEndpointConfig()); + xdsServer.setCdsResource(cluster.getClusterConfig()); + xdsServer.setRdsResource(routeGroup.getRouteConfiguration()); + xdsServer.setLdsResource(routeGroup.getListener()); + xdsServer.addResponseListener((typeUrl, responseState) => { + if (responseState.state === 'NACKED') { + client.stopCalls(); + assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`); + } + }) + client = XdsTestClient.createFromServer('listener1', xdsServer); + client.sendOneCall(done); + }, reason => done(reason)); + }); + it('Should route requests to the single backend with the new load_balancing_policy field', done => { + const lbPolicy: AnyExtension & RingHash = { + '@type': 'type.googleapis.com/envoy.extensions.load_balancing_policies.ring_hash.v3.RingHash', + hash_function: 'XX_HASH' + }; + const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [new Backend()], locality:{region: 'region1'}}], lbPolicy); + const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: cluster}]); + routeGroup.startAllBackends().then(() => { + xdsServer.setEdsResource(cluster.getEndpointConfig()); + xdsServer.setCdsResource(cluster.getClusterConfig()); + xdsServer.setRdsResource(routeGroup.getRouteConfiguration()); + xdsServer.setLdsResource(routeGroup.getListener()); + xdsServer.addResponseListener((typeUrl, responseState) => { + if (responseState.state === 'NACKED') { + client.stopCalls(); + assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`); + } + }) + client = XdsTestClient.createFromServer('listener1', xdsServer); + client.sendOneCall(done); + }, reason => done(reason)); + }); + it('Should route all identical requests to the same backend', done => { + const backend1 = new Backend(); + const backend2 = new Backend() + const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [backend1, backend2], locality:{region: 'region1'}}], 'RING_HASH'); + const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: cluster}]); + routeGroup.startAllBackends().then(() => { + xdsServer.setEdsResource(cluster.getEndpointConfig()); + xdsServer.setCdsResource(cluster.getClusterConfig()); + xdsServer.setRdsResource(routeGroup.getRouteConfiguration()); + xdsServer.setLdsResource(routeGroup.getListener()); + xdsServer.addResponseListener((typeUrl, responseState) => { + if (responseState.state === 'NACKED') { + client.stopCalls(); + assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`); + } + }) + client = XdsTestClient.createFromServer('listener1', xdsServer); + client.sendNCalls(10, error => { + assert.ifError(error); + assert((backend1.getCallCount() === 0) !== (backend2.getCallCount() === 0)); + done(); + }) + }, reason => done(reason)); + }); +}) diff --git a/packages/grpc-js-xds/test/xds-server.ts b/packages/grpc-js-xds/test/xds-server.ts index 6f021c176..2aa62bdd8 100644 --- a/packages/grpc-js-xds/test/xds-server.ts +++ b/packages/grpc-js-xds/test/xds-server.ts @@ -44,6 +44,7 @@ const loadedProtos = loadPackageDefinition(loadSync( 'envoy/extensions/clusters/aggregate/v3/cluster.proto', 'envoy/extensions/load_balancing_policies/round_robin/v3/round_robin.proto', 'envoy/extensions/load_balancing_policies/wrr_locality/v3/wrr_locality.proto', + 'envoy/extensions/load_balancing_policies/ring_hash/v3/ring_hash.proto', 'xds/type/v3/typed_struct.proto' ], { diff --git a/packages/grpc-js-xds/tsconfig.json b/packages/grpc-js-xds/tsconfig.json index c121a5f6d..24212dfc2 100644 --- a/packages/grpc-js-xds/tsconfig.json +++ b/packages/grpc-js-xds/tsconfig.json @@ -3,8 +3,8 @@ "compilerOptions": { "rootDir": ".", "outDir": "build", - "target": "es2017", - "lib": ["es2017"], + "target": "es2020", + "lib": ["es2020"], "module": "commonjs", "incremental": true }, diff --git a/packages/grpc-js/src/channel-options.ts b/packages/grpc-js/src/channel-options.ts index f2bb8bcf7..aa1e6c83e 100644 --- a/packages/grpc-js/src/channel-options.ts +++ b/packages/grpc-js/src/channel-options.ts @@ -61,6 +61,7 @@ export interface ChannelOptions { * Set the enableTrace option in TLS clients and servers */ 'grpc-node.tls_enable_trace'?: number; + 'grpc.lb.ring_hash.ring_size_cap'?: number; // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; } @@ -96,6 +97,7 @@ export const recognizedOptions = { 'grpc.service_config_disable_resolution': true, 'grpc.client_idle_timeout_ms': true, 'grpc-node.tls_enable_trace': true, + 'grpc.lb.ring_hash.ring_size_cap': true, }; export function channelOptionsEqual( diff --git a/packages/grpc-js/src/experimental.ts b/packages/grpc-js/src/experimental.ts index 870fbe285..1e7a1e143 100644 --- a/packages/grpc-js/src/experimental.ts +++ b/packages/grpc-js/src/experimental.ts @@ -26,6 +26,7 @@ export { Endpoint, endpointToString, endpointHasAddress, + EndpointMap, } from './subchannel-address'; export { ChildLoadBalancerHandler } from './load-balancer-child-handler'; export { diff --git a/packages/grpc-js/src/internal-channel.ts b/packages/grpc-js/src/internal-channel.ts index 2817201e2..10d865cef 100644 --- a/packages/grpc-js/src/internal-channel.ts +++ b/packages/grpc-js/src/internal-channel.ts @@ -193,6 +193,15 @@ export class InternalChannel { private readonly callTracker = new ChannelzCallTracker(); private readonly childrenTracker = new ChannelzChildrenTracker(); + /** + * Randomly generated ID to be passed to the config selector, for use by + * ring_hash in xDS. An integer distributed approximately uniformly between + * 0 and MAX_SAFE_INTEGER. + */ + private readonly randomChannelId = Math.floor( + Math.random() * Number.MAX_SAFE_INTEGER + ); + constructor( target: string, private readonly credentials: ChannelCredentials, @@ -528,7 +537,7 @@ export class InternalChannel { if (this.configSelector) { return { type: 'SUCCESS', - config: this.configSelector(method, metadata), + config: this.configSelector(method, metadata, this.randomChannelId), }; } else { if (this.currentResolutionError) { diff --git a/packages/grpc-js/src/load-balancer-outlier-detection.ts b/packages/grpc-js/src/load-balancer-outlier-detection.ts index 8e0513611..8f2097f46 100644 --- a/packages/grpc-js/src/load-balancer-outlier-detection.ts +++ b/packages/grpc-js/src/load-balancer-outlier-detection.ts @@ -33,10 +33,9 @@ import { ChildLoadBalancerHandler } from './load-balancer-child-handler'; import { PickArgs, Picker, PickResult, PickResultType } from './picker'; import { Endpoint, + EndpointMap, SubchannelAddress, - endpointHasAddress, endpointToString, - subchannelAddressEqual, } from './subchannel-address'; import { BaseSubchannelWrapper, @@ -461,126 +460,9 @@ interface MapEntry { subchannelWrappers: OutlierDetectionSubchannelWrapper[]; } -interface EndpointMapEntry { - key: Endpoint; - value: MapEntry; -} - -function endpointEqualUnordered( - endpoint1: Endpoint, - endpoint2: Endpoint -): boolean { - if (endpoint1.addresses.length !== endpoint2.addresses.length) { - return false; - } - for (const address1 of endpoint1.addresses) { - let matchFound = false; - for (const address2 of endpoint2.addresses) { - if (subchannelAddressEqual(address1, address2)) { - matchFound = true; - break; - } - } - if (!matchFound) { - return false; - } - } - return true; -} - -class EndpointMap { - private map: Set = new Set(); - - get size() { - return this.map.size; - } - - getForSubchannelAddress(address: SubchannelAddress): MapEntry | undefined { - for (const entry of this.map) { - if (endpointHasAddress(entry.key, address)) { - return entry.value; - } - } - return undefined; - } - - /** - * Delete any entries in this map with keys that are not in endpoints - * @param endpoints - */ - deleteMissing(endpoints: Endpoint[]) { - for (const entry of this.map) { - let foundEntry = false; - for (const endpoint of endpoints) { - if (endpointEqualUnordered(endpoint, entry.key)) { - foundEntry = true; - } - } - if (!foundEntry) { - this.map.delete(entry); - } - } - } - - get(endpoint: Endpoint): MapEntry | undefined { - for (const entry of this.map) { - if (endpointEqualUnordered(endpoint, entry.key)) { - return entry.value; - } - } - return undefined; - } - - set(endpoint: Endpoint, mapEntry: MapEntry) { - for (const entry of this.map) { - if (endpointEqualUnordered(endpoint, entry.key)) { - entry.value = mapEntry; - return; - } - } - this.map.add({ key: endpoint, value: mapEntry }); - } - - delete(endpoint: Endpoint) { - for (const entry of this.map) { - if (endpointEqualUnordered(endpoint, entry.key)) { - this.map.delete(entry); - return; - } - } - } - - has(endpoint: Endpoint): boolean { - for (const entry of this.map) { - if (endpointEqualUnordered(endpoint, entry.key)) { - return true; - } - } - return false; - } - - *keys(): IterableIterator { - for (const entry of this.map) { - yield entry.key; - } - } - - *values(): IterableIterator { - for (const entry of this.map) { - yield entry.value; - } - } - - *entries(): IterableIterator<[Endpoint, MapEntry]> { - for (const entry of this.map) { - yield [entry.key, entry.value]; - } - } -} - export class OutlierDetectionLoadBalancer implements LoadBalancer { private childBalancer: ChildLoadBalancerHandler; - private entryMap = new EndpointMap(); + private entryMap = new EndpointMap(); private latestConfig: OutlierDetectionLoadBalancingConfig | null = null; private ejectionTimer: NodeJS.Timeout; private timerStartTime: Date | null = null; diff --git a/packages/grpc-js/src/load-balancer-pick-first.ts b/packages/grpc-js/src/load-balancer-pick-first.ts index 9af662662..5cbd11cf3 100644 --- a/packages/grpc-js/src/load-balancer-pick-first.ts +++ b/packages/grpc-js/src/load-balancer-pick-first.ts @@ -541,6 +541,19 @@ export class LeafLoadBalancer { this.pickFirstBalancer.updateAddressList([this.endpoint], LEAF_CONFIG); } + /** + * Update the endpoint associated with this LeafLoadBalancer to a new + * endpoint. Does not trigger connection establishment if a connection + * attempt is not already in progress. + * @param newEndpoint + */ + updateEndpoint(newEndpoint: Endpoint) { + this.endpoint = newEndpoint; + if (this.latestState !== ConnectivityState.IDLE) { + this.startConnecting(); + } + } + getConnectivityState() { return this.latestState; } diff --git a/packages/grpc-js/src/picker.ts b/packages/grpc-js/src/picker.ts index 6474269f7..e0526f7a3 100644 --- a/packages/grpc-js/src/picker.ts +++ b/packages/grpc-js/src/picker.ts @@ -17,9 +17,10 @@ import { StatusObject } from './call-interface'; import { Metadata } from './metadata'; -import { Status } from './constants'; +import { LogVerbosity, Status } from './constants'; import { LoadBalancer } from './load-balancer'; import { SubchannelInterface } from './subchannel-interface'; +import { trace } from './logging'; export enum PickResultType { COMPLETE, @@ -122,25 +123,40 @@ export class UnavailablePicker implements Picker { * indicating that the pick should be tried again with the next `Picker`. Also * reports back to the load balancer that a connection should be established * once any pick is attempted. + * If the childPicker is provided, delegate to it instead of returning the + * hardcoded QUEUE pick result, but still calls exitIdle. */ export class QueuePicker { private calledExitIdle = false; // Constructed with a load balancer. Calls exitIdle on it the first time pick is called - constructor(private loadBalancer: LoadBalancer) {} + constructor( + private loadBalancer: LoadBalancer, + private childPicker?: Picker + ) {} - pick(pickArgs: PickArgs): QueuePickResult { + pick(pickArgs: PickArgs): PickResult { + trace( + LogVerbosity.DEBUG, + 'picker', + 'Queue picker called for load balancer of type ' + + this.loadBalancer.constructor.name + ); if (!this.calledExitIdle) { process.nextTick(() => { this.loadBalancer.exitIdle(); }); this.calledExitIdle = true; } - return { - pickResultType: PickResultType.QUEUE, - subchannel: null, - status: null, - onCallStarted: null, - onCallEnded: null, - }; + if (this.childPicker) { + return this.childPicker.pick(pickArgs); + } else { + return { + pickResultType: PickResultType.QUEUE, + subchannel: null, + status: null, + onCallStarted: null, + onCallEnded: null, + }; + } } } diff --git a/packages/grpc-js/src/resolver.ts b/packages/grpc-js/src/resolver.ts index 4dfa8d133..1c84c0490 100644 --- a/packages/grpc-js/src/resolver.ts +++ b/packages/grpc-js/src/resolver.ts @@ -37,7 +37,7 @@ export interface CallConfig { * https://github.com/grpc/proposal/blob/master/A31-xds-timeout-support-and-config-selector.md#new-functionality-in-grpc */ export interface ConfigSelector { - (methodName: string, metadata: Metadata): CallConfig; + (methodName: string, metadata: Metadata, channelId: number): CallConfig; } /** diff --git a/packages/grpc-js/src/resolving-load-balancer.ts b/packages/grpc-js/src/resolving-load-balancer.ts index df480400a..e600047e8 100644 --- a/packages/grpc-js/src/resolving-load-balancer.ts +++ b/packages/grpc-js/src/resolving-load-balancer.ts @@ -279,7 +279,7 @@ export class ResolvingLoadBalancer implements LoadBalancer { ); // Ensure that this.exitIdle() is called by the picker if (connectivityState === ConnectivityState.IDLE) { - picker = new QueuePicker(this); + picker = new QueuePicker(this, picker); } this.currentState = connectivityState; this.channelControlHelper.updateState(connectivityState, picker); diff --git a/packages/grpc-js/src/subchannel-address.ts b/packages/grpc-js/src/subchannel-address.ts index 36fd99ea6..70a7962f7 100644 --- a/packages/grpc-js/src/subchannel-address.ts +++ b/packages/grpc-js/src/subchannel-address.ts @@ -122,3 +122,127 @@ export function endpointHasAddress( } return false; } + +interface EndpointMapEntry { + key: Endpoint; + value: ValueType; +} + +function endpointEqualUnordered( + endpoint1: Endpoint, + endpoint2: Endpoint +): boolean { + if (endpoint1.addresses.length !== endpoint2.addresses.length) { + return false; + } + for (const address1 of endpoint1.addresses) { + let matchFound = false; + for (const address2 of endpoint2.addresses) { + if (subchannelAddressEqual(address1, address2)) { + matchFound = true; + break; + } + } + if (!matchFound) { + return false; + } + } + return true; +} + +export class EndpointMap { + private map: Set> = new Set(); + + get size() { + return this.map.size; + } + + getForSubchannelAddress(address: SubchannelAddress): ValueType | undefined { + for (const entry of this.map) { + if (endpointHasAddress(entry.key, address)) { + return entry.value; + } + } + return undefined; + } + + /** + * Delete any entries in this map with keys that are not in endpoints + * @param endpoints + */ + deleteMissing(endpoints: Endpoint[]): ValueType[] { + const removedValues: ValueType[] = []; + for (const entry of this.map) { + let foundEntry = false; + for (const endpoint of endpoints) { + if (endpointEqualUnordered(endpoint, entry.key)) { + foundEntry = true; + } + } + if (!foundEntry) { + removedValues.push(entry.value); + this.map.delete(entry); + } + } + return removedValues; + } + + get(endpoint: Endpoint): ValueType | undefined { + for (const entry of this.map) { + if (endpointEqualUnordered(endpoint, entry.key)) { + return entry.value; + } + } + return undefined; + } + + set(endpoint: Endpoint, mapEntry: ValueType) { + for (const entry of this.map) { + if (endpointEqualUnordered(endpoint, entry.key)) { + entry.value = mapEntry; + return; + } + } + this.map.add({ key: endpoint, value: mapEntry }); + } + + delete(endpoint: Endpoint) { + for (const entry of this.map) { + if (endpointEqualUnordered(endpoint, entry.key)) { + this.map.delete(entry); + return; + } + } + } + + has(endpoint: Endpoint): boolean { + for (const entry of this.map) { + if (endpointEqualUnordered(endpoint, entry.key)) { + return true; + } + } + return false; + } + + clear() { + this.map.clear(); + } + + *keys(): IterableIterator { + for (const entry of this.map) { + yield entry.key; + } + } + + *values(): IterableIterator { + for (const entry of this.map) { + yield entry.value; + } + } + + *entries(): IterableIterator<[Endpoint, ValueType]> { + for (const entry of this.map) { + yield [entry.key, entry.value]; + } + } +} From 036e0e1b7f9e16f3b1ccde949e963f66911df4ec Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 7 Sep 2023 17:15:20 -0700 Subject: [PATCH 029/109] grpc-js-xds: Enable xDS affinity test --- packages/grpc-js-xds/scripts/xds_k8s_lb.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh b/packages/grpc-js-xds/scripts/xds_k8s_lb.sh index a1e24ff1f..e4a0cf214 100755 --- a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh +++ b/packages/grpc-js-xds/scripts/xds_k8s_lb.sh @@ -166,6 +166,7 @@ main() { cd "${TEST_DRIVER_FULL_DIR}" local failed_tests=0 test_suites=( + "affinity_test" "api_listener_test" "baseline_test" "change_backend_service_test" From 4bff372df7c005a27618f180f561cb5256fe3191 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 7 Sep 2023 17:24:17 -0700 Subject: [PATCH 030/109] grpc-js: Remove logging in QueuePicker --- packages/grpc-js/src/picker.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/grpc-js/src/picker.ts b/packages/grpc-js/src/picker.ts index e0526f7a3..ac79c9fee 100644 --- a/packages/grpc-js/src/picker.ts +++ b/packages/grpc-js/src/picker.ts @@ -17,10 +17,9 @@ import { StatusObject } from './call-interface'; import { Metadata } from './metadata'; -import { LogVerbosity, Status } from './constants'; +import { Status } from './constants'; import { LoadBalancer } from './load-balancer'; import { SubchannelInterface } from './subchannel-interface'; -import { trace } from './logging'; export enum PickResultType { COMPLETE, @@ -135,12 +134,6 @@ export class QueuePicker { ) {} pick(pickArgs: PickArgs): PickResult { - trace( - LogVerbosity.DEBUG, - 'picker', - 'Queue picker called for load balancer of type ' + - this.loadBalancer.constructor.name - ); if (!this.calledExitIdle) { process.nextTick(() => { this.loadBalancer.exitIdle(); From 9974f7704dac5073a97bb108d5ad96c827fc4677 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 7 Sep 2023 17:59:35 -0700 Subject: [PATCH 031/109] grpc-js-xds: Drop support for Node versions below 16 --- packages/grpc-js-xds/package.json | 2 +- run-tests.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grpc-js-xds/package.json b/packages/grpc-js-xds/package.json index c240bfee5..8910b856f 100644 --- a/packages/grpc-js-xds/package.json +++ b/packages/grpc-js-xds/package.json @@ -53,7 +53,7 @@ "@grpc/grpc-js": "~1.8.0" }, "engines": { - "node": ">=10.10.0" + "node": ">=16.0.0" }, "files": [ "src/**/*.ts", diff --git a/run-tests.sh b/run-tests.sh index 0adcc0f17..16dff5cbf 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -28,7 +28,7 @@ cd $ROOT git submodule update --init --recursive if [ ! -n "$node_versions" ] ; then - node_versions="14 16" + node_versions="16" fi set +ex From 9e487e44ab74ef85dafd8e7b89aacf7dc17758c1 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 7 Sep 2023 18:07:20 -0700 Subject: [PATCH 032/109] grpc-js-xds: Update gts dependency for compatibility with TypeScript update --- packages/grpc-js-xds/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/package.json b/packages/grpc-js-xds/package.json index 8910b856f..9d254854c 100644 --- a/packages/grpc-js-xds/package.json +++ b/packages/grpc-js-xds/package.json @@ -38,7 +38,7 @@ "@types/mocha": "^5.2.6", "@types/node": "^13.11.1", "@types/yargs": "^15.0.5", - "gts": "^2.0.2", + "gts": "^5.0.1", "typescript": "^4.9.5", "yargs": "^15.4.1" }, From 0b2281b02804b339eea2f703b112bbc5e0a734f2 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 8 Sep 2023 10:12:14 -0700 Subject: [PATCH 033/109] Revert version support change, run ring_hash tests conditionallly --- packages/grpc-js-xds/gulpfile.ts | 4 +++- packages/grpc-js-xds/package.json | 2 +- packages/grpc-js-xds/src/resolver-xds.ts | 9 +++++++-- .../grpc-js-xds/test/test-confg-parsing.ts | 19 ++++++++++++++----- packages/grpc-js-xds/test/test-ring-hash.ts | 18 ++++++++++++++---- run-tests.sh | 2 +- 6 files changed, 40 insertions(+), 14 deletions(-) diff --git a/packages/grpc-js-xds/gulpfile.ts b/packages/grpc-js-xds/gulpfile.ts index 2bb43a7f1..93f93d8fc 100644 --- a/packages/grpc-js-xds/gulpfile.ts +++ b/packages/grpc-js-xds/gulpfile.ts @@ -63,7 +63,9 @@ const compile = checkTask(() => execNpmCommand('compile')); const runTests = checkTask(() => { process.env.GRPC_EXPERIMENTAL_XDS_FEDERATION = 'true'; process.env.GRPC_EXPERIMENTAL_XDS_CUSTOM_LB_CONFIG = 'true'; - process.env.GRPC_XDS_EXPERIMENTAL_ENABLE_RING_HASH = 'true'; + if (Number(process.versions.node.split('.')[0]) > 14) { + process.env.GRPC_XDS_EXPERIMENTAL_ENABLE_RING_HASH = 'true'; + } return gulp.src(`${outDir}/test/**/*.js`) .pipe(mocha({reporter: 'mocha-jenkins-reporter', require: ['ts-node/register']})); diff --git a/packages/grpc-js-xds/package.json b/packages/grpc-js-xds/package.json index 9d254854c..6ec4548d5 100644 --- a/packages/grpc-js-xds/package.json +++ b/packages/grpc-js-xds/package.json @@ -53,7 +53,7 @@ "@grpc/grpc-js": "~1.8.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=10.10.0" }, "files": [ "src/**/*.ts", diff --git a/packages/grpc-js-xds/src/resolver-xds.ts b/packages/grpc-js-xds/src/resolver-xds.ts index 3acdd2329..5182e1005 100644 --- a/packages/grpc-js-xds/src/resolver-xds.ts +++ b/packages/grpc-js-xds/src/resolver-xds.ts @@ -587,11 +587,16 @@ class XdsResolver implements Resolver { const onCommitted = () => { this.unrefCluster(clusterResult.name); } - const hash = action.getHash(metadata, channelId); + let hash: string; + if (EXPERIMENTAL_RING_HASH) { + hash = `${action.getHash(metadata, channelId)}`; + } else { + hash = ''; + } return { methodConfig: clusterResult.methodConfig, onCommitted: onCommitted, - pickInformation: {cluster: clusterResult.name, hash: `${hash}`}, + pickInformation: {cluster: clusterResult.name, hash: hash}, status: status.OK, dynamicFilterFactories: clusterResult.dynamicFilterFactories }; diff --git a/packages/grpc-js-xds/test/test-confg-parsing.ts b/packages/grpc-js-xds/test/test-confg-parsing.ts index f2065805f..c185c8527 100644 --- a/packages/grpc-js-xds/test/test-confg-parsing.ts +++ b/packages/grpc-js-xds/test/test-confg-parsing.ts @@ -19,6 +19,7 @@ import { experimental, LoadBalancingConfig } from "@grpc/grpc-js"; import { register } from "../src"; import assert = require("assert"); import parseLoadbalancingConfig = experimental.parseLoadBalancingConfig; +import { EXPERIMENTAL_RING_HASH } from "../src/environment"; register(); @@ -34,6 +35,7 @@ interface TestCase { input: object, output?: object; error?: RegExp; + skipIf?: boolean; } /* The main purpose of these tests is to verify that configs that are expected @@ -319,28 +321,32 @@ const allTestCases: {[lbPolicyName: string]: TestCase[]} = { output: { min_ring_size: 1024, max_ring_size: 4096 - } + }, + skipIf: !EXPERIMENTAL_RING_HASH }, { name: 'populated config', input: { min_ring_size: 2048, max_ring_size: 8192 - } + }, + skipIf: !EXPERIMENTAL_RING_HASH }, { name: 'min_ring_size too large', input: { min_ring_size: 8_388_609 }, - error: /min_ring_size/ + error: /min_ring_size/, + skipIf: !EXPERIMENTAL_RING_HASH }, { name: 'max_ring_size too large', input: { max_ring_size: 8_388_609 }, - error: /max_ring_size/ + error: /max_ring_size/, + skipIf: !EXPERIMENTAL_RING_HASH } ] } @@ -349,7 +355,10 @@ describe('Load balancing policy config parsing', () => { for (const [lbPolicyName, testCases] of Object.entries(allTestCases)) { describe(lbPolicyName, () => { for (const testCase of testCases) { - it(testCase.name, () => { + it(testCase.name, function() { + if (testCase.skipIf) { + this.skip(); + } const lbConfigInput = {[lbPolicyName]: testCase.input}; if (testCase.error) { assert.throws(() => { diff --git a/packages/grpc-js-xds/test/test-ring-hash.ts b/packages/grpc-js-xds/test/test-ring-hash.ts index 0a9e893ef..af795bc97 100644 --- a/packages/grpc-js-xds/test/test-ring-hash.ts +++ b/packages/grpc-js-xds/test/test-ring-hash.ts @@ -25,6 +25,7 @@ import assert = require("assert"); import { Any } from "../src/generated/google/protobuf/Any"; import { AnyExtension } from "@grpc/proto-loader"; import { RingHash } from "../src/generated/envoy/extensions/load_balancing_policies/ring_hash/v3/RingHash"; +import { EXPERIMENTAL_RING_HASH } from "../src/environment"; register(); @@ -41,7 +42,10 @@ describe('Ring hash LB policy', () => { client?.close(); xdsServer?.shutdownServer(); }); - it('Should route requests to the single backend with the old lbPolicy field', done => { + it('Should route requests to the single backend with the old lbPolicy field', function(done) { + if (!EXPERIMENTAL_RING_HASH) { + this.skip(); + } const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [new Backend()], locality:{region: 'region1'}}], 'RING_HASH'); const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: cluster}]); routeGroup.startAllBackends().then(() => { @@ -59,7 +63,10 @@ describe('Ring hash LB policy', () => { client.sendOneCall(done); }, reason => done(reason)); }); - it('Should route requests to the single backend with the new load_balancing_policy field', done => { + it('Should route requests to the single backend with the new load_balancing_policy field', function(done) { + if (!EXPERIMENTAL_RING_HASH) { + this.skip(); + } const lbPolicy: AnyExtension & RingHash = { '@type': 'type.googleapis.com/envoy.extensions.load_balancing_policies.ring_hash.v3.RingHash', hash_function: 'XX_HASH' @@ -81,7 +88,10 @@ describe('Ring hash LB policy', () => { client.sendOneCall(done); }, reason => done(reason)); }); - it('Should route all identical requests to the same backend', done => { + it('Should route all identical requests to the same backend', function(done) { + if (!EXPERIMENTAL_RING_HASH) { + this.skip(); + } const backend1 = new Backend(); const backend2 = new Backend() const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [backend1, backend2], locality:{region: 'region1'}}], 'RING_HASH'); @@ -105,4 +115,4 @@ describe('Ring hash LB policy', () => { }) }, reason => done(reason)); }); -}) +}); diff --git a/run-tests.sh b/run-tests.sh index 16dff5cbf..0adcc0f17 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -28,7 +28,7 @@ cd $ROOT git submodule update --init --recursive if [ ! -n "$node_versions" ] ; then - node_versions="16" + node_versions="14 16" fi set +ex From c41c3dae7b2be1a1846984544f4d48de647a6ece Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 8 Sep 2023 14:51:58 -0700 Subject: [PATCH 034/109] Test ring_hash fallback on dropped connection --- packages/grpc-js-xds/test/test-ring-hash.ts | 55 +++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/packages/grpc-js-xds/test/test-ring-hash.ts b/packages/grpc-js-xds/test/test-ring-hash.ts index af795bc97..20d9eeed1 100644 --- a/packages/grpc-js-xds/test/test-ring-hash.ts +++ b/packages/grpc-js-xds/test/test-ring-hash.ts @@ -115,4 +115,59 @@ describe('Ring hash LB policy', () => { }) }, reason => done(reason)); }); + it('Should fallback to a second backend if the first one goes down', function(done) { + if (!EXPERIMENTAL_RING_HASH) { + this.skip(); + } + const backends = [new Backend(), new Backend(), new Backend()]; + const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: backends, locality:{region: 'region1'}}], 'RING_HASH'); + const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: cluster}]); + routeGroup.startAllBackends().then(() => { + xdsServer.setEdsResource(cluster.getEndpointConfig()); + xdsServer.setCdsResource(cluster.getClusterConfig()); + xdsServer.setRdsResource(routeGroup.getRouteConfiguration()); + xdsServer.setLdsResource(routeGroup.getListener()); + xdsServer.addResponseListener((typeUrl, responseState) => { + if (responseState.state === 'NACKED') { + client.stopCalls(); + assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`); + } + }) + client = XdsTestClient.createFromServer('listener1', xdsServer); + client.sendNCalls(100, error => { + assert.ifError(error); + let backendWithTraffic: number | null = null; + for (let i = 0; i < backends.length; i++) { + if (backendWithTraffic === null) { + if (backends[i].getCallCount() > 0) { + backendWithTraffic = i; + } + } else { + assert.strictEqual(backends[i].getCallCount(), 0, `Backends ${backendWithTraffic} and ${i} both got traffic`); + } + } + assert.notStrictEqual(backendWithTraffic, null, 'No backend got traffic'); + backends[backendWithTraffic!].shutdown(error => { + assert.ifError(error); + backends[backendWithTraffic!].resetCallCount(); + client.sendNCalls(100, error => { + assert.ifError(error); + let backendWithTraffic2: number | null = null; + for (let i = 0; i < backends.length; i++) { + if (backendWithTraffic2 === null) { + if (backends[i].getCallCount() > 0) { + backendWithTraffic2 = i; + } + } else { + assert.strictEqual(backends[i].getCallCount(), 0, `Backends ${backendWithTraffic2} and ${i} both got traffic`); + } + } + assert.notStrictEqual(backendWithTraffic2, null, 'No backend got traffic'); + assert.notStrictEqual(backendWithTraffic2, backendWithTraffic, `Traffic went to the same backend ${backendWithTraffic} after shutdown`); + done(); + }); + }); + }); + }, reason => done(reason)); + }) }); From 5c8b11b0be738666ec00f02fb679c9ff8fef947d Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 11 Sep 2023 15:39:19 -0700 Subject: [PATCH 035/109] Trace parsed unvalidated resources --- .../src/xds-resource-type/cluster-resource-type.ts | 1 + .../src/xds-resource-type/endpoint-resource-type.ts | 1 + .../src/xds-resource-type/listener-resource-type.ts | 1 + .../src/xds-resource-type/route-config-resource-type.ts | 7 +++++++ 4 files changed, 10 insertions(+) diff --git a/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts index c13a91d7e..0038fe912 100644 --- a/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts +++ b/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts @@ -285,6 +285,7 @@ export class ClusterResourceType extends XdsResourceType { ); } const message = decodeSingleResource(CDS_TYPE_URL, resource.value); + trace('Decoded raw resource of type ' + CDS_TYPE_URL + ': ' + JSON.stringify(message)); const validatedMessage = this.validateResource(context, message); if (validatedMessage) { return { diff --git a/packages/grpc-js-xds/src/xds-resource-type/endpoint-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/endpoint-resource-type.ts index 6ffd7788d..10d8cc3c7 100644 --- a/packages/grpc-js-xds/src/xds-resource-type/endpoint-resource-type.ts +++ b/packages/grpc-js-xds/src/xds-resource-type/endpoint-resource-type.ts @@ -101,6 +101,7 @@ export class EndpointResourceType extends XdsResourceType { ); } const message = decodeSingleResource(EDS_TYPE_URL, resource.value); + trace('Decoded raw resource of type ' + EDS_TYPE_URL + ': ' + JSON.stringify(message)); const validatedMessage = this.validateResource(message); if (validatedMessage) { return { diff --git a/packages/grpc-js-xds/src/xds-resource-type/listener-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/listener-resource-type.ts index 20e243b86..73782c380 100644 --- a/packages/grpc-js-xds/src/xds-resource-type/listener-resource-type.ts +++ b/packages/grpc-js-xds/src/xds-resource-type/listener-resource-type.ts @@ -106,6 +106,7 @@ export class ListenerResourceType extends XdsResourceType { ); } const message = decodeSingleResource(LDS_TYPE_URL, resource.value); + trace('Decoded raw resource of type ' + LDS_TYPE_URL + ': ' + JSON.stringify(message)); const validatedMessage = this.validateResource(message); if (validatedMessage) { return { diff --git a/packages/grpc-js-xds/src/xds-resource-type/route-config-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/route-config-resource-type.ts index cdc2e3196..6c0bc93e9 100644 --- a/packages/grpc-js-xds/src/xds-resource-type/route-config-resource-type.ts +++ b/packages/grpc-js-xds/src/xds-resource-type/route-config-resource-type.ts @@ -15,6 +15,7 @@ * */ +import { experimental, logVerbosity } from "@grpc/grpc-js"; import { EXPERIMENTAL_FAULT_INJECTION, EXPERIMENTAL_RETRY } from "../environment"; import { RetryPolicy__Output } from "../generated/envoy/config/route/v3/RetryPolicy"; import { RouteConfiguration__Output } from "../generated/envoy/config/route/v3/RouteConfiguration"; @@ -24,6 +25,11 @@ import { validateOverrideFilter } from "../http-filter"; import { RDS_TYPE_URL, decodeSingleResource } from "../resources"; import { Watcher, XdsClient } from "../xds-client"; import { XdsDecodeContext, XdsDecodeResult, XdsResourceType } from "./xds-resource-type"; +const TRACER_NAME = 'xds_client'; + +function trace(text: string): void { + experimental.trace(logVerbosity.DEBUG, TRACER_NAME, text); +} const SUPPORTED_PATH_SPECIFIERS = ['prefix', 'path', 'safe_regex']; const SUPPPORTED_HEADER_MATCH_SPECIFIERS = [ @@ -169,6 +175,7 @@ export class RouteConfigurationResourceType extends XdsResourceType { ); } const message = decodeSingleResource(RDS_TYPE_URL, resource.value); + trace('Decoded raw resource of type ' + RDS_TYPE_URL + ': ' + JSON.stringify(message)); const validatedMessage = this.validateResource(message); if (validatedMessage) { return { From e570a99d6df993fbf2174c08811819310909dac8 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 11 Sep 2023 17:29:01 -0700 Subject: [PATCH 036/109] Improve unvalidated resource log formatting --- .../grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts | 2 +- .../grpc-js-xds/src/xds-resource-type/endpoint-resource-type.ts | 2 +- .../grpc-js-xds/src/xds-resource-type/listener-resource-type.ts | 2 +- .../src/xds-resource-type/route-config-resource-type.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts index 0038fe912..9934c1760 100644 --- a/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts +++ b/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts @@ -285,7 +285,7 @@ export class ClusterResourceType extends XdsResourceType { ); } const message = decodeSingleResource(CDS_TYPE_URL, resource.value); - trace('Decoded raw resource of type ' + CDS_TYPE_URL + ': ' + JSON.stringify(message)); + trace('Decoded raw resource of type ' + CDS_TYPE_URL + ': ' + JSON.stringify(message, undefined, 2)); const validatedMessage = this.validateResource(context, message); if (validatedMessage) { return { diff --git a/packages/grpc-js-xds/src/xds-resource-type/endpoint-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/endpoint-resource-type.ts index 10d8cc3c7..093ca52e5 100644 --- a/packages/grpc-js-xds/src/xds-resource-type/endpoint-resource-type.ts +++ b/packages/grpc-js-xds/src/xds-resource-type/endpoint-resource-type.ts @@ -101,7 +101,7 @@ export class EndpointResourceType extends XdsResourceType { ); } const message = decodeSingleResource(EDS_TYPE_URL, resource.value); - trace('Decoded raw resource of type ' + EDS_TYPE_URL + ': ' + JSON.stringify(message)); + trace('Decoded raw resource of type ' + EDS_TYPE_URL + ': ' + JSON.stringify(message, undefined, 2)); const validatedMessage = this.validateResource(message); if (validatedMessage) { return { diff --git a/packages/grpc-js-xds/src/xds-resource-type/listener-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/listener-resource-type.ts index 73782c380..a557e1988 100644 --- a/packages/grpc-js-xds/src/xds-resource-type/listener-resource-type.ts +++ b/packages/grpc-js-xds/src/xds-resource-type/listener-resource-type.ts @@ -106,7 +106,7 @@ export class ListenerResourceType extends XdsResourceType { ); } const message = decodeSingleResource(LDS_TYPE_URL, resource.value); - trace('Decoded raw resource of type ' + LDS_TYPE_URL + ': ' + JSON.stringify(message)); + trace('Decoded raw resource of type ' + LDS_TYPE_URL + ': ' + JSON.stringify(message, undefined, 2)); const validatedMessage = this.validateResource(message); if (validatedMessage) { return { diff --git a/packages/grpc-js-xds/src/xds-resource-type/route-config-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/route-config-resource-type.ts index 6c0bc93e9..766a84388 100644 --- a/packages/grpc-js-xds/src/xds-resource-type/route-config-resource-type.ts +++ b/packages/grpc-js-xds/src/xds-resource-type/route-config-resource-type.ts @@ -175,7 +175,7 @@ export class RouteConfigurationResourceType extends XdsResourceType { ); } const message = decodeSingleResource(RDS_TYPE_URL, resource.value); - trace('Decoded raw resource of type ' + RDS_TYPE_URL + ': ' + JSON.stringify(message)); + trace('Decoded raw resource of type ' + RDS_TYPE_URL + ': ' + JSON.stringify(message, undefined, 2)); const validatedMessage = this.validateResource(message); if (validatedMessage) { return { From 57c1bd2ede8445c4f76390e2715ea20a3a20f06e Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 11 Sep 2023 17:32:41 -0700 Subject: [PATCH 037/109] grpc-js-xds: interop client: reduce periodic logging --- packages/grpc-js-xds/interop/xds-interop-client.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/grpc-js-xds/interop/xds-interop-client.ts b/packages/grpc-js-xds/interop/xds-interop-client.ts index f26278abb..1fdaa3a69 100644 --- a/packages/grpc-js-xds/interop/xds-interop-client.ts +++ b/packages/grpc-js-xds/interop/xds-interop-client.ts @@ -467,9 +467,11 @@ function sendConstantQps(client: TestServiceClient, qps: number, failOnFailedRpc makeSingleRequest(client, callType, failOnFailedRpcs, callStatsTracker, callStartTimestampsTrackers[callType]); } }, 1000/qps); - setInterval(() => { - console.log(`Accumulated stats: ${JSON.stringify(accumulatedStats, undefined, 2)}`); - }, 1000); + if (VERBOSITY >= 2) { + setInterval(() => { + console.log(`Accumulated stats: ${JSON.stringify(accumulatedStats, undefined, 2)}`); + }, 1000); + } } const callTypeEnumMap = { From 8df1bd712f13ae9639a76f1fb60410edebdbf31c Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 12 Sep 2023 10:08:25 -0700 Subject: [PATCH 038/109] Treat ring_hash_lb_config field as optional --- .../src/xds-resource-type/cluster-resource-type.ts | 9 +++------ packages/grpc-js-xds/test/framework.ts | 3 --- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts index 9934c1760..c4081baa8 100644 --- a/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts +++ b/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts @@ -151,17 +151,14 @@ export class ClusterResourceType extends XdsResourceType { } }; } else if(EXPERIMENTAL_RING_HASH && message.lb_policy === 'RING_HASH') { - if (!message.ring_hash_lb_config) { + if (message.ring_hash_lb_config && message.ring_hash_lb_config.hash_function !== 'XX_HASH') { return null; } - if (message.ring_hash_lb_config.hash_function !== 'XX_HASH') { - return null; - } - const minRingSize = message.ring_hash_lb_config.minimum_ring_size ? Number(message.ring_hash_lb_config.minimum_ring_size.value) : 1024; + const minRingSize = message.ring_hash_lb_config?.minimum_ring_size ? Number(message.ring_hash_lb_config.minimum_ring_size.value) : 1024; if (minRingSize > 8_388_608) { return null; } - const maxRingSize = message.ring_hash_lb_config.maximum_ring_size ? Number(message.ring_hash_lb_config.maximum_ring_size.value) : 8_388_608; + const maxRingSize = message.ring_hash_lb_config?.maximum_ring_size ? Number(message.ring_hash_lb_config.maximum_ring_size.value) : 8_388_608; if (maxRingSize > 8_388_608) { return null; } diff --git a/packages/grpc-js-xds/test/framework.ts b/packages/grpc-js-xds/test/framework.ts index d38437a10..bd6f270d6 100644 --- a/packages/grpc-js-xds/test/framework.ts +++ b/packages/grpc-js-xds/test/framework.ts @@ -96,9 +96,6 @@ export class FakeEdsCluster implements FakeCluster { }; if (this.loadBalancingPolicyOverride === 'RING_HASH') { result.lb_policy = 'RING_HASH'; - result.ring_hash_lb_config = { - hash_function: 'XX_HASH' - }; } else if (this.loadBalancingPolicyOverride) { result.load_balancing_policy = { policies: [ From 506748b8a44adef2e10d8cb2af382efe4a4166d1 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 12 Sep 2023 12:41:35 -0700 Subject: [PATCH 039/109] Enable ring_hash tracing in interop tests --- packages/grpc-js-xds/interop/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/interop/Dockerfile b/packages/grpc-js-xds/interop/Dockerfile index 63a321061..6239b5f22 100644 --- a/packages/grpc-js-xds/interop/Dockerfile +++ b/packages/grpc-js-xds/interop/Dockerfile @@ -33,6 +33,6 @@ COPY --from=build /node/src/grpc-node/packages/grpc-js ./packages/grpc-js/ COPY --from=build /node/src/grpc-node/packages/grpc-js-xds ./packages/grpc-js-xds/ ENV GRPC_VERBOSITY="DEBUG" -ENV GRPC_TRACE=xds_client,xds_resolver,xds_cluster_manager,cds_balancer,xds_cluster_resolver,xds_cluster_impl,priority,weighted_target,round_robin,resolving_load_balancer,subchannel,keepalive,dns_resolver,fault_injection,http_filter,csds,outlier_detection,server,server_call +ENV GRPC_TRACE=xds_client,xds_resolver,xds_cluster_manager,cds_balancer,xds_cluster_resolver,xds_cluster_impl,priority,weighted_target,round_robin,resolving_load_balancer,subchannel,keepalive,dns_resolver,fault_injection,http_filter,csds,outlier_detection,server,server_call,ring_hash ENTRYPOINT [ "/nodejs/bin/node", "/node/src/grpc-node/packages/grpc-js-xds/build/interop/xds-interop-client" ] From a02622572aec2b236257b86842d26eb32e5805f0 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 12 Sep 2023 13:00:15 -0700 Subject: [PATCH 040/109] Improve Listener resource log formatting --- packages/grpc-js-xds/src/xds-client.ts | 2 +- .../grpc-js-xds/src/xds-resource-type/listener-resource-type.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grpc-js-xds/src/xds-client.ts b/packages/grpc-js-xds/src/xds-client.ts index baabf2e76..d1aed9265 100644 --- a/packages/grpc-js-xds/src/xds-client.ts +++ b/packages/grpc-js-xds/src/xds-client.ts @@ -250,7 +250,7 @@ class AdsResponseParser { if (!decodeResult.value) { return; } - this.adsCallState.client.trace('Parsed resource of type ' + this.result.type.getTypeUrl() + ': ' + JSON.stringify(decodeResult.value, undefined, 2)); + this.adsCallState.client.trace('Parsed resource of type ' + this.result.type.getTypeUrl() + ': ' + JSON.stringify(decodeResult.value, (key, value) => (value && value.type === 'Buffer' && Array.isArray(value.data)) ? (value.data as Number[]).map(n => n.toString(16)).join('') : value, 2)); this.result.haveValidResources = true; if (this.result.type.resourcesEqual(resourceState.cachedResource, decodeResult.value)) { return; diff --git a/packages/grpc-js-xds/src/xds-resource-type/listener-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/listener-resource-type.ts index a557e1988..cf5d4d591 100644 --- a/packages/grpc-js-xds/src/xds-resource-type/listener-resource-type.ts +++ b/packages/grpc-js-xds/src/xds-resource-type/listener-resource-type.ts @@ -106,7 +106,7 @@ export class ListenerResourceType extends XdsResourceType { ); } const message = decodeSingleResource(LDS_TYPE_URL, resource.value); - trace('Decoded raw resource of type ' + LDS_TYPE_URL + ': ' + JSON.stringify(message, undefined, 2)); + trace('Decoded raw resource of type ' + LDS_TYPE_URL + ': ' + JSON.stringify(message, (key, value) => (value && value.type === 'Buffer' && Array.isArray(value.data)) ? (value.data as Number[]).map(n => n.toString(16)).join('') : value, 2)); const validatedMessage = this.validateResource(message); if (validatedMessage) { return { From 6567f8d7cd908bf259d7e93bfc2ee06cdfe015fe Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 13 Sep 2023 14:07:22 -0700 Subject: [PATCH 041/109] Update code generation with PickFirst message --- packages/grpc-js-xds/package.json | 2 +- .../pick_first/v3/PickFirst.ts | 26 ++++++++++ .../generated/google/protobuf/EnumOptions.ts | 3 -- .../google/protobuf/EnumValueOptions.ts | 7 --- .../generated/google/protobuf/FieldOptions.ts | 13 ----- .../generated/google/protobuf/FileOptions.ts | 6 --- .../google/protobuf/MessageOptions.ts | 11 ---- .../generated/google/protobuf/OneofOptions.ts | 2 - .../grpc-js-xds/src/generated/pick_first.ts | 52 +++++++++++++++++++ 9 files changed, 79 insertions(+), 43 deletions(-) create mode 100644 packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/pick_first/v3/PickFirst.ts create mode 100644 packages/grpc-js-xds/src/generated/pick_first.ts diff --git a/packages/grpc-js-xds/package.json b/packages/grpc-js-xds/package.json index 6ec4548d5..389f2b731 100644 --- a/packages/grpc-js-xds/package.json +++ b/packages/grpc-js-xds/package.json @@ -12,7 +12,7 @@ "prepare": "npm run compile", "pretest": "npm run compile", "posttest": "npm run check", - "generate-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --includeDirs deps/envoy-api/ deps/xds/ deps/googleapis/ deps/protoc-gen-validate/ -O src/generated/ --grpcLib @grpc/grpc-js envoy/service/discovery/v3/ads.proto envoy/service/load_stats/v3/lrs.proto envoy/config/listener/v3/listener.proto envoy/config/route/v3/route.proto envoy/config/cluster/v3/cluster.proto envoy/config/endpoint/v3/endpoint.proto envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto udpa/type/v1/typed_struct.proto xds/type/v3/typed_struct.proto envoy/extensions/filters/http/fault/v3/fault.proto envoy/service/status/v3/csds.proto envoy/extensions/load_balancing_policies/wrr_locality/v3/wrr_locality.proto envoy/extensions/load_balancing_policies/ring_hash/v3/ring_hash.proto", + "generate-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --includeDirs deps/envoy-api/ deps/xds/ deps/googleapis/ deps/protoc-gen-validate/ -O src/generated/ --grpcLib @grpc/grpc-js envoy/service/discovery/v3/ads.proto envoy/service/load_stats/v3/lrs.proto envoy/config/listener/v3/listener.proto envoy/config/route/v3/route.proto envoy/config/cluster/v3/cluster.proto envoy/config/endpoint/v3/endpoint.proto envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto udpa/type/v1/typed_struct.proto xds/type/v3/typed_struct.proto envoy/extensions/filters/http/fault/v3/fault.proto envoy/service/status/v3/csds.proto envoy/extensions/load_balancing_policies/wrr_locality/v3/wrr_locality.proto envoy/extensions/load_balancing_policies/ring_hash/v3/ring_hash.proto envoy/extensions/load_balancing_policies/pick_first/v3/pick_first.proto", "generate-interop-types": "proto-loader-gen-types --keep-case --longs String --enums String --defaults --oneofs --json --includeComments --includeDirs proto/ -O interop/generated --grpcLib @grpc/grpc-js grpc/testing/test.proto", "generate-test-types": "proto-loader-gen-types --keep-case --longs String --enums String --defaults --oneofs --json --includeComments --includeDirs proto/ -O test/generated --grpcLib @grpc/grpc-js grpc/testing/echo.proto" }, diff --git a/packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/pick_first/v3/PickFirst.ts b/packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/pick_first/v3/PickFirst.ts new file mode 100644 index 000000000..1208575d0 --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/pick_first/v3/PickFirst.ts @@ -0,0 +1,26 @@ +// Original file: deps/envoy-api/envoy/extensions/load_balancing_policies/pick_first/v3/pick_first.proto + + +/** + * This configuration allows the built-in PICK_FIRST LB policy to be configured + * via the LB policy extension point. + */ +export interface PickFirst { + /** + * If set to true, instructs the LB policy to shuffle the list of addresses + * received from the name resolver before attempting to connect to them. + */ + 'shuffle_address_list'?: (boolean); +} + +/** + * This configuration allows the built-in PICK_FIRST LB policy to be configured + * via the LB policy extension point. + */ +export interface PickFirst__Output { + /** + * If set to true, instructs the LB policy to shuffle the list of addresses + * received from the name resolver before attempting to connect to them. + */ + 'shuffle_address_list': (boolean); +} diff --git a/packages/grpc-js-xds/src/generated/google/protobuf/EnumOptions.ts b/packages/grpc-js-xds/src/generated/google/protobuf/EnumOptions.ts index 777901a54..b92ade4f9 100644 --- a/packages/grpc-js-xds/src/generated/google/protobuf/EnumOptions.ts +++ b/packages/grpc-js-xds/src/generated/google/protobuf/EnumOptions.ts @@ -1,18 +1,15 @@ // Original file: null import type { UninterpretedOption as _google_protobuf_UninterpretedOption, UninterpretedOption__Output as _google_protobuf_UninterpretedOption__Output } from '../../google/protobuf/UninterpretedOption'; -import type { MigrateAnnotation as _udpa_annotations_MigrateAnnotation, MigrateAnnotation__Output as _udpa_annotations_MigrateAnnotation__Output } from '../../udpa/annotations/MigrateAnnotation'; export interface EnumOptions { 'allowAlias'?: (boolean); 'deprecated'?: (boolean); 'uninterpretedOption'?: (_google_protobuf_UninterpretedOption)[]; - '.udpa.annotations.enum_migrate'?: (_udpa_annotations_MigrateAnnotation | null); } export interface EnumOptions__Output { 'allowAlias': (boolean); 'deprecated': (boolean); 'uninterpretedOption': (_google_protobuf_UninterpretedOption__Output)[]; - '.udpa.annotations.enum_migrate': (_udpa_annotations_MigrateAnnotation__Output | null); } diff --git a/packages/grpc-js-xds/src/generated/google/protobuf/EnumValueOptions.ts b/packages/grpc-js-xds/src/generated/google/protobuf/EnumValueOptions.ts index 9ba51ed60..e60ee6f4c 100644 --- a/packages/grpc-js-xds/src/generated/google/protobuf/EnumValueOptions.ts +++ b/packages/grpc-js-xds/src/generated/google/protobuf/EnumValueOptions.ts @@ -1,20 +1,13 @@ // Original file: null import type { UninterpretedOption as _google_protobuf_UninterpretedOption, UninterpretedOption__Output as _google_protobuf_UninterpretedOption__Output } from '../../google/protobuf/UninterpretedOption'; -import type { MigrateAnnotation as _udpa_annotations_MigrateAnnotation, MigrateAnnotation__Output as _udpa_annotations_MigrateAnnotation__Output } from '../../udpa/annotations/MigrateAnnotation'; export interface EnumValueOptions { 'deprecated'?: (boolean); 'uninterpretedOption'?: (_google_protobuf_UninterpretedOption)[]; - '.envoy.annotations.disallowed_by_default_enum'?: (boolean); - '.udpa.annotations.enum_value_migrate'?: (_udpa_annotations_MigrateAnnotation | null); - '.envoy.annotations.deprecated_at_minor_version_enum'?: (string); } export interface EnumValueOptions__Output { 'deprecated': (boolean); 'uninterpretedOption': (_google_protobuf_UninterpretedOption__Output)[]; - '.envoy.annotations.disallowed_by_default_enum': (boolean); - '.udpa.annotations.enum_value_migrate': (_udpa_annotations_MigrateAnnotation__Output | null); - '.envoy.annotations.deprecated_at_minor_version_enum': (string); } diff --git a/packages/grpc-js-xds/src/generated/google/protobuf/FieldOptions.ts b/packages/grpc-js-xds/src/generated/google/protobuf/FieldOptions.ts index f59acfbfe..3c3b446c9 100644 --- a/packages/grpc-js-xds/src/generated/google/protobuf/FieldOptions.ts +++ b/packages/grpc-js-xds/src/generated/google/protobuf/FieldOptions.ts @@ -1,9 +1,6 @@ // Original file: null import type { UninterpretedOption as _google_protobuf_UninterpretedOption, UninterpretedOption__Output as _google_protobuf_UninterpretedOption__Output } from '../../google/protobuf/UninterpretedOption'; -import type { FieldRules as _validate_FieldRules, FieldRules__Output as _validate_FieldRules__Output } from '../../validate/FieldRules'; -import type { FieldMigrateAnnotation as _udpa_annotations_FieldMigrateAnnotation, FieldMigrateAnnotation__Output as _udpa_annotations_FieldMigrateAnnotation__Output } from '../../udpa/annotations/FieldMigrateAnnotation'; -import type { FieldStatusAnnotation as _xds_annotations_v3_FieldStatusAnnotation, FieldStatusAnnotation__Output as _xds_annotations_v3_FieldStatusAnnotation__Output } from '../../xds/annotations/v3/FieldStatusAnnotation'; // Original file: null @@ -29,11 +26,6 @@ export interface FieldOptions { 'jstype'?: (_google_protobuf_FieldOptions_JSType | keyof typeof _google_protobuf_FieldOptions_JSType); 'weak'?: (boolean); 'uninterpretedOption'?: (_google_protobuf_UninterpretedOption)[]; - '.validate.rules'?: (_validate_FieldRules | null); - '.envoy.annotations.deprecated_at_minor_version'?: (string); - '.udpa.annotations.field_migrate'?: (_udpa_annotations_FieldMigrateAnnotation | null); - '.envoy.annotations.disallowed_by_default'?: (boolean); - '.xds.annotations.v3.field_status'?: (_xds_annotations_v3_FieldStatusAnnotation | null); } export interface FieldOptions__Output { @@ -44,9 +36,4 @@ export interface FieldOptions__Output { 'jstype': (keyof typeof _google_protobuf_FieldOptions_JSType); 'weak': (boolean); 'uninterpretedOption': (_google_protobuf_UninterpretedOption__Output)[]; - '.validate.rules': (_validate_FieldRules__Output | null); - '.envoy.annotations.deprecated_at_minor_version': (string); - '.udpa.annotations.field_migrate': (_udpa_annotations_FieldMigrateAnnotation__Output | null); - '.envoy.annotations.disallowed_by_default': (boolean); - '.xds.annotations.v3.field_status': (_xds_annotations_v3_FieldStatusAnnotation__Output | null); } diff --git a/packages/grpc-js-xds/src/generated/google/protobuf/FileOptions.ts b/packages/grpc-js-xds/src/generated/google/protobuf/FileOptions.ts index 48a376cd0..84500fc30 100644 --- a/packages/grpc-js-xds/src/generated/google/protobuf/FileOptions.ts +++ b/packages/grpc-js-xds/src/generated/google/protobuf/FileOptions.ts @@ -1,9 +1,7 @@ // Original file: null import type { UninterpretedOption as _google_protobuf_UninterpretedOption, UninterpretedOption__Output as _google_protobuf_UninterpretedOption__Output } from '../../google/protobuf/UninterpretedOption'; -import type { FileMigrateAnnotation as _udpa_annotations_FileMigrateAnnotation, FileMigrateAnnotation__Output as _udpa_annotations_FileMigrateAnnotation__Output } from '../../udpa/annotations/FileMigrateAnnotation'; import type { StatusAnnotation as _udpa_annotations_StatusAnnotation, StatusAnnotation__Output as _udpa_annotations_StatusAnnotation__Output } from '../../udpa/annotations/StatusAnnotation'; -import type { FileStatusAnnotation as _xds_annotations_v3_FileStatusAnnotation, FileStatusAnnotation__Output as _xds_annotations_v3_FileStatusAnnotation__Output } from '../../xds/annotations/v3/FileStatusAnnotation'; // Original file: null @@ -29,9 +27,7 @@ export interface FileOptions { 'objcClassPrefix'?: (string); 'csharpNamespace'?: (string); 'uninterpretedOption'?: (_google_protobuf_UninterpretedOption)[]; - '.udpa.annotations.file_migrate'?: (_udpa_annotations_FileMigrateAnnotation | null); '.udpa.annotations.file_status'?: (_udpa_annotations_StatusAnnotation | null); - '.xds.annotations.v3.file_status'?: (_xds_annotations_v3_FileStatusAnnotation | null); } export interface FileOptions__Output { @@ -50,7 +46,5 @@ export interface FileOptions__Output { 'objcClassPrefix': (string); 'csharpNamespace': (string); 'uninterpretedOption': (_google_protobuf_UninterpretedOption__Output)[]; - '.udpa.annotations.file_migrate': (_udpa_annotations_FileMigrateAnnotation__Output | null); '.udpa.annotations.file_status': (_udpa_annotations_StatusAnnotation__Output | null); - '.xds.annotations.v3.file_status': (_xds_annotations_v3_FileStatusAnnotation__Output | null); } diff --git a/packages/grpc-js-xds/src/generated/google/protobuf/MessageOptions.ts b/packages/grpc-js-xds/src/generated/google/protobuf/MessageOptions.ts index 71d8c855b..31f669eb0 100644 --- a/packages/grpc-js-xds/src/generated/google/protobuf/MessageOptions.ts +++ b/packages/grpc-js-xds/src/generated/google/protobuf/MessageOptions.ts @@ -1,9 +1,6 @@ // Original file: null import type { UninterpretedOption as _google_protobuf_UninterpretedOption, UninterpretedOption__Output as _google_protobuf_UninterpretedOption__Output } from '../../google/protobuf/UninterpretedOption'; -import type { VersioningAnnotation as _udpa_annotations_VersioningAnnotation, VersioningAnnotation__Output as _udpa_annotations_VersioningAnnotation__Output } from '../../udpa/annotations/VersioningAnnotation'; -import type { MigrateAnnotation as _udpa_annotations_MigrateAnnotation, MigrateAnnotation__Output as _udpa_annotations_MigrateAnnotation__Output } from '../../udpa/annotations/MigrateAnnotation'; -import type { MessageStatusAnnotation as _xds_annotations_v3_MessageStatusAnnotation, MessageStatusAnnotation__Output as _xds_annotations_v3_MessageStatusAnnotation__Output } from '../../xds/annotations/v3/MessageStatusAnnotation'; export interface MessageOptions { 'messageSetWireFormat'?: (boolean); @@ -11,10 +8,6 @@ export interface MessageOptions { 'deprecated'?: (boolean); 'mapEntry'?: (boolean); 'uninterpretedOption'?: (_google_protobuf_UninterpretedOption)[]; - '.validate.disabled'?: (boolean); - '.udpa.annotations.versioning'?: (_udpa_annotations_VersioningAnnotation | null); - '.udpa.annotations.message_migrate'?: (_udpa_annotations_MigrateAnnotation | null); - '.xds.annotations.v3.message_status'?: (_xds_annotations_v3_MessageStatusAnnotation | null); } export interface MessageOptions__Output { @@ -23,8 +16,4 @@ export interface MessageOptions__Output { 'deprecated': (boolean); 'mapEntry': (boolean); 'uninterpretedOption': (_google_protobuf_UninterpretedOption__Output)[]; - '.validate.disabled': (boolean); - '.udpa.annotations.versioning': (_udpa_annotations_VersioningAnnotation__Output | null); - '.udpa.annotations.message_migrate': (_udpa_annotations_MigrateAnnotation__Output | null); - '.xds.annotations.v3.message_status': (_xds_annotations_v3_MessageStatusAnnotation__Output | null); } diff --git a/packages/grpc-js-xds/src/generated/google/protobuf/OneofOptions.ts b/packages/grpc-js-xds/src/generated/google/protobuf/OneofOptions.ts index b54ecb0b1..d81d34797 100644 --- a/packages/grpc-js-xds/src/generated/google/protobuf/OneofOptions.ts +++ b/packages/grpc-js-xds/src/generated/google/protobuf/OneofOptions.ts @@ -4,10 +4,8 @@ import type { UninterpretedOption as _google_protobuf_UninterpretedOption, Unint export interface OneofOptions { 'uninterpretedOption'?: (_google_protobuf_UninterpretedOption)[]; - '.validate.required'?: (boolean); } export interface OneofOptions__Output { 'uninterpretedOption': (_google_protobuf_UninterpretedOption__Output)[]; - '.validate.required': (boolean); } diff --git a/packages/grpc-js-xds/src/generated/pick_first.ts b/packages/grpc-js-xds/src/generated/pick_first.ts new file mode 100644 index 000000000..9bf20e03f --- /dev/null +++ b/packages/grpc-js-xds/src/generated/pick_first.ts @@ -0,0 +1,52 @@ +import type * as grpc from '@grpc/grpc-js'; +import type { EnumTypeDefinition, MessageTypeDefinition } from '@grpc/proto-loader'; + + +type SubtypeConstructor any, Subtype> = { + new(...args: ConstructorParameters): Subtype; +}; + +export interface ProtoGrpcType { + envoy: { + extensions: { + load_balancing_policies: { + pick_first: { + v3: { + PickFirst: MessageTypeDefinition + } + } + } + } + } + google: { + protobuf: { + DescriptorProto: MessageTypeDefinition + EnumDescriptorProto: MessageTypeDefinition + EnumOptions: MessageTypeDefinition + EnumValueDescriptorProto: MessageTypeDefinition + EnumValueOptions: MessageTypeDefinition + FieldDescriptorProto: MessageTypeDefinition + FieldOptions: MessageTypeDefinition + FileDescriptorProto: MessageTypeDefinition + FileDescriptorSet: MessageTypeDefinition + FileOptions: MessageTypeDefinition + GeneratedCodeInfo: MessageTypeDefinition + MessageOptions: MessageTypeDefinition + MethodDescriptorProto: MessageTypeDefinition + MethodOptions: MessageTypeDefinition + OneofDescriptorProto: MessageTypeDefinition + OneofOptions: MessageTypeDefinition + ServiceDescriptorProto: MessageTypeDefinition + ServiceOptions: MessageTypeDefinition + SourceCodeInfo: MessageTypeDefinition + UninterpretedOption: MessageTypeDefinition + } + } + udpa: { + annotations: { + PackageVersionStatus: EnumTypeDefinition + StatusAnnotation: MessageTypeDefinition + } + } +} + From fe74b60440d124adfd38afa6338117501fb17c13 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 13 Sep 2023 14:27:25 -0700 Subject: [PATCH 042/109] grpc-js-xds: Add support for pick_first in xDS config --- packages/grpc-js-xds/gulpfile.ts | 1 + packages/grpc-js-xds/src/environment.ts | 1 + packages/grpc-js-xds/src/index.ts | 2 + .../src/lb-policy-registry/pick-first.ts | 77 +++++++++++++++++++ .../test/test-custom-lb-policies.ts | 26 ++++++- packages/grpc-js-xds/test/xds-server.ts | 1 + 6 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 packages/grpc-js-xds/src/lb-policy-registry/pick-first.ts diff --git a/packages/grpc-js-xds/gulpfile.ts b/packages/grpc-js-xds/gulpfile.ts index 93f93d8fc..becf109a6 100644 --- a/packages/grpc-js-xds/gulpfile.ts +++ b/packages/grpc-js-xds/gulpfile.ts @@ -63,6 +63,7 @@ const compile = checkTask(() => execNpmCommand('compile')); const runTests = checkTask(() => { process.env.GRPC_EXPERIMENTAL_XDS_FEDERATION = 'true'; process.env.GRPC_EXPERIMENTAL_XDS_CUSTOM_LB_CONFIG = 'true'; + process.env.GRPC_EXPERIMENTAL_PICKFIRST_LB_CONFIG = 'true'; if (Number(process.versions.node.split('.')[0]) > 14) { process.env.GRPC_XDS_EXPERIMENTAL_ENABLE_RING_HASH = 'true'; } diff --git a/packages/grpc-js-xds/src/environment.ts b/packages/grpc-js-xds/src/environment.ts index 858903111..02f14dabc 100644 --- a/packages/grpc-js-xds/src/environment.ts +++ b/packages/grpc-js-xds/src/environment.ts @@ -21,3 +21,4 @@ export const EXPERIMENTAL_RETRY = (process.env.GRPC_XDS_EXPERIMENTAL_ENABLE_RETR export const EXPERIMENTAL_FEDERATION = (process.env.GRPC_EXPERIMENTAL_XDS_FEDERATION ?? 'false') === 'true'; export const EXPERIMENTAL_CUSTOM_LB_CONFIG = (process.env.GRPC_EXPERIMENTAL_XDS_CUSTOM_LB_CONFIG ?? 'false') === 'true'; export const EXPERIMENTAL_RING_HASH = (process.env.GRPC_XDS_EXPERIMENTAL_ENABLE_RING_HASH ?? 'false') === 'true'; +export const EXPERIMENTAL_PICK_FIRST = (process.env.GRPC_EXPERIMENTAL_PICKFIRST_LB_CONFIG ?? 'false') === 'true'; diff --git a/packages/grpc-js-xds/src/index.ts b/packages/grpc-js-xds/src/index.ts index 70aa5bef2..aa603c9b7 100644 --- a/packages/grpc-js-xds/src/index.ts +++ b/packages/grpc-js-xds/src/index.ts @@ -29,6 +29,7 @@ import * as fault_injection_filter from './http-filter/fault-injection-filter'; import * as csds from './csds'; import * as round_robin_lb from './lb-policy-registry/round-robin'; import * as typed_struct_lb from './lb-policy-registry/typed-struct'; +import * as pick_first_lb from './lb-policy-registry/pick-first'; /** * Register the "xds:" name scheme with the @grpc/grpc-js library. @@ -48,4 +49,5 @@ export function register() { csds.setup(); round_robin_lb.setup(); typed_struct_lb.setup(); + pick_first_lb.setup(); } diff --git a/packages/grpc-js-xds/src/lb-policy-registry/pick-first.ts b/packages/grpc-js-xds/src/lb-policy-registry/pick-first.ts new file mode 100644 index 000000000..bfe2793d8 --- /dev/null +++ b/packages/grpc-js-xds/src/lb-policy-registry/pick-first.ts @@ -0,0 +1,77 @@ +/* + * 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. + * + */ + +// https://github.com/grpc/proposal/blob/master/A62-pick-first.md#pick_first-via-xds-1 + +import { LoadBalancingConfig } from "@grpc/grpc-js"; +import { LoadBalancingPolicy__Output } from "../generated/envoy/config/cluster/v3/LoadBalancingPolicy"; +import { TypedExtensionConfig__Output } from "../generated/envoy/config/core/v3/TypedExtensionConfig"; +import { loadProtosWithOptionsSync } from "@grpc/proto-loader/build/src/util"; +import { Any__Output } from "../generated/google/protobuf/Any"; +import { PickFirst__Output } from "../generated/envoy/extensions/load_balancing_policies/pick_first/v3/PickFirst"; +import { EXPERIMENTAL_PICK_FIRST } from "../environment"; +import { registerLbPolicy } from "../lb-policy-registry"; + +const PICK_FIRST_TYPE_URL = 'type.googleapis.com/envoy.extensions.load_balancing_policies.pick_first.v3.PickFirst'; + +const resourceRoot = loadProtosWithOptionsSync([ + 'envoy/extensions/load_balancing_policies/pick_first/v3/pick_first.proto'], { + keepCase: true, + includeDirs: [ + // Paths are relative to src/build/lb-policy-registry + __dirname + '/../../../deps/envoy-api/', + __dirname + '/../../../deps/xds/', + __dirname + '/../../../deps/protoc-gen-validate' + ], + } +); + +const toObjectOptions = { + longs: String, + enums: String, + defaults: true, + oneofs: true +} + +function decodePickFirstConfig(message: Any__Output): PickFirst__Output { + const name = message.type_url.substring(message.type_url.lastIndexOf('/') + 1); + const type = resourceRoot.lookup(name); + if (type) { + const decodedMessage = (type as any).decode(message.value); + return decodedMessage.$type.toObject(decodedMessage, toObjectOptions) as PickFirst__Output; + } else { + throw new Error(`TypedStruct parsing error: unexpected type URL ${message.type_url}`); + } +} + +function convertToLoadBalancingPolicy(protoPolicy: TypedExtensionConfig__Output, selectChildPolicy: (childPolicy: LoadBalancingPolicy__Output) => LoadBalancingConfig): LoadBalancingConfig | null { + if (protoPolicy.typed_config?.type_url !== PICK_FIRST_TYPE_URL) { + throw new Error(`Pick first LB policy parsing error: unexpected type URL ${protoPolicy.typed_config?.type_url}`); + } + const pickFirstMessage = decodePickFirstConfig(protoPolicy.typed_config); + return { + pick_first: { + shuffleAddressList: pickFirstMessage.shuffle_address_list + } + }; +} + +export function setup() { + if (EXPERIMENTAL_PICK_FIRST) { + registerLbPolicy(PICK_FIRST_TYPE_URL, convertToLoadBalancingPolicy); + } +} diff --git a/packages/grpc-js-xds/test/test-custom-lb-policies.ts b/packages/grpc-js-xds/test/test-custom-lb-policies.ts index 6da6fecc8..443601e36 100644 --- a/packages/grpc-js-xds/test/test-custom-lb-policies.ts +++ b/packages/grpc-js-xds/test/test-custom-lb-policies.ts @@ -38,6 +38,7 @@ import PickResultType = experimental.PickResultType; import createChildChannelControlHelper = experimental.createChildChannelControlHelper; import parseLoadBalancingConfig = experimental.parseLoadBalancingConfig; import registerLoadBalancerType = experimental.registerLoadBalancerType; +import { PickFirst } from "../src/generated/envoy/extensions/load_balancing_policies/pick_first/v3/PickFirst"; const LB_POLICY_NAME = 'test.RpcBehaviorLoadBalancer'; @@ -297,5 +298,28 @@ describe('Custom LB policies', () => { done(); }); }, reason => done(reason)); - }) + }); + it('Should handle pick_first', done => { + const lbPolicy: PickFirst & AnyExtension = { + '@type': 'type.googleapis.com/envoy.extensions.load_balancing_policies.pick_first.v3.PickFirst', + shuffle_address_list: true + }; + const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [new Backend()], locality:{region: 'region1'}}], lbPolicy); + const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: cluster}]); + routeGroup.startAllBackends().then(() => { + xdsServer.setEdsResource(cluster.getEndpointConfig()); + xdsServer.setCdsResource(cluster.getClusterConfig()); + xdsServer.setRdsResource(routeGroup.getRouteConfiguration()); + xdsServer.setLdsResource(routeGroup.getListener()); + xdsServer.addResponseListener((typeUrl, responseState) => { + if (responseState.state === 'NACKED') { + client.stopCalls(); + assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`); + } + }) + client = XdsTestClient.createFromServer('listener1', xdsServer); + client.sendOneCall(done); + }, reason => done(reason)); + }); + }); diff --git a/packages/grpc-js-xds/test/xds-server.ts b/packages/grpc-js-xds/test/xds-server.ts index 2aa62bdd8..d8500c836 100644 --- a/packages/grpc-js-xds/test/xds-server.ts +++ b/packages/grpc-js-xds/test/xds-server.ts @@ -45,6 +45,7 @@ const loadedProtos = loadPackageDefinition(loadSync( 'envoy/extensions/load_balancing_policies/round_robin/v3/round_robin.proto', 'envoy/extensions/load_balancing_policies/wrr_locality/v3/wrr_locality.proto', 'envoy/extensions/load_balancing_policies/ring_hash/v3/ring_hash.proto', + 'envoy/extensions/load_balancing_policies/pick_first/v3/pick_first.proto', 'xds/type/v3/typed_struct.proto' ], { From ab02dc0be4096d027d86141ba68a96e54c23761c Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 13 Sep 2023 16:45:46 -0700 Subject: [PATCH 043/109] proto-loader: Allow the grpcLib option to be omitted in the type generator --- packages/proto-loader/README.md | 3 +- .../bin/proto-loader-gen-types.ts | 45 ++++++++++++------- packages/proto-loader/package.json | 2 +- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/packages/proto-loader/README.md b/packages/proto-loader/README.md index 2a7af61e3..5abf4d1dd 100644 --- a/packages/proto-loader/README.md +++ b/packages/proto-loader/README.md @@ -87,7 +87,8 @@ Options: -I, --includeDirs Directories to search for included files [array] -O, --outDir Directory in which to output files [string] [required] --grpcLib The gRPC implementation library that these types will - be used with [string] [required] + be used with. If not provided, some types will not be + generated [string] --inputTemplate Template for mapping input or "permissive" type names [string] [default: "%s"] --outputTemplate Template for mapping output or "restricted" type names diff --git a/packages/proto-loader/bin/proto-loader-gen-types.ts b/packages/proto-loader/bin/proto-loader-gen-types.ts index 944790ad5..6db109904 100644 --- a/packages/proto-loader/bin/proto-loader-gen-types.ts +++ b/packages/proto-loader/bin/proto-loader-gen-types.ts @@ -39,7 +39,7 @@ const useNameFmter = ({outputTemplate, inputTemplate}: GeneratorOptions) => { type GeneratorOptions = Protobuf.IParseOptions & Protobuf.IConversionOptions & { includeDirs?: string[]; - grpcLib: string; + grpcLib?: string; outDir: string; verbose?: boolean; includeComments?: boolean; @@ -522,12 +522,12 @@ function generateEnumInterface(formatter: TextFormatter, enumType: Protobuf.Enum * We always generate two service client methods per service method: one camel * cased, and one with the original casing. So we will still generate one * service client method for any conflicting name. - * + * * Technically, at runtime conflicting name in the service client method * actually shadows the original method, but TypeScript does not have a good * way to represent that. So this change is not 100% accurate, but it gets the * generated code to compile. - * + * * This is just a list of the methods in the Client class definitions in * grpc@1.24.11 and @grpc/grpc-js@1.4.0. */ @@ -640,7 +640,11 @@ function generateServiceHandlerInterface(formatter: TextFormatter, serviceType: function generateServiceDefinitionInterface(formatter: TextFormatter, serviceType: Protobuf.Service, options: GeneratorOptions) { const {inputName, outputName} = useNameFmter(options); - formatter.writeLine(`export interface ${serviceType.name}Definition extends grpc.ServiceDefinition {`); + if (options.grpcLib) { + formatter.writeLine(`export interface ${serviceType.name}Definition extends grpc.ServiceDefinition {`); + } else { + formatter.writeLine(`export interface ${serviceType.name}Definition {`); + } formatter.indent(); for (const methodName of Object.keys(serviceType.methods).sort()) { const method = serviceType.methods[methodName]; @@ -655,8 +659,10 @@ function generateServiceDefinitionInterface(formatter: TextFormatter, serviceTyp function generateServiceInterfaces(formatter: TextFormatter, serviceType: Protobuf.Service, options: GeneratorOptions) { formatter.writeLine(`// Original file: ${(serviceType.filename ?? 'null')?.replace(/\\/g, '/')}`); formatter.writeLine(''); - const grpcImportPath = options.grpcLib.startsWith('.') ? getPathToRoot(serviceType) + options.grpcLib : options.grpcLib; - formatter.writeLine(`import type * as grpc from '${grpcImportPath}'`); + if (options.grpcLib) { + const grpcImportPath = options.grpcLib.startsWith('.') ? getPathToRoot(serviceType) + options.grpcLib : options.grpcLib; + formatter.writeLine(`import type * as grpc from '${grpcImportPath}'`); + } formatter.writeLine(`import type { MethodDefinition } from '@grpc/proto-loader'`) const dependencies: Set = new Set(); for (const method of serviceType.methodsArray) { @@ -668,11 +674,13 @@ function generateServiceInterfaces(formatter: TextFormatter, serviceType: Protob } formatter.writeLine(''); - generateServiceClientInterface(formatter, serviceType, options); - formatter.writeLine(''); + if (options.grpcLib) { + generateServiceClientInterface(formatter, serviceType, options); + formatter.writeLine(''); - generateServiceHandlerInterface(formatter, serviceType, options); - formatter.writeLine(''); + generateServiceHandlerInterface(formatter, serviceType, options); + formatter.writeLine(''); + } generateServiceDefinitionInterface(formatter, serviceType, options); } @@ -742,6 +750,9 @@ function generateLoadedDefinitionTypes(formatter: TextFormatter, namespace: Prot } function generateRootFile(formatter: TextFormatter, root: Protobuf.Root, options: GeneratorOptions) { + if (!options.grpcLib) { + return; + } formatter.writeLine(`import type * as grpc from '${options.grpcLib}';`); generateDefinitionImports(formatter, root, options); formatter.writeLine(''); @@ -802,11 +813,13 @@ function writeFilesForRoot(root: Protobuf.Root, masterFileName: string, options: const filePromises: Promise[] = []; const masterFileFormatter = new TextFormatter(); - generateRootFile(masterFileFormatter, root, options); - if (options.verbose) { - console.log(`Writing ${options.outDir}/${masterFileName}`); + if (options.grpcLib) { + generateRootFile(masterFileFormatter, root, options); + if (options.verbose) { + console.log(`Writing ${options.outDir}/${masterFileName}`); + } + filePromises.push(writeFile(`${options.outDir}/${masterFileName}`, masterFileFormatter.getFullText())); } - filePromises.push(writeFile(`${options.outDir}/${masterFileName}`, masterFileFormatter.getFullText())); filePromises.push(...generateFilesForNamespace(root, options)); @@ -898,12 +911,12 @@ async function runScript() { includeComments: 'Generate doc comments from comments in the original files', includeDirs: 'Directories to search for included files', outDir: 'Directory in which to output files', - grpcLib: 'The gRPC implementation library that these types will be used with', + grpcLib: 'The gRPC implementation library that these types will be used with. If not provided, some types will not be generated', inputTemplate: 'Template for mapping input or "permissive" type names', outputTemplate: 'Template for mapping output or "restricted" type names', inputBranded: 'Output property for branded type for "permissive" types with fullName of the Message as its value', outputBranded: 'Output property for branded type for "restricted" types with fullName of the Message as its value', - }).demandOption(['outDir', 'grpcLib']) + }).demandOption(['outDir']) .demand(1) .usage('$0 [options] filenames...') .epilogue('WARNING: This tool is in alpha. The CLI and generated code are subject to change') diff --git a/packages/proto-loader/package.json b/packages/proto-loader/package.json index c53976be6..a7f4d159f 100644 --- a/packages/proto-loader/package.json +++ b/packages/proto-loader/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/proto-loader", - "version": "0.7.9", + "version": "0.7.10", "author": "Google Inc.", "contributors": [ { From afbdbdeec38f92ebcde0e5147910172b524605a1 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 18 Sep 2023 13:50:42 -0700 Subject: [PATCH 044/109] grpc-health-check: Add generated code for version 2.0 --- .../src/generated/grpc/health/v1/Health.ts | 10 ++ .../grpc/health/v1/HealthCheckRequest.ts | 10 ++ .../grpc/health/v1/HealthCheckResponse.ts | 37 +++++ .../test/generated/grpc/health/v1/Health.ts | 129 ++++++++++++++++++ .../grpc/health/v1/HealthCheckRequest.ts | 10 ++ .../grpc/health/v1/HealthCheckResponse.ts | 37 +++++ .../test/generated/health.ts | 26 ++++ 7 files changed, 259 insertions(+) create mode 100644 packages/grpc-health-check/src/generated/grpc/health/v1/Health.ts create mode 100644 packages/grpc-health-check/src/generated/grpc/health/v1/HealthCheckRequest.ts create mode 100644 packages/grpc-health-check/src/generated/grpc/health/v1/HealthCheckResponse.ts create mode 100644 packages/grpc-health-check/test/generated/grpc/health/v1/Health.ts create mode 100644 packages/grpc-health-check/test/generated/grpc/health/v1/HealthCheckRequest.ts create mode 100644 packages/grpc-health-check/test/generated/grpc/health/v1/HealthCheckResponse.ts create mode 100644 packages/grpc-health-check/test/generated/health.ts diff --git a/packages/grpc-health-check/src/generated/grpc/health/v1/Health.ts b/packages/grpc-health-check/src/generated/grpc/health/v1/Health.ts new file mode 100644 index 000000000..a308498f4 --- /dev/null +++ b/packages/grpc-health-check/src/generated/grpc/health/v1/Health.ts @@ -0,0 +1,10 @@ +// Original file: proto/health/v1/health.proto + +import type { MethodDefinition } from '@grpc/proto-loader' +import type { HealthCheckRequest as _grpc_health_v1_HealthCheckRequest, HealthCheckRequest__Output as _grpc_health_v1_HealthCheckRequest__Output } from '../../../grpc/health/v1/HealthCheckRequest'; +import type { HealthCheckResponse as _grpc_health_v1_HealthCheckResponse, HealthCheckResponse__Output as _grpc_health_v1_HealthCheckResponse__Output } from '../../../grpc/health/v1/HealthCheckResponse'; + +export interface HealthDefinition { + Check: MethodDefinition<_grpc_health_v1_HealthCheckRequest, _grpc_health_v1_HealthCheckResponse, _grpc_health_v1_HealthCheckRequest__Output, _grpc_health_v1_HealthCheckResponse__Output> + Watch: MethodDefinition<_grpc_health_v1_HealthCheckRequest, _grpc_health_v1_HealthCheckResponse, _grpc_health_v1_HealthCheckRequest__Output, _grpc_health_v1_HealthCheckResponse__Output> +} diff --git a/packages/grpc-health-check/src/generated/grpc/health/v1/HealthCheckRequest.ts b/packages/grpc-health-check/src/generated/grpc/health/v1/HealthCheckRequest.ts new file mode 100644 index 000000000..71ae9df4e --- /dev/null +++ b/packages/grpc-health-check/src/generated/grpc/health/v1/HealthCheckRequest.ts @@ -0,0 +1,10 @@ +// Original file: proto/health/v1/health.proto + + +export interface HealthCheckRequest { + 'service'?: (string); +} + +export interface HealthCheckRequest__Output { + 'service': (string); +} diff --git a/packages/grpc-health-check/src/generated/grpc/health/v1/HealthCheckResponse.ts b/packages/grpc-health-check/src/generated/grpc/health/v1/HealthCheckResponse.ts new file mode 100644 index 000000000..ee4f375ae --- /dev/null +++ b/packages/grpc-health-check/src/generated/grpc/health/v1/HealthCheckResponse.ts @@ -0,0 +1,37 @@ +// Original file: proto/health/v1/health.proto + + +// Original file: proto/health/v1/health.proto + +export const _grpc_health_v1_HealthCheckResponse_ServingStatus = { + UNKNOWN: 'UNKNOWN', + SERVING: 'SERVING', + NOT_SERVING: 'NOT_SERVING', + /** + * Used only by the Watch method. + */ + SERVICE_UNKNOWN: 'SERVICE_UNKNOWN', +} as const; + +export type _grpc_health_v1_HealthCheckResponse_ServingStatus = + | 'UNKNOWN' + | 0 + | 'SERVING' + | 1 + | 'NOT_SERVING' + | 2 + /** + * Used only by the Watch method. + */ + | 'SERVICE_UNKNOWN' + | 3 + +export type _grpc_health_v1_HealthCheckResponse_ServingStatus__Output = typeof _grpc_health_v1_HealthCheckResponse_ServingStatus[keyof typeof _grpc_health_v1_HealthCheckResponse_ServingStatus] + +export interface HealthCheckResponse { + 'status'?: (_grpc_health_v1_HealthCheckResponse_ServingStatus); +} + +export interface HealthCheckResponse__Output { + 'status': (_grpc_health_v1_HealthCheckResponse_ServingStatus__Output); +} diff --git a/packages/grpc-health-check/test/generated/grpc/health/v1/Health.ts b/packages/grpc-health-check/test/generated/grpc/health/v1/Health.ts new file mode 100644 index 000000000..320958e3c --- /dev/null +++ b/packages/grpc-health-check/test/generated/grpc/health/v1/Health.ts @@ -0,0 +1,129 @@ +// Original file: proto/health/v1/health.proto + +import type * as grpc from '@grpc/grpc-js' +import type { MethodDefinition } from '@grpc/proto-loader' +import type { HealthCheckRequest as _grpc_health_v1_HealthCheckRequest, HealthCheckRequest__Output as _grpc_health_v1_HealthCheckRequest__Output } from '../../../grpc/health/v1/HealthCheckRequest'; +import type { HealthCheckResponse as _grpc_health_v1_HealthCheckResponse, HealthCheckResponse__Output as _grpc_health_v1_HealthCheckResponse__Output } from '../../../grpc/health/v1/HealthCheckResponse'; + +/** + * Health is gRPC's mechanism for checking whether a server is able to handle + * RPCs. Its semantics are documented in + * https://github.com/grpc/grpc/blob/master/doc/health-checking.md. + */ +export interface HealthClient extends grpc.Client { + /** + * Check gets the health of the specified service. If the requested service + * is unknown, the call will fail with status NOT_FOUND. If the caller does + * not specify a service name, the server should respond with its overall + * health status. + * + * Clients should set a deadline when calling Check, and can declare the + * server unhealthy if they do not receive a timely response. + * + * Check implementations should be idempotent and side effect free. + */ + Check(argument: _grpc_health_v1_HealthCheckRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_health_v1_HealthCheckResponse__Output>): grpc.ClientUnaryCall; + Check(argument: _grpc_health_v1_HealthCheckRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_health_v1_HealthCheckResponse__Output>): grpc.ClientUnaryCall; + Check(argument: _grpc_health_v1_HealthCheckRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_health_v1_HealthCheckResponse__Output>): grpc.ClientUnaryCall; + Check(argument: _grpc_health_v1_HealthCheckRequest, callback: grpc.requestCallback<_grpc_health_v1_HealthCheckResponse__Output>): grpc.ClientUnaryCall; + /** + * Check gets the health of the specified service. If the requested service + * is unknown, the call will fail with status NOT_FOUND. If the caller does + * not specify a service name, the server should respond with its overall + * health status. + * + * Clients should set a deadline when calling Check, and can declare the + * server unhealthy if they do not receive a timely response. + * + * Check implementations should be idempotent and side effect free. + */ + check(argument: _grpc_health_v1_HealthCheckRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_health_v1_HealthCheckResponse__Output>): grpc.ClientUnaryCall; + check(argument: _grpc_health_v1_HealthCheckRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_health_v1_HealthCheckResponse__Output>): grpc.ClientUnaryCall; + check(argument: _grpc_health_v1_HealthCheckRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_health_v1_HealthCheckResponse__Output>): grpc.ClientUnaryCall; + check(argument: _grpc_health_v1_HealthCheckRequest, callback: grpc.requestCallback<_grpc_health_v1_HealthCheckResponse__Output>): grpc.ClientUnaryCall; + + /** + * Performs a watch for the serving status of the requested service. + * The server will immediately send back a message indicating the current + * serving status. It will then subsequently send a new message whenever + * the service's serving status changes. + * + * If the requested service is unknown when the call is received, the + * server will send a message setting the serving status to + * SERVICE_UNKNOWN but will *not* terminate the call. If at some + * future point, the serving status of the service becomes known, the + * server will send a new message with the service's serving status. + * + * If the call terminates with status UNIMPLEMENTED, then clients + * should assume this method is not supported and should not retry the + * call. If the call terminates with any other status (including OK), + * clients should retry the call with appropriate exponential backoff. + */ + Watch(argument: _grpc_health_v1_HealthCheckRequest, metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_health_v1_HealthCheckResponse__Output>; + Watch(argument: _grpc_health_v1_HealthCheckRequest, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_health_v1_HealthCheckResponse__Output>; + /** + * Performs a watch for the serving status of the requested service. + * The server will immediately send back a message indicating the current + * serving status. It will then subsequently send a new message whenever + * the service's serving status changes. + * + * If the requested service is unknown when the call is received, the + * server will send a message setting the serving status to + * SERVICE_UNKNOWN but will *not* terminate the call. If at some + * future point, the serving status of the service becomes known, the + * server will send a new message with the service's serving status. + * + * If the call terminates with status UNIMPLEMENTED, then clients + * should assume this method is not supported and should not retry the + * call. If the call terminates with any other status (including OK), + * clients should retry the call with appropriate exponential backoff. + */ + watch(argument: _grpc_health_v1_HealthCheckRequest, metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_health_v1_HealthCheckResponse__Output>; + watch(argument: _grpc_health_v1_HealthCheckRequest, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_health_v1_HealthCheckResponse__Output>; + +} + +/** + * Health is gRPC's mechanism for checking whether a server is able to handle + * RPCs. Its semantics are documented in + * https://github.com/grpc/grpc/blob/master/doc/health-checking.md. + */ +export interface HealthHandlers extends grpc.UntypedServiceImplementation { + /** + * Check gets the health of the specified service. If the requested service + * is unknown, the call will fail with status NOT_FOUND. If the caller does + * not specify a service name, the server should respond with its overall + * health status. + * + * Clients should set a deadline when calling Check, and can declare the + * server unhealthy if they do not receive a timely response. + * + * Check implementations should be idempotent and side effect free. + */ + Check: grpc.handleUnaryCall<_grpc_health_v1_HealthCheckRequest__Output, _grpc_health_v1_HealthCheckResponse>; + + /** + * Performs a watch for the serving status of the requested service. + * The server will immediately send back a message indicating the current + * serving status. It will then subsequently send a new message whenever + * the service's serving status changes. + * + * If the requested service is unknown when the call is received, the + * server will send a message setting the serving status to + * SERVICE_UNKNOWN but will *not* terminate the call. If at some + * future point, the serving status of the service becomes known, the + * server will send a new message with the service's serving status. + * + * If the call terminates with status UNIMPLEMENTED, then clients + * should assume this method is not supported and should not retry the + * call. If the call terminates with any other status (including OK), + * clients should retry the call with appropriate exponential backoff. + */ + Watch: grpc.handleServerStreamingCall<_grpc_health_v1_HealthCheckRequest__Output, _grpc_health_v1_HealthCheckResponse>; + +} + +export interface HealthDefinition extends grpc.ServiceDefinition { + Check: MethodDefinition<_grpc_health_v1_HealthCheckRequest, _grpc_health_v1_HealthCheckResponse, _grpc_health_v1_HealthCheckRequest__Output, _grpc_health_v1_HealthCheckResponse__Output> + Watch: MethodDefinition<_grpc_health_v1_HealthCheckRequest, _grpc_health_v1_HealthCheckResponse, _grpc_health_v1_HealthCheckRequest__Output, _grpc_health_v1_HealthCheckResponse__Output> +} diff --git a/packages/grpc-health-check/test/generated/grpc/health/v1/HealthCheckRequest.ts b/packages/grpc-health-check/test/generated/grpc/health/v1/HealthCheckRequest.ts new file mode 100644 index 000000000..71ae9df4e --- /dev/null +++ b/packages/grpc-health-check/test/generated/grpc/health/v1/HealthCheckRequest.ts @@ -0,0 +1,10 @@ +// Original file: proto/health/v1/health.proto + + +export interface HealthCheckRequest { + 'service'?: (string); +} + +export interface HealthCheckRequest__Output { + 'service': (string); +} diff --git a/packages/grpc-health-check/test/generated/grpc/health/v1/HealthCheckResponse.ts b/packages/grpc-health-check/test/generated/grpc/health/v1/HealthCheckResponse.ts new file mode 100644 index 000000000..ee4f375ae --- /dev/null +++ b/packages/grpc-health-check/test/generated/grpc/health/v1/HealthCheckResponse.ts @@ -0,0 +1,37 @@ +// Original file: proto/health/v1/health.proto + + +// Original file: proto/health/v1/health.proto + +export const _grpc_health_v1_HealthCheckResponse_ServingStatus = { + UNKNOWN: 'UNKNOWN', + SERVING: 'SERVING', + NOT_SERVING: 'NOT_SERVING', + /** + * Used only by the Watch method. + */ + SERVICE_UNKNOWN: 'SERVICE_UNKNOWN', +} as const; + +export type _grpc_health_v1_HealthCheckResponse_ServingStatus = + | 'UNKNOWN' + | 0 + | 'SERVING' + | 1 + | 'NOT_SERVING' + | 2 + /** + * Used only by the Watch method. + */ + | 'SERVICE_UNKNOWN' + | 3 + +export type _grpc_health_v1_HealthCheckResponse_ServingStatus__Output = typeof _grpc_health_v1_HealthCheckResponse_ServingStatus[keyof typeof _grpc_health_v1_HealthCheckResponse_ServingStatus] + +export interface HealthCheckResponse { + 'status'?: (_grpc_health_v1_HealthCheckResponse_ServingStatus); +} + +export interface HealthCheckResponse__Output { + 'status': (_grpc_health_v1_HealthCheckResponse_ServingStatus__Output); +} diff --git a/packages/grpc-health-check/test/generated/health.ts b/packages/grpc-health-check/test/generated/health.ts new file mode 100644 index 000000000..afb2ced5f --- /dev/null +++ b/packages/grpc-health-check/test/generated/health.ts @@ -0,0 +1,26 @@ +import type * as grpc from '@grpc/grpc-js'; +import type { MessageTypeDefinition } from '@grpc/proto-loader'; + +import type { HealthClient as _grpc_health_v1_HealthClient, HealthDefinition as _grpc_health_v1_HealthDefinition } from './grpc/health/v1/Health'; + +type SubtypeConstructor any, Subtype> = { + new(...args: ConstructorParameters): Subtype; +}; + +export interface ProtoGrpcType { + grpc: { + health: { + v1: { + /** + * Health is gRPC's mechanism for checking whether a server is able to handle + * RPCs. Its semantics are documented in + * https://github.com/grpc/grpc/blob/master/doc/health-checking.md. + */ + Health: SubtypeConstructor & { service: _grpc_health_v1_HealthDefinition } + HealthCheckRequest: MessageTypeDefinition + HealthCheckResponse: MessageTypeDefinition + } + } + } +} + From 524bb7d34142d8e8bfeb120b40e9a06ae6c9ad35 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 18 Sep 2023 14:55:16 -0700 Subject: [PATCH 045/109] grpc-health-check: Implement version 2.0 update --- packages/grpc-health-check/README.md | 34 ++-- packages/grpc-health-check/gulpfile.ts | 28 ++-- packages/grpc-health-check/health.js | 55 ------- packages/grpc-health-check/package.json | 27 ++-- .../proto/health/v1/health.proto | 73 +++++++++ packages/grpc-health-check/src/health.ts | 112 +++++++++++++ .../grpc-health-check/src/object-stream.ts | 75 +++++++++ packages/grpc-health-check/src/server-type.ts | 103 ++++++++++++ .../grpc-health-check/test/health_test.js | 103 ------------ .../grpc-health-check/test/test-health.ts | 152 ++++++++++++++++++ packages/grpc-health-check/tsconfig.json | 29 ++++ 11 files changed, 599 insertions(+), 192 deletions(-) delete mode 100644 packages/grpc-health-check/health.js create mode 100644 packages/grpc-health-check/proto/health/v1/health.proto create mode 100644 packages/grpc-health-check/src/health.ts create mode 100644 packages/grpc-health-check/src/object-stream.ts create mode 100644 packages/grpc-health-check/src/server-type.ts delete mode 100644 packages/grpc-health-check/test/health_test.js create mode 100644 packages/grpc-health-check/test/test-health.ts create mode 100644 packages/grpc-health-check/tsconfig.json diff --git a/packages/grpc-health-check/README.md b/packages/grpc-health-check/README.md index 62a88347f..659dab140 100644 --- a/packages/grpc-health-check/README.md +++ b/packages/grpc-health-check/README.md @@ -4,11 +4,7 @@ Health check client and service for use with gRPC-node. ## Background -This package exports both a client and server that adhere to the [gRPC Health Checking Protocol](https://github.com/grpc/grpc/blob/master/doc/health-checking.md). - -By using this package, clients and servers can rely on common proto and service definitions. This means: -- Clients can use the generated stubs to health check _any_ server that adheres to the protocol. -- Servers do not reimplement common logic for publishing health statuses. +This package provides an implementation of the [gRPC Health Checking Protocol](https://github.com/grpc/grpc/blob/master/doc/health-checking.md) service, as described in [gRFC L106](https://github.com/grpc/proposal/blob/master/L106-node-heath-check-library.md). ## Installation @@ -22,33 +18,39 @@ npm install grpc-health-check ### Server -Any gRPC-node server can use `grpc-health-check` to adhere to the gRPC Health Checking Protocol. +Any gRPC-node server can use `grpc-health-check` to adhere to the gRPC Health Checking Protocol. The following shows how this package can be added to a pre-existing gRPC server. -```javascript 1.8 +```typescript // Import package -let health = require('grpc-health-check'); +import { HealthImplementation, ServingStatusMap } from 'grpc-health-check'; // Define service status map. Key is the service name, value is the corresponding status. -// By convention, the empty string "" key represents that status of the entire server. +// By convention, the empty string '' key represents that status of the entire server. const statusMap = { - "ServiceFoo": proto.grpc.health.v1.HealthCheckResponse.ServingStatus.SERVING, - "ServiceBar": proto.grpc.health.v1.HealthCheckResponse.ServingStatus.NOT_SERVING, - "": proto.grpc.health.v1.HealthCheckResponse.ServingStatus.NOT_SERVING, + 'ServiceFoo': 'SERVING', + 'ServiceBar': 'NOT_SERVING', + '': 'NOT_SERVING', }; // Construct the service implementation -let healthImpl = new health.Implementation(statusMap); +const healthImpl = new HealthImplementation(statusMap); + +healthImpl.addToServer(server); -// Add the service and implementation to your pre-existing gRPC-node server -server.addService(health.service, healthImpl); +// When ServiceBar comes up +healthImpl.setStatus('serviceBar', 'SERVING'); ``` Congrats! Your server now allows any client to run a health check against it. ### Client -Any gRPC-node client can use `grpc-health-check` to run health checks against other servers that follow the protocol. +Any gRPC-node client can use the `service` object exported by `grpc-health-check` to generate clients that can make health check requests. + +### Command Line Usage + +The absolute path to `health.proto` can be obtained on the command line with `node -p 'require("grpc-health-check").protoPath'`. ## Contributing diff --git a/packages/grpc-health-check/gulpfile.ts b/packages/grpc-health-check/gulpfile.ts index 0ddaa257e..f47087b14 100644 --- a/packages/grpc-health-check/gulpfile.ts +++ b/packages/grpc-health-check/gulpfile.ts @@ -19,22 +19,32 @@ import * as gulp from 'gulp'; import * as mocha from 'gulp-mocha'; import * as execa from 'execa'; import * as path from 'path'; -import * as del from 'del'; -import {linkSync} from '../../util'; const healthCheckDir = __dirname; -const baseDir = path.resolve(healthCheckDir, '..', '..'); -const testDir = path.resolve(healthCheckDir, 'test'); +const outDir = path.resolve(healthCheckDir, 'build'); -const runInstall = () => execa('npm', ['install', '--unsafe-perm'], {cwd: healthCheckDir, stdio: 'inherit'}); +const execNpmVerb = (verb: string, ...args: string[]) => + execa('npm', [verb, ...args], {cwd: healthCheckDir, stdio: 'inherit'}); +const execNpmCommand = execNpmVerb.bind(null, 'run'); -const runRebuild = () => execa('npm', ['rebuild', '--unsafe-perm'], {cwd: healthCheckDir, stdio: 'inherit'}); +const install = () => execNpmVerb('install', '--unsafe-perm'); -const install = gulp.series(runInstall, runRebuild); +/** + * Transpiles TypeScript files in src/ to JavaScript according to the settings + * found in tsconfig.json. + */ +const compile = () => execNpmCommand('compile'); + +const runTests = () => { + return gulp.src(`${outDir}/test/**/*.js`) + .pipe(mocha({reporter: 'mocha-jenkins-reporter', + require: ['ts-node/register']})); +}; -const test = () => gulp.src(`${testDir}/*.js`).pipe(mocha({reporter: 'mocha-jenkins-reporter'})); +const test = gulp.series(install, runTests); export { install, + compile, test -} \ No newline at end of file +} diff --git a/packages/grpc-health-check/health.js b/packages/grpc-health-check/health.js deleted file mode 100644 index cfa9c8348..000000000 --- a/packages/grpc-health-check/health.js +++ /dev/null @@ -1,55 +0,0 @@ -/* - * - * Copyright 2015 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. - * - */ - -'use strict'; - -var grpc = require('grpc'); - -var _get = require('lodash.get'); -var _clone = require('lodash.clone') - -var health_messages = require('./v1/health_pb'); -var health_service = require('./v1/health_grpc_pb'); - -function HealthImplementation(statusMap) { - this.statusMap = _clone(statusMap); -} - -HealthImplementation.prototype.setStatus = function(service, status) { - this.statusMap[service] = status; -}; - -HealthImplementation.prototype.check = function(call, callback){ - var service = call.request.getService(); - var status = _get(this.statusMap, service, null); - if (status === null) { - // TODO(murgatroid99): Do this without an explicit reference to grpc. - callback({code:grpc.status.NOT_FOUND}); - } else { - var response = new health_messages.HealthCheckResponse(); - response.setStatus(status); - callback(null, response); - } -}; - -module.exports = { - Client: health_service.HealthClient, - messages: health_messages, - service: health_service.HealthService, - Implementation: HealthImplementation -}; diff --git a/packages/grpc-health-check/package.json b/packages/grpc-health-check/package.json index e9b836346..a7fe1c3fd 100644 --- a/packages/grpc-health-check/package.json +++ b/packages/grpc-health-check/package.json @@ -1,6 +1,6 @@ { "name": "grpc-health-check", - "version": "1.8.0", + "version": "2.0.0", "author": "Google Inc.", "description": "Health check client and service for use with gRPC-node", "repository": { @@ -14,18 +14,27 @@ "email": "mlumish@google.com" } ], + "scripts": { + "compile": "tsc -p .", + "prepare": "npm run generate-types && npm run compile", + "generate-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --includeDirs proto/ -O src/generated health/v1/health.proto", + "generate-test-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --includeDirs proto/ -O test/generated --grpcLib=@grpc/grpc-js health/v1/health.proto" + }, "dependencies": { - "google-protobuf": "^3.4.0", - "grpc": "^1.6.0", - "lodash.clone": "^4.5.0", - "lodash.get": "^4.4.2" + "@grpc/proto-loader": "^0.7.10", + "typescript": "^5.2.2" }, "files": [ "LICENSE", "README.md", - "health.js", - "v1" + "src", + "build", + "proto" ], - "main": "health.js", - "license": "Apache-2.0" + "main": "build/src/health.js", + "types": "build/src/health.d.ts", + "license": "Apache-2.0", + "devDependencies": { + "@grpc/grpc-js": "file:../grpc-js" + } } diff --git a/packages/grpc-health-check/proto/health/v1/health.proto b/packages/grpc-health-check/proto/health/v1/health.proto new file mode 100644 index 000000000..13b03f567 --- /dev/null +++ b/packages/grpc-health-check/proto/health/v1/health.proto @@ -0,0 +1,73 @@ +// Copyright 2015 The 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. + +// The canonical version of this proto can be found at +// https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto + +syntax = "proto3"; + +package grpc.health.v1; + +option csharp_namespace = "Grpc.Health.V1"; +option go_package = "google.golang.org/grpc/health/grpc_health_v1"; +option java_multiple_files = true; +option java_outer_classname = "HealthProto"; +option java_package = "io.grpc.health.v1"; + +message HealthCheckRequest { + string service = 1; +} + +message HealthCheckResponse { + enum ServingStatus { + UNKNOWN = 0; + SERVING = 1; + NOT_SERVING = 2; + SERVICE_UNKNOWN = 3; // Used only by the Watch method. + } + ServingStatus status = 1; +} + +// Health is gRPC's mechanism for checking whether a server is able to handle +// RPCs. Its semantics are documented in +// https://github.com/grpc/grpc/blob/master/doc/health-checking.md. +service Health { + // Check gets the health of the specified service. If the requested service + // is unknown, the call will fail with status NOT_FOUND. If the caller does + // not specify a service name, the server should respond with its overall + // health status. + // + // Clients should set a deadline when calling Check, and can declare the + // server unhealthy if they do not receive a timely response. + // + // Check implementations should be idempotent and side effect free. + rpc Check(HealthCheckRequest) returns (HealthCheckResponse); + + // Performs a watch for the serving status of the requested service. + // The server will immediately send back a message indicating the current + // serving status. It will then subsequently send a new message whenever + // the service's serving status changes. + // + // If the requested service is unknown when the call is received, the + // server will send a message setting the serving status to + // SERVICE_UNKNOWN but will *not* terminate the call. If at some + // future point, the serving status of the service becomes known, the + // server will send a new message with the service's serving status. + // + // If the call terminates with status UNIMPLEMENTED, then clients + // should assume this method is not supported and should not retry the + // call. If the call terminates with any other status (including OK), + // clients should retry the call with appropriate exponential backoff. + rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse); +} diff --git a/packages/grpc-health-check/src/health.ts b/packages/grpc-health-check/src/health.ts new file mode 100644 index 000000000..86ca1af0d --- /dev/null +++ b/packages/grpc-health-check/src/health.ts @@ -0,0 +1,112 @@ +/* + * + * 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 * as path from 'path'; +import { loadSync, ServiceDefinition } from '@grpc/proto-loader'; +import { HealthCheckRequest__Output } from './generated/grpc/health/v1/HealthCheckRequest'; +import { HealthCheckResponse } from './generated/grpc/health/v1/HealthCheckResponse'; +import { sendUnaryData, Server, ServerUnaryCall, ServerWritableStream } from './server-type'; + +const loadedProto = loadSync('health/v1/health.proto', { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, + includeDirs: [`${__dirname}/../../proto`], +}); + +export const service = loadedProto['grpc.health.v1.Health'] as ServiceDefinition; + +const GRPC_STATUS_NOT_FOUND = 5; + +export type ServingStatus = 'UNKNOWN' | 'SERVING' | 'NOT_SERVING'; + +export interface ServingStatusMap { + [serviceName: string]: ServingStatus; +} + +interface StatusWatcher { + (status: ServingStatus): void; +} + +export class HealthImplementation { + private statusMap: Map = new Map(); + private watchers: Map> = new Map(); + constructor(initialStatusMap?: ServingStatusMap) { + if (initialStatusMap) { + for (const [serviceName, status] of Object.entries(initialStatusMap)) { + this.statusMap.set(serviceName, status); + } + } + } + + setStatus(service: string, status: ServingStatus) { + this.statusMap.set(service, status); + for (const watcher of this.watchers.get(service) ?? []) { + watcher(status); + } + } + + private addWatcher(service: string, watcher: StatusWatcher) { + const existingWatcherSet = this.watchers.get(service); + if (existingWatcherSet) { + existingWatcherSet.add(watcher); + } else { + const newWatcherSet = new Set(); + newWatcherSet.add(watcher); + this.watchers.set(service, newWatcherSet); + } + } + + private removeWatcher(service: string, watcher: StatusWatcher) { + this.watchers.get(service)?.delete(watcher); + } + + addToServer(server: Server) { + server.addService(service, { + check: (call: ServerUnaryCall, callback: sendUnaryData) => { + const serviceName = call.request.service; + const status = this.statusMap.get(serviceName); + if (status) { + callback(null, {status: status}); + } else { + callback({code: GRPC_STATUS_NOT_FOUND, details: `Health status unknown for service ${serviceName}`}); + } + }, + watch: (call: ServerWritableStream) => { + const serviceName = call.request.service; + const statusWatcher = (status: ServingStatus) => { + call.write({status: status}); + }; + this.addWatcher(serviceName, statusWatcher); + call.on('cancelled', () => { + this.removeWatcher(serviceName, statusWatcher); + }); + const currentStatus = this.statusMap.get(serviceName); + if (currentStatus) { + call.write({status: currentStatus}); + } else { + call.write({status: 'SERVICE_UNKNOWN'}); + } + } + }); + } +} + +export const protoPath = path.resolve(__dirname, '../../proto/health/v1/health.proto'); diff --git a/packages/grpc-health-check/src/object-stream.ts b/packages/grpc-health-check/src/object-stream.ts new file mode 100644 index 000000000..2f70cfa7e --- /dev/null +++ b/packages/grpc-health-check/src/object-stream.ts @@ -0,0 +1,75 @@ +/* + * 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 { Readable, Writable } from 'stream'; + +interface EmitterAugmentation1 { + addListener(event: Name, listener: (arg1: Arg) => void): this; + emit(event: Name, arg1: Arg): boolean; + on(event: Name, listener: (arg1: Arg) => void): this; + once(event: Name, listener: (arg1: Arg) => void): this; + prependListener(event: Name, listener: (arg1: Arg) => void): this; + prependOnceListener(event: Name, listener: (arg1: Arg) => void): this; + removeListener(event: Name, listener: (arg1: Arg) => void): this; +} + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export type WriteCallback = (error: Error | null | undefined) => void; + +export interface IntermediateObjectReadable extends Readable { + read(size?: number): any & T; +} + +export type ObjectReadable = { + read(size?: number): T; +} & EmitterAugmentation1<'data', T> & + IntermediateObjectReadable; + +export interface IntermediateObjectWritable extends Writable { + _write(chunk: any & T, encoding: string, callback: Function): void; + write(chunk: any & T, cb?: WriteCallback): boolean; + write(chunk: any & T, encoding?: any, cb?: WriteCallback): boolean; + setDefaultEncoding(encoding: string): this; + end(): ReturnType extends Writable ? this : void; + end( + chunk: any & T, + cb?: Function + ): ReturnType extends Writable ? this : void; + end( + chunk: any & T, + encoding?: any, + cb?: Function + ): ReturnType extends Writable ? this : void; +} + +export interface ObjectWritable extends IntermediateObjectWritable { + _write(chunk: T, encoding: string, callback: Function): void; + write(chunk: T, cb?: Function): boolean; + write(chunk: T, encoding?: any, cb?: Function): boolean; + setDefaultEncoding(encoding: string): this; + end(): ReturnType extends Writable ? this : void; + end( + chunk: T, + cb?: Function + ): ReturnType extends Writable ? this : void; + end( + chunk: T, + encoding?: any, + cb?: Function + ): ReturnType extends Writable ? this : void; +} diff --git a/packages/grpc-health-check/src/server-type.ts b/packages/grpc-health-check/src/server-type.ts new file mode 100644 index 000000000..f07704e87 --- /dev/null +++ b/packages/grpc-health-check/src/server-type.ts @@ -0,0 +1,103 @@ +/* + * 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 { ServiceDefinition } from '@grpc/proto-loader'; +import { ObjectReadable, ObjectWritable } from './object-stream'; +import { EventEmitter } from 'events'; + +type Metadata = any; + +interface StatusObject { + code: number; + details: string; + metadata: Metadata; +} + +type Deadline = Date | number; + +type ServerStatusResponse = Partial; + +type ServerErrorResponse = ServerStatusResponse & Error; + +type ServerSurfaceCall = { + cancelled: boolean; + readonly metadata: Metadata; + getPeer(): string; + sendMetadata(responseMetadata: Metadata): void; + getDeadline(): Deadline; + getPath(): string; +} & EventEmitter; + +export type ServerUnaryCall = ServerSurfaceCall & { + request: RequestType; +}; +type ServerReadableStream = + ServerSurfaceCall & ObjectReadable; +export type ServerWritableStream = + ServerSurfaceCall & + ObjectWritable & { + request: RequestType; + end: (metadata?: Metadata) => void; + }; +type ServerDuplexStream = ServerSurfaceCall & + ObjectReadable & + ObjectWritable & { end: (metadata?: Metadata) => void }; + +// Unary response callback signature. +export type sendUnaryData = ( + error: ServerErrorResponse | ServerStatusResponse | null, + value?: ResponseType | null, + trailer?: Metadata, + flags?: number +) => void; + +// User provided handler for unary calls. +type handleUnaryCall = ( + call: ServerUnaryCall, + callback: sendUnaryData +) => void; + +// User provided handler for client streaming calls. +type handleClientStreamingCall = ( + call: ServerReadableStream, + callback: sendUnaryData +) => void; + +// User provided handler for server streaming calls. +type handleServerStreamingCall = ( + call: ServerWritableStream +) => void; + +// User provided handler for bidirectional streaming calls. +type handleBidiStreamingCall = ( + call: ServerDuplexStream +) => void; + +export type HandleCall = + | handleUnaryCall + | handleClientStreamingCall + | handleServerStreamingCall + | handleBidiStreamingCall; + +export type UntypedHandleCall = HandleCall; +export interface UntypedServiceImplementation { + [name: string]: UntypedHandleCall; +} + +export interface Server { + addService(service: ServiceDefinition, implementation: UntypedServiceImplementation): void; +} diff --git a/packages/grpc-health-check/test/health_test.js b/packages/grpc-health-check/test/health_test.js deleted file mode 100644 index a31d3b371..000000000 --- a/packages/grpc-health-check/test/health_test.js +++ /dev/null @@ -1,103 +0,0 @@ -/* - * - * Copyright 2015 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. - * - */ - -'use strict'; - -var assert = require('assert'); - -var health = require('../health'); - -var health_messages = require('../v1/health_pb'); - -var ServingStatus = health_messages.HealthCheckResponse.ServingStatus; - -var grpc = require('grpc'); - -describe('Health Checking', function() { - var statusMap = { - '': ServingStatus.SERVING, - 'grpc.test.TestServiceNotServing': ServingStatus.NOT_SERVING, - 'grpc.test.TestServiceServing': ServingStatus.SERVING - }; - var healthServer; - var healthImpl; - var healthClient; - before(function() { - healthServer = new grpc.Server(); - healthImpl = new health.Implementation(statusMap); - healthServer.addService(health.service, healthImpl); - var port_num = healthServer.bind('0.0.0.0:0', - grpc.ServerCredentials.createInsecure()); - healthServer.start(); - healthClient = new health.Client('localhost:' + port_num, - grpc.credentials.createInsecure()); - }); - after(function() { - healthServer.forceShutdown(); - }); - it('should say an enabled service is SERVING', function(done) { - var request = new health_messages.HealthCheckRequest(); - request.setService(''); - healthClient.check(request, function(err, response) { - assert.ifError(err); - assert.strictEqual(response.getStatus(), ServingStatus.SERVING); - done(); - }); - }); - it('should say that a disabled service is NOT_SERVING', function(done) { - var request = new health_messages.HealthCheckRequest(); - request.setService('grpc.test.TestServiceNotServing'); - healthClient.check(request, function(err, response) { - assert.ifError(err); - assert.strictEqual(response.getStatus(), ServingStatus.NOT_SERVING); - done(); - }); - }); - it('should say that an enabled service is SERVING', function(done) { - var request = new health_messages.HealthCheckRequest(); - request.setService('grpc.test.TestServiceServing'); - healthClient.check(request, function(err, response) { - assert.ifError(err); - assert.strictEqual(response.getStatus(), ServingStatus.SERVING); - done(); - }); - }); - it('should get NOT_FOUND if the service is not registered', function(done) { - var request = new health_messages.HealthCheckRequest(); - request.setService('not_registered'); - healthClient.check(request, function(err, response) { - assert(err); - assert.strictEqual(err.code, grpc.status.NOT_FOUND); - done(); - }); - }); - it('should get a different response if the status changes', function(done) { - var request = new health_messages.HealthCheckRequest(); - request.setService('transient'); - healthClient.check(request, function(err, response) { - assert(err); - assert.strictEqual(err.code, grpc.status.NOT_FOUND); - healthImpl.setStatus('transient', ServingStatus.SERVING); - healthClient.check(request, function(err, response) { - assert.ifError(err); - assert.strictEqual(response.getStatus(), ServingStatus.SERVING); - done(); - }); - }); - }); -}); diff --git a/packages/grpc-health-check/test/test-health.ts b/packages/grpc-health-check/test/test-health.ts new file mode 100644 index 000000000..80d60a234 --- /dev/null +++ b/packages/grpc-health-check/test/test-health.ts @@ -0,0 +1,152 @@ +/* + * + * 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 * as assert from 'assert'; +import * as grpc from '@grpc/grpc-js'; +import { HealthImplementation, ServingStatusMap, service as healthServiceDefinition } from '../src/health'; +import { HealthClient } from './generated/grpc/health/v1/Health'; +import { HealthCheckResponse__Output, _grpc_health_v1_HealthCheckResponse_ServingStatus__Output } from './generated/grpc/health/v1/HealthCheckResponse'; + +describe('Health checking', () => { + const statusMap: ServingStatusMap = { + '': 'SERVING', + 'grpc.test.TestServiceNotServing': 'NOT_SERVING', + 'grpc.test.TestServiceServing': 'SERVING' + }; + let healthServer: grpc.Server; + let healthClient: HealthClient; + let healthImpl: HealthImplementation; + beforeEach(done => { + healthServer = new grpc.Server(); + healthImpl = new HealthImplementation(statusMap); + healthImpl.addToServer(healthServer); + healthServer.bindAsync('localhost:0', grpc.ServerCredentials.createInsecure(), (error, port) => { + if (error) { + done(error); + return; + } + const HealthClientConstructor = grpc.makeClientConstructor(healthServiceDefinition, 'grpc.health.v1.HealthService'); + healthClient = new HealthClientConstructor(`localhost:${port}`, grpc.credentials.createInsecure()) as unknown as HealthClient; + healthServer.start(); + done(); + }); + }); + afterEach((done) => { + healthClient.close(); + healthServer.tryShutdown(done); + }); + describe('check', () => { + it('Should say that an enabled service is SERVING', done => { + healthClient.check({service: ''}, (error, value) => { + assert.ifError(error); + assert.strictEqual(value?.status, 'SERVING'); + done(); + }); + }); + it('Should say that a disabled service is NOT_SERVING', done => { + healthClient.check({service: 'grpc.test.TestServiceNotServing'}, (error, value) => { + assert.ifError(error); + assert.strictEqual(value?.status, 'NOT_SERVING'); + done(); + }); + }); + it('Should get NOT_FOUND if the service is not registered', done => { + healthClient.check({service: 'not_registered'}, (error, value) => { + assert(error); + assert.strictEqual(error.code, grpc.status.NOT_FOUND); + done(); + }); + }); + it('Should get a different response if the health status changes', done => { + healthClient.check({service: 'transient'}, (error, value) => { + assert(error); + assert.strictEqual(error.code, grpc.status.NOT_FOUND); + healthImpl.setStatus('transient', 'SERVING'); + healthClient.check({service: 'transient'}, (error, value) => { + assert.ifError(error); + assert.strictEqual(value?.status, 'SERVING'); + done(); + }); + }); + }); + }); + describe('watch', () => { + it('Should respond with the health status for an existing service', done => { + const call = healthClient.watch({service: ''}); + call.on('data', (response: HealthCheckResponse__Output) => { + assert.strictEqual(response.status, 'SERVING'); + call.cancel(); + }); + call.on('error', () => {}); + call.on('status', status => { + assert.strictEqual(status.code, grpc.status.CANCELLED); + done(); + }); + }); + it('Should send a new update when the status changes', done => { + const receivedStatusList: _grpc_health_v1_HealthCheckResponse_ServingStatus__Output[] = []; + const call = healthClient.watch({service: 'grpc.test.TestServiceServing'}); + call.on('data', (response: HealthCheckResponse__Output) => { + switch (receivedStatusList.length) { + case 0: + assert.strictEqual(response.status, 'SERVING'); + healthImpl.setStatus('grpc.test.TestServiceServing', 'NOT_SERVING'); + break; + case 1: + assert.strictEqual(response.status, 'NOT_SERVING'); + call.cancel(); + break; + default: + assert.fail(`Unexpected third status update ${response.status}`); + } + receivedStatusList.push(response.status); + }); + call.on('error', () => {}); + call.on('status', status => { + assert.deepStrictEqual(receivedStatusList, ['SERVING', 'NOT_SERVING']); + assert.strictEqual(status.code, grpc.status.CANCELLED); + done(); + }); + }); + it('Should update when a service that did not exist is added', done => { + const receivedStatusList: _grpc_health_v1_HealthCheckResponse_ServingStatus__Output[] = []; + const call = healthClient.watch({service: 'transient'}); + call.on('data', (response: HealthCheckResponse__Output) => { + switch (receivedStatusList.length) { + case 0: + assert.strictEqual(response.status, 'SERVICE_UNKNOWN'); + healthImpl.setStatus('transient', 'SERVING'); + break; + case 1: + assert.strictEqual(response.status, 'SERVING'); + call.cancel(); + break; + default: + assert.fail(`Unexpected third status update ${response.status}`); + } + receivedStatusList.push(response.status); + }); + call.on('error', () => {}); + call.on('status', status => { + assert.deepStrictEqual(receivedStatusList, ['SERVICE_UNKNOWN', 'SERVING']); + assert.strictEqual(status.code, grpc.status.CANCELLED); + done(); + }); + }) + }); +}); diff --git a/packages/grpc-health-check/tsconfig.json b/packages/grpc-health-check/tsconfig.json new file mode 100644 index 000000000..763ceda98 --- /dev/null +++ b/packages/grpc-health-check/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "declaration": true, + "forceConsistentCasingInFileNames": true, + "noEmitOnError": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "pretty": true, + "sourceMap": true, + "strict": true, + "lib": ["es2017"], + "outDir": "build", + "target": "es2017", + "module": "commonjs", + "resolveJsonModule": true, + "incremental": true, + "types": ["mocha"], + "noUnusedLocals": true + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} From 5be024f060a7cc7377167aabde95e3aefd54fd87 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 18 Sep 2023 17:32:29 -0700 Subject: [PATCH 046/109] grpc-js: Delegate to child picker in ResolvingLoadBalancer#updateResolution --- packages/grpc-js/src/resolving-load-balancer.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/grpc-js/src/resolving-load-balancer.ts b/packages/grpc-js/src/resolving-load-balancer.ts index e600047e8..3f52093a1 100644 --- a/packages/grpc-js/src/resolving-load-balancer.ts +++ b/packages/grpc-js/src/resolving-load-balancer.ts @@ -264,7 +264,11 @@ export class ResolvingLoadBalancer implements LoadBalancer { private updateResolution() { this.innerResolver.updateResolution(); if (this.currentState === ConnectivityState.IDLE) { - this.updateState(ConnectivityState.CONNECTING, new QueuePicker(this)); + /* this.latestChildPicker is initialized as new QueuePicker(this), which + * is an appropriate value here if the child LB policy is unset. + * Otherwise, we want to delegate to the child here, in case that + * triggers something. */ + this.updateState(ConnectivityState.CONNECTING, this.latestChildPicker); } this.backoffTimeout.runOnce(); } From 86debcd83b28432c6be17759832621fc274a8ff8 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 25 Sep 2023 17:20:36 -0700 Subject: [PATCH 047/109] Add cancellation example --- examples/cancellation/README.md | 18 +++++++++ examples/cancellation/client.js | 64 ++++++++++++++++++++++++++++++++ examples/cancellation/server.js | 66 +++++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 examples/cancellation/README.md create mode 100644 examples/cancellation/client.js create mode 100644 examples/cancellation/server.js diff --git a/examples/cancellation/README.md b/examples/cancellation/README.md new file mode 100644 index 000000000..87d773475 --- /dev/null +++ b/examples/cancellation/README.md @@ -0,0 +1,18 @@ +# Cancellation + +This example shows how clients can cancel in-flight RPCs by cancelling the +call object returned by the method invocation. The client will receive a status +with code `CANCELLED` and the server handler's call object will emit a +`'cancelled'` event. + +## Start the server + +``` +node server.js +``` + +## Run the client + +``` +node client.js +``` diff --git a/examples/cancellation/client.js b/examples/cancellation/client.js new file mode 100644 index 000000000..c76487dfe --- /dev/null +++ b/examples/cancellation/client.js @@ -0,0 +1,64 @@ +/* + * + * 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. + * + */ + +const grpc = require('@grpc/grpc-js'); +const protoLoader = require('@grpc/proto-loader'); +const parseArgs = require('minimist'); + +const PROTO_PATH = __dirname + '/../protos/echo.proto'; + +const packageDefinition = protoLoader.loadSync( + PROTO_PATH, + {keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true + }); +const echoProto = grpc.loadPackageDefinition(packageDefinition).grpc.examples.echo; + +function main() { + let argv = parseArgs(process.argv.slice(2), { + string: 'target', + default: {target: 'localhost:50052'} + }); + const client = new echoProto.Echo(argv.target, grpc.credentials.createInsecure()); + const call = client.bidirectionalStreamingEcho(); + const EXPECTED_MESSAGES = 2; + let receivedMessages = 0; + call.on('data', value => { + console.log(`received message "${value.message}"`) + receivedMessages += 1; + if (receivedMessages >= EXPECTED_MESSAGES) { + console.log('cancelling call'); + call.cancel(); + } + }); + call.on('status', statusObject => { + console.log(`received call status with code ${grpc.status[statusObject.code]}`); + }); + call.on('error', error => { + console.log(`received error ${error}`); + }) + console.log('sending message "hello"'); + call.write({message: 'hello'}); + console.log('sending message "world"') + call.write({message: 'world'}); +} + +main(); diff --git a/examples/cancellation/server.js b/examples/cancellation/server.js new file mode 100644 index 000000000..b9ff13fcc --- /dev/null +++ b/examples/cancellation/server.js @@ -0,0 +1,66 @@ +/* + * + * 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. + * + */ + +const grpc = require('@grpc/grpc-js'); +const protoLoader = require('@grpc/proto-loader'); +const parseArgs = require('minimist'); + +const PROTO_PATH = __dirname + '/../protos/echo.proto'; + +const packageDefinition = protoLoader.loadSync( + PROTO_PATH, + {keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true + }); +const echoProto = grpc.loadPackageDefinition(packageDefinition).grpc.examples.echo; + +function bidirectionalStreamingEcho(call) { + call.on('data', value => { + const message = value.message; + console.log(`echoing message "${message}"`); + call.write({message: message}); + }); + call.on('end', () => { + call.end(); + }); + call.on('cancelled', () => { + console.log('received cancelled event'); + }); +} + +const serviceImplementation = { + bidirectionalStreamingEcho +} + +function main() { + const argv = parseArgs(process.argv.slice(2), { + string: 'port', + default: {port: '50052'} + }); + const server = new grpc.Server(); + server.addService(echoProto.Echo.service, serviceImplementation); + server.bindAsync(`0.0.0.0:${argv.port}`, grpc.ServerCredentials.createInsecure(), () => { + server.start(); + }); + client = new echoProto.Echo(`localhost:${argv.port}`, grpc.credentials.createInsecure()); +} + +main(); From 0ebfe60bf24496ef24479adeb1cf4e4a6c18efec Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 26 Sep 2023 09:46:56 -0700 Subject: [PATCH 048/109] Add error handling example --- examples/error_handling/README.md | 23 ++++++++ examples/error_handling/client.js | 89 +++++++++++++++++++++++++++++++ examples/error_handling/server.js | 68 +++++++++++++++++++++++ 3 files changed, 180 insertions(+) create mode 100644 examples/error_handling/README.md create mode 100644 examples/error_handling/client.js create mode 100644 examples/error_handling/server.js diff --git a/examples/error_handling/README.md b/examples/error_handling/README.md new file mode 100644 index 000000000..c1ba71d68 --- /dev/null +++ b/examples/error_handling/README.md @@ -0,0 +1,23 @@ +# Error Handling + +This example demonstrates basic RPC error handling in gRPC for unary and +streaming response cardinalities. + +## Start the server + +Run the server, whcih returns an error if the RPC request's `name` field is +empty. + +``` +node server.js +``` + +## Run the client + +Then run the client in another terminal, which makes two requests for each of +unary and streaming responses: one with an empty Name field and one with it +populated with the current username provided by os/user. + +``` +node client.js +``` diff --git a/examples/error_handling/client.js b/examples/error_handling/client.js new file mode 100644 index 000000000..1a8eff8ea --- /dev/null +++ b/examples/error_handling/client.js @@ -0,0 +1,89 @@ +/* + * + * 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. + * + */ + +const grpc = require('@grpc/grpc-js'); +const protoLoader = require('@grpc/proto-loader'); +const parseArgs = require('minimist'); +const os = require('os'); + +const PROTO_PATH = __dirname + '/../protos/helloworld.proto'; + +const packageDefinition = protoLoader.loadSync( + PROTO_PATH, + {keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true + }); +const helloProto = grpc.loadPackageDefinition(packageDefinition).helloworld; + +function unaryCall(client, requestId, name, expectedCode) { + console.log(`[${requestId}] Calling SayHello with name:"${name}"`); + return new Promise((resolve, reject) => { + client.sayHello({name: name}, (error, value) => { + if (error) { + if (error.code === expectedCode) { + console.log(`[${requestId}] Received error ${error.message}`); + } else { + console.log(`[${requestId}] Received unexpected error ${error.message}`); + } + } + if (value) { + console.log(`[${requestId}] Received response ${value.message}`); + } + resolve(); + }); + }); +} + +function streamingCall(client, requestId, name, expectedCode) { + console.log(`[${requestId}] Calling SayHelloStreamReply with name:"${name}"`); + return new Promise((resolve, reject) => { + const call = client.sayHelloStreamReply({name: name}); + call.on('data', value => { + console.log(`[${requestId}] Received response ${value.message}`); + }); + call.on('status', status => { + console.log(`[${requestId}] Received status with code=${grpc.status[status.code]} details=${status.details}`); + resolve(); + }); + call.on('error', error => { + if (error.code === expectedCode) { + console.log(`[${requestId}] Received expected error ${error.message}`); + } else { + console.log(`[${requestId}] Received unexpected error ${error.message}`); + } + }); + }); +} + +async function main() { + let argv = parseArgs(process.argv.slice(2), { + string: 'target', + default: {target: 'localhost:50052'} + }); + const client = new helloProto.Greeter(argv.target, grpc.credentials.createInsecure()); + const name = os.userInfo().username ?? 'unknown'; + await unaryCall(client, 1, '', grpc.status.INVALID_ARGUMENT); + await unaryCall(client, 2, name, grpc.status.OK); + await streamingCall(client, 3, '', grpc.status.INVALID_ARGUMENT); + await streamingCall(client, 4, name, grpc.status.OK); +} + +main(); diff --git a/examples/error_handling/server.js b/examples/error_handling/server.js new file mode 100644 index 000000000..e77701848 --- /dev/null +++ b/examples/error_handling/server.js @@ -0,0 +1,68 @@ +/* + * + * 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. + * + */ + +var PROTO_PATH = __dirname + '/../protos/helloworld.proto'; + +var grpc = require('@grpc/grpc-js'); +var protoLoader = require('@grpc/proto-loader'); +var packageDefinition = protoLoader.loadSync( + PROTO_PATH, + {keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true + }); +var hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld; + +/** + * Implements the SayHello RPC method. + */ +function sayHello(call, callback) { + if (call.request.name === '') { + callback({code: grpc.status.INVALID_ARGUMENT, details: 'request missing required field: name'}); + } + callback(null, {message: 'Hello ' + call.request.name}); +} + +const REPLY_COUNT = 5; + +function sayHelloStreamReply(call) { + if (call.request.name === '') { + call.emit('error', {code: grpc.status.INVALID_ARGUMENT, details: 'request missing required field: name'}); + } else { + for (let i = 0; i < REPLY_COUNT; i++) { + call.write({message: 'Hello ' + call.request.name}); + } + call.end(); + } +} + +/** + * Starts an RPC server that receives requests for the Greeter service at the + * sample server port + */ +function main() { + var server = new grpc.Server(); + server.addService(hello_proto.Greeter.service, {sayHello: sayHello, sayHelloStreamReply: sayHelloStreamReply}); + server.bindAsync('0.0.0.0:50052', grpc.ServerCredentials.createInsecure(), () => { + server.start(); + }); +} + +main(); From 2003c8859c0740eac33fd4ab12fad5d087b9c121 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 2 Oct 2023 10:12:03 -0700 Subject: [PATCH 049/109] Cancellation example: corrected information --- examples/cancellation/README.md | 4 ++-- examples/cancellation/server.js | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/cancellation/README.md b/examples/cancellation/README.md index 87d773475..5dcd76c09 100644 --- a/examples/cancellation/README.md +++ b/examples/cancellation/README.md @@ -2,8 +2,8 @@ This example shows how clients can cancel in-flight RPCs by cancelling the call object returned by the method invocation. The client will receive a status -with code `CANCELLED` and the server handler's call object will emit a -`'cancelled'` event. +with code `CANCELLED` and the server handler's call object will emit either a +`'cancelled'` event or an `'end'` event. ## Start the server diff --git a/examples/cancellation/server.js b/examples/cancellation/server.js index b9ff13fcc..d68033d42 100644 --- a/examples/cancellation/server.js +++ b/examples/cancellation/server.js @@ -38,11 +38,13 @@ function bidirectionalStreamingEcho(call) { console.log(`echoing message "${message}"`); call.write({message: message}); }); + // Either 'end' or 'cancelled' will be emitted when the call is cancelled call.on('end', () => { + console.log('server received end event') call.end(); }); call.on('cancelled', () => { - console.log('received cancelled event'); + console.log('server received cancelled event'); }); } From abac01a9cf6c66afc1dace2832ef445c3d45a947 Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Wed, 11 Oct 2023 17:43:14 +0900 Subject: [PATCH 050/109] chore(grpc-js): remove unused callcredentials parameter from insecure impl --- packages/grpc-js/src/channel-credentials.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grpc-js/src/channel-credentials.ts b/packages/grpc-js/src/channel-credentials.ts index 72b19d65e..2ed18507f 100644 --- a/packages/grpc-js/src/channel-credentials.ts +++ b/packages/grpc-js/src/channel-credentials.ts @@ -163,8 +163,8 @@ export abstract class ChannelCredentials { } class InsecureChannelCredentialsImpl extends ChannelCredentials { - constructor(callCredentials?: CallCredentials) { - super(callCredentials); + constructor() { + super(); } compose(callCredentials: CallCredentials): never { From 976567395e91bf7064b86d8f609fa8791454abbc Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 16 Oct 2023 15:16:58 -0700 Subject: [PATCH 051/109] grpc-js: Deprecate Server#start --- packages/grpc-js/src/server.ts | 36 +++++++++++++++------------- packages/grpc-js/test/test-server.ts | 21 ---------------- 2 files changed, 20 insertions(+), 37 deletions(-) diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index 4099f1350..f04cd9810 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -16,6 +16,7 @@ */ import * as http2 from 'http2'; +import * as util from 'util'; import { AddressInfo } from 'net'; import { ServiceError } from './call'; @@ -89,6 +90,17 @@ interface BindResult { function noop(): void {} +/** + * Decorator to wrap a class method with util.deprecate + * @param message The message to output if the deprecated method is called + * @returns + */ +function deprecate(message: string) { + return function (target: (this: This, ...args: Args) => Return, context: ClassMethodDecoratorContext Return>) { + return util.deprecate(target, message); + } +} + function getUnimplementedStatusResponse( methodName: string ): Partial { @@ -160,6 +172,10 @@ export class Server { UntypedHandler >(); private sessions = new Map(); + /** + * This field only exists to ensure that the start method throws an error if + * it is called twice, as it did previously. + */ private started = false; private options: ChannelOptions; private serverAddressString = 'null'; @@ -371,10 +387,6 @@ export class Server { creds: ServerCredentials, callback: (error: Error | null, port: number) => void ): void { - if (this.started === true) { - throw new Error('server is already started'); - } - if (typeof port !== 'string') { throw new TypeError('port must be a string'); } @@ -709,8 +721,6 @@ export class Server { } } - this.started = false; - // Always destroy any available sessions. It's possible that one or more // tryShutdown() calls are in progress. Don't wait on them to finish. this.sessions.forEach((channelzInfo, session) => { @@ -750,6 +760,10 @@ export class Server { return this.handlers.delete(name); } + /** + * @deprecated No longer needed as of version 1.10.x + */ + @deprecate('Calling start() is no longer necessary. It can be safely omitted.') start(): void { if ( this.http2ServerList.length === 0 || @@ -763,9 +777,6 @@ export class Server { if (this.started === true) { throw new Error('server is already started'); } - if (this.channelzEnabled) { - this.channelzTrace.addTrace('CT_INFO', 'Starting'); - } this.started = true; } @@ -786,9 +797,6 @@ export class Server { } } - // Close the server if necessary. - this.started = false; - for (const { server: http2Server, channelzRef: ref } of this .http2ServerList) { if (http2Server.listening) { @@ -1053,10 +1061,6 @@ export class Server { http2Server.on('stream', handler.bind(this)); http2Server.on('session', session => { - if (!this.started) { - session.destroy(); - return; - } const channelzRef = registerChannelzSocket( session.socket.remoteAddress ?? 'unknown', diff --git a/packages/grpc-js/test/test-server.ts b/packages/grpc-js/test/test-server.ts index 2a7a48181..d1b485ec3 100644 --- a/packages/grpc-js/test/test-server.ts +++ b/packages/grpc-js/test/test-server.ts @@ -113,27 +113,6 @@ describe('Server', () => { }); }); - it('throws if bind is called after the server is started', done => { - const server = new Server(); - - server.bindAsync( - 'localhost:0', - ServerCredentials.createInsecure(), - (err, port) => { - assert.ifError(err); - server.start(); - assert.throws(() => { - server.bindAsync( - 'localhost:0', - ServerCredentials.createInsecure(), - noop - ); - }, /server is already started/); - server.tryShutdown(done); - } - ); - }); - it('throws on invalid inputs', () => { const server = new Server(); From 54df17727f1e1fa40c2c8386c105196fe8731c81 Mon Sep 17 00:00:00 2001 From: Justin Timmons Date: Thu, 2 Nov 2023 14:07:40 -0400 Subject: [PATCH 052/109] feat(grpc-reflection): created new grpc-reflection package ported from nestjs-grpc-reflection library --- README.md | 8 + gulpfile.ts | 5 +- packages/grpc-reflection/LICENSE | 201 ++++++++++++++ packages/grpc-reflection/README.md | 42 +++ packages/grpc-reflection/gulpfile.ts | 50 ++++ packages/grpc-reflection/images/example.gif | Bin 0 -> 1065106 bytes packages/grpc-reflection/package.json | 47 ++++ .../proto/grpc/reflection/v1/reflection.proto | 149 +++++++++++ .../grpc/reflection/v1alpha/reflection.proto | 139 ++++++++++ .../grpc-reflection/proto/sample/sample.proto | 39 +++ .../proto/sample/vendor/common.proto | 15 ++ .../sample/vendor/dependency/dependency.proto | 7 + .../proto/sample/vendor/vendor.proto | 10 + .../grpc/reflection/v1/ErrorResponse.ts | 24 ++ .../reflection/v1/ExtensionNumberResponse.ts | 28 ++ .../grpc/reflection/v1/ExtensionRequest.ts | 26 ++ .../reflection/v1/FileDescriptorResponse.ts | 30 +++ .../grpc/reflection/v1/ListServiceResponse.ts | 25 ++ .../grpc/reflection/v1/ServerReflection.ts | 9 + .../reflection/v1/ServerReflectionRequest.ts | 91 +++++++ .../reflection/v1/ServerReflectionResponse.ts | 75 ++++++ .../grpc/reflection/v1/ServiceResponse.ts | 26 ++ .../grpc/reflection/v1alpha/ErrorResponse.ts | 24 ++ .../v1alpha/ExtensionNumberResponse.ts | 28 ++ .../reflection/v1alpha/ExtensionRequest.ts | 26 ++ .../v1alpha/FileDescriptorResponse.ts | 30 +++ .../reflection/v1alpha/ListServiceResponse.ts | 25 ++ .../reflection/v1alpha/ServerReflection.ts | 9 + .../v1alpha/ServerReflectionRequest.ts | 91 +++++++ .../v1alpha/ServerReflectionResponse.ts | 75 ++++++ .../reflection/v1alpha/ServiceResponse.ts | 26 ++ .../grpc-reflection/src/protobuf-visitor.ts | 110 ++++++++ .../src/reflection-v1-implementation.ts | 252 ++++++++++++++++++ packages/grpc-reflection/src/utils.ts | 11 + .../test/test-reflection-v1-implementation.ts | 182 +++++++++++++ packages/grpc-reflection/test/test-utils.ts | 14 + packages/grpc-reflection/tsconfig.json | 29 ++ 37 files changed, 1976 insertions(+), 2 deletions(-) create mode 100644 packages/grpc-reflection/LICENSE create mode 100644 packages/grpc-reflection/README.md create mode 100644 packages/grpc-reflection/gulpfile.ts create mode 100644 packages/grpc-reflection/images/example.gif create mode 100644 packages/grpc-reflection/package.json create mode 100644 packages/grpc-reflection/proto/grpc/reflection/v1/reflection.proto create mode 100644 packages/grpc-reflection/proto/grpc/reflection/v1alpha/reflection.proto create mode 100644 packages/grpc-reflection/proto/sample/sample.proto create mode 100644 packages/grpc-reflection/proto/sample/vendor/common.proto create mode 100644 packages/grpc-reflection/proto/sample/vendor/dependency/dependency.proto create mode 100644 packages/grpc-reflection/proto/sample/vendor/vendor.proto create mode 100644 packages/grpc-reflection/src/generated/grpc/reflection/v1/ErrorResponse.ts create mode 100644 packages/grpc-reflection/src/generated/grpc/reflection/v1/ExtensionNumberResponse.ts create mode 100644 packages/grpc-reflection/src/generated/grpc/reflection/v1/ExtensionRequest.ts create mode 100644 packages/grpc-reflection/src/generated/grpc/reflection/v1/FileDescriptorResponse.ts create mode 100644 packages/grpc-reflection/src/generated/grpc/reflection/v1/ListServiceResponse.ts create mode 100644 packages/grpc-reflection/src/generated/grpc/reflection/v1/ServerReflection.ts create mode 100644 packages/grpc-reflection/src/generated/grpc/reflection/v1/ServerReflectionRequest.ts create mode 100644 packages/grpc-reflection/src/generated/grpc/reflection/v1/ServerReflectionResponse.ts create mode 100644 packages/grpc-reflection/src/generated/grpc/reflection/v1/ServiceResponse.ts create mode 100644 packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ErrorResponse.ts create mode 100644 packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ExtensionNumberResponse.ts create mode 100644 packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ExtensionRequest.ts create mode 100644 packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/FileDescriptorResponse.ts create mode 100644 packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ListServiceResponse.ts create mode 100644 packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ServerReflection.ts create mode 100644 packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ServerReflectionRequest.ts create mode 100644 packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ServerReflectionResponse.ts create mode 100644 packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ServiceResponse.ts create mode 100644 packages/grpc-reflection/src/protobuf-visitor.ts create mode 100644 packages/grpc-reflection/src/reflection-v1-implementation.ts create mode 100644 packages/grpc-reflection/src/utils.ts create mode 100644 packages/grpc-reflection/test/test-reflection-v1-implementation.ts create mode 100644 packages/grpc-reflection/test/test-utils.ts create mode 100644 packages/grpc-reflection/tsconfig.json diff --git a/README.md b/README.md index f0f215ce3..de4cc752b 100644 --- a/README.md +++ b/README.md @@ -46,3 +46,11 @@ Directory: [`packages/grpc-health-check`](https://github.com/grpc/grpc-node/tree npm package: [grpc-health-check](https://www.npmjs.com/package/grpc-health-check) Health check service for gRPC servers. + +### gRPC Reflection API Service + +Directory: [`packages/grpc-reflection`](https://github.com/grpc/grpc-node/tree/master/packages/grpc-reflection) + +npm package: [@grpc/reflection](https://www.npmjs.com/package/@grpc/reflection) + +Reflection API service for gRPC servers. diff --git a/gulpfile.ts b/gulpfile.ts index 7ac4e9a0b..3bdd6bb68 100644 --- a/gulpfile.ts +++ b/gulpfile.ts @@ -19,10 +19,11 @@ import * as gulp from 'gulp'; import * as healthCheck from './packages/grpc-health-check/gulpfile'; import * as jsCore from './packages/grpc-js/gulpfile'; import * as jsXds from './packages/grpc-js-xds/gulpfile'; +import * as reflection from './packages/grpc-reflection/gulpfile'; import * as protobuf from './packages/proto-loader/gulpfile'; import * as internalTest from './test/gulpfile'; -const installAll = gulp.series(jsCore.install, healthCheck.install, protobuf.install, internalTest.install, jsXds.install); +const installAll = gulp.series(jsCore.install, healthCheck.install, protobuf.install, internalTest.install, jsXds.install, reflection.install); const lint = gulp.parallel(jsCore.lint); @@ -36,7 +37,7 @@ const clean = gulp.series(jsCore.clean, protobuf.clean, jsXds.clean); const cleanAll = gulp.series(jsXds.cleanAll, jsCore.cleanAll, internalTest.cleanAll, protobuf.cleanAll); -const nativeTestOnly = gulp.parallel(healthCheck.test); +const nativeTestOnly = gulp.parallel(healthCheck.test, reflection.test); const nativeTest = gulp.series(build, nativeTestOnly); diff --git a/packages/grpc-reflection/LICENSE b/packages/grpc-reflection/LICENSE new file mode 100644 index 000000000..8dada3eda --- /dev/null +++ b/packages/grpc-reflection/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. diff --git a/packages/grpc-reflection/README.md b/packages/grpc-reflection/README.md new file mode 100644 index 000000000..cb2501877 --- /dev/null +++ b/packages/grpc-reflection/README.md @@ -0,0 +1,42 @@ +# gRPC Reflection + +gRPC reflection API service for use with gRPC-node. + +## Background + +This package provides an implementation of the [gRPC Server Reflection Protocol](https://github.com/grpc/grpc/blob/master/doc/server-reflection.md) service which can be added to an existing gRPC server. Adding this service to your server will allow clients [such as postman](https://blog.postman.com/postman-now-supports-grpc/) to dynamically load the API specification from your running application rather than needing to pass around and load proto files manually. + +![example of reflection working with postman](https://gitlab.com/jtimmons/nestjs-grpc-reflection-module/-/raw/master/images/example.gif) + +## Installation + +Use the package manager [npm](https://www.npmjs.com/get-npm) to install `@grpc/reflection`. + +```bash +npm install @grpc/reflection +``` + +## Usage + +Any gRPC-node server can use `@grpc/reflection` to expose reflection information about their gRPC API. + +```typescript +import { ReflectionServer } from '@grpc/reflection'; + +const pkg = protoLoader.load(...); // Load your gRPC package definition as normal + +// Create the reflection implementation based on your gRPC package and add it to your existing server +const reflection = new ReflectionServer(pkg); +reflection.addToServer(server); +``` + +Congrats! Your server now allows any client to request reflection information about its API. + +## Contributing + +Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. The original proposal for this library can be found in [gRFC L108](https://github.com/grpc/proposal/blob/master/L108-node-grpc-reflection-library.md) + +Please make sure to update tests as appropriate. + +## License +[Apache License 2.0](https://choosealicense.com/licenses/apache-2.0/) diff --git a/packages/grpc-reflection/gulpfile.ts b/packages/grpc-reflection/gulpfile.ts new file mode 100644 index 000000000..f95b91bc0 --- /dev/null +++ b/packages/grpc-reflection/gulpfile.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2019 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 * as gulp from 'gulp'; +import * as mocha from 'gulp-mocha'; +import * as execa from 'execa'; +import * as path from 'path'; + +const reflectionDir = __dirname; +const outDir = path.resolve(reflectionDir, 'build'); + +const execNpmVerb = (verb: string, ...args: string[]) => + execa('npm', [verb, ...args], {cwd: reflectionDir, stdio: 'inherit'}); +const execNpmCommand = execNpmVerb.bind(null, 'run'); + +const install = () => execNpmVerb('install', '--unsafe-perm'); + +/** + * Transpiles TypeScript files in src/ to JavaScript according to the settings + * found in tsconfig.json. + */ +const compile = () => execNpmCommand('compile'); + +const runTests = () => { + return gulp.src(`${outDir}/test/**/*.js`) + .pipe(mocha({reporter: 'mocha-jenkins-reporter', + require: ['ts-node/register']})); +}; + +const test = gulp.series(install, runTests); + +export { + install, + compile, + test +} diff --git a/packages/grpc-reflection/images/example.gif b/packages/grpc-reflection/images/example.gif new file mode 100644 index 0000000000000000000000000000000000000000..65c92d78fa56659c55a20b3d53f741f1120d894a GIT binary patch literal 1065106 zcmV(_K-9lSNk%v~VK51F1@`~|0000B4h#|w7#$rQAs!(eA|WCoB7h?xC?+Np4$3sR_5R8&S*R6kd6$XHe{ zSvWCSi(Fh>U|eLTTv;PvsX<_g5Mj9+VX-e^pLAhc{$W`PW5iZtXaHo=1!T-*WMqnD zR_JA6YG!V8YHDk1Ykh2K|73ppjL~muVQ_9Ia>Z(LZ*p>S zbaHP$b5C<~d((4Qd~|l6bW`MXPY!k5Hg&-rcF!bsKOT4040z*Od87h*?n!&RYsHc{y$fByLtE;Plt>tyD_p+|G%&w`9uiLP%uC%eat+TYRv#zQwtu>jwYs&uyu87^z4X0I{=F|ozE_aG_@2J# zy1vG)!QK49tC_-^!otGA!@s-5!=A?Y#m39b#>6SeL8Qv5zslV8%E7M7@v+Oc#LLOa z%g4*h%gxi%(bLb<)6?D4(5=<6!PW8D*4yvat*+Sq(AeJF+uQBi&%4~e_T0bO-Q>UC z{^8%_|KQck;?LLO@z3P`+vMQ=<>cn(=IG|<=;rLe?#$%w?C9?2?(Xi#@BiBG;Oy`7 z@$c^0@c;es-01TD?DFjM^6vBV^ZxYk^Y;Ay`tj%d?DPEf*!}<5{^#WW|Lp$%=>PNe z|NsC0{{R30A^!_bMO0HmK~P09E-(WD0000X`2+ zoJq5$&6_xL>fFh*r_Y~2g9;r=w5ZXeNRujE%CxD|r%LvEp(5qU6C-zx=-tb=uiw9b0}CEZxUk{Fh!ZPb%($`R z$B-jSo=my2<;$2eYu?Pcv**vCLyI0wy0q!js8g$6&APSg*RW&Do=v;9?c2C>>)u^C zFW!e#s6Hu?07xUT1Gc3Los)<(+uuspp=2_UY%JfCeh)poA7`=%I)vs_3HJeUO3?R7mHB zSZcQEMhYoPcO93EcIxS;poS{ysHB!^>Zz!vs_Lq&w)*3VPYC~!LLO*wdY`5*U(q_T{Knme)xE%!z~44M}+Wz;=DKRxZMyqFhKwO6 z!3DdIF8=tVoBg=p@|jQm`s}yw?THC|P&eawOAx~nKm35h{y5~nL=3gNo%(PWxa<_} z26bS?EQ&{q1}=~lb-2Ou9Pqf@5s-Ji(?AREP>T)<4CrmG+;gKf$)nT%$^n|K*0j?@Qi4z*(YA;LKm*_g@yp)d_Giy80w)6 zY0RS@_vj}Kz<@;ai@*?JXT(D8ZwX3l!2BFnxGE@+ijbV*ELc$mES_hJTJRtkH`&4N znb3lK45cXj^@%l_vW=x2#Ud<#!}UM_2BvhxC}&B_S{CVt(s3TA8sGs!8q$zI@Ic={ z2|W%K?g*@iq%s{i3m;I>JPzPu_BKh)5RMUit+b^!w~4Jcx=;^;a6}uw$)oe3?>k)j z+uvaMLRqTsL!8o_k8HTIx?Rw3+{~vwL%IJlb?MSa4Uhl}eo0Uqu%LFh<4*W|7XnEz zaG8;ug(O1IfM<$Fi!GqUG^@!;O0lEKfyuZ13jRE zOy{(n4*=n-FNlCwb83W^lJu--ZJ$5ABO(a=E(Qf9=nrDhfRJv-0S6c!L?JqhIy|79 z;hEkYI7--B+(83?O{-!TJ1TF6H9INrCJaPWhE$?b4<74*H{Do?(@EitpdE!Cu%lTQ z8UhGzgkuGO5ZX_iltUG$EKpszN)i9D^Ao3iA!whPO~(56w^>`Omja5vxB_ztwX z9uR^@?p2wSR9HD@KS%q1*Qj-yIf>G zm#k2;l@Q6p$wOJBOM~p zgGS`I#*G+*hI)WX{TBHXKV~BkP&&>==;0DTU2y~mY~LzNfy7wOa?Kk2odygv!oOsJ zBFFQ9Iz05kknHPU$D;v_dbt0ZwV;FsLagOF+c`oe*3M_!*fGo2wgQ7NFMlz>-W`>| z5sqlYAb7;iQOL4_MAoevt}}*lUczc6;Djk<904O-;?sEKkRI5`h7eKNg@d>jEOngN zJG=VT|KhTD8USH17m@|#c>tRCDhUm1;LU`k1chNu>|zV3zH@?;8!SELan@8r7yzvi zB4B~~mf>^>%?^QGSP9-mx|ExWKzM`7qw=nCwCW@B8VzA;pt2G>qlTlg=S}Z<$+|lY z@TIMbxP-cr`OLefA}iJ`9u0ID*k7#0oatQehC6&c^|h>hSL~uSUSd0jU}LDosozWJ z!3aQHryJcFfeJw2+wA}5QIa1Ef%Seu4~%1o6k_V%HZHo^YivZ1T%PiW`~2s}x;J+k zux~>yA=d$4<}AwdYlKs?7Q+U*)TiE^)^7F_PmKT-+GvP*y5Ws(z)oZV?8dM&nvIWs zT%>W4dPSQZ*6Mb)qqzC0Q_i7ne_|0-54_-0CG_nyU5!BJ!Nb#Lr*BfutscDQsUBMPUV;vfjer!|sPl4;dX1cpKnd8;d2*UMm8DlZ*R}KZ zQN+E|=*5l^I=|AqV`uN@^E==-PyFI1s_@$3W#ajVL=Bf2-~sO|)9r9@gxA6jnK!=n z=?z+4rC?X;4AKAGKd~Kgu2Bj$WSJ50i^{V3J?$Y{d)sS&`?;e#B%hx|jluZ!u0FDHCl2l0yI04jWc_4Isb4Gv~NNk`LdWoe}sI+A5 z)<%VpR6~A8emx?8w8v*WVt>?zO4#;OsP=no#COg1Lc_O#Kp2EhVt_&< z0s6LY05oh3*jx}uJgg7`A+|-;_hz-g2ra;HLYReVWQ&g^9L!iw$ybW)uMeRdhcv1s_mX=F>zG-~b^YMbRe-FTi;7LtT?d z2`XShxHyd>rHSD20-$wDpQs&n7hf*q2-p?@@ivFExQcKmhpvZ>sbz6_$YZLwZU0A& z@;HzEp^Lf0OGg-2h{TKvwNMY>1I~3}lXOLoI6oSYNmKZAwa`5du#5Egka}c|*I09G zv%;B#ZB;iVzu-GPxc^m>ol8 zh;sk61UJBsY^6@oq4**3$30)FJkmj)^(WC_$m5?1o37(WW zUfGv^`Imr6jYJqY6hHy?gFlAoKQR!D3}`{o;{XVxKxeiunMPQ6nyR^)2Qruigg30i0xYluOCX!HlQ$KlmVP554ZvK&BMGd) z3dd6g%0oEwWQmU>JsM<}cS#9)={=c~nyfjU)LEU@$yi{CZ-TU)ilj(s*<0zv9e@8d zo)JKv1!+zfbv{QK0f^O}5ikMo30Ts3o%A_A^I4zxX`MCsANPqK{h6Nt%0vGdpalAt z`{^G8S{?~npbSbr3)-L%3Xuoe9}k+I5qhC|RG}EUp_n+K`=Oy;!l5FHMj$$(COUu~ zsvjlFRwvq`D72z38l!@jq8>t_GJ2!sgQGfXi!_QKFse{H8l;=^qe5DwuhyeRnxsm) zq)ZxI7ulpx8l_S?r406@RC=XYnx$I$F-W?lUizhA8m7w9rD9s9W_qS*Dk@}}rfk}# zZu+KpvZio4r*vAUb}A(55}kUwr+nI{e)^|?8mNLgsDxUmhI*)ony8ApsEq&GsE+!m zkQ%9yI;oUesg`=Fn3}1Yx~ZJnsh;|&pc<;8I;x~vs-}9XsG6#(x~i<&s;>H~uo|nf zI;*r=tG0TphN>F4MI<3@N zt=4+2*qW`{x~<&Wt={^r;2N&tIRuFtEH3umVf42Ai-7d$0uCuns%03mdTv`>+%%u?tJF7+bIw zo3I&Mu^Vf!9s95!3$h{Gup=9=C2O)LJFz9ZvMd|2E}OClyRtG1u{8f%voVXaH!HI_ z%d9LMydOJF-lhv_?y{S!=aV%e7JK zwNneWRr|45d$bvgwMc8W7>l-NtF|7iwP_2tZ7a8H>$W0mw+_p;bxXHzE4Eupwp|Oj zUn{s_OSogZv1QA*cdNL0i?=GvxR2|%aVxoVOSyY%xqWN6e~Y+*%ejN=xrGb5hikEk zi@A$yx{V9DGpo8b%eqaAx{*t`B#W@hpbY+SySSUXy1To)+q=H|yTBW~!aKagTfD}5 zyvUop%DcSG+q};EywDrH(mTD>TfNqMz1W+*+Pl5n+r8fVz2N^FzT!K+d6kNd;e8Cu;!5X~59NfVk{J|g`!XiAvBwWHKe8MQ4!YaJNEZo8_ z{K7CC!!kU>G+e_re8V`L!#cdfJlw-R{KG&T#6mp8L|nv1e8fnc#7exxOx(my{KQZk z#Zo-QR9wYYe8pIt#ag_@T-?Q8{Ka4##$r6iWL(B(e8y;;#%jFAY~03f{Kjw`$8tQ! zbX>=Fe8+g4$9lZSeB8%={KtSC$bt;P^-#!$e8`Ag4}<@_$c)^`j{L}w9LbVA$&@U- z^H2}mfDNed2ygHPL|_Gdt(kPtLF8$Il9n&&R(&GOh4l~`+8g0~4t-(-T)mDAgBVE&29ns=j52)bD*!0}y+>RaG$j!dSz1&z$ z+gq)@o1NRcz1zGj+R1(0*nP&#z1`g1-AwIOy2f=-}sHwVB8P=un+$I-vA!q0zTjbUf>3P;0T`J3SQv+P2by_-wk(Bdxs;xHcLFWwL7 z+uzfW3V+Z9UXTSl-s4-~1wYQ?JpSWB?&Cn-<3f()N6*Uj zoZji4{^_6|>YC04SI`8Vu;NBd4Ro*p7q9`Yo&g!)>J{(-FE9izKr7^H=(;Y^a&Fbv zyVG~>(?}o(e}21uEz};K%~oLDg5K=TZo|7C?b06C3EU6&!06bX?Jj-~U;e%Q{Rclz z>7qXF#!c{7hpR#a04m80jwVDz97ESUg*7E)z<6R zb{_28T-e5548+dieQpH`{p=7Q@h$xC6kqX9jlhYn?HZ5m?asaZ&7;y zdH?v3|Ir1U@mpW{ioWvJ+YhM_@?_uno^SG`o(h<+&*tFru+Hiiz%uBNyZcV}8Ibxf zAP#sR`Q*L#7f-$J;K_dv+VgDPUR~IjtPAjr1aDC6jeq>e|G>1r{LDYm1I*u+AN`9y z`qR4)e<1FkpZ(fj?p*%>2LVkDuD=0=2?93|`?>4-r{4fC5Ck7U4cy!O-EI3#O}zw< z_`yEURzS*+Fa^b|)2ILtsNA`;f`rIHgh;IDs^`z)Lx>S2PNZ1T;zf)ZHE!hC(c?#u zAw`ZPS<>W5lqprNWZBZ?OPDcb&ZJq>=1rV8am;PL-FlHh`x5dj@v}x6@W!u*6TexxM&ZV1n%RZ}l_3q`H zk=lj~fdg;YP@%+)8y_g-dAJF%V8Ml7+@Mfl12vgcHE-tJ*|Sq|;yyE7Sr3OngsD}p zW~~~@l1J?3xzqpkMh-T2_1qnLBndU^H+_WG$bhg7n4pG^3kV#nfx|vnaKZgx;%-9@J8UX64}DsR8!K8f@x;^=(j~WgR9TUm zHy+Uil}CPS%?fTBs&7Xgd-U-~AcGWgNFs|gQl}7;RB}lsn+!2X^un^oo_wmbGM_1< z>}o8R%98I(FvAoxtt{{|P6##%;RT)Ll*unYWuD{YO-}ae=dr;ms9>@eD4-z12G=kr zkTsl8;llp~4I83_3L;EYN0*$Gbkfg2TvDZZbU|^`){u~c8-H|D#hHfYd80%dO|(K4 zNRM>&Raj${byiwywe{BJC}k>^McBZPjYX8vby#AHCdaVQoc>U8c2Awm;kjJ@c@99uhA;NMm|OZm8v znp^*>t3Intz*z(_xY;)B z1`wKgX=lKPjW|h_xS2v^P8CX|WKl^Cm77CwKpcrFN`Cor%rn<~bIv>WJh{Mkn)BUL zs@bWUQ)m{VlF(asy+a?d`kB0ZhB=y%VbsZopK^w|CV9P1mT7kILM#^CUD`%nl=uuVEn>1AYP@{>-TW* zCsC>gn}QHOQK3iF<72Aum;j6M2yRGDWD3DXt3sj-2#ReY&48faG)F-QPDCCMR7U^5 zB7#Bhodkmt{9LT6!NCwpWP>Q=U^P4lkrj5(A|tHG2{TwE8>aAN;$UF~Us%GE-}I{QUYikpeu?6N|oauDqmOykFt^6?5S*r6QfN!GPivX3Uf zhYMSPL@K6{3P>ogd2^Hx)|ev>>E(rj+UrjqAL6SgaDtD@uwY-5(Y2aAL5DLr24O0| zfwwhk17)DbIyf>9bOeGydrHh>juELThO#>N^H56afeMTHk2MMb#mC&C3Tp_k9=dph zJ5ZrYE1<$8`6)z2e1)dX6(T<*bO`sJI1!trM0JZmVyqC^E{c$?X8FlOAszoBO(B@8 zBEl5H6N(s-MdZgwmB7yvR)(%=;-?VbY-UKfnNIu!=q2IoW;+wv*}@u^*SzPQi4C0apgh>6E}e8OSck+N%F%?iu{rdn=@uMB|^`4Nu(M1aVknw*GvC<8fmd~iY7ea z1QBd%lOpdN>_iazz$b*3oqa*7IaTIF%NjO1oCqzvr1MUA%F`u8jcjHBM>DY!#ff9! z3{T(m1YS5PoWWHJVt-qRw05Q$&48#=B>FQdy<%ub8{=2(kwqpU|Rhq6m$7cpltC)M$Cv`dBUyS%m+ zay~(`5#a^98atC}X!UIrc#MNYvW_uKrU8x`jDJs}qZ^JI(xNDl0i9Y;N-3c7`qy z7lmg1lnT0@Pzgewf}2cak}!#zlyDXz7H2(0C1{q1YoAv|_lf1_%Fh4OLxdoCuH21C;@Pg9AbG zS6}{$bH6n0L$sZrYD46mMlA`G>nx1O0X3swHHorqO+@*w5koyK36eW$HN?W=< zKGU>4z(|R5SP9gW(8AKP)J0N*g}WtzT3xQt1bpD52+F&H8uoz?W4q@HgxE(vw2}8O zaUm6PCxr5NkdAarL%rIvW7cl!5Swn>CSc>TL{L{ci_GcSIbm1EE#haI@aa!@q60?O zK@M<;V;yVv2PW`TpbrdT-bunY)9YPg{G6Cr{^tLVDFdD&%7IE$vBYa4gun2BTcO87 z__0KS9A#ecLWU}=Ml}j4jx|}H7caNCGGa0P;fzk^=K@#r{XtEMpraYlf4TKtxK!yo z#N!rUZZo*eLtfaQ`qh6(HKecRYl=RlvIzCdKaNwO^~=ADkO%+Uz5}$Cm@BLY!>p1! zto*PbmD{XmVz!5HxryMR@8dRfs0Q(KDAO66lbnOK8F~y zhZw;Wq!pWUK5cdO?3#KV?9|ocOtm za6uNtEGitE>Z8FUoQR<7zv$CH7;FZy*`WVe_@RFi!gc~2U(r3GAR9KI3bL7=pIC&O z`H2y9p`p+kK=cW8C@2PGu6?UMpny7gP(P;&ilY08#k#|v5X49939T{2op6_S5r;_g z#GM$yN*u(oQo~0q#I!<(5xhjgxisj?hoLElQX`MA$OlRw1G6KKwEGIy8;Mk-3odAa zbl3+qkeVhSgnmee__zW<=sQ}=mOuCd_2PmexHV-www&0BjnKA!At#fA32pmEe~^dh z`7POEF#G^OnBbnx)3PL!iE+4r!XS_hSO$qOhfxR^26?v&Q2{X61WS5|bzp*dK@b&C z$B>}6cL9pp3yp#rK}{U1hjK`sC`A85Yz9p%L{nr%d2m6Vc!D0#Cqk@-h*T2Z+YVDo zCAb10Yyzt2#52(X@Fr^51wnXwtM;ToG6Iima} zYQjRBm@L(D2(am*v>j0o}jCUKxk zyYz=iqc(Y%%3tv?oO`C#;W+<#8Z5-B3f%;kp90EvsZG5o%!+`@1-rP$y3I8yPBs{~ zGW0vxPEit&^R?xDn@KnJ`Gmy+ZKrhI}$;J0w{KAOoh^+cp~ z>WSfM2BypdI`oN1{0Vm9x5>;)px}f6^G}|r89lrYNNdb)icfMxO{3!p_e41f<%#($ zP<1NF{Hc#jixMiy2cqF3DG9Zt!Gm1Xl3o;vwgbj3m}|ege&FqD<6wE zV1fuF1D7#)p~9*L-{h^!%#R;}%9>Hmyy%+E>l!e7h}X1DE$$Imk!tyPu>?U?9us6{mbQlA71sA=v*Pb#edCg9ba3T)WAYWn4zObHSidW04 zK*JPQlEaTUb%_6<$~kNDz=yEb-wL5=9VUMeKkczr2~t=gkga@8r^*w}ciK6clLw|$ zN{b-a1w2^e_*QSdz#I(MW6D8{qfGo*B!AVph*-An%pQLz&Nq38j)mC1U`~pd&2rM7 zLI_FTa>AaSrW6XgWqp%%eTaA!IVaTF)d3>$q_m?b%hqX3&HD1=_gy`DfVRfSMU z)6ms<7q{gJVQSk&V3#LA#IsD7Ct!n0=-YRR)d2Mgb}_^dd)wda3AQZ+M?2h|DBLHi z1VXdJLQvJI#N2SUqtPz$D2FIO8jhmf z@i?RSfCK-MnzfU{2RK;9X7syYQ-oXV1VGS+df10|=!76u1AOoYe6TNSoCfOsgDuDf zl8S^Tz+R`phd(${K==i8aK=agJh?zNIVDYrxJ-4qkIs_cn=mHUc?e;W%WLwL=0hqu zqlv}Ui2HCYn9w9aZ5vX6h-EMsz<9kt9gIeJh_!*L8%Ti)$bdj-RNA|}5#!*d#C0@O-@+9cbZs=$W5Jlz;OhdFFlUJvY?%`%BVz4hh5Bw2&&V( zy$<|A{;IcA0H~155Y#~|%w?jn!P!E76WNNnHSEtu3otpZw_kDF8>~vH#5j-8<|C?NZe}rNN`|spSZ;=L zCS$&ET8g;MGF4`GrplSBg!lBIp6kv(I|beRrY|R`w6v_Xh zAe%>p3Z$xp|KtgBLN1=jVV(%LLp#}iqNg_OiH%M!_1xs0AnA{$nOOzUlKlya<_T!U z%}sRaQ>9$ddE7=s+Y#)kOldvr$j+FtMh z7}8(ZhkfwgY3MtL=m+s#BSOFjzfSC5^Xj+YQkn2JU-@K**uaOlp6jUzZKIyWDysDB zugV@jtGH~85bc&=0(e`H4frMKK9FeL^0Jt8oOg=on~Xr*8cVjT;PCq8I6Gc&TZxMcELBKzZV>KAnA?f%Pe`Rjom zXlBR@OTFmM3_-148l7kBng@F)yJSiUO3)u-vQ_S;abuSr*gPj1T>Zvp+kDxFuri^2 zi0&+;OEyb=J#l~N&JexWn97P%<%gA6iO@~4L1m4rN^JVE z$n*!*(eVs#zGvOct}OAmR9a;A2MLD=UkS+yr=}mGGvEe^96!J|F4+I!kmkZ7;Gp^@ zdLrQ49NSh7Nu27={-trm`dc!*@+ls_$g9hyEwlQrJn>@_&&+3Z#ozqEDVn=D4lJx^ zdts9cs&KBi5(1pW)h*_eH1V_X4)O{4bvm%=Cm!+%GPPUi-o$R=w-siCf=V=gLzhAr zic8-KVS?^(vbvqv?2hK?Mzp`4@M&o}vB<{6Zo))9!?|>^+gLa0WOr)U6;bSBgr(sJ z?b^kzD35$7hu?XIWZ_-;P%lV;wO}J&XY_(APz1c#hjd7VRM3Xl#)l^00#ERVeXs?y zxB^Y^>sPyiTqvnXh_5;5yLO01Y21T$&}&G515Zc?f6#|Gq6`1_l?h+*8(-0Je{f9V zu)|iCGjw^dIou1!FB~9pbWuK z0o?Ww-G)>TR-Ig%`L}lQFLI7N2LQi_CTUsBc%6+1qSR#|;?w}BF z9rnY>=KAv1pF?2c#L*a3ID|tuVvy+{^iJX*W@OT=0*-ON~d zHsz7WiHpm)sjRj$j@Wm!&Hcvjon`X=mWanX+RK8WQHH$jgkr#dd?z%j+mxpdZpXDu z=0n%=DJM!ACkbZu7f)V}+kCKm>VeukOsJ)F#m8hV6hHs(iOiT|r+ngU{swISc|@w;)3SbZ33edI`ChU(%s(I%3u&D*pfZZ0z0_lIAqS{A3; zR{mLv2)dlRIpPRf^os~`?R|*gW~2IG0;ajUPi1Ni{>lRgc|r#A@@LQ>x`9HP9c<|E zA;gFhCsM3v@ghZe8aHz6=ZATOxdYCP{nOzBZO;5t<^N{95Ys_QjH0P za70&4$dithcy=5(@@G(uOgoy)`|>Z~jTi?LF8u$HAL7J{7w2Q7Wmvv^_7+o49QiWl zVOkjFtL)iu#eG}6kS=Zd^l2_!u54+2(luc{ZJ$0utbaj|#2bsge#~QUv9)iXA}md?%<0$9)|S|{PiNT&`u)=}2IYBW zb5P#Qfv4W{r}(zeL3TxykbMRfB1AR)DQM6@gEhEdMH^%=VFnvOvI&w9yj-E#g#;LLjvkf*cJ67mfslq?Ac^ zY2+1M8zmVeeFKdJ(?EH|cx6c9fCU&j^`-xnl$S8NXiz<7fp+bQQ8fbz3%p{*fHVUDPK~IX)(39vi^pk-8%ynZ&;wY5h znElPP2!jJX>Q9deQp%P=%Je|cLrfxSXq6DnRLFq?#UiUhg)}2iL{Bp6&`gCsBu_P$ z;*_dFze*&KtsY$ppg%x9Q3w- z{>Y;(w_~04ZX~=LXui)?!yiznrx#AfqRgmTW)l6&sl4o5tdzhjbD>KVfyt);+(={P8!Ws zNF{B%ec+Bx#ZpNjvkJwMTttmDR7)Ja6;(+13d+Wo3GthEQC(Jf%B~%))z;s6H?@(H zNomx~DRkSd7v3Dr=oi^uZ@sl=mF1I$7V1ov*y^ma9@#5)kU_>BjFc1EFj16V8Mpha z_q%FWBtb|hw%ig499MY3MG;N>PTLiL$ArkYRvyJ2RYg~|9oj#j2^SX=2M>~o#b5m`QKnn0yBR+C?XA< zOFMgl7=(BO?EyqAx)`fekzB&&HraKx{1%H1#57Asg~8ppAiN??PEbN@);3 z0tjCRIV2*L1u&yY%^#ddWRmuyD5Y7lTF+#fK|Uohkhu~xxdfb7c-hOrd~8rpK&C$| z8Ov3s%vKLM)ua3f&q}TeK{EsAIN{PNS{5P}N?T_#&2`5+POTw_M2SOyv`>HF(;zo9 z2p=8d#{*8YTd`CnLPM$`E0UBXHkuMh%B75wdW07_Y8*5LWhIR$1Bq-nkYD)Lkprz% zDxKR%G( zps@5xB8AZI7-RnjEl8$ym6?n^^6?5S%#I9cn4K10fjhURwRfZ8Uuw3Hige&ZABDvS zKH#t##3~Q4q`A%Xc;lOW{GzeoGoN&NqnuVW0TSOyMS8A49eqUNHBG?Ba{jsn_d&<7 zh5ZgbnxG#3wP%qB@@ACSpp~ec)jyjIRmL7-xPyqLw+ze|Ml9Gf6=aBo4hh7BM)3}R z@PZB!(qIZRKnGv=V;!Sl(L+WwA?J3gtbn0mjjC9xur$kCTT~0N%(X+5szq>cDdba= z){*tX5@bI#uS>9)MK39i4POFGr|N+XJ-LxhZ<

>H)?EyAcUXzy_cOVljs>sZc5t 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

Ix>gV)^of(i>Q~2l*0sKMu6Nz* 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ΜBq=c8sQ(?{Y{(z<#PcN3`5`rp1b>v;57(tU1_ub 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

=z%uR@0#u4(s@ouZSZ0Yj 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

T&+oh}4bzP8!T_3w$ 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�_iv2IE4+J|s+SD^yQxB;P0oKJ>=NBK6qM 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�v~QRzYM)G& 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 literal 0 HcmV?d00001 diff --git a/packages/grpc-reflection/package.json b/packages/grpc-reflection/package.json new file mode 100644 index 000000000..b413dddae --- /dev/null +++ b/packages/grpc-reflection/package.json @@ -0,0 +1,47 @@ +{ + "name": "@grpc/reflection", + "version": "1.0.0", + "author": "Justin Timmons", + "description": "Reflection API service for use with gRPC-node", + "repository": { + "type": "git", + "url": "https://github.com/grpc/grpc-node.git", + "directory": "packages/grpc-reflection" + }, + "bugs": "https://github.com/grpc/grpc-node/issues", + "contributors": [ + { + "name": "Justin Timmons", + "email": "justinmtimmons@gmail.com" + } + ], + "files": [ + "LICENSE", + "README.md", + "src", + "build", + "proto" + ], + "license": "Apache-2.0", + "scripts": { + "compile": "tsc -p .", + "postcompile": "copyfiles './proto/**/*.proto' build/", + "prepare": "npm run generate-types && npm run compile", + "test": "mocha --require ts-node/register test/**.ts", + "generate-types": "proto-loader-gen-types --longs String --enums String --bytes Array --defaults --oneofs --includeComments --includeDirs proto/ -O src/generated grpc/reflection/v1/reflection.proto grpc/reflection/v1alpha/reflection.proto" + }, + "dependencies": { + "google-protobuf": "^3.21.2" + }, + "peerDependencies": { + "@grpc/grpc-js": ">=1.5.4", + "@grpc/proto-loader": ">=0.6.9" + }, + "devDependencies": { + "@grpc/grpc-js": "^1.8.21", + "@grpc/proto-loader": "^0.7.10", + "@types/google-protobuf": "^3.15.7", + "copyfiles": "^2.4.1", + "typescript": "^5.2.2" + } +} diff --git a/packages/grpc-reflection/proto/grpc/reflection/v1/reflection.proto b/packages/grpc-reflection/proto/grpc/reflection/v1/reflection.proto new file mode 100644 index 000000000..1c106af7f --- /dev/null +++ b/packages/grpc-reflection/proto/grpc/reflection/v1/reflection.proto @@ -0,0 +1,149 @@ +// Taken from spec https://raw.githubusercontent.com/grpc/grpc/master/src/proto/grpc/reflection/v1/reflection.proto +// Additional versions can be found here: https://github.com/grpc/grpc/tree/master/src/proto/grpc/reflection + +// Copyright 2016 The 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. + +// Service exported by server reflection. A more complete description of how +// server reflection works can be found at +// https://github.com/grpc/grpc/blob/master/doc/server-reflection.md +// +// The canonical version of this proto can be found at +// https://github.com/grpc/grpc-proto/blob/master/grpc/reflection/v1/reflection.proto + +syntax = "proto3"; + +package grpc.reflection.v1; + +option go_package = "google.golang.org/grpc/reflection/grpc_reflection_v1"; +option java_multiple_files = true; +option java_package = "io.grpc.reflection.v1"; +option java_outer_classname = "ServerReflectionProto"; + +service ServerReflection { + // The reflection service is structured as a bidirectional stream, ensuring + // all related requests go to a single server. + rpc ServerReflectionInfo(stream ServerReflectionRequest) + returns (stream ServerReflectionResponse); +} + +// The message sent by the client when calling ServerReflectionInfo method. +message ServerReflectionRequest { + string host = 1; + // To use reflection service, the client should set one of the following + // fields in message_request. The server distinguishes requests by their + // defined field and then handles them using corresponding methods. + oneof message_request { + // Find a proto file by the file name. + string file_by_filename = 3; + + // Find the proto file that declares the given fully-qualified symbol name. + // This field should be a fully-qualified symbol name + // (e.g. .[.] or .). + string file_containing_symbol = 4; + + // Find the proto file which defines an extension extending the given + // message type with the given field number. + ExtensionRequest file_containing_extension = 5; + + // Finds the tag numbers used by all known extensions of the given message + // type, and appends them to ExtensionNumberResponse in an undefined order. + // Its corresponding method is best-effort: it's not guaranteed that the + // reflection service will implement this method, and it's not guaranteed + // that this method will provide all extensions. Returns + // StatusCode::UNIMPLEMENTED if it's not implemented. + // This field should be a fully-qualified type name. The format is + // . + string all_extension_numbers_of_type = 6; + + // List the full names of registered services. The content will not be + // checked. + string list_services = 7; + } +} + +// The type name and extension number sent by the client when requesting +// file_containing_extension. +message ExtensionRequest { + // Fully-qualified type name. The format should be . + string containing_type = 1; + int32 extension_number = 2; +} + +// The message sent by the server to answer ServerReflectionInfo method. +message ServerReflectionResponse { + string valid_host = 1; + ServerReflectionRequest original_request = 2; + // The server sets one of the following fields according to the message_request + // in the request. + oneof message_response { + // This message is used to answer file_by_filename, file_containing_symbol, + // file_containing_extension requests with transitive dependencies. + // As the repeated label is not allowed in oneof fields, we use a + // FileDescriptorResponse message to encapsulate the repeated fields. + // The reflection service is allowed to avoid sending FileDescriptorProtos + // that were previously sent in response to earlier requests in the stream. + FileDescriptorResponse file_descriptor_response = 4; + + // This message is used to answer all_extension_numbers_of_type requests. + ExtensionNumberResponse all_extension_numbers_response = 5; + + // This message is used to answer list_services requests. + ListServiceResponse list_services_response = 6; + + // This message is used when an error occurs. + ErrorResponse error_response = 7; + } +} + +// Serialized FileDescriptorProto messages sent by the server answering +// a file_by_filename, file_containing_symbol, or file_containing_extension +// request. +message FileDescriptorResponse { + // Serialized FileDescriptorProto messages. We avoid taking a dependency on + // descriptor.proto, which uses proto2 only features, by making them opaque + // bytes instead. + repeated bytes file_descriptor_proto = 1; +} + +// A list of extension numbers sent by the server answering +// all_extension_numbers_of_type request. +message ExtensionNumberResponse { + // Full name of the base type, including the package name. The format + // is . + string base_type_name = 1; + repeated int32 extension_number = 2; +} + +// A list of ServiceResponse sent by the server answering list_services request. +message ListServiceResponse { + // The information of each service may be expanded in the future, so we use + // ServiceResponse message to encapsulate it. + repeated ServiceResponse service = 1; +} + +// The information of a single service used by ListServiceResponse to answer +// list_services request. +message ServiceResponse { + // Full name of a registered service, including its package name. The format + // is . + string name = 1; +} + +// The error code and error message sent by the server when an error occurs. +message ErrorResponse { + // This field uses the error codes defined in grpc::StatusCode. + int32 error_code = 1; + string error_message = 2; +} diff --git a/packages/grpc-reflection/proto/grpc/reflection/v1alpha/reflection.proto b/packages/grpc-reflection/proto/grpc/reflection/v1alpha/reflection.proto new file mode 100644 index 000000000..781659af2 --- /dev/null +++ b/packages/grpc-reflection/proto/grpc/reflection/v1alpha/reflection.proto @@ -0,0 +1,139 @@ +// Taken from spec https://raw.githubusercontent.com/grpc/grpc/master/src/proto/grpc/reflection/v1alpha/reflection.proto +// Additional versions can be found here: https://github.com/grpc/grpc/tree/master/src/proto/grpc/reflection + +// Copyright 2016 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. + +// Service exported by server reflection + +syntax = "proto3"; + +package grpc.reflection.v1alpha; + +service ServerReflection { + // The reflection service is structured as a bidirectional stream, ensuring + // all related requests go to a single server. + rpc ServerReflectionInfo(stream ServerReflectionRequest) + returns (stream ServerReflectionResponse); +} + +// The message sent by the client when calling ServerReflectionInfo method. +message ServerReflectionRequest { + string host = 1; + // To use reflection service, the client should set one of the following + // fields in message_request. The server distinguishes requests by their + // defined field and then handles them using corresponding methods. + oneof message_request { + // Find a proto file by the file name. + string file_by_filename = 3; + + // Find the proto file that declares the given fully-qualified symbol name. + // This field should be a fully-qualified symbol name + // (e.g. .[.] or .). + string file_containing_symbol = 4; + + // Find the proto file which defines an extension extending the given + // message type with the given field number. + ExtensionRequest file_containing_extension = 5; + + // Finds the tag numbers used by all known extensions of the given message + // type, and appends them to ExtensionNumberResponse in an undefined order. + // Its corresponding method is best-effort: it's not guaranteed that the + // reflection service will implement this method, and it's not guaranteed + // that this method will provide all extensions. Returns + // StatusCode::UNIMPLEMENTED if it's not implemented. + // This field should be a fully-qualified type name. The format is + // . + string all_extension_numbers_of_type = 6; + + // List the full names of registered services. The content will not be + // checked. + string list_services = 7; + } +} + +// The type name and extension number sent by the client when requesting +// file_containing_extension. +message ExtensionRequest { + // Fully-qualified type name. The format should be . + string containing_type = 1; + int32 extension_number = 2; +} + +// The message sent by the server to answer ServerReflectionInfo method. +message ServerReflectionResponse { + string valid_host = 1; + ServerReflectionRequest original_request = 2; + // The server set one of the following fields accroding to the message_request + // in the request. + oneof message_response { + // This message is used to answer file_by_filename, file_containing_symbol, + // file_containing_extension requests with transitive dependencies. As + // the repeated label is not allowed in oneof fields, we use a + // FileDescriptorResponse message to encapsulate the repeated fields. + // The reflection service is allowed to avoid sending FileDescriptorProtos + // that were previously sent in response to earlier requests in the stream. + FileDescriptorResponse file_descriptor_response = 4; + + // This message is used to answer all_extension_numbers_of_type requst. + ExtensionNumberResponse all_extension_numbers_response = 5; + + // This message is used to answer list_services request. + ListServiceResponse list_services_response = 6; + + // This message is used when an error occurs. + ErrorResponse error_response = 7; + } +} + +// Serialized FileDescriptorProto messages sent by the server answering +// a file_by_filename, file_containing_symbol, or file_containing_extension +// request. +message FileDescriptorResponse { + // Serialized FileDescriptorProto messages. We avoid taking a dependency on + // descriptor.proto, which uses proto2 only features, by making them opaque + // bytes instead. + repeated bytes file_descriptor_proto = 1; +} + +// A list of extension numbers sent by the server answering +// all_extension_numbers_of_type request. +message ExtensionNumberResponse { + // Full name of the base type, including the package name. The format + // is . + string base_type_name = 1; + repeated int32 extension_number = 2; +} + +// A list of ServiceResponse sent by the server answering list_services request. +message ListServiceResponse { + // The information of each service may be expanded in the future, so we use + // ServiceResponse message to encapsulate it. + repeated ServiceResponse service = 1; +} + +// The information of a single service used by ListServiceResponse to answer +// list_services request. +message ServiceResponse { + // Full name of a registered service, including its package name. The format + // is . + string name = 1; +} + +// The error code and error message sent by the server when an error occurs. +message ErrorResponse { + // This field uses the error codes defined in grpc::StatusCode. + int32 error_code = 1; + string error_message = 2; +} diff --git a/packages/grpc-reflection/proto/sample/sample.proto b/packages/grpc-reflection/proto/sample/sample.proto new file mode 100644 index 000000000..b5b532087 --- /dev/null +++ b/packages/grpc-reflection/proto/sample/sample.proto @@ -0,0 +1,39 @@ +syntax = "proto3"; + +package sample; + +import 'vendor.proto'; + +service SampleService { + rpc Hello (HelloRequest) returns (HelloResponse) {} + rpc Hello2 (HelloRequest) returns (CommonMessage) {} +} + +message HelloRequest { + string hello = 1; + HelloNested nested = 2; + ShadowedMessage nestedShadowedMessage = 3; + + message HelloNested { + string hello = 1; + CommonMessage field = 2; + } + + message ShadowedMessage { + int32 item = 1; + } +} + +enum HelloStatus { + HELLO = 1; + WORLD = 2; +} + +message HelloResponse { + string world = 1; + HelloStatus status = 2; +} + +message ShadowedMessage { + string hello = 1; +} diff --git a/packages/grpc-reflection/proto/sample/vendor/common.proto b/packages/grpc-reflection/proto/sample/vendor/common.proto new file mode 100644 index 000000000..c89246b5d --- /dev/null +++ b/packages/grpc-reflection/proto/sample/vendor/common.proto @@ -0,0 +1,15 @@ +syntax = "proto2"; + +// NOTE: intentionally using the same 'vendor' package here to document the +// file/package merging behavior of the reflection service. +// +// this file should be combined with vendor.proto to a single definition because +// it's under the same 'vendor' package +package vendor; + +message CommonMessage { + optional string common = 1; + optional DependentMessage dependency = 2; + + extensions 100 to 199; +} diff --git a/packages/grpc-reflection/proto/sample/vendor/dependency/dependency.proto b/packages/grpc-reflection/proto/sample/vendor/dependency/dependency.proto new file mode 100644 index 000000000..44cadfba1 --- /dev/null +++ b/packages/grpc-reflection/proto/sample/vendor/dependency/dependency.proto @@ -0,0 +1,7 @@ +syntax = "proto2"; + +package vendor.dependency; + +message DependentMessage { + optional string something = 1; +} diff --git a/packages/grpc-reflection/proto/sample/vendor/vendor.proto b/packages/grpc-reflection/proto/sample/vendor/vendor.proto new file mode 100644 index 000000000..f8e0c1e5a --- /dev/null +++ b/packages/grpc-reflection/proto/sample/vendor/vendor.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; + +package vendor; + +import "./common.proto"; +import "./dependency/dependency.proto"; + +extend CommonMessage { + optional bool ext = 101; +} diff --git a/packages/grpc-reflection/src/generated/grpc/reflection/v1/ErrorResponse.ts b/packages/grpc-reflection/src/generated/grpc/reflection/v1/ErrorResponse.ts new file mode 100644 index 000000000..e8168c36d --- /dev/null +++ b/packages/grpc-reflection/src/generated/grpc/reflection/v1/ErrorResponse.ts @@ -0,0 +1,24 @@ +// Original file: proto/grpc/reflection/v1/reflection.proto + + +/** + * The error code and error message sent by the server when an error occurs. + */ +export interface ErrorResponse { + /** + * This field uses the error codes defined in grpc::StatusCode. + */ + 'errorCode'?: (number); + 'errorMessage'?: (string); +} + +/** + * The error code and error message sent by the server when an error occurs. + */ +export interface ErrorResponse__Output { + /** + * This field uses the error codes defined in grpc::StatusCode. + */ + 'errorCode': (number); + 'errorMessage': (string); +} diff --git a/packages/grpc-reflection/src/generated/grpc/reflection/v1/ExtensionNumberResponse.ts b/packages/grpc-reflection/src/generated/grpc/reflection/v1/ExtensionNumberResponse.ts new file mode 100644 index 000000000..fdb88119c --- /dev/null +++ b/packages/grpc-reflection/src/generated/grpc/reflection/v1/ExtensionNumberResponse.ts @@ -0,0 +1,28 @@ +// Original file: proto/grpc/reflection/v1/reflection.proto + + +/** + * A list of extension numbers sent by the server answering + * all_extension_numbers_of_type request. + */ +export interface ExtensionNumberResponse { + /** + * Full name of the base type, including the package name. The format + * is . + */ + 'baseTypeName'?: (string); + 'extensionNumber'?: (number)[]; +} + +/** + * A list of extension numbers sent by the server answering + * all_extension_numbers_of_type request. + */ +export interface ExtensionNumberResponse__Output { + /** + * Full name of the base type, including the package name. The format + * is . + */ + 'baseTypeName': (string); + 'extensionNumber': (number)[]; +} diff --git a/packages/grpc-reflection/src/generated/grpc/reflection/v1/ExtensionRequest.ts b/packages/grpc-reflection/src/generated/grpc/reflection/v1/ExtensionRequest.ts new file mode 100644 index 000000000..34c6fefeb --- /dev/null +++ b/packages/grpc-reflection/src/generated/grpc/reflection/v1/ExtensionRequest.ts @@ -0,0 +1,26 @@ +// Original file: proto/grpc/reflection/v1/reflection.proto + + +/** + * The type name and extension number sent by the client when requesting + * file_containing_extension. + */ +export interface ExtensionRequest { + /** + * Fully-qualified type name. The format should be . + */ + 'containingType'?: (string); + 'extensionNumber'?: (number); +} + +/** + * The type name and extension number sent by the client when requesting + * file_containing_extension. + */ +export interface ExtensionRequest__Output { + /** + * Fully-qualified type name. The format should be . + */ + 'containingType': (string); + 'extensionNumber': (number); +} diff --git a/packages/grpc-reflection/src/generated/grpc/reflection/v1/FileDescriptorResponse.ts b/packages/grpc-reflection/src/generated/grpc/reflection/v1/FileDescriptorResponse.ts new file mode 100644 index 000000000..253e650f9 --- /dev/null +++ b/packages/grpc-reflection/src/generated/grpc/reflection/v1/FileDescriptorResponse.ts @@ -0,0 +1,30 @@ +// Original file: proto/grpc/reflection/v1/reflection.proto + + +/** + * Serialized FileDescriptorProto messages sent by the server answering + * a file_by_filename, file_containing_symbol, or file_containing_extension + * request. + */ +export interface FileDescriptorResponse { + /** + * Serialized FileDescriptorProto messages. We avoid taking a dependency on + * descriptor.proto, which uses proto2 only features, by making them opaque + * bytes instead. + */ + 'fileDescriptorProto'?: (Buffer | Uint8Array | string)[]; +} + +/** + * Serialized FileDescriptorProto messages sent by the server answering + * a file_by_filename, file_containing_symbol, or file_containing_extension + * request. + */ +export interface FileDescriptorResponse__Output { + /** + * Serialized FileDescriptorProto messages. We avoid taking a dependency on + * descriptor.proto, which uses proto2 only features, by making them opaque + * bytes instead. + */ + 'fileDescriptorProto': (Uint8Array)[]; +} diff --git a/packages/grpc-reflection/src/generated/grpc/reflection/v1/ListServiceResponse.ts b/packages/grpc-reflection/src/generated/grpc/reflection/v1/ListServiceResponse.ts new file mode 100644 index 000000000..f1824d4cf --- /dev/null +++ b/packages/grpc-reflection/src/generated/grpc/reflection/v1/ListServiceResponse.ts @@ -0,0 +1,25 @@ +// Original file: proto/grpc/reflection/v1/reflection.proto + +import type { ServiceResponse as _grpc_reflection_v1_ServiceResponse, ServiceResponse__Output as _grpc_reflection_v1_ServiceResponse__Output } from '../../../grpc/reflection/v1/ServiceResponse'; + +/** + * A list of ServiceResponse sent by the server answering list_services request. + */ +export interface ListServiceResponse { + /** + * The information of each service may be expanded in the future, so we use + * ServiceResponse message to encapsulate it. + */ + 'service'?: (_grpc_reflection_v1_ServiceResponse)[]; +} + +/** + * A list of ServiceResponse sent by the server answering list_services request. + */ +export interface ListServiceResponse__Output { + /** + * The information of each service may be expanded in the future, so we use + * ServiceResponse message to encapsulate it. + */ + 'service': (_grpc_reflection_v1_ServiceResponse__Output)[]; +} diff --git a/packages/grpc-reflection/src/generated/grpc/reflection/v1/ServerReflection.ts b/packages/grpc-reflection/src/generated/grpc/reflection/v1/ServerReflection.ts new file mode 100644 index 000000000..65d3b571b --- /dev/null +++ b/packages/grpc-reflection/src/generated/grpc/reflection/v1/ServerReflection.ts @@ -0,0 +1,9 @@ +// Original file: proto/grpc/reflection/v1/reflection.proto + +import type { MethodDefinition } from '@grpc/proto-loader' +import type { ServerReflectionRequest as _grpc_reflection_v1_ServerReflectionRequest, ServerReflectionRequest__Output as _grpc_reflection_v1_ServerReflectionRequest__Output } from '../../../grpc/reflection/v1/ServerReflectionRequest'; +import type { ServerReflectionResponse as _grpc_reflection_v1_ServerReflectionResponse, ServerReflectionResponse__Output as _grpc_reflection_v1_ServerReflectionResponse__Output } from '../../../grpc/reflection/v1/ServerReflectionResponse'; + +export interface ServerReflectionDefinition { + ServerReflectionInfo: MethodDefinition<_grpc_reflection_v1_ServerReflectionRequest, _grpc_reflection_v1_ServerReflectionResponse, _grpc_reflection_v1_ServerReflectionRequest__Output, _grpc_reflection_v1_ServerReflectionResponse__Output> +} diff --git a/packages/grpc-reflection/src/generated/grpc/reflection/v1/ServerReflectionRequest.ts b/packages/grpc-reflection/src/generated/grpc/reflection/v1/ServerReflectionRequest.ts new file mode 100644 index 000000000..301bd3953 --- /dev/null +++ b/packages/grpc-reflection/src/generated/grpc/reflection/v1/ServerReflectionRequest.ts @@ -0,0 +1,91 @@ +// Original file: proto/grpc/reflection/v1/reflection.proto + +import type { ExtensionRequest as _grpc_reflection_v1_ExtensionRequest, ExtensionRequest__Output as _grpc_reflection_v1_ExtensionRequest__Output } from '../../../grpc/reflection/v1/ExtensionRequest'; + +/** + * The message sent by the client when calling ServerReflectionInfo method. + */ +export interface ServerReflectionRequest { + 'host'?: (string); + /** + * Find a proto file by the file name. + */ + 'fileByFilename'?: (string); + /** + * Find the proto file that declares the given fully-qualified symbol name. + * This field should be a fully-qualified symbol name + * (e.g. .[.] or .). + */ + 'fileContainingSymbol'?: (string); + /** + * Find the proto file which defines an extension extending the given + * message type with the given field number. + */ + 'fileContainingExtension'?: (_grpc_reflection_v1_ExtensionRequest | null); + /** + * Finds the tag numbers used by all known extensions of the given message + * type, and appends them to ExtensionNumberResponse in an undefined order. + * Its corresponding method is best-effort: it's not guaranteed that the + * reflection service will implement this method, and it's not guaranteed + * that this method will provide all extensions. Returns + * StatusCode::UNIMPLEMENTED if it's not implemented. + * This field should be a fully-qualified type name. The format is + * . + */ + 'allExtensionNumbersOfType'?: (string); + /** + * List the full names of registered services. The content will not be + * checked. + */ + 'listServices'?: (string); + /** + * To use reflection service, the client should set one of the following + * fields in message_request. The server distinguishes requests by their + * defined field and then handles them using corresponding methods. + */ + 'messageRequest'?: "fileByFilename"|"fileContainingSymbol"|"fileContainingExtension"|"allExtensionNumbersOfType"|"listServices"; +} + +/** + * The message sent by the client when calling ServerReflectionInfo method. + */ +export interface ServerReflectionRequest__Output { + 'host': (string); + /** + * Find a proto file by the file name. + */ + 'fileByFilename'?: (string); + /** + * Find the proto file that declares the given fully-qualified symbol name. + * This field should be a fully-qualified symbol name + * (e.g. .[.] or .). + */ + 'fileContainingSymbol'?: (string); + /** + * Find the proto file which defines an extension extending the given + * message type with the given field number. + */ + 'fileContainingExtension'?: (_grpc_reflection_v1_ExtensionRequest__Output | null); + /** + * Finds the tag numbers used by all known extensions of the given message + * type, and appends them to ExtensionNumberResponse in an undefined order. + * Its corresponding method is best-effort: it's not guaranteed that the + * reflection service will implement this method, and it's not guaranteed + * that this method will provide all extensions. Returns + * StatusCode::UNIMPLEMENTED if it's not implemented. + * This field should be a fully-qualified type name. The format is + * . + */ + 'allExtensionNumbersOfType'?: (string); + /** + * List the full names of registered services. The content will not be + * checked. + */ + 'listServices'?: (string); + /** + * To use reflection service, the client should set one of the following + * fields in message_request. The server distinguishes requests by their + * defined field and then handles them using corresponding methods. + */ + 'messageRequest': "fileByFilename"|"fileContainingSymbol"|"fileContainingExtension"|"allExtensionNumbersOfType"|"listServices"; +} diff --git a/packages/grpc-reflection/src/generated/grpc/reflection/v1/ServerReflectionResponse.ts b/packages/grpc-reflection/src/generated/grpc/reflection/v1/ServerReflectionResponse.ts new file mode 100644 index 000000000..bc2790c15 --- /dev/null +++ b/packages/grpc-reflection/src/generated/grpc/reflection/v1/ServerReflectionResponse.ts @@ -0,0 +1,75 @@ +// Original file: proto/grpc/reflection/v1/reflection.proto + +import type { ServerReflectionRequest as _grpc_reflection_v1_ServerReflectionRequest, ServerReflectionRequest__Output as _grpc_reflection_v1_ServerReflectionRequest__Output } from '../../../grpc/reflection/v1/ServerReflectionRequest'; +import type { FileDescriptorResponse as _grpc_reflection_v1_FileDescriptorResponse, FileDescriptorResponse__Output as _grpc_reflection_v1_FileDescriptorResponse__Output } from '../../../grpc/reflection/v1/FileDescriptorResponse'; +import type { ExtensionNumberResponse as _grpc_reflection_v1_ExtensionNumberResponse, ExtensionNumberResponse__Output as _grpc_reflection_v1_ExtensionNumberResponse__Output } from '../../../grpc/reflection/v1/ExtensionNumberResponse'; +import type { ListServiceResponse as _grpc_reflection_v1_ListServiceResponse, ListServiceResponse__Output as _grpc_reflection_v1_ListServiceResponse__Output } from '../../../grpc/reflection/v1/ListServiceResponse'; +import type { ErrorResponse as _grpc_reflection_v1_ErrorResponse, ErrorResponse__Output as _grpc_reflection_v1_ErrorResponse__Output } from '../../../grpc/reflection/v1/ErrorResponse'; + +/** + * The message sent by the server to answer ServerReflectionInfo method. + */ +export interface ServerReflectionResponse { + 'validHost'?: (string); + 'originalRequest'?: (_grpc_reflection_v1_ServerReflectionRequest | null); + /** + * This message is used to answer file_by_filename, file_containing_symbol, + * file_containing_extension requests with transitive dependencies. + * As the repeated label is not allowed in oneof fields, we use a + * FileDescriptorResponse message to encapsulate the repeated fields. + * The reflection service is allowed to avoid sending FileDescriptorProtos + * that were previously sent in response to earlier requests in the stream. + */ + 'fileDescriptorResponse'?: (_grpc_reflection_v1_FileDescriptorResponse | null); + /** + * This message is used to answer all_extension_numbers_of_type requests. + */ + 'allExtensionNumbersResponse'?: (_grpc_reflection_v1_ExtensionNumberResponse | null); + /** + * This message is used to answer list_services requests. + */ + 'listServicesResponse'?: (_grpc_reflection_v1_ListServiceResponse | null); + /** + * This message is used when an error occurs. + */ + 'errorResponse'?: (_grpc_reflection_v1_ErrorResponse | null); + /** + * The server sets one of the following fields according to the message_request + * in the request. + */ + 'messageResponse'?: "fileDescriptorResponse"|"allExtensionNumbersResponse"|"listServicesResponse"|"errorResponse"; +} + +/** + * The message sent by the server to answer ServerReflectionInfo method. + */ +export interface ServerReflectionResponse__Output { + 'validHost': (string); + 'originalRequest': (_grpc_reflection_v1_ServerReflectionRequest__Output | null); + /** + * This message is used to answer file_by_filename, file_containing_symbol, + * file_containing_extension requests with transitive dependencies. + * As the repeated label is not allowed in oneof fields, we use a + * FileDescriptorResponse message to encapsulate the repeated fields. + * The reflection service is allowed to avoid sending FileDescriptorProtos + * that were previously sent in response to earlier requests in the stream. + */ + 'fileDescriptorResponse'?: (_grpc_reflection_v1_FileDescriptorResponse__Output | null); + /** + * This message is used to answer all_extension_numbers_of_type requests. + */ + 'allExtensionNumbersResponse'?: (_grpc_reflection_v1_ExtensionNumberResponse__Output | null); + /** + * This message is used to answer list_services requests. + */ + 'listServicesResponse'?: (_grpc_reflection_v1_ListServiceResponse__Output | null); + /** + * This message is used when an error occurs. + */ + 'errorResponse'?: (_grpc_reflection_v1_ErrorResponse__Output | null); + /** + * The server sets one of the following fields according to the message_request + * in the request. + */ + 'messageResponse': "fileDescriptorResponse"|"allExtensionNumbersResponse"|"listServicesResponse"|"errorResponse"; +} diff --git a/packages/grpc-reflection/src/generated/grpc/reflection/v1/ServiceResponse.ts b/packages/grpc-reflection/src/generated/grpc/reflection/v1/ServiceResponse.ts new file mode 100644 index 000000000..d529538e2 --- /dev/null +++ b/packages/grpc-reflection/src/generated/grpc/reflection/v1/ServiceResponse.ts @@ -0,0 +1,26 @@ +// Original file: proto/grpc/reflection/v1/reflection.proto + + +/** + * The information of a single service used by ListServiceResponse to answer + * list_services request. + */ +export interface ServiceResponse { + /** + * Full name of a registered service, including its package name. The format + * is . + */ + 'name'?: (string); +} + +/** + * The information of a single service used by ListServiceResponse to answer + * list_services request. + */ +export interface ServiceResponse__Output { + /** + * Full name of a registered service, including its package name. The format + * is . + */ + 'name': (string); +} diff --git a/packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ErrorResponse.ts b/packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ErrorResponse.ts new file mode 100644 index 000000000..dc6c3a2e2 --- /dev/null +++ b/packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ErrorResponse.ts @@ -0,0 +1,24 @@ +// Original file: proto/grpc/reflection/v1alpha/reflection.proto + + +/** + * The error code and error message sent by the server when an error occurs. + */ +export interface ErrorResponse { + /** + * This field uses the error codes defined in grpc::StatusCode. + */ + 'errorCode'?: (number); + 'errorMessage'?: (string); +} + +/** + * The error code and error message sent by the server when an error occurs. + */ +export interface ErrorResponse__Output { + /** + * This field uses the error codes defined in grpc::StatusCode. + */ + 'errorCode': (number); + 'errorMessage': (string); +} diff --git a/packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ExtensionNumberResponse.ts b/packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ExtensionNumberResponse.ts new file mode 100644 index 000000000..b6c322c4e --- /dev/null +++ b/packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ExtensionNumberResponse.ts @@ -0,0 +1,28 @@ +// Original file: proto/grpc/reflection/v1alpha/reflection.proto + + +/** + * A list of extension numbers sent by the server answering + * all_extension_numbers_of_type request. + */ +export interface ExtensionNumberResponse { + /** + * Full name of the base type, including the package name. The format + * is . + */ + 'baseTypeName'?: (string); + 'extensionNumber'?: (number)[]; +} + +/** + * A list of extension numbers sent by the server answering + * all_extension_numbers_of_type request. + */ +export interface ExtensionNumberResponse__Output { + /** + * Full name of the base type, including the package name. The format + * is . + */ + 'baseTypeName': (string); + 'extensionNumber': (number)[]; +} diff --git a/packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ExtensionRequest.ts b/packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ExtensionRequest.ts new file mode 100644 index 000000000..4a378b35a --- /dev/null +++ b/packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ExtensionRequest.ts @@ -0,0 +1,26 @@ +// Original file: proto/grpc/reflection/v1alpha/reflection.proto + + +/** + * The type name and extension number sent by the client when requesting + * file_containing_extension. + */ +export interface ExtensionRequest { + /** + * Fully-qualified type name. The format should be . + */ + 'containingType'?: (string); + 'extensionNumber'?: (number); +} + +/** + * The type name and extension number sent by the client when requesting + * file_containing_extension. + */ +export interface ExtensionRequest__Output { + /** + * Fully-qualified type name. The format should be . + */ + 'containingType': (string); + 'extensionNumber': (number); +} diff --git a/packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/FileDescriptorResponse.ts b/packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/FileDescriptorResponse.ts new file mode 100644 index 000000000..cb1cc38c4 --- /dev/null +++ b/packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/FileDescriptorResponse.ts @@ -0,0 +1,30 @@ +// Original file: proto/grpc/reflection/v1alpha/reflection.proto + + +/** + * Serialized FileDescriptorProto messages sent by the server answering + * a file_by_filename, file_containing_symbol, or file_containing_extension + * request. + */ +export interface FileDescriptorResponse { + /** + * Serialized FileDescriptorProto messages. We avoid taking a dependency on + * descriptor.proto, which uses proto2 only features, by making them opaque + * bytes instead. + */ + 'fileDescriptorProto'?: (Buffer | Uint8Array | string)[]; +} + +/** + * Serialized FileDescriptorProto messages sent by the server answering + * a file_by_filename, file_containing_symbol, or file_containing_extension + * request. + */ +export interface FileDescriptorResponse__Output { + /** + * Serialized FileDescriptorProto messages. We avoid taking a dependency on + * descriptor.proto, which uses proto2 only features, by making them opaque + * bytes instead. + */ + 'fileDescriptorProto': (Uint8Array)[]; +} diff --git a/packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ListServiceResponse.ts b/packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ListServiceResponse.ts new file mode 100644 index 000000000..7793a16eb --- /dev/null +++ b/packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ListServiceResponse.ts @@ -0,0 +1,25 @@ +// Original file: proto/grpc/reflection/v1alpha/reflection.proto + +import type { ServiceResponse as _grpc_reflection_v1alpha_ServiceResponse, ServiceResponse__Output as _grpc_reflection_v1alpha_ServiceResponse__Output } from '../../../grpc/reflection/v1alpha/ServiceResponse'; + +/** + * A list of ServiceResponse sent by the server answering list_services request. + */ +export interface ListServiceResponse { + /** + * The information of each service may be expanded in the future, so we use + * ServiceResponse message to encapsulate it. + */ + 'service'?: (_grpc_reflection_v1alpha_ServiceResponse)[]; +} + +/** + * A list of ServiceResponse sent by the server answering list_services request. + */ +export interface ListServiceResponse__Output { + /** + * The information of each service may be expanded in the future, so we use + * ServiceResponse message to encapsulate it. + */ + 'service': (_grpc_reflection_v1alpha_ServiceResponse__Output)[]; +} diff --git a/packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ServerReflection.ts b/packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ServerReflection.ts new file mode 100644 index 000000000..2ab03e93c --- /dev/null +++ b/packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ServerReflection.ts @@ -0,0 +1,9 @@ +// Original file: proto/grpc/reflection/v1alpha/reflection.proto + +import type { MethodDefinition } from '@grpc/proto-loader' +import type { ServerReflectionRequest as _grpc_reflection_v1alpha_ServerReflectionRequest, ServerReflectionRequest__Output as _grpc_reflection_v1alpha_ServerReflectionRequest__Output } from '../../../grpc/reflection/v1alpha/ServerReflectionRequest'; +import type { ServerReflectionResponse as _grpc_reflection_v1alpha_ServerReflectionResponse, ServerReflectionResponse__Output as _grpc_reflection_v1alpha_ServerReflectionResponse__Output } from '../../../grpc/reflection/v1alpha/ServerReflectionResponse'; + +export interface ServerReflectionDefinition { + ServerReflectionInfo: MethodDefinition<_grpc_reflection_v1alpha_ServerReflectionRequest, _grpc_reflection_v1alpha_ServerReflectionResponse, _grpc_reflection_v1alpha_ServerReflectionRequest__Output, _grpc_reflection_v1alpha_ServerReflectionResponse__Output> +} diff --git a/packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ServerReflectionRequest.ts b/packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ServerReflectionRequest.ts new file mode 100644 index 000000000..097d848d0 --- /dev/null +++ b/packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ServerReflectionRequest.ts @@ -0,0 +1,91 @@ +// Original file: proto/grpc/reflection/v1alpha/reflection.proto + +import type { ExtensionRequest as _grpc_reflection_v1alpha_ExtensionRequest, ExtensionRequest__Output as _grpc_reflection_v1alpha_ExtensionRequest__Output } from '../../../grpc/reflection/v1alpha/ExtensionRequest'; + +/** + * The message sent by the client when calling ServerReflectionInfo method. + */ +export interface ServerReflectionRequest { + 'host'?: (string); + /** + * Find a proto file by the file name. + */ + 'fileByFilename'?: (string); + /** + * Find the proto file that declares the given fully-qualified symbol name. + * This field should be a fully-qualified symbol name + * (e.g. .[.] or .). + */ + 'fileContainingSymbol'?: (string); + /** + * Find the proto file which defines an extension extending the given + * message type with the given field number. + */ + 'fileContainingExtension'?: (_grpc_reflection_v1alpha_ExtensionRequest | null); + /** + * Finds the tag numbers used by all known extensions of the given message + * type, and appends them to ExtensionNumberResponse in an undefined order. + * Its corresponding method is best-effort: it's not guaranteed that the + * reflection service will implement this method, and it's not guaranteed + * that this method will provide all extensions. Returns + * StatusCode::UNIMPLEMENTED if it's not implemented. + * This field should be a fully-qualified type name. The format is + * . + */ + 'allExtensionNumbersOfType'?: (string); + /** + * List the full names of registered services. The content will not be + * checked. + */ + 'listServices'?: (string); + /** + * To use reflection service, the client should set one of the following + * fields in message_request. The server distinguishes requests by their + * defined field and then handles them using corresponding methods. + */ + 'messageRequest'?: "fileByFilename"|"fileContainingSymbol"|"fileContainingExtension"|"allExtensionNumbersOfType"|"listServices"; +} + +/** + * The message sent by the client when calling ServerReflectionInfo method. + */ +export interface ServerReflectionRequest__Output { + 'host': (string); + /** + * Find a proto file by the file name. + */ + 'fileByFilename'?: (string); + /** + * Find the proto file that declares the given fully-qualified symbol name. + * This field should be a fully-qualified symbol name + * (e.g. .[.] or .). + */ + 'fileContainingSymbol'?: (string); + /** + * Find the proto file which defines an extension extending the given + * message type with the given field number. + */ + 'fileContainingExtension'?: (_grpc_reflection_v1alpha_ExtensionRequest__Output | null); + /** + * Finds the tag numbers used by all known extensions of the given message + * type, and appends them to ExtensionNumberResponse in an undefined order. + * Its corresponding method is best-effort: it's not guaranteed that the + * reflection service will implement this method, and it's not guaranteed + * that this method will provide all extensions. Returns + * StatusCode::UNIMPLEMENTED if it's not implemented. + * This field should be a fully-qualified type name. The format is + * . + */ + 'allExtensionNumbersOfType'?: (string); + /** + * List the full names of registered services. The content will not be + * checked. + */ + 'listServices'?: (string); + /** + * To use reflection service, the client should set one of the following + * fields in message_request. The server distinguishes requests by their + * defined field and then handles them using corresponding methods. + */ + 'messageRequest': "fileByFilename"|"fileContainingSymbol"|"fileContainingExtension"|"allExtensionNumbersOfType"|"listServices"; +} diff --git a/packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ServerReflectionResponse.ts b/packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ServerReflectionResponse.ts new file mode 100644 index 000000000..eb81a0c2a --- /dev/null +++ b/packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ServerReflectionResponse.ts @@ -0,0 +1,75 @@ +// Original file: proto/grpc/reflection/v1alpha/reflection.proto + +import type { ServerReflectionRequest as _grpc_reflection_v1alpha_ServerReflectionRequest, ServerReflectionRequest__Output as _grpc_reflection_v1alpha_ServerReflectionRequest__Output } from '../../../grpc/reflection/v1alpha/ServerReflectionRequest'; +import type { FileDescriptorResponse as _grpc_reflection_v1alpha_FileDescriptorResponse, FileDescriptorResponse__Output as _grpc_reflection_v1alpha_FileDescriptorResponse__Output } from '../../../grpc/reflection/v1alpha/FileDescriptorResponse'; +import type { ExtensionNumberResponse as _grpc_reflection_v1alpha_ExtensionNumberResponse, ExtensionNumberResponse__Output as _grpc_reflection_v1alpha_ExtensionNumberResponse__Output } from '../../../grpc/reflection/v1alpha/ExtensionNumberResponse'; +import type { ListServiceResponse as _grpc_reflection_v1alpha_ListServiceResponse, ListServiceResponse__Output as _grpc_reflection_v1alpha_ListServiceResponse__Output } from '../../../grpc/reflection/v1alpha/ListServiceResponse'; +import type { ErrorResponse as _grpc_reflection_v1alpha_ErrorResponse, ErrorResponse__Output as _grpc_reflection_v1alpha_ErrorResponse__Output } from '../../../grpc/reflection/v1alpha/ErrorResponse'; + +/** + * The message sent by the server to answer ServerReflectionInfo method. + */ +export interface ServerReflectionResponse { + 'validHost'?: (string); + 'originalRequest'?: (_grpc_reflection_v1alpha_ServerReflectionRequest | null); + /** + * This message is used to answer file_by_filename, file_containing_symbol, + * file_containing_extension requests with transitive dependencies. As + * the repeated label is not allowed in oneof fields, we use a + * FileDescriptorResponse message to encapsulate the repeated fields. + * The reflection service is allowed to avoid sending FileDescriptorProtos + * that were previously sent in response to earlier requests in the stream. + */ + 'fileDescriptorResponse'?: (_grpc_reflection_v1alpha_FileDescriptorResponse | null); + /** + * This message is used to answer all_extension_numbers_of_type requst. + */ + 'allExtensionNumbersResponse'?: (_grpc_reflection_v1alpha_ExtensionNumberResponse | null); + /** + * This message is used to answer list_services request. + */ + 'listServicesResponse'?: (_grpc_reflection_v1alpha_ListServiceResponse | null); + /** + * This message is used when an error occurs. + */ + 'errorResponse'?: (_grpc_reflection_v1alpha_ErrorResponse | null); + /** + * The server set one of the following fields accroding to the message_request + * in the request. + */ + 'messageResponse'?: "fileDescriptorResponse"|"allExtensionNumbersResponse"|"listServicesResponse"|"errorResponse"; +} + +/** + * The message sent by the server to answer ServerReflectionInfo method. + */ +export interface ServerReflectionResponse__Output { + 'validHost': (string); + 'originalRequest': (_grpc_reflection_v1alpha_ServerReflectionRequest__Output | null); + /** + * This message is used to answer file_by_filename, file_containing_symbol, + * file_containing_extension requests with transitive dependencies. As + * the repeated label is not allowed in oneof fields, we use a + * FileDescriptorResponse message to encapsulate the repeated fields. + * The reflection service is allowed to avoid sending FileDescriptorProtos + * that were previously sent in response to earlier requests in the stream. + */ + 'fileDescriptorResponse'?: (_grpc_reflection_v1alpha_FileDescriptorResponse__Output | null); + /** + * This message is used to answer all_extension_numbers_of_type requst. + */ + 'allExtensionNumbersResponse'?: (_grpc_reflection_v1alpha_ExtensionNumberResponse__Output | null); + /** + * This message is used to answer list_services request. + */ + 'listServicesResponse'?: (_grpc_reflection_v1alpha_ListServiceResponse__Output | null); + /** + * This message is used when an error occurs. + */ + 'errorResponse'?: (_grpc_reflection_v1alpha_ErrorResponse__Output | null); + /** + * The server set one of the following fields accroding to the message_request + * in the request. + */ + 'messageResponse': "fileDescriptorResponse"|"allExtensionNumbersResponse"|"listServicesResponse"|"errorResponse"; +} diff --git a/packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ServiceResponse.ts b/packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ServiceResponse.ts new file mode 100644 index 000000000..ff35cf522 --- /dev/null +++ b/packages/grpc-reflection/src/generated/grpc/reflection/v1alpha/ServiceResponse.ts @@ -0,0 +1,26 @@ +// Original file: proto/grpc/reflection/v1alpha/reflection.proto + + +/** + * The information of a single service used by ListServiceResponse to answer + * list_services request. + */ +export interface ServiceResponse { + /** + * Full name of a registered service, including its package name. The format + * is . + */ + 'name'?: (string); +} + +/** + * The information of a single service used by ListServiceResponse to answer + * list_services request. + */ +export interface ServiceResponse__Output { + /** + * Full name of a registered service, including its package name. The format + * is . + */ + 'name': (string); +} diff --git a/packages/grpc-reflection/src/protobuf-visitor.ts b/packages/grpc-reflection/src/protobuf-visitor.ts new file mode 100644 index 000000000..349b37d4d --- /dev/null +++ b/packages/grpc-reflection/src/protobuf-visitor.ts @@ -0,0 +1,110 @@ +import { + DescriptorProto, + EnumDescriptorProto, + EnumValueDescriptorProto, + FieldDescriptorProto, + FileDescriptorProto, + MethodDescriptorProto, + OneofDescriptorProto, + ServiceDescriptorProto, +} from 'google-protobuf/google/protobuf/descriptor_pb'; + +/** A set of functions for operating on protobuf objects as we visit them in a traversal */ +interface Visitor { + field?: (fqn: string, file: FileDescriptorProto, field: FieldDescriptorProto) => void; + extension?: (fqn: string, file: FileDescriptorProto, extension: FieldDescriptorProto) => void; + oneOf?: (fqn: string, file: FileDescriptorProto, decl: OneofDescriptorProto) => void; + message?: (fqn: string, file: FileDescriptorProto, msg: DescriptorProto) => void; + enum?: (fqn: string, file: FileDescriptorProto, msg: EnumDescriptorProto) => void; + enumValue?: (fqn: string, file: FileDescriptorProto, msg: EnumValueDescriptorProto) => void; + service?: (fqn: string, file: FileDescriptorProto, msg: ServiceDescriptorProto) => void; + method?: (fqn: string, file: FileDescriptorProto, method: MethodDescriptorProto) => void; +} + +/** Visit each node in a protobuf file and perform an operation on it + * + * This is useful because protocol buffers has nested objects so if we need to + * traverse them multiple times then we don't want to duplicate that traversal + * logic + * + * @see Visitor for the interface to interact with the nodes + */ +export const visit = (file: FileDescriptorProto, visitor: Visitor): void => { + const processField = (prefix: string, file: FileDescriptorProto, field: FieldDescriptorProto) => { + const fqn = `${prefix}.${field.getName()}`; + if (visitor.field) { + visitor.field(fqn, file, field); + } + }; + + const processExtension = ( + prefix: string, + file: FileDescriptorProto, + ext: FieldDescriptorProto, + ) => { + const fqn = `${prefix}.${ext.getName()}`; + if (visitor.extension) { + visitor.extension(fqn, file, ext); + } + }; + + const processOneOf = (prefix: string, file: FileDescriptorProto, decl: OneofDescriptorProto) => { + const fqn = `${prefix}.${decl.getName()}`; + if (visitor.oneOf) { + visitor.oneOf(fqn, file, decl); + } + }; + + const processEnum = (prefix: string, file: FileDescriptorProto, decl: EnumDescriptorProto) => { + const fqn = `${prefix}.${decl.getName()}`; + + if (visitor.enum) { + visitor.enum(fqn, file, decl); + } + + decl.getValueList().forEach((value) => { + const valueFqn = `${fqn}.${value.getName()}`; + if (visitor.enumValue) { + visitor.enumValue(valueFqn, file, value); + } + }); + }; + + const processMessage = (prefix: string, file: FileDescriptorProto, msg: DescriptorProto) => { + const fqn = `${prefix}.${msg.getName()}`; + if (visitor.message) { + visitor.message(fqn, file, msg); + } + + msg.getNestedTypeList().forEach((type) => processMessage(fqn, file, type)); + msg.getEnumTypeList().forEach((type) => processEnum(fqn, file, type)); + msg.getFieldList().forEach((field) => processField(fqn, file, field)); + msg.getOneofDeclList().forEach((decl) => processOneOf(fqn, file, decl)); + msg.getExtensionList().forEach((ext) => processExtension(fqn, file, ext)); + }; + + const processService = ( + prefix: string, + file: FileDescriptorProto, + service: ServiceDescriptorProto, + ) => { + const fqn = `${prefix}.${service.getName()}`; + if (visitor.service) { + visitor.service(fqn, file, service); + } + + service.getMethodList().forEach((method) => { + const methodFqn = `${fqn}.${method.getName()}`; + if (visitor.method) { + visitor.method(methodFqn, file, method); + } + }); + }; + + const packageName = file.getPackage(); + file.getEnumTypeList().forEach((type) => processEnum(packageName, file, type)); + file.getMessageTypeList().forEach((type) => processMessage(packageName, file, type)); + file.getServiceList().forEach((service) => processService(packageName, file, service)); + + file.getExtensionList().forEach((ext) => processExtension(packageName, file, ext)); +}; diff --git a/packages/grpc-reflection/src/reflection-v1-implementation.ts b/packages/grpc-reflection/src/reflection-v1-implementation.ts new file mode 100644 index 000000000..311b40979 --- /dev/null +++ b/packages/grpc-reflection/src/reflection-v1-implementation.ts @@ -0,0 +1,252 @@ +import { + FileDescriptorProto, + FileDescriptorSet, +} from 'google-protobuf/google/protobuf/descriptor_pb'; + +import * as grpc from '@grpc/grpc-js'; +import * as protoLoader from '@grpc/proto-loader'; + +import { ExtensionNumberResponse__Output } from './generated/grpc/reflection/v1/ExtensionNumberResponse'; +import { FileDescriptorResponse__Output } from './generated/grpc/reflection/v1/FileDescriptorResponse'; +import { ListServiceResponse__Output } from './generated/grpc/reflection/v1/ListServiceResponse'; +import { visit } from './protobuf-visitor'; +import { scope } from './utils'; + +export class ReflectionError extends Error { + constructor( + readonly statusCode: grpc.status, + readonly message: string, + ) { + super(message); + } +} + +/** Analyzes a gRPC server and exposes methods to reflect on it + * + * NOTE: the files returned by this service may not match the handwritten ones 1:1. + * This is because proto-loader reorients files based on their package definition, + * combining any that have the same package. + * + * For example: if files 'a.proto' and 'b.proto' are both for the same package 'c' then + * we will always return a reference to a combined 'c.proto' instead of the 2 files. + */ +export class ReflectionV1Implementation { + + /** The full list of proto files (including imported deps) that the gRPC server includes */ + private fileDescriptorSet = new FileDescriptorSet(); + + /** An index of proto files by file name (eg. 'sample.proto') */ + private fileNameIndex: Record = {}; + + /** An index of proto files by type extension relationship + * + * extensionIndex[.][] contains a reference to the file containing an + * extension for the type "." and field number "" + */ + private extensionIndex: Record> = {}; + + /** An index of fully qualified symbol names (eg. 'sample.Message') to the files that contain them */ + private symbolMap: Record = {}; + + constructor(root: protoLoader.PackageDefinition) { + Object.values(root).forEach(({ fileDescriptorProtos }) => { + // Add file descriptors to the FileDescriptorSet. + // We use the Array check here because a ServiceDefinition could have a method named the same thing + if (Array.isArray(fileDescriptorProtos)) { + fileDescriptorProtos.forEach((bin) => { + const proto = FileDescriptorProto.deserializeBinary(bin); + const isFileInSet = this.fileDescriptorSet + .getFileList() + .map((f) => f.getName()) + .includes(proto.getName()); + if (!isFileInSet) { + this.fileDescriptorSet.addFile(proto); + } + }); + } + }); + + this.fileNameIndex = Object.fromEntries( + this.fileDescriptorSet.getFileList().map((f) => [f.getName(), f]), + ); + + // Pass 1: Index Values + const index = (fqn: string, file: FileDescriptorProto) => (this.symbolMap[fqn] = file); + this.fileDescriptorSet.getFileList().forEach((file) => + visit(file, { + field: index, + oneOf: index, + message: index, + service: index, + method: index, + enum: index, + enumValue: index, + extension: (fqn, file, ext) => { + index(fqn, file); + + const extendeeName = ext.getExtendee(); + this.extensionIndex[extendeeName] = { + ...(this.extensionIndex[extendeeName] || {}), + [ext.getNumber()]: file, + }; + }, + }), + ); + + // Pass 2: Link References To Values + const addReference = (ref: string, sourceFile: FileDescriptorProto, pkgScope: string) => { + if (!ref) { + return; // nothing to do + } + + let referencedFile: FileDescriptorProto | null = null; + if (ref.startsWith('.')) { + // absolute reference -- just remove the leading '.' and use the ref directly + referencedFile = this.symbolMap[ref.replace(/^\./, '')]; + } else { + // relative reference -- need to seek upwards up the current package scope until we find it + let pkg = pkgScope; + while (pkg && !referencedFile) { + referencedFile = this.symbolMap[`${pkg}.${ref}`]; + pkg = scope(pkg); + } + + // if we didn't find anything then try just a FQN lookup + if (!referencedFile) { + referencedFile = this.symbolMap[ref]; + } + } + + if (!referencedFile) { + console.warn(`Could not find file associated with reference ${ref}`); + return; + } + + if (referencedFile !== sourceFile) { + sourceFile.addDependency(referencedFile.getName()); + } + }; + + this.fileDescriptorSet.getFileList().forEach((file) => + visit(file, { + field: (fqn, file, field) => addReference(field.getTypeName(), file, scope(fqn)), + extension: (fqn, file, ext) => addReference(ext.getTypeName(), file, scope(fqn)), + method: (fqn, file, method) => { + addReference(method.getInputType(), file, scope(fqn)); + addReference(method.getOutputType(), file, scope(fqn)); + }, + }), + ); + } + + /** List the full names of registered gRPC services + * + * note: the spec is unclear as to what the 'listServices' param can be; most + * clients seem to only pass '*' but unsure if this should behave like a + * filter. Until we know how this should behave with different inputs this + * just always returns *all* services. + * + * @returns full-qualified service names (eg. 'sample.SampleService') + */ + listServices(listServices: string): ListServiceResponse__Output { + const services = this.fileDescriptorSet + .getFileList() + .map((file) => + file.getServiceList().map((service) => `${file.getPackage()}.${service.getName()}`), + ) + .flat(); + + return { service: services.map((service) => ({ name: service })) }; + } + + /** Find the proto file(s) that declares the given fully-qualified symbol name + * + * @param symbol fully-qualified name of the symbol to lookup + * (e.g. package.service[.method] or package.type) + * + * @returns descriptors of the file which contains this symbol and its imports + */ + fileContainingSymbol(symbol: string): FileDescriptorResponse__Output { + const file = this.symbolMap[symbol]; + + if (!file) { + throw new ReflectionError(grpc.status.NOT_FOUND, `Symbol not found: ${symbol}`); + } + + const deps = this.getFileDependencies(file); + + return { + fileDescriptorProto: [file, ...deps].map((proto) => proto.serializeBinary()), + }; + } + + /** Find a proto file by the file name + * + * @returns descriptors of the file which contains this symbol and its imports + */ + fileByFilename(filename: string): FileDescriptorResponse__Output { + const file = this.fileNameIndex[filename]; + + if (!file) { + throw new ReflectionError(grpc.status.NOT_FOUND, `Proto file not found: ${filename}`); + } + + const deps = this.getFileDependencies(file); + + return { + fileDescriptorProto: [file, ...deps].map((f) => f.serializeBinary()), + }; + } + + /** Find a proto file containing an extension to a message type + * + * @returns descriptors of the file which contains this symbol and its imports + */ + fileContainingExtension(symbol: string, field: number): FileDescriptorResponse__Output { + const extensionsByFieldNumber = this.extensionIndex[symbol] || {}; + const file = extensionsByFieldNumber[field]; + + if (!file) { + throw new ReflectionError( + grpc.status.NOT_FOUND, + `Extension not found for symbol ${symbol} at field ${field}`, + ); + } + + const deps = this.getFileDependencies(file); + + return { + fileDescriptorProto: [file, ...deps].map((f) => f.serializeBinary()), + }; + } + + allExtensionNumbersOfType(symbol: string): ExtensionNumberResponse__Output { + if (!(symbol in this.extensionIndex)) { + throw new ReflectionError(grpc.status.NOT_FOUND, `Extensions not found for symbol ${symbol}`); + } + + const fieldNumbers = Object.keys(this.extensionIndex[symbol]).map((key) => Number(key)); + + return { + baseTypeName: symbol, + extensionNumber: fieldNumbers, + }; + } + + private getFileDependencies( + file: FileDescriptorProto, + visited: Set = new Set(), + ): FileDescriptorProto[] { + const newVisited = visited.add(file); + + const directDeps = file.getDependencyList().map((dep) => this.fileNameIndex[dep]); + const transitiveDeps = directDeps + .filter((dep) => !newVisited.has(dep)) + .map((dep) => this.getFileDependencies(dep, newVisited)) + .flat(); + + const allDeps = [...directDeps, ...transitiveDeps]; + + return [...new Set(allDeps)]; + } +} diff --git a/packages/grpc-reflection/src/utils.ts b/packages/grpc-reflection/src/utils.ts new file mode 100644 index 000000000..4490abd6e --- /dev/null +++ b/packages/grpc-reflection/src/utils.ts @@ -0,0 +1,11 @@ +/** Gets the package scope for a type name + * + * @example scope('grpc.reflection.v1.Type') == 'grpc.reflection.v1' + */ +export const scope = (path: string, separator: string = '.') => { + if (!path.includes(separator)) { + return ''; + } + + return path.split(separator).slice(0, -1).join(separator); +}; diff --git a/packages/grpc-reflection/test/test-reflection-v1-implementation.ts b/packages/grpc-reflection/test/test-reflection-v1-implementation.ts new file mode 100644 index 000000000..3879da70b --- /dev/null +++ b/packages/grpc-reflection/test/test-reflection-v1-implementation.ts @@ -0,0 +1,182 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import { FileDescriptorProto } from 'google-protobuf/google/protobuf/descriptor_pb'; +import * as protoLoader from '@grpc/proto-loader'; + +import { ReflectionV1Implementation } from '../src/reflection-v1-implementation'; + +describe('GrpcReflectionService', () => { + let reflectionService: ReflectionV1Implementation; + + beforeEach(async () => { + console.log(path.join(__dirname, '../proto/sample/sample.proto')); + console.log([path.join(__dirname, '../proto/sample/vendor')]); + const root = protoLoader.loadSync(path.join(__dirname, '../proto/sample/sample.proto'), { + includeDirs: [path.join(__dirname, '../proto/sample/vendor')] + }); + + reflectionService = new ReflectionV1Implementation(root); + }); + + describe('listServices()', () => { + it('lists all services', () => { + const { service: services } = reflectionService.listServices('*'); + assert.equal(services.length, 1); + assert(services.find((s) => s.name === 'sample.SampleService')); + }); + }); + + describe('fileByFilename()', () => { + it('finds files with transitive dependencies', () => { + const descriptors = reflectionService + .fileByFilename('sample.proto') + .fileDescriptorProto.map(FileDescriptorProto.deserializeBinary); + + const names = descriptors.map((desc) => desc.getName()); + assert.deepEqual( + new Set(names), + new Set(['sample.proto', 'vendor.proto', 'vendor_dependency.proto']) + ); + }); + + it('finds files with fewer transitive dependencies', () => { + const descriptors = reflectionService + .fileByFilename('vendor.proto') + .fileDescriptorProto.map(FileDescriptorProto.deserializeBinary); + + const names = descriptors.map((desc) => desc.getName()); + assert.deepEqual(new Set(names), new Set(['vendor.proto', 'vendor_dependency.proto'])); + }); + + it('finds files with no transitive dependencies', () => { + const descriptors = reflectionService + .fileByFilename('vendor_dependency.proto') + .fileDescriptorProto.map(FileDescriptorProto.deserializeBinary); + + assert.equal(descriptors.length, 1); + assert.equal(descriptors[0].getName(), 'vendor_dependency.proto'); + }); + + it('merges files based on package name', () => { + const descriptors = reflectionService + .fileByFilename('vendor.proto') + .fileDescriptorProto.map(FileDescriptorProto.deserializeBinary); + + const names = descriptors.map((desc) => desc.getName()); + assert(!names.includes('common.proto')); // file merged into vendor.proto + }); + + it('errors with no file found', () => { + assert.throws( + () => reflectionService.fileByFilename('nonexistent.proto'), + 'Proto file not found', + ); + }); + }); + + describe('fileContainingSymbol()', () => { + it('finds symbols and returns transitive file dependencies', () => { + const descriptors = reflectionService + .fileContainingSymbol('sample.HelloRequest') + .fileDescriptorProto.map(FileDescriptorProto.deserializeBinary); + + const names = descriptors.map((desc) => desc.getName()); + assert.deepEqual( + new Set(names), + new Set(['sample.proto', 'vendor.proto', 'vendor_dependency.proto']), + ); + }); + + it('finds imported message types', () => { + const descriptors = reflectionService + .fileContainingSymbol('vendor.CommonMessage') + .fileDescriptorProto.map(FileDescriptorProto.deserializeBinary); + + const names = descriptors.map((desc) => desc.getName()); + assert.deepEqual(new Set(names), new Set(['vendor.proto', 'vendor_dependency.proto'])); + }); + + it('finds transitively imported message types', () => { + const descriptors = reflectionService + .fileContainingSymbol('vendor.dependency.DependentMessage') + .fileDescriptorProto.map(FileDescriptorProto.deserializeBinary); + + assert.equal(descriptors.length, 1); + assert.equal(descriptors[0].getName(), 'vendor_dependency.proto'); + }); + + it('finds nested message types', () => { + const descriptors = reflectionService + .fileContainingSymbol('sample.HelloRequest.HelloNested') + .fileDescriptorProto.map(FileDescriptorProto.deserializeBinary); + + const names = descriptors.map((desc) => desc.getName()); + assert.deepEqual( + new Set(names), + new Set(['sample.proto', 'vendor.proto', 'vendor_dependency.proto']), + ); + }); + + it('merges files based on package name', () => { + const descriptors = reflectionService + .fileContainingSymbol('vendor.CommonMessage') + .fileDescriptorProto.map(FileDescriptorProto.deserializeBinary); + + const names = descriptors.map((desc) => desc.getName()); + assert(!names.includes('common.proto')); // file merged into vendor.proto + }); + + it('errors with no symbol found', () => { + assert.throws( + () => reflectionService.fileContainingSymbol('non.existant.symbol'), + 'Symbol not found:', + ); + }); + + it('resolves references to method types', () => { + const descriptors = reflectionService + .fileContainingSymbol('sample.SampleService.Hello2') + .fileDescriptorProto.map(FileDescriptorProto.deserializeBinary); + + const names = descriptors.map((desc) => desc.getName()); + assert.deepEqual( + new Set(names), + new Set(['sample.proto', 'vendor.proto', 'vendor_dependency.proto']), + ); + }); + }); + + describe('fileContainingExtension()', () => { + it('finds extensions and returns transitive file dependencies', () => { + const descriptors = reflectionService + .fileContainingExtension('.vendor.CommonMessage', 101) + .fileDescriptorProto.map(FileDescriptorProto.deserializeBinary); + + const names = descriptors.map((desc) => desc.getName()); + assert.deepEqual(new Set(names), new Set(['vendor.proto', 'vendor_dependency.proto'])); + }); + + it('errors with no symbol found', () => { + assert.throws( + () => reflectionService.fileContainingExtension('non.existant.symbol', 0), + 'Extension not found', + ); + }); + }); + + describe('allExtensionNumbersOfType()', () => { + it('finds extensions and returns transitive file dependencies', () => { + const response = reflectionService.allExtensionNumbersOfType('.vendor.CommonMessage'); + + assert.equal(response.extensionNumber.length, 1); + assert.equal(response.extensionNumber[0], 101); + }); + + it('errors with no symbol found', () => { + assert.throws( + () => reflectionService.allExtensionNumbersOfType('non.existant.symbol'), + 'Extensions not found', + ); + }); + }); +}); diff --git a/packages/grpc-reflection/test/test-utils.ts b/packages/grpc-reflection/test/test-utils.ts new file mode 100644 index 000000000..bd8bc786b --- /dev/null +++ b/packages/grpc-reflection/test/test-utils.ts @@ -0,0 +1,14 @@ +import * as assert from 'assert'; + +import { scope } from '../src/utils'; + +describe('scope', () => { + it('traverses upwards in the package scope', () => { + assert.strictEqual(scope('grpc.health.v1.HealthCheckResponse.ServiceStatus'), 'grpc.health.v1.HealthCheckResponse'); + assert.strictEqual(scope(scope(scope(scope('grpc.health.v1.HealthCheckResponse.ServiceStatus')))), 'grpc'); + }); + it('returns an empty package when at the top', () => { + assert.strictEqual(scope('Message'), ''); + assert.strictEqual(scope(''), ''); + }); +}); diff --git a/packages/grpc-reflection/tsconfig.json b/packages/grpc-reflection/tsconfig.json new file mode 100644 index 000000000..e8a746d0d --- /dev/null +++ b/packages/grpc-reflection/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "declaration": true, + "forceConsistentCasingInFileNames": true, + "noEmitOnError": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "pretty": true, + "sourceMap": true, + "strictNullChecks": false, + "lib": ["es2017"], + "outDir": "build", + "target": "es2017", + "module": "commonjs", + "resolveJsonModule": true, + "incremental": true, + "types": ["mocha"], + "noUnusedLocals": true + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} From 215078f49a385ea6e9b3d95fbb19038a1c4a0bdc Mon Sep 17 00:00:00 2001 From: Justin Timmons Date: Sat, 4 Nov 2023 17:46:13 -0400 Subject: [PATCH 053/109] feat(grpc-reflection): added reflection service to add capability to a users server --- packages/grpc-reflection/example/server.ts | 22 ++++++ packages/grpc-reflection/package.json | 8 +-- packages/grpc-reflection/src/constants.ts | 14 ++++ packages/grpc-reflection/src/index.ts | 1 + .../src/reflection-v1-implementation.ts | 68 +++++++++++++++++++ .../grpc-reflection/src/reflection-v1alpha.ts | 42 ++++++++++++ packages/grpc-reflection/src/service.ts | 55 +++++++++++++++ 7 files changed, 205 insertions(+), 5 deletions(-) create mode 100644 packages/grpc-reflection/example/server.ts create mode 100644 packages/grpc-reflection/src/constants.ts create mode 100644 packages/grpc-reflection/src/index.ts create mode 100644 packages/grpc-reflection/src/reflection-v1alpha.ts create mode 100644 packages/grpc-reflection/src/service.ts diff --git a/packages/grpc-reflection/example/server.ts b/packages/grpc-reflection/example/server.ts new file mode 100644 index 000000000..fa691f769 --- /dev/null +++ b/packages/grpc-reflection/example/server.ts @@ -0,0 +1,22 @@ +import * as path from 'path'; +import * as grpc from '@grpc/grpc-js'; +import * as protoLoader from '@grpc/proto-loader'; + +import { ReflectionService } from '../src'; + +const PROTO_PATH = path.join(__dirname, '../proto/sample/sample.proto'); +const INCLUDE_PATH = path.join(__dirname, '../proto/sample/vendor'); + +const server = new grpc.Server(); +const packageDefinition = protoLoader.loadSync(PROTO_PATH, { includeDirs: [INCLUDE_PATH] }); +const reflection = new ReflectionService(packageDefinition); +reflection.addToServer(server); + +server.bindAsync('localhost:5000', grpc.ServerCredentials.createInsecure(), () => { + server.start(); +}); + +// const protoDescriptor = grpc.loadPackageDefinition(packageDefinition); + + + diff --git a/packages/grpc-reflection/package.json b/packages/grpc-reflection/package.json index b413dddae..8f8ea8ef2 100644 --- a/packages/grpc-reflection/package.json +++ b/packages/grpc-reflection/package.json @@ -15,12 +15,10 @@ "email": "justinmtimmons@gmail.com" } ], + "main": "build/src/index.js", + "types": "build/src/index.d.ts", "files": [ - "LICENSE", - "README.md", - "src", - "build", - "proto" + "build" ], "license": "Apache-2.0", "scripts": { diff --git a/packages/grpc-reflection/src/constants.ts b/packages/grpc-reflection/src/constants.ts new file mode 100644 index 000000000..93ad27825 --- /dev/null +++ b/packages/grpc-reflection/src/constants.ts @@ -0,0 +1,14 @@ +import * as protoLoader from '@grpc/proto-loader'; + +/** Options to use when loading protobuf files in this repo +* +* @remarks *must* match the proto-loader-gen-types usage in the package.json +* otherwise the generated types may not match the data coming into this service +*/ +export const PROTO_LOADER_OPTS: protoLoader.Options = { + longs: String, + enums: String, + bytes: Array, + defaults: true, + oneofs: true +}; diff --git a/packages/grpc-reflection/src/index.ts b/packages/grpc-reflection/src/index.ts new file mode 100644 index 000000000..deba7fe33 --- /dev/null +++ b/packages/grpc-reflection/src/index.ts @@ -0,0 +1 @@ +export { ReflectionService } from './service'; diff --git a/packages/grpc-reflection/src/reflection-v1-implementation.ts b/packages/grpc-reflection/src/reflection-v1-implementation.ts index 311b40979..4f298f862 100644 --- a/packages/grpc-reflection/src/reflection-v1-implementation.ts +++ b/packages/grpc-reflection/src/reflection-v1-implementation.ts @@ -1,3 +1,4 @@ +import * as path from 'path'; import { FileDescriptorProto, FileDescriptorSet, @@ -9,8 +10,11 @@ import * as protoLoader from '@grpc/proto-loader'; import { ExtensionNumberResponse__Output } from './generated/grpc/reflection/v1/ExtensionNumberResponse'; import { FileDescriptorResponse__Output } from './generated/grpc/reflection/v1/FileDescriptorResponse'; import { ListServiceResponse__Output } from './generated/grpc/reflection/v1/ListServiceResponse'; +import { ServerReflectionRequest } from './generated/grpc/reflection/v1/ServerReflectionRequest'; +import { ServerReflectionResponse } from './generated/grpc/reflection/v1/ServerReflectionResponse'; import { visit } from './protobuf-visitor'; import { scope } from './utils'; +import { PROTO_LOADER_OPTS } from './constants'; export class ReflectionError extends Error { constructor( @@ -139,6 +143,70 @@ export class ReflectionV1Implementation { ); } + addToServer(server: Pick) { + const protoPath = path.join(__dirname, '../proto/grpc/reflection/v1/reflection.proto'); + const pkgDefinition = protoLoader.loadSync(protoPath, PROTO_LOADER_OPTS); + const pkg = grpc.loadPackageDefinition(pkgDefinition) as any; + + server.addService(pkg.grpc.reflection.v1.ServerReflection.service, { + ServerReflectionInfo: ( + stream: grpc.ServerDuplexStream + ) => { + stream.on('end', () => stream.end()); + + stream.on('data', (message: ServerReflectionRequest) => { + stream.write(this.handleServerReflectionRequest(message)); + }); + } + }); + } + + /** Assemble a response for a single server reflection request in the stream */ + handleServerReflectionRequest(message: ServerReflectionRequest): ServerReflectionResponse { + const response: ServerReflectionResponse = { + validHost: message.host, + originalRequest: message, + fileDescriptorResponse: undefined, + allExtensionNumbersResponse: undefined, + listServicesResponse: undefined, + errorResponse: undefined, + }; + + try { + if (message.listServices !== undefined) { + response.listServicesResponse = this.listServices(message.listServices); + } else if (message.fileContainingSymbol !== undefined) { + response.fileDescriptorResponse = this.fileContainingSymbol(message.fileContainingSymbol); + } else if (message.fileByFilename !== undefined) { + response.fileDescriptorResponse = this.fileByFilename(message.fileByFilename); + } else if (message.fileContainingExtension !== undefined) { + const { containingType, extensionNumber } = message.fileContainingExtension; + response.fileDescriptorResponse = this.fileContainingExtension(containingType, extensionNumber); + } else if (message.allExtensionNumbersOfType) { + response.allExtensionNumbersResponse = this.allExtensionNumbersOfType(message.allExtensionNumbersOfType); + } else { + throw new ReflectionError( + grpc.status.UNIMPLEMENTED, + `Unimplemented method for request: ${message}`, + ); + } + } catch (e) { + if (e instanceof ReflectionError) { + response.errorResponse = { + errorCode: e.statusCode, + errorMessage: e.message, + }; + } else { + response.errorResponse = { + errorCode: grpc.status.UNKNOWN, + errorMessage: 'Failed to process gRPC reflection request: unknown error', + }; + } + } + + return response; + } + /** List the full names of registered gRPC services * * note: the spec is unclear as to what the 'listServices' param can be; most diff --git a/packages/grpc-reflection/src/reflection-v1alpha.ts b/packages/grpc-reflection/src/reflection-v1alpha.ts new file mode 100644 index 000000000..b0d86e594 --- /dev/null +++ b/packages/grpc-reflection/src/reflection-v1alpha.ts @@ -0,0 +1,42 @@ +import * as path from 'path'; + +import * as grpc from '@grpc/grpc-js'; +import * as protoLoader from '@grpc/proto-loader'; + +import { ServerReflectionRequest } from './generated/grpc/reflection/v1/ServerReflectionRequest'; +import { ServerReflectionResponse } from './generated/grpc/reflection/v1/ServerReflectionResponse'; +import { PROTO_LOADER_OPTS } from './constants'; +import { ReflectionV1Implementation } from './reflection-v1-implementation'; + + +/** Analyzes a gRPC server and exposes methods to reflect on it + * + * NOTE: the files returned by this service may not match the handwritten ones 1:1. + * This is because proto-loader reorients files based on their package definition, + * combining any that have the same package. + * + * For example: if files 'a.proto' and 'b.proto' are both for the same package 'c' then + * we will always return a reference to a combined 'c.proto' instead of the 2 files. + * + * @remarks as the v1 and v1alpha specs are identical, this implementation extends v1 + * and just exposes it at the v1alpha package instead + */ +export class ReflectionV1AlphaImplementation extends ReflectionV1Implementation { + addToServer(server: Pick) { + const protoPath = path.join(__dirname, '../proto/grpc/reflection/v1alpha/reflection.proto'); + const pkgDefinition = protoLoader.loadSync(protoPath, PROTO_LOADER_OPTS); + const pkg = grpc.loadPackageDefinition(pkgDefinition) as any; + + server.addService(pkg.grpc.reflection.v1alpha.ServerReflection.service, { + ServerReflectionInfo: ( + stream: grpc.ServerDuplexStream + ) => { + stream.on('end', () => stream.end()); + + stream.on('data', (message: ServerReflectionRequest) => { + stream.write(this.handleServerReflectionRequest(message)); + }); + } + }); + } +} diff --git a/packages/grpc-reflection/src/service.ts b/packages/grpc-reflection/src/service.ts new file mode 100644 index 000000000..7f08610b8 --- /dev/null +++ b/packages/grpc-reflection/src/service.ts @@ -0,0 +1,55 @@ +import * as grpc from '@grpc/grpc-js'; +import * as protoLoader from '@grpc/proto-loader'; + +import { ReflectionV1Implementation } from './reflection-v1-implementation'; +import { ReflectionV1AlphaImplementation } from './reflection-v1alpha'; + +interface ReflectionServerOptions { + /** whitelist of fully-qualified service names to expose. (Default: expose all) */ + services?: string[]; +} + +/** Analyzes a gRPC package and exposes endpoints providing information about + * it according to the gRPC Server Reflection API Specification + * + * @see https://github.com/grpc/grpc/blob/master/doc/server-reflection.md + * + * @remarks + * + * in order to keep backwards compatibility as the reflection schema evolves + * this service contains implementations for each of the published versions + * + * @privateRemarks + * + * this class acts mostly as a facade to several underlying implementations. This + * allows us to add or remove support for different versions of the reflection + * schema without affecting the consumer + * + */ +export class ReflectionService { + private readonly v1: ReflectionV1Implementation; + private readonly v1Alpha: ReflectionV1AlphaImplementation; + + constructor(pkg: protoLoader.PackageDefinition, options?: ReflectionServerOptions) { + + if (options.services) { + const whitelist = new Set(options.services); + + for (const key in Object.keys(pkg)) { + const value = pkg[key]; + const isService = value.format !== 'Protocol Buffer 3 DescriptorProto' && value.format !== 'Protocol Buffer 3 EnumDescriptorProto'; + if (isService && !whitelist.has(key)) { + delete pkg[key]; + } + } + } + + this.v1 = new ReflectionV1Implementation(pkg); + this.v1Alpha = new ReflectionV1AlphaImplementation(pkg); + } + + addToServer(server: Pick) { + this.v1.addToServer(server); + this.v1Alpha.addToServer(server); + } +} From 3b4f92ee6201c99ef7bb1ab56c6520f35b15b1c0 Mon Sep 17 00:00:00 2001 From: Justin Timmons Date: Wed, 8 Nov 2023 20:42:20 -0500 Subject: [PATCH 054/109] refactor(grpc-reflection): file cleanup and enabled ts strict mode --- packages/grpc-reflection/example/server.ts | 7 +- .../grpc-reflection/proto/sample/sample.proto | 4 ++ .../{ => implementations/common}/constants.ts | 0 .../src/implementations/common/interfaces.ts | 5 ++ .../common}/protobuf-visitor.ts | 2 +- .../src/{ => implementations/common}/utils.ts | 0 .../reflection-v1.ts} | 64 +++++++++++-------- .../reflection-v1alpha.ts | 14 ++-- packages/grpc-reflection/src/service.ts | 27 ++------ .../test/test-reflection-v1-implementation.ts | 16 ++++- packages/grpc-reflection/test/test-utils.ts | 2 +- packages/grpc-reflection/tsconfig.json | 2 +- 12 files changed, 77 insertions(+), 66 deletions(-) rename packages/grpc-reflection/src/{ => implementations/common}/constants.ts (100%) create mode 100644 packages/grpc-reflection/src/implementations/common/interfaces.ts rename packages/grpc-reflection/src/{ => implementations/common}/protobuf-visitor.ts (98%) rename packages/grpc-reflection/src/{ => implementations/common}/utils.ts (100%) rename packages/grpc-reflection/src/{reflection-v1-implementation.ts => implementations/reflection-v1.ts} (81%) rename packages/grpc-reflection/src/{ => implementations}/reflection-v1alpha.ts (69%) diff --git a/packages/grpc-reflection/example/server.ts b/packages/grpc-reflection/example/server.ts index fa691f769..00a3e1150 100644 --- a/packages/grpc-reflection/example/server.ts +++ b/packages/grpc-reflection/example/server.ts @@ -9,14 +9,9 @@ const INCLUDE_PATH = path.join(__dirname, '../proto/sample/vendor'); const server = new grpc.Server(); const packageDefinition = protoLoader.loadSync(PROTO_PATH, { includeDirs: [INCLUDE_PATH] }); -const reflection = new ReflectionService(packageDefinition); +const reflection = new ReflectionService(packageDefinition, { services: ['sample.SampleService'] }); reflection.addToServer(server); server.bindAsync('localhost:5000', grpc.ServerCredentials.createInsecure(), () => { server.start(); }); - -// const protoDescriptor = grpc.loadPackageDefinition(packageDefinition); - - - diff --git a/packages/grpc-reflection/proto/sample/sample.proto b/packages/grpc-reflection/proto/sample/sample.proto index b5b532087..acf969c1a 100644 --- a/packages/grpc-reflection/proto/sample/sample.proto +++ b/packages/grpc-reflection/proto/sample/sample.proto @@ -9,6 +9,10 @@ service SampleService { rpc Hello2 (HelloRequest) returns (CommonMessage) {} } +service IgnoreService { + rpc Hello (HelloRequest) returns (HelloResponse) {} +} + message HelloRequest { string hello = 1; HelloNested nested = 2; diff --git a/packages/grpc-reflection/src/constants.ts b/packages/grpc-reflection/src/implementations/common/constants.ts similarity index 100% rename from packages/grpc-reflection/src/constants.ts rename to packages/grpc-reflection/src/implementations/common/constants.ts diff --git a/packages/grpc-reflection/src/implementations/common/interfaces.ts b/packages/grpc-reflection/src/implementations/common/interfaces.ts new file mode 100644 index 000000000..c6b07bccb --- /dev/null +++ b/packages/grpc-reflection/src/implementations/common/interfaces.ts @@ -0,0 +1,5 @@ +/** Options to create a reflection server */ +export interface ReflectionServerOptions { + /** whitelist of fully-qualified service names to expose. (Default: expose all) */ + services?: string[]; +} diff --git a/packages/grpc-reflection/src/protobuf-visitor.ts b/packages/grpc-reflection/src/implementations/common/protobuf-visitor.ts similarity index 98% rename from packages/grpc-reflection/src/protobuf-visitor.ts rename to packages/grpc-reflection/src/implementations/common/protobuf-visitor.ts index 349b37d4d..556a8bf3e 100644 --- a/packages/grpc-reflection/src/protobuf-visitor.ts +++ b/packages/grpc-reflection/src/implementations/common/protobuf-visitor.ts @@ -101,7 +101,7 @@ export const visit = (file: FileDescriptorProto, visitor: Visitor): void => { }); }; - const packageName = file.getPackage(); + const packageName = file.getPackage() || ''; file.getEnumTypeList().forEach((type) => processEnum(packageName, file, type)); file.getMessageTypeList().forEach((type) => processMessage(packageName, file, type)); file.getServiceList().forEach((service) => processService(packageName, file, service)); diff --git a/packages/grpc-reflection/src/utils.ts b/packages/grpc-reflection/src/implementations/common/utils.ts similarity index 100% rename from packages/grpc-reflection/src/utils.ts rename to packages/grpc-reflection/src/implementations/common/utils.ts diff --git a/packages/grpc-reflection/src/reflection-v1-implementation.ts b/packages/grpc-reflection/src/implementations/reflection-v1.ts similarity index 81% rename from packages/grpc-reflection/src/reflection-v1-implementation.ts rename to packages/grpc-reflection/src/implementations/reflection-v1.ts index 4f298f862..2f43d99f4 100644 --- a/packages/grpc-reflection/src/reflection-v1-implementation.ts +++ b/packages/grpc-reflection/src/implementations/reflection-v1.ts @@ -7,14 +7,15 @@ import { import * as grpc from '@grpc/grpc-js'; import * as protoLoader from '@grpc/proto-loader'; -import { ExtensionNumberResponse__Output } from './generated/grpc/reflection/v1/ExtensionNumberResponse'; -import { FileDescriptorResponse__Output } from './generated/grpc/reflection/v1/FileDescriptorResponse'; -import { ListServiceResponse__Output } from './generated/grpc/reflection/v1/ListServiceResponse'; -import { ServerReflectionRequest } from './generated/grpc/reflection/v1/ServerReflectionRequest'; -import { ServerReflectionResponse } from './generated/grpc/reflection/v1/ServerReflectionResponse'; -import { visit } from './protobuf-visitor'; -import { scope } from './utils'; -import { PROTO_LOADER_OPTS } from './constants'; +import { ExtensionNumberResponse__Output } from '../generated/grpc/reflection/v1/ExtensionNumberResponse'; +import { FileDescriptorResponse__Output } from '../generated/grpc/reflection/v1/FileDescriptorResponse'; +import { ListServiceResponse__Output } from '../generated/grpc/reflection/v1/ListServiceResponse'; +import { ServerReflectionRequest } from '../generated/grpc/reflection/v1/ServerReflectionRequest'; +import { ServerReflectionResponse } from '../generated/grpc/reflection/v1/ServerReflectionResponse'; +import { visit } from './common/protobuf-visitor'; +import { scope } from './common/utils'; +import { PROTO_LOADER_OPTS } from './common/constants'; +import { ReflectionServerOptions } from './common/interfaces'; export class ReflectionError extends Error { constructor( @@ -37,22 +38,27 @@ export class ReflectionError extends Error { export class ReflectionV1Implementation { /** The full list of proto files (including imported deps) that the gRPC server includes */ - private fileDescriptorSet = new FileDescriptorSet(); + private readonly fileDescriptorSet = new FileDescriptorSet(); /** An index of proto files by file name (eg. 'sample.proto') */ - private fileNameIndex: Record = {}; + private readonly fileNameIndex: Record = {}; /** An index of proto files by type extension relationship * * extensionIndex[.][] contains a reference to the file containing an * extension for the type "." and field number "" */ - private extensionIndex: Record> = {}; + private readonly extensionIndex: Record> = {}; /** An index of fully qualified symbol names (eg. 'sample.Message') to the files that contain them */ - private symbolMap: Record = {}; + private readonly symbolMap: Record = {}; + + /** Options that the user provided for this service */ + private readonly options?: ReflectionServerOptions; + + constructor(root: protoLoader.PackageDefinition, options?: ReflectionServerOptions) { + this.options = options; - constructor(root: protoLoader.PackageDefinition) { Object.values(root).forEach(({ fileDescriptorProtos }) => { // Add file descriptors to the FileDescriptorSet. // We use the Array check here because a ServiceDefinition could have a method named the same thing @@ -88,10 +94,10 @@ export class ReflectionV1Implementation { extension: (fqn, file, ext) => { index(fqn, file); - const extendeeName = ext.getExtendee(); + const extendeeName = ext.getExtendee() || ''; this.extensionIndex[extendeeName] = { ...(this.extensionIndex[extendeeName] || {}), - [ext.getNumber()]: file, + [ext.getNumber() || -1]: file, }; }, }), @@ -126,25 +132,26 @@ export class ReflectionV1Implementation { return; } - if (referencedFile !== sourceFile) { - sourceFile.addDependency(referencedFile.getName()); + const fname = referencedFile.getName(); + if (referencedFile !== sourceFile && fname) { + sourceFile.addDependency(fname); } }; this.fileDescriptorSet.getFileList().forEach((file) => visit(file, { - field: (fqn, file, field) => addReference(field.getTypeName(), file, scope(fqn)), - extension: (fqn, file, ext) => addReference(ext.getTypeName(), file, scope(fqn)), + field: (fqn, file, field) => addReference(field.getTypeName() || '', file, scope(fqn)), + extension: (fqn, file, ext) => addReference(ext.getTypeName() || '', file, scope(fqn)), method: (fqn, file, method) => { - addReference(method.getInputType(), file, scope(fqn)); - addReference(method.getOutputType(), file, scope(fqn)); + addReference(method.getInputType() || '', file, scope(fqn)); + addReference(method.getOutputType() || '', file, scope(fqn)); }, }), ); } addToServer(server: Pick) { - const protoPath = path.join(__dirname, '../proto/grpc/reflection/v1/reflection.proto'); + const protoPath = path.join(__dirname, '../../proto/grpc/reflection/v1/reflection.proto'); const pkgDefinition = protoLoader.loadSync(protoPath, PROTO_LOADER_OPTS); const pkg = grpc.loadPackageDefinition(pkgDefinition) as any; @@ -180,8 +187,10 @@ export class ReflectionV1Implementation { } else if (message.fileByFilename !== undefined) { response.fileDescriptorResponse = this.fileByFilename(message.fileByFilename); } else if (message.fileContainingExtension !== undefined) { - const { containingType, extensionNumber } = message.fileContainingExtension; - response.fileDescriptorResponse = this.fileContainingExtension(containingType, extensionNumber); + response.fileDescriptorResponse = this.fileContainingExtension( + message.fileContainingExtension?.containingType || '', + message.fileContainingExtension?.extensionNumber || -1 + ); } else if (message.allExtensionNumbersOfType) { response.allExtensionNumbersResponse = this.allExtensionNumbersOfType(message.allExtensionNumbersOfType); } else { @@ -224,7 +233,12 @@ export class ReflectionV1Implementation { ) .flat(); - return { service: services.map((service) => ({ name: service })) }; + const whitelist = new Set(this.options?.services ?? undefined); + const exposedServices = this.options?.services ? + services.filter(service => whitelist.has(service)) + : services; + + return { service: exposedServices.map((service) => ({ name: service })) }; } /** Find the proto file(s) that declares the given fully-qualified symbol name diff --git a/packages/grpc-reflection/src/reflection-v1alpha.ts b/packages/grpc-reflection/src/implementations/reflection-v1alpha.ts similarity index 69% rename from packages/grpc-reflection/src/reflection-v1alpha.ts rename to packages/grpc-reflection/src/implementations/reflection-v1alpha.ts index b0d86e594..f3fdcafe0 100644 --- a/packages/grpc-reflection/src/reflection-v1alpha.ts +++ b/packages/grpc-reflection/src/implementations/reflection-v1alpha.ts @@ -3,10 +3,10 @@ import * as path from 'path'; import * as grpc from '@grpc/grpc-js'; import * as protoLoader from '@grpc/proto-loader'; -import { ServerReflectionRequest } from './generated/grpc/reflection/v1/ServerReflectionRequest'; -import { ServerReflectionResponse } from './generated/grpc/reflection/v1/ServerReflectionResponse'; -import { PROTO_LOADER_OPTS } from './constants'; -import { ReflectionV1Implementation } from './reflection-v1-implementation'; +import { ServerReflectionRequest } from '../generated/grpc/reflection/v1/ServerReflectionRequest'; +import { ServerReflectionResponse } from '../generated/grpc/reflection/v1/ServerReflectionResponse'; +import { PROTO_LOADER_OPTS } from './common/constants'; +import { ReflectionV1Implementation } from './reflection-v1'; /** Analyzes a gRPC server and exposes methods to reflect on it @@ -18,12 +18,12 @@ import { ReflectionV1Implementation } from './reflection-v1-implementation'; * For example: if files 'a.proto' and 'b.proto' are both for the same package 'c' then * we will always return a reference to a combined 'c.proto' instead of the 2 files. * - * @remarks as the v1 and v1alpha specs are identical, this implementation extends v1 - * and just exposes it at the v1alpha package instead + * @privateRemarks as the v1 and v1alpha specs are identical, this implementation extends + * reflection-v1 and exposes it at the v1alpha package instead */ export class ReflectionV1AlphaImplementation extends ReflectionV1Implementation { addToServer(server: Pick) { - const protoPath = path.join(__dirname, '../proto/grpc/reflection/v1alpha/reflection.proto'); + const protoPath = path.join(__dirname, '../../proto/grpc/reflection/v1alpha/reflection.proto'); const pkgDefinition = protoLoader.loadSync(protoPath, PROTO_LOADER_OPTS); const pkg = grpc.loadPackageDefinition(pkgDefinition) as any; diff --git a/packages/grpc-reflection/src/service.ts b/packages/grpc-reflection/src/service.ts index 7f08610b8..616d210de 100644 --- a/packages/grpc-reflection/src/service.ts +++ b/packages/grpc-reflection/src/service.ts @@ -1,13 +1,9 @@ import * as grpc from '@grpc/grpc-js'; import * as protoLoader from '@grpc/proto-loader'; -import { ReflectionV1Implementation } from './reflection-v1-implementation'; -import { ReflectionV1AlphaImplementation } from './reflection-v1alpha'; - -interface ReflectionServerOptions { - /** whitelist of fully-qualified service names to expose. (Default: expose all) */ - services?: string[]; -} +import { ReflectionV1Implementation } from './implementations/reflection-v1'; +import { ReflectionV1AlphaImplementation } from './implementations/reflection-v1alpha'; +import { ReflectionServerOptions } from './implementations/common/interfaces'; /** Analyzes a gRPC package and exposes endpoints providing information about * it according to the gRPC Server Reflection API Specification @@ -31,21 +27,8 @@ export class ReflectionService { private readonly v1Alpha: ReflectionV1AlphaImplementation; constructor(pkg: protoLoader.PackageDefinition, options?: ReflectionServerOptions) { - - if (options.services) { - const whitelist = new Set(options.services); - - for (const key in Object.keys(pkg)) { - const value = pkg[key]; - const isService = value.format !== 'Protocol Buffer 3 DescriptorProto' && value.format !== 'Protocol Buffer 3 EnumDescriptorProto'; - if (isService && !whitelist.has(key)) { - delete pkg[key]; - } - } - } - - this.v1 = new ReflectionV1Implementation(pkg); - this.v1Alpha = new ReflectionV1AlphaImplementation(pkg); + this.v1 = new ReflectionV1Implementation(pkg, options); + this.v1Alpha = new ReflectionV1AlphaImplementation(pkg, options); } addToServer(server: Pick) { diff --git a/packages/grpc-reflection/test/test-reflection-v1-implementation.ts b/packages/grpc-reflection/test/test-reflection-v1-implementation.ts index 3879da70b..81613ac69 100644 --- a/packages/grpc-reflection/test/test-reflection-v1-implementation.ts +++ b/packages/grpc-reflection/test/test-reflection-v1-implementation.ts @@ -3,14 +3,12 @@ import * as path from 'path'; import { FileDescriptorProto } from 'google-protobuf/google/protobuf/descriptor_pb'; import * as protoLoader from '@grpc/proto-loader'; -import { ReflectionV1Implementation } from '../src/reflection-v1-implementation'; +import { ReflectionV1Implementation } from '../src/implementations/reflection-v1'; describe('GrpcReflectionService', () => { let reflectionService: ReflectionV1Implementation; beforeEach(async () => { - console.log(path.join(__dirname, '../proto/sample/sample.proto')); - console.log([path.join(__dirname, '../proto/sample/vendor')]); const root = protoLoader.loadSync(path.join(__dirname, '../proto/sample/sample.proto'), { includeDirs: [path.join(__dirname, '../proto/sample/vendor')] }); @@ -20,6 +18,18 @@ describe('GrpcReflectionService', () => { describe('listServices()', () => { it('lists all services', () => { + const { service: services } = reflectionService.listServices('*'); + assert.equal(services.length, 2); + assert(services.find((s) => s.name === 'sample.SampleService')); + }); + + it('whitelists services properly', () => { + const root = protoLoader.loadSync(path.join(__dirname, '../proto/sample/sample.proto'), { + includeDirs: [path.join(__dirname, '../proto/sample/vendor')] + }); + + reflectionService = new ReflectionV1Implementation(root, { services: ['sample.SampleService'] }); + const { service: services } = reflectionService.listServices('*'); assert.equal(services.length, 1); assert(services.find((s) => s.name === 'sample.SampleService')); diff --git a/packages/grpc-reflection/test/test-utils.ts b/packages/grpc-reflection/test/test-utils.ts index bd8bc786b..2f197652a 100644 --- a/packages/grpc-reflection/test/test-utils.ts +++ b/packages/grpc-reflection/test/test-utils.ts @@ -1,6 +1,6 @@ import * as assert from 'assert'; -import { scope } from '../src/utils'; +import { scope } from '../src/implementations/common/utils'; describe('scope', () => { it('traverses upwards in the package scope', () => { diff --git a/packages/grpc-reflection/tsconfig.json b/packages/grpc-reflection/tsconfig.json index e8a746d0d..763ceda98 100644 --- a/packages/grpc-reflection/tsconfig.json +++ b/packages/grpc-reflection/tsconfig.json @@ -9,7 +9,7 @@ "noImplicitReturns": true, "pretty": true, "sourceMap": true, - "strictNullChecks": false, + "strict": true, "lib": ["es2017"], "outDir": "build", "target": "es2017", From 66f972cb8758ca16eb15f69fc0d4c195d086a5d0 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 27 Oct 2023 10:06:08 -0700 Subject: [PATCH 055/109] grpc-js: Implement unbind --- packages/grpc-js/src/server-credentials.ts | 89 ++- packages/grpc-js/src/server.ts | 795 ++++++++++++--------- packages/grpc-js/src/subchannel.ts | 1 + packages/grpc-js/src/uri-parser.ts | 13 + packages/grpc-js/test/test-server.ts | 86 +++ 5 files changed, 655 insertions(+), 329 deletions(-) diff --git a/packages/grpc-js/src/server-credentials.ts b/packages/grpc-js/src/server-credentials.ts index 17ab29805..0dd5f8cae 100644 --- a/packages/grpc-js/src/server-credentials.ts +++ b/packages/grpc-js/src/server-credentials.ts @@ -26,6 +26,7 @@ export interface KeyCertPair { export abstract class ServerCredentials { abstract _isSecure(): boolean; abstract _getSettings(): SecureServerOptions | null; + abstract _equals(other: ServerCredentials): boolean; static createInsecure(): ServerCredentials { return new InsecureServerCredentials(); @@ -48,8 +49,8 @@ export abstract class ServerCredentials { throw new TypeError('checkClientCertificate must be a boolean'); } - const cert = []; - const key = []; + const cert: Buffer[] = []; + const key: Buffer[] = []; for (let i = 0; i < keyCertPairs.length; i++) { const pair = keyCertPairs[i]; @@ -71,7 +72,7 @@ export abstract class ServerCredentials { } return new SecureServerCredentials({ - ca: rootCerts || getDefaultRootsData() || undefined, + ca: rootCerts ?? getDefaultRootsData() ?? undefined, cert, key, requestCert: checkClientCertificate, @@ -88,6 +89,10 @@ class InsecureServerCredentials extends ServerCredentials { _getSettings(): null { return null; } + + _equals(other: ServerCredentials): boolean { + return other instanceof InsecureServerCredentials; + } } class SecureServerCredentials extends ServerCredentials { @@ -105,4 +110,82 @@ class SecureServerCredentials extends ServerCredentials { _getSettings(): SecureServerOptions { return this.options; } + + /** + * Checks equality by checking the options that are actually set by + * createSsl. + * @param other + * @returns + */ + _equals(other: ServerCredentials): boolean { + if (this === other) { + return true; + } + if (!(other instanceof SecureServerCredentials)) { + return false; + } + // options.ca equality check + if (Buffer.isBuffer(this.options.ca) && Buffer.isBuffer(other.options.ca)) { + if (!this.options.ca.equals(other.options.ca)) { + return false; + } + } else { + if (this.options.ca !== other.options.ca) { + return false; + } + } + // options.cert equality check + if (Array.isArray(this.options.cert) && Array.isArray(other.options.cert)) { + if (this.options.cert.length !== other.options.cert.length) { + return false; + } + for (let i = 0; i < this.options.cert.length; i++) { + const thisCert = this.options.cert[i]; + const otherCert = other.options.cert[i]; + if (Buffer.isBuffer(thisCert) && Buffer.isBuffer(otherCert)) { + if (!thisCert.equals(otherCert)) { + return false; + } + } else { + if (thisCert !== otherCert) { + return false; + } + } + } + } else { + if (this.options.cert !== other.options.cert) { + return false; + } + } + // options.key equality check + if (Array.isArray(this.options.key) && Array.isArray(other.options.key)) { + if (this.options.key.length !== other.options.key.length) { + return false; + } + for (let i = 0; i < this.options.key.length; i++) { + const thisKey = this.options.key[i]; + const otherKey = other.options.key[i]; + if (Buffer.isBuffer(thisKey) && Buffer.isBuffer(otherKey)) { + if (!thisKey.equals(otherKey)) { + return false; + } + } else { + if (thisKey !== otherKey) { + return false; + } + } + } + } else { + if (this.options.key !== other.options.key) { + return false; + } + } + // options.requestCert equality check + if (this.options.requestCert !== other.options.requestCert) { + return false; + } + /* ciphers is derived from a value that is constant for the process, so no + * equality check is needed. */ + return true; + } } diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index f04cd9810..8ff0d0f22 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -17,7 +17,6 @@ import * as http2 from 'http2'; import * as util from 'util'; -import { AddressInfo } from 'net'; import { ServiceError } from './call'; import { Status, LogVerbosity } from './constants'; @@ -54,12 +53,11 @@ import { import * as logging from './logging'; import { SubchannelAddress, - TcpSubchannelAddress, isTcpSubchannelAddress, subchannelAddressToString, stringToSubchannelAddress, } from './subchannel-address'; -import { parseUri } from './uri-parser'; +import { GrpcUri, combineHostPort, parseUri, splitHostPort, uriToString } from './uri-parser'; import { ChannelzCallTracker, ChannelzChildrenTracker, @@ -83,9 +81,17 @@ const { HTTP2_HEADER_PATH } = http2.constants; const TRACER_NAME = 'server'; +type AnyHttp2Server = http2.Http2Server | http2.Http2SecureServer; + interface BindResult { port: number; count: number; + errors: string[]; +} + +interface SingleAddressBindResult { + port: number; + error?: string; } function noop(): void {} @@ -161,11 +167,61 @@ interface ChannelzSessionInfo { lastMessageReceivedTimestamp: Date | null; } +/** + * Information related to a single invocation of bindAsync. This should be + * tracked in a map keyed by target string, normalized with a pass through + * parseUri -> mapUriDefaultScheme -> uriToString. If the target has a port + * number and the port number is 0, the target string is modified with the + * concrete bound port. + */ +interface BoundPort { + /** + * The key used to refer to this object in the boundPorts map. + */ + mapKey: string; + /** + * The target string, passed through parseUri -> mapUriDefaultScheme. Used + * to determine the final key when the port number is 0. + */ + originalUri: GrpcUri; + /** + * If there is a pending bindAsync operation, this is a promise that resolves + * with the port number when that operation succeeds. If there is no such + * operation pending, this is null. + */ + completionPromise: Promise | null; + /** + * The port number that was actually bound. Populated only after + * completionPromise resolves. + */ + portNumber: number; + /** + * Set by unbind if called while pending is true. + */ + cancelled: boolean; + /** + * The credentials object passed to the original bindAsync call. + */ + credentials: ServerCredentials; + /** + * The set of servers associated with this listening port. A target string + * that expands to multiple addresses will result in multiple listening + * servers. + */ + listeningServers: Set +} + +/** + * Should be in a map keyed by AnyHttp2Server. + */ +interface Http2ServerInfo { + channelzRef: SocketRef; + sessions: Set; +} + export class Server { - private http2ServerList: { - server: http2.Http2Server | http2.Http2SecureServer; - channelzRef: SocketRef; - }[] = []; + private boundPorts: Map= new Map(); + private http2Servers: Map = new Map(); private handlers: Map = new Map< string, @@ -194,6 +250,12 @@ export class Server { private readonly keepaliveTimeMs: number; private readonly keepaliveTimeoutMs: number; + /** + * Options that will be used to construct all Http2Server instances for this + * Server. + */ + private commonServerOptions: http2.ServerOptions; + constructor(options?: ChannelOptions) { this.options = options ?? {}; if (this.options['grpc.enable_channelz'] === 0) { @@ -215,6 +277,24 @@ export class Server { this.options['grpc.keepalive_time_ms'] ?? KEEPALIVE_MAX_TIME_MS; this.keepaliveTimeoutMs = this.options['grpc.keepalive_timeout_ms'] ?? KEEPALIVE_TIMEOUT_MS; + this.commonServerOptions = { + maxSendHeaderBlockLength: Number.MAX_SAFE_INTEGER, + }; + if ('grpc-node.max_session_memory' in this.options) { + this.commonServerOptions.maxSessionMemory = + this.options['grpc-node.max_session_memory']; + } else { + /* By default, set a very large max session memory limit, to effectively + * disable enforcement of the limit. Some testing indicates that Node's + * behavior degrades badly when this limit is reached, so we solve that + * by disabling the check entirely. */ + this.commonServerOptions.maxSessionMemory = Number.MAX_SAFE_INTEGER; + } + if ('grpc.max_concurrent_streams' in this.options) { + this.commonServerOptions.settings = { + maxConcurrentStreams: this.options['grpc.max_concurrent_streams'], + }; + } this.trace('Server constructed'); } @@ -382,6 +462,238 @@ export class Server { throw new Error('Not implemented. Use bindAsync() instead'); } + private registerListenerToChannelz(boundAddress: SubchannelAddress) { + return registerChannelzSocket( + subchannelAddressToString(boundAddress), + () => { + return { + localAddress: boundAddress, + remoteAddress: null, + security: null, + remoteName: null, + streamsStarted: 0, + streamsSucceeded: 0, + streamsFailed: 0, + messagesSent: 0, + messagesReceived: 0, + keepAlivesSent: 0, + lastLocalStreamCreatedTimestamp: null, + lastRemoteStreamCreatedTimestamp: null, + lastMessageSentTimestamp: null, + lastMessageReceivedTimestamp: null, + localFlowControlWindow: null, + remoteFlowControlWindow: null, + }; + }, + this.channelzEnabled + ); + } + + private createHttp2Server(credentials: ServerCredentials) { + let http2Server: http2.Http2Server | http2.Http2SecureServer; + if (credentials._isSecure()) { + const secureServerOptions = Object.assign( + this.commonServerOptions, + credentials._getSettings()! + ); + secureServerOptions.enableTrace = + this.options['grpc-node.tls_enable_trace'] === 1; + http2Server = http2.createSecureServer(secureServerOptions); + http2Server.on('secureConnection', (socket: TLSSocket) => { + /* These errors need to be handled by the user of Http2SecureServer, + * according to https://github.com/nodejs/node/issues/35824 */ + socket.on('error', (e: Error) => { + this.trace( + 'An incoming TLS connection closed with error: ' + e.message + ); + }); + }); + } else { + http2Server = http2.createServer(this.commonServerOptions); + } + + http2Server.setTimeout(0, noop); + this._setupHandlers(http2Server); + return http2Server; + } + + private bindOneAddress(address: SubchannelAddress, boundPortObject: BoundPort): Promise { + this.trace( + 'Attempting to bind ' + subchannelAddressToString(address) + ); + const http2Server = this.createHttp2Server(boundPortObject.credentials); + return new Promise((resolve, reject) => { + const onError = (err: Error) => { + this.trace( + 'Failed to bind ' + + subchannelAddressToString(address) + + ' with error ' + + err.message + ); + resolve({ + port: 'port' in address ? address.port : 1, + error: err.message + }); + }; + + http2Server.once('error', onError); + + http2Server.listen(address, () => { + const boundAddress = http2Server.address()!; + let boundSubchannelAddress: SubchannelAddress; + if (typeof boundAddress === 'string') { + boundSubchannelAddress = { + path: boundAddress, + }; + } else { + boundSubchannelAddress = { + host: boundAddress.address, + port: boundAddress.port, + }; + } + + const channelzRef = this.registerListenerToChannelz(boundSubchannelAddress); + if (this.channelzEnabled) { + this.listenerChildrenTracker.refChild(channelzRef); + } + this.http2Servers.set(http2Server, { + channelzRef: channelzRef, + sessions: new Set() + }); + boundPortObject.listeningServers.add(http2Server); + this.trace( + 'Successfully bound ' + + subchannelAddressToString(boundSubchannelAddress) + ); + resolve({ + port: 'port' in boundSubchannelAddress + ? boundSubchannelAddress.port + : 1 + }); + http2Server.removeListener('error', onError); + }); + }); + } + + private async bindManyPorts(addressList: SubchannelAddress[], boundPortObject: BoundPort): Promise { + if (addressList.length === 0) { + return { + count: 0, + port: 0, + errors: [] + }; + } + if (isTcpSubchannelAddress(addressList[0]) && addressList[0].port === 0) { + /* If binding to port 0, first try to bind the first address, then bind + * the rest of the address list to the specific port that it binds. */ + const firstAddressResult = await this.bindOneAddress(addressList[0], boundPortObject); + if (firstAddressResult.error) { + /* If the first address fails to bind, try the same operation starting + * from the second item in the list. */ + const restAddressResult = await this.bindManyPorts(addressList.slice(1), boundPortObject); + return { + ...restAddressResult, + errors: [firstAddressResult.error, ...restAddressResult.errors] + }; + } else { + const restAddresses = addressList.slice(1).map(address => isTcpSubchannelAddress(address) ? {host: address.host, port: firstAddressResult.port} : address) + const restAddressResult = await Promise.all(restAddresses.map(address => this.bindOneAddress(address, boundPortObject))); + const allResults = [firstAddressResult, ...restAddressResult]; + return { + count: allResults.filter(result => result.error === undefined).length, + port: firstAddressResult.port, + errors: allResults.filter(result => result.error).map(result => result.error!) + }; + } + } else { + const allResults = await Promise.all(addressList.map(address => this.bindOneAddress(address, boundPortObject))); + return { + count: allResults.filter(result => result.error === undefined).length, + port: allResults[0].port, + errors: allResults.filter(result => result.error).map(result => result.error!) + }; + } + } + + private async bindAddressList(addressList: SubchannelAddress[], boundPortObject: BoundPort): Promise { + let bindResult: BindResult; + try { + bindResult = await this.bindManyPorts(addressList, boundPortObject); + } catch (error) { + throw error; + } + if (bindResult.count > 0) { + if (bindResult.count < addressList.length) { + logging.log( + LogVerbosity.INFO, + `WARNING Only ${bindResult.count} addresses added out of total ${addressList.length} resolved` + ); + } + return bindResult.port; + } else { + const errorString = `No address added out of total ${addressList.length} resolved`; + logging.log(LogVerbosity.ERROR, errorString); + throw new Error(`${errorString} errors: [${bindResult.errors.join(',')}]`); + } + } + + private resolvePort(port: GrpcUri): Promise { + return new Promise((resolve, reject) => { + const resolverListener: ResolverListener = { + onSuccessfulResolution: ( + endpointList, + serviceConfig, + serviceConfigError + ) => { + // We only want one resolution result. Discard all future results + resolverListener.onSuccessfulResolution = () => {}; + const addressList = ([] as SubchannelAddress[]).concat( + ...endpointList.map(endpoint => endpoint.addresses) + ); + if (addressList.length === 0) { + reject( + new Error(`No addresses resolved for port ${port}`) + ); + return; + } + resolve(addressList); + }, + onError: error => { + reject(new Error(error.details)); + }, + }; + const resolver = createResolver(port, resolverListener, this.options); + resolver.updateResolution(); + }); + } + + private async bindPort(port: GrpcUri, boundPortObject: BoundPort): Promise { + const addressList = await this.resolvePort(port); + if (boundPortObject.cancelled) { + this.completeUnbind(boundPortObject); + throw new Error('bindAsync operation cancelled by unbind call'); + } + const portNumber = await this.bindAddressList(addressList, boundPortObject); + if (boundPortObject.cancelled) { + this.completeUnbind(boundPortObject); + throw new Error('bindAsync operation cancelled by unbind call'); + } + return portNumber; + } + + private normalizePort(port: string): GrpcUri { + + const initialPortUri = parseUri(port); + if (initialPortUri === null) { + throw new Error(`Could not parse port "${port}"`); + } + const portUri = mapUriDefaultScheme(initialPortUri); + if (portUri === null) { + throw new Error(`Could not get a default scheme for port "${port}"`); + } + return portUri; + } + bindAsync( port: string, creds: ServerCredentials, @@ -399,331 +711,162 @@ export class Server { throw new TypeError('callback must be a function'); } - const initialPortUri = parseUri(port); - if (initialPortUri === null) { - throw new Error(`Could not parse port "${port}"`); - } - const portUri = mapUriDefaultScheme(initialPortUri); - if (portUri === null) { - throw new Error(`Could not get a default scheme for port "${port}"`); - } + this.trace('bindAsync port=' + port); - const serverOptions: http2.ServerOptions = { - maxSendHeaderBlockLength: Number.MAX_SAFE_INTEGER, - }; - if ('grpc-node.max_session_memory' in this.options) { - serverOptions.maxSessionMemory = - this.options['grpc-node.max_session_memory']; - } else { - /* By default, set a very large max session memory limit, to effectively - * disable enforcement of the limit. Some testing indicates that Node's - * behavior degrades badly when this limit is reached, so we solve that - * by disabling the check entirely. */ - serverOptions.maxSessionMemory = Number.MAX_SAFE_INTEGER; - } - if ('grpc.max_concurrent_streams' in this.options) { - serverOptions.settings = { - maxConcurrentStreams: this.options['grpc.max_concurrent_streams'], - }; - } + const portUri = this.normalizePort(port); const deferredCallback = (error: Error | null, port: number) => { process.nextTick(() => callback(error, port)); }; - const setupServer = (): http2.Http2Server | http2.Http2SecureServer => { - let http2Server: http2.Http2Server | http2.Http2SecureServer; - if (creds._isSecure()) { - const secureServerOptions = Object.assign( - serverOptions, - creds._getSettings()! - ); - secureServerOptions.enableTrace = - this.options['grpc-node.tls_enable_trace'] === 1; - http2Server = http2.createSecureServer(secureServerOptions); - http2Server.on('secureConnection', (socket: TLSSocket) => { - /* These errors need to be handled by the user of Http2SecureServer, - * according to https://github.com/nodejs/node/issues/35824 */ - socket.on('error', (e: Error) => { - this.trace( - 'An incoming TLS connection closed with error: ' + e.message - ); - }); - }); + /* First, if this port is already bound or that bind operation is in + * progress, use that result. */ + let boundPortObject = this.boundPorts.get(uriToString(portUri)); + if (boundPortObject) { + if (!creds._equals(boundPortObject.credentials)) { + deferredCallback(new Error(`${port} already bound with incompatible credentials`), 0); + return; + } + /* If that operation has previously been cancelled by an unbind call, + * uncancel it. */ + boundPortObject.cancelled = false; + if (boundPortObject.completionPromise) { + boundPortObject.completionPromise.then(portNum => callback(null, portNum), error => callback(error as Error, 0)); } else { - http2Server = http2.createServer(serverOptions); + deferredCallback(null, boundPortObject.portNumber); } - - http2Server.setTimeout(0, noop); - this._setupHandlers(http2Server); - return http2Server; + return; + } + boundPortObject = { + mapKey: uriToString(portUri), + originalUri: portUri, + completionPromise: null, + cancelled: false, + portNumber: 0, + credentials: creds, + listeningServers: new Set() }; - - const bindSpecificPort = ( - addressList: SubchannelAddress[], - portNum: number, - previousCount: number - ): Promise => { - if (addressList.length === 0) { - return Promise.resolve({ port: portNum, count: previousCount }); - } - return Promise.all( - addressList.map(address => { - this.trace( - 'Attempting to bind ' + subchannelAddressToString(address) - ); - let addr: SubchannelAddress; - if (isTcpSubchannelAddress(address)) { - addr = { - host: (address as TcpSubchannelAddress).host, - port: portNum, - }; - } else { - addr = address; - } - - const http2Server = setupServer(); - return new Promise((resolve, reject) => { - const onError = (err: Error) => { - this.trace( - 'Failed to bind ' + - subchannelAddressToString(address) + - ' with error ' + - err.message - ); - resolve(err); - }; - - http2Server.once('error', onError); - - http2Server.listen(addr, () => { - const boundAddress = http2Server.address()!; - let boundSubchannelAddress: SubchannelAddress; - if (typeof boundAddress === 'string') { - boundSubchannelAddress = { - path: boundAddress, - }; - } else { - boundSubchannelAddress = { - host: boundAddress.address, - port: boundAddress.port, - }; - } - - const channelzRef = registerChannelzSocket( - subchannelAddressToString(boundSubchannelAddress), - () => { - return { - localAddress: boundSubchannelAddress, - remoteAddress: null, - security: null, - remoteName: null, - streamsStarted: 0, - streamsSucceeded: 0, - streamsFailed: 0, - messagesSent: 0, - messagesReceived: 0, - keepAlivesSent: 0, - lastLocalStreamCreatedTimestamp: null, - lastRemoteStreamCreatedTimestamp: null, - lastMessageSentTimestamp: null, - lastMessageReceivedTimestamp: null, - localFlowControlWindow: null, - remoteFlowControlWindow: null, - }; - }, - this.channelzEnabled - ); - if (this.channelzEnabled) { - this.listenerChildrenTracker.refChild(channelzRef); - } - this.http2ServerList.push({ - server: http2Server, - channelzRef: channelzRef, - }); - this.trace( - 'Successfully bound ' + - subchannelAddressToString(boundSubchannelAddress) - ); - resolve( - 'port' in boundSubchannelAddress - ? boundSubchannelAddress.port - : portNum - ); - http2Server.removeListener('error', onError); - }); - }); - }) - ).then(results => { - let count = 0; - for (const result of results) { - if (typeof result === 'number') { - count += 1; - if (result !== portNum) { - throw new Error( - 'Invalid state: multiple port numbers added from single address' - ); - } - } - } - return { - port: portNum, - count: count + previousCount, + const splitPort = splitHostPort(portUri.path); + const completionPromise = this.bindPort(portUri, boundPortObject); + boundPortObject.completionPromise = completionPromise; + /* If the port number is 0, defer populating the map entry until after the + * bind operation completes and we have a specific port number. Otherwise, + * populate it immediately. */ + if (splitPort?.port === 0) { + completionPromise.then(portNum => { + const finalUri: GrpcUri = { + scheme: portUri.scheme, + authority: portUri.authority, + path: combineHostPort({host: splitPort.host, port: portNum}) }; + boundPortObject!.mapKey = uriToString(finalUri); + boundPortObject!.completionPromise = null; + boundPortObject!.portNumber = portNum; + this.boundPorts.set(boundPortObject!.mapKey, boundPortObject!); + callback(null, portNum); + }, error => { + callback(error, 0); + }) + } else { + this.boundPorts.set(boundPortObject.mapKey, boundPortObject); + completionPromise.then(portNum => { + boundPortObject!.completionPromise = null; + boundPortObject!.portNumber = portNum; + callback(null, portNum); + }, error => { + callback(error, 0); }); - }; + } + } - const bindWildcardPort = ( - addressList: SubchannelAddress[] - ): Promise => { - if (addressList.length === 0) { - return Promise.resolve({ port: 0, count: 0 }); + private closeServer(server: AnyHttp2Server, callback?: () => void) { + this.trace('Closing server with address ' + JSON.stringify(server.address())); + const serverInfo = this.http2Servers.get(server); + server.close(() => { + if (this.channelzEnabled && serverInfo) { + this.listenerChildrenTracker.unrefChild(serverInfo.channelzRef); + unregisterChannelzRef(serverInfo.channelzRef); } - const address = addressList[0]; - const http2Server = setupServer(); - return new Promise((resolve, reject) => { - const onError = (err: Error) => { - this.trace( - 'Failed to bind ' + - subchannelAddressToString(address) + - ' with error ' + - err.message - ); - resolve(bindWildcardPort(addressList.slice(1))); - }; + this.http2Servers.delete(server); + callback?.(); + }); - http2Server.once('error', onError); + } - http2Server.listen(address, () => { - const boundAddress = http2Server.address() as AddressInfo; - const boundSubchannelAddress: SubchannelAddress = { - host: boundAddress.address, - port: boundAddress.port, - }; - const channelzRef = registerChannelzSocket( - subchannelAddressToString(boundSubchannelAddress), - () => { - return { - localAddress: boundSubchannelAddress, - remoteAddress: null, - security: null, - remoteName: null, - streamsStarted: 0, - streamsSucceeded: 0, - streamsFailed: 0, - messagesSent: 0, - messagesReceived: 0, - keepAlivesSent: 0, - lastLocalStreamCreatedTimestamp: null, - lastRemoteStreamCreatedTimestamp: null, - lastMessageSentTimestamp: null, - lastMessageReceivedTimestamp: null, - localFlowControlWindow: null, - remoteFlowControlWindow: null, - }; - }, - this.channelzEnabled - ); - if (this.channelzEnabled) { - this.listenerChildrenTracker.refChild(channelzRef); - } - this.http2ServerList.push({ - server: http2Server, - channelzRef: channelzRef, - }); - this.trace( - 'Successfully bound ' + - subchannelAddressToString(boundSubchannelAddress) - ); - resolve(bindSpecificPort(addressList.slice(1), boundAddress.port, 1)); - http2Server.removeListener('error', onError); - }); - }); + private closeSession(session: http2.ServerHttp2Session, callback?: () => void) { + this.trace('Closing session initiated by ' + session.socket?.remoteAddress); + const sessionInfo = this.sessions.get(session); + const closeCallback = () => { + if (this.channelzEnabled && sessionInfo) { + this.sessionChildrenTracker.unrefChild(sessionInfo.ref); + unregisterChannelzRef(sessionInfo.ref); + } + this.sessions.delete(session); + callback?.(); }; + if (session.closed) { + process.nextTick(closeCallback); + } else { + session.close(closeCallback); + } + } - const resolverListener: ResolverListener = { - onSuccessfulResolution: ( - endpointList, - serviceConfig, - serviceConfigError - ) => { - // We only want one resolution result. Discard all future results - resolverListener.onSuccessfulResolution = () => {}; - const addressList = ([] as SubchannelAddress[]).concat( - ...endpointList.map(endpoint => endpoint.addresses) - ); - if (addressList.length === 0) { - deferredCallback( - new Error(`No addresses resolved for port ${port}`), - 0 - ); - return; - } - let bindResultPromise: Promise; - if (isTcpSubchannelAddress(addressList[0])) { - if (addressList[0].port === 0) { - bindResultPromise = bindWildcardPort(addressList); - } else { - bindResultPromise = bindSpecificPort( - addressList, - addressList[0].port, - 0 - ); - } - } else { - // Use an arbitrary non-zero port for non-TCP addresses - bindResultPromise = bindSpecificPort(addressList, 1, 0); + private completeUnbind(boundPortObject: BoundPort) { + for (const server of boundPortObject.listeningServers) { + const serverInfo = this.http2Servers.get(server); + this.closeServer(server, () => { + boundPortObject.listeningServers.delete(server); + }); + if (serverInfo) { + for (const session of serverInfo.sessions) { + this.closeSession(session); } - bindResultPromise.then( - bindResult => { - if (bindResult.count === 0) { - const errorString = `No address added out of total ${addressList.length} resolved`; - logging.log(LogVerbosity.ERROR, errorString); - deferredCallback(new Error(errorString), 0); - } else { - if (bindResult.count < addressList.length) { - logging.log( - LogVerbosity.INFO, - `WARNING Only ${bindResult.count} addresses added out of total ${addressList.length} resolved` - ); - } - deferredCallback(null, bindResult.port); - } - }, - error => { - const errorString = `No address added out of total ${addressList.length} resolved`; - logging.log(LogVerbosity.ERROR, errorString); - deferredCallback(new Error(errorString), 0); - } - ); - }, - onError: error => { - deferredCallback(new Error(error.details), 0); - }, - }; + } + } + this.boundPorts.delete(boundPortObject.mapKey); + } - const resolver = createResolver(portUri, resolverListener, this.options); - resolver.updateResolution(); + /** + * Unbind a previously bound port, or cancel an in-progress bindAsync + * operation. If port 0 was bound, only the actual bound port can be + * unbound. For example, if bindAsync was called with "localhost:0" and the + * bound port result was 54321, it can be unbound as "localhost:54321". + * @param port + */ + unbind(port: string): void { + this.trace('unbind port=' + port); + const portUri = this.normalizePort(port); + const splitPort = splitHostPort(portUri.path); + if (splitPort?.port === 0) { + throw new Error('Cannot unbind port 0'); + } + const boundPortObject = this.boundPorts.get(uriToString(portUri)); + if (boundPortObject) { + this.trace('unbinding ' + boundPortObject.mapKey + ' originally bound as ' + uriToString(boundPortObject.originalUri)); + /* If the bind operation is pending, the cancelled flag will trigger + * the unbind operation later. */ + if (boundPortObject.completionPromise) { + boundPortObject.cancelled = true; + } else { + this.completeUnbind(boundPortObject); + } + } } forceShutdown(): void { + for (const boundPortObject of this.boundPorts.values()) { + boundPortObject.cancelled = true; + } + this.boundPorts.clear(); // Close the server if it is still running. - - for (const { server: http2Server, channelzRef: ref } of this - .http2ServerList) { - if (http2Server.listening) { - http2Server.close(() => { - if (this.channelzEnabled) { - this.listenerChildrenTracker.unrefChild(ref); - unregisterChannelzRef(ref); - } - }); - } + for (const server of this.http2Servers.keys()) { + this.closeServer(server); } // Always destroy any available sessions. It's possible that one or more // tryShutdown() calls are in progress. Don't wait on them to finish. this.sessions.forEach((channelzInfo, session) => { + this.closeSession(session); // Cast NGHTTP2_CANCEL to any because TypeScript doesn't seem to // recognize destroy(code) as a valid signature. // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -766,9 +909,9 @@ export class Server { @deprecate('Calling start() is no longer necessary. It can be safely omitted.') start(): void { if ( - this.http2ServerList.length === 0 || - this.http2ServerList.every( - ({ server: http2Server }) => http2Server.listening !== true + this.http2Servers.size === 0 || + [...this.http2Servers.keys()].every( + server => !server.listening ) ) { throw new Error('server must be bound in order to start'); @@ -797,26 +940,24 @@ export class Server { } } - for (const { server: http2Server, channelzRef: ref } of this - .http2ServerList) { - if (http2Server.listening) { - pendingChecks++; - http2Server.close(() => { - if (this.channelzEnabled) { - this.listenerChildrenTracker.unrefChild(ref); - unregisterChannelzRef(ref); - } - maybeCallback(); - }); - } + for (const server of this.http2Servers.keys()) { + pendingChecks++; + const serverString = this.http2Servers.get(server)!.channelzRef.name; + this.trace('Waiting for server ' + serverString + ' to close'); + this.closeServer(server, () => { + this.trace('Server ' + serverString + ' finished closing'); + maybeCallback(); + }); + } + for (const session of this.sessions.keys()) { + pendingChecks++; + const sessionString = session.socket?.remoteAddress; + this.trace('Waiting for session ' + sessionString + ' to close'); + this.closeSession(session, () => { + this.trace('Session ' + sessionString + ' finished closing'); + maybeCallback(); + }); } - - this.sessions.forEach((channelzInfo, session) => { - if (!session.closed) { - pendingChecks += 1; - session.close(maybeCallback); - } - }); if (pendingChecks === 0) { wrappedCallback(); } @@ -1077,6 +1218,7 @@ export class Server { lastMessageReceivedTimestamp: null, }; + this.http2Servers.get(http2Server)?.sessions.add(session); this.sessions.set(session, channelzSessionInfo); const clientAddress = session.socket.remoteAddress; if (this.channelzEnabled) { @@ -1164,6 +1306,7 @@ export class Server { if (keeapliveTimeTimer) { clearTimeout(keeapliveTimeTimer); } + this.http2Servers.get(http2Server)?.sessions.delete(session); this.sessions.delete(session); }); }); diff --git a/packages/grpc-js/src/subchannel.ts b/packages/grpc-js/src/subchannel.ts index cf49ced77..d9a2dbd80 100644 --- a/packages/grpc-js/src/subchannel.ts +++ b/packages/grpc-js/src/subchannel.ts @@ -120,6 +120,7 @@ export class Subchannel { this.backoffTimeout = new BackoffTimeout(() => { this.handleBackoffTimer(); }, backoffOptions); + this.backoffTimeout.unref(); this.subchannelAddressString = subchannelAddressToString(subchannelAddress); this.keepaliveTime = options['grpc.keepalive_time_ms'] ?? -1; diff --git a/packages/grpc-js/src/uri-parser.ts b/packages/grpc-js/src/uri-parser.ts index 20c3d53b3..2b2efeca0 100644 --- a/packages/grpc-js/src/uri-parser.ts +++ b/packages/grpc-js/src/uri-parser.ts @@ -101,6 +101,19 @@ export function splitHostPort(path: string): HostPort | null { } } +export function combineHostPort(hostPort: HostPort): string { + if (hostPort.port === undefined) { + return hostPort.host; + } else { + // Only an IPv6 host should include a colon + if (hostPort.host.includes(':')) { + return `[${hostPort.host}]:${hostPort.port}`; + } else { + return `${hostPort.host}:${hostPort.port}`; + } + } +} + export function uriToString(uri: GrpcUri): string { let result = ''; if (uri.scheme !== undefined) { diff --git a/packages/grpc-js/test/test-server.ts b/packages/grpc-js/test/test-server.ts index d1b485ec3..56388a868 100644 --- a/packages/grpc-js/test/test-server.ts +++ b/packages/grpc-js/test/test-server.ts @@ -63,6 +63,13 @@ const cert = fs.readFileSync(path.join(__dirname, 'fixtures', 'server1.pem')); function noop(): void {} describe('Server', () => { + let server: Server; + beforeEach(() => { + server = new Server(); + }); + afterEach(() => { + server.forceShutdown(); + }); describe('constructor', () => { it('should work with no arguments', () => { assert.doesNotThrow(() => { @@ -140,6 +147,85 @@ describe('Server', () => { ); }, /callback must be a function/); }); + + it('succeeds when called with an already bound port', done => { + server.bindAsync('localhost:0', ServerCredentials.createInsecure(), (err, port) => { + assert.ifError(err); + server.bindAsync(`localhost:${port}`, ServerCredentials.createInsecure(), (err2, port2) => { + assert.ifError(err2); + assert.strictEqual(port, port2); + done(); + }); + }); + }); + + it('fails when called on a bound port with different credentials', done => { + const secureCreds = ServerCredentials.createSsl( + ca, + [{ private_key: key, cert_chain: cert }], + true + ); + server.bindAsync('localhost:0', ServerCredentials.createInsecure(), (err, port) => { + assert.ifError(err); + server.bindAsync(`localhost:${port}`, secureCreds, (err2, port2) => { + assert(err2 !== null); + assert.match(err2.message, /credentials/); + done(); + }) + }); + }) + }); + + describe('unbind', () => { + let client: grpc.Client | null = null; + beforeEach(() => { + client = null; + }); + afterEach(() => { + client?.close(); + }); + it('refuses to unbind port 0', done => { + assert.throws(() => { + server.unbind('localhost:0'); + }, /port 0/); + server.bindAsync('localhost:0', ServerCredentials.createInsecure(), (err, port) => { + assert.ifError(err); + assert.notStrictEqual(port, 0); + assert.throws(() => { + server.unbind('localhost:0'); + }, /port 0/); + done(); + }) + }); + + it('successfully unbinds a bound ephemeral port', done => { + server.bindAsync('localhost:0', ServerCredentials.createInsecure(), (err, port) => { + client = new grpc.Client(`localhost:${port}`, grpc.credentials.createInsecure()); + client.makeUnaryRequest('/math.Math/Div', x => x, x => x, Buffer.from('abc'), (callError1, result) => { + assert(callError1); + // UNIMPLEMENTED means that the request reached the call handling code + assert.strictEqual(callError1.code, grpc.status.UNIMPLEMENTED); + server.unbind(`localhost:${port}`); + const deadline = new Date(); + deadline.setSeconds(deadline.getSeconds() + 1); + client!.makeUnaryRequest('/math.Math/Div', x => x, x => x, Buffer.from('abc'), {deadline: deadline}, (callError2, result) => { + assert(callError2); + // DEADLINE_EXCEEDED means that the server is unreachable + assert.strictEqual(callError2.code, grpc.status.DEADLINE_EXCEEDED); + done(); + }); + }); + }) + }); + + it('cancels a bindAsync in progress', done => { + server.bindAsync('localhost:50051', ServerCredentials.createInsecure(), (err, port) => { + assert(err); + assert.match(err.message, /cancelled by unbind/); + done(); + }); + server.unbind('localhost:50051'); + }); }); describe('start', () => { From 3a161874513653090b3f211e5cc70d1b813b2c08 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 14 Nov 2023 14:37:13 -0800 Subject: [PATCH 056/109] grpc-js: Implement server drain method --- packages/grpc-js/src/server.ts | 44 +++++++++++++++++++++ packages/grpc-js/test/test-server.ts | 58 ++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index 8ff0d0f22..9bd71fd42 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -853,6 +853,50 @@ export class Server { } } + /** + * Gracefully close all connections associated with a previously bound port. + * After the grace time, forcefully close all remaining open connections. + * + * If port 0 was bound, only the actual bound port can be + * drained. For example, if bindAsync was called with "localhost:0" and the + * bound port result was 54321, it can be drained as "localhost:54321". + * @param port + * @param graceTimeMs + * @returns + */ + drain(port: string, graceTimeMs: number): void { + this.trace('drain port=' + port + ' graceTimeMs=' + graceTimeMs); + const portUri = this.normalizePort(port); + const splitPort = splitHostPort(portUri.path); + if (splitPort?.port === 0) { + throw new Error('Cannot drain port 0'); + } + const boundPortObject = this.boundPorts.get(uriToString(portUri)); + if (!boundPortObject) { + return; + } + const allSessions: Set = new Set(); + for (const http2Server of boundPortObject.listeningServers) { + const serverEntry = this.http2Servers.get(http2Server); + if (!serverEntry) { + continue; + } + for (const session of serverEntry.sessions) { + allSessions.add(session); + this.closeSession(session, () => { + allSessions.delete(session); + }); + } + } + /* After the grace time ends, send another goaway to all remaining sessions + * with the CANCEL code. */ + setTimeout(() => { + for (const session of allSessions) { + session.destroy(http2.constants.NGHTTP2_CANCEL as any); + } + }, graceTimeMs).unref?.(); + } + forceShutdown(): void { for (const boundPortObject of this.boundPorts.values()) { boundPortObject.cancelled = true; diff --git a/packages/grpc-js/test/test-server.ts b/packages/grpc-js/test/test-server.ts index 56388a868..c497b8e27 100644 --- a/packages/grpc-js/test/test-server.ts +++ b/packages/grpc-js/test/test-server.ts @@ -228,6 +228,64 @@ describe('Server', () => { }); }); + describe.only('drain', () => { + let client: ServiceClient; + let portNumber: number; + const protoFile = path.join(__dirname, 'fixtures', 'echo_service.proto'); + const echoService = loadProtoFile(protoFile) + .EchoService as ServiceClientConstructor; + + const serviceImplementation = { + echo(call: ServerUnaryCall, callback: sendUnaryData) { + callback(null, call.request); + }, + echoBidiStream(call: ServerDuplexStream) { + call.on('data', data => { + call.write(data); + }); + call.on('end', () => { + call.end(); + }); + }, + }; + + beforeEach(done => { + server.addService(echoService.service, serviceImplementation); + + server.bindAsync( + 'localhost:0', + ServerCredentials.createInsecure(), + (err, port) => { + assert.ifError(err); + portNumber = port; + client = new echoService( + `localhost:${port}`, + grpc.credentials.createInsecure() + ); + server.start(); + done(); + } + ); + }); + + afterEach(done => { + client.close(); + server.tryShutdown(done); + }); + + it('Should cancel open calls after the grace period ends', done => { + const call = client.echoBidiStream(); + call.on('error', (error: ServiceError) => { + assert.strictEqual(error.code, grpc.status.CANCELLED); + done(); + }); + call.on('data', () => { + server.drain(`localhost:${portNumber!}`, 100); + }); + call.write({value: 'abc'}); + }); + }); + describe('start', () => { let server: Server; From 89a5cbbdf48ae9279d3ef82e39a8a6cbf01579bd Mon Sep 17 00:00:00 2001 From: Justin Timmons Date: Tue, 14 Nov 2023 22:28:08 -0500 Subject: [PATCH 057/109] chore(grpc-reflection): cleaned up package dependencies --- packages/grpc-reflection/package.json | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/grpc-reflection/package.json b/packages/grpc-reflection/package.json index 8f8ea8ef2..e89f71427 100644 --- a/packages/grpc-reflection/package.json +++ b/packages/grpc-reflection/package.json @@ -1,7 +1,9 @@ { "name": "@grpc/reflection", "version": "1.0.0", - "author": "Justin Timmons", + "author": { + "name": "Google Inc." + }, "description": "Reflection API service for use with gRPC-node", "repository": { "type": "git", @@ -29,15 +31,14 @@ "generate-types": "proto-loader-gen-types --longs String --enums String --bytes Array --defaults --oneofs --includeComments --includeDirs proto/ -O src/generated grpc/reflection/v1/reflection.proto grpc/reflection/v1alpha/reflection.proto" }, "dependencies": { - "google-protobuf": "^3.21.2" + "google-protobuf": "^3.21.2", + "@grpc/proto-loader": "^0.7.10" }, "peerDependencies": { - "@grpc/grpc-js": ">=1.5.4", - "@grpc/proto-loader": ">=0.6.9" + "@grpc/grpc-js": "^1.8.21" }, "devDependencies": { - "@grpc/grpc-js": "^1.8.21", - "@grpc/proto-loader": "^0.7.10", + "@grpc/grpc-js": "file:../grpc-js", "@types/google-protobuf": "^3.15.7", "copyfiles": "^2.4.1", "typescript": "^5.2.2" From 2449abe3987ee46fdc10a944d0a67fb74f063f6c Mon Sep 17 00:00:00 2001 From: Justin Timmons Date: Wed, 15 Nov 2023 08:28:47 -0500 Subject: [PATCH 058/109] refactor(grpc-reflection): simplified request handling and file dependency logic --- .../src/implementations/reflection-v1.ts | 82 ++++++++++--------- 1 file changed, 45 insertions(+), 37 deletions(-) diff --git a/packages/grpc-reflection/src/implementations/reflection-v1.ts b/packages/grpc-reflection/src/implementations/reflection-v1.ts index 2f43d99f4..ac676e7ec 100644 --- a/packages/grpc-reflection/src/implementations/reflection-v1.ts +++ b/packages/grpc-reflection/src/implementations/reflection-v1.ts @@ -26,7 +26,7 @@ export class ReflectionError extends Error { } } -/** Analyzes a gRPC server and exposes methods to reflect on it +/** Analyzes a gRPC package definition and exposes methods to reflect on it * * NOTE: the files returned by this service may not match the handwritten ones 1:1. * This is because proto-loader reorients files based on their package definition, @@ -37,7 +37,7 @@ export class ReflectionError extends Error { */ export class ReflectionV1Implementation { - /** The full list of proto files (including imported deps) that the gRPC server includes */ + /** The full list of proto files (including imported deps) that the gRPC package includes */ private readonly fileDescriptorSet = new FileDescriptorSet(); /** An index of proto files by file name (eg. 'sample.proto') */ @@ -172,32 +172,34 @@ export class ReflectionV1Implementation { handleServerReflectionRequest(message: ServerReflectionRequest): ServerReflectionResponse { const response: ServerReflectionResponse = { validHost: message.host, - originalRequest: message, - fileDescriptorResponse: undefined, - allExtensionNumbersResponse: undefined, - listServicesResponse: undefined, - errorResponse: undefined, + originalRequest: message }; try { - if (message.listServices !== undefined) { - response.listServicesResponse = this.listServices(message.listServices); - } else if (message.fileContainingSymbol !== undefined) { - response.fileDescriptorResponse = this.fileContainingSymbol(message.fileContainingSymbol); - } else if (message.fileByFilename !== undefined) { - response.fileDescriptorResponse = this.fileByFilename(message.fileByFilename); - } else if (message.fileContainingExtension !== undefined) { - response.fileDescriptorResponse = this.fileContainingExtension( - message.fileContainingExtension?.containingType || '', - message.fileContainingExtension?.extensionNumber || -1 - ); - } else if (message.allExtensionNumbersOfType) { - response.allExtensionNumbersResponse = this.allExtensionNumbersOfType(message.allExtensionNumbersOfType); - } else { - throw new ReflectionError( - grpc.status.UNIMPLEMENTED, - `Unimplemented method for request: ${message}`, - ); + switch(message.messageRequest) { + case 'listServices': + response.listServicesResponse = this.listServices(message.listServices || ''); + break; + case 'fileContainingSymbol': + response.fileDescriptorResponse = this.fileContainingSymbol(message.fileContainingSymbol || ''); + break; + case 'fileByFilename': + response.fileDescriptorResponse = this.fileByFilename(message.fileByFilename || ''); + break; + case 'fileContainingExtension': + response.fileDescriptorResponse = this.fileContainingExtension( + message.fileContainingExtension?.containingType || '', + message.fileContainingExtension?.extensionNumber || -1 + ); + break; + case 'allExtensionNumbersOfType': + response.allExtensionNumbersResponse = this.allExtensionNumbersOfType(message.allExtensionNumbersOfType || ''); + break; + default: + throw new ReflectionError( + grpc.status.UNIMPLEMENTED, + `Unimplemented method for request: ${message.messageRequest}`, + ); } } catch (e) { if (e instanceof ReflectionError) { @@ -315,20 +317,26 @@ export class ReflectionV1Implementation { }; } - private getFileDependencies( - file: FileDescriptorProto, - visited: Set = new Set(), - ): FileDescriptorProto[] { - const newVisited = visited.add(file); + private getFileDependencies(file: FileDescriptorProto): FileDescriptorProto[] { + const visited: Set = new Set(); + const toVisit: FileDescriptorProto[] = file.getDependencyList().map((dep) => this.fileNameIndex[dep]); - const directDeps = file.getDependencyList().map((dep) => this.fileNameIndex[dep]); - const transitiveDeps = directDeps - .filter((dep) => !newVisited.has(dep)) - .map((dep) => this.getFileDependencies(dep, newVisited)) - .flat(); + while (toVisit.length > 0) { + const current = toVisit.pop(); - const allDeps = [...directDeps, ...transitiveDeps]; + if (!current || visited.has(current)) { + continue; + } - return [...new Set(allDeps)]; + visited.add(current); + toVisit.push( + ...current.getDependencyList() + .map((dep) => this.fileNameIndex[dep]) + .filter((dep) => !visited.has(dep)) + ); + } + + return Array.from(visited); } + } From 87e1f798460f028dccf104d3e417baa7a44311a7 Mon Sep 17 00:00:00 2001 From: Justin Timmons Date: Wed, 15 Nov 2023 20:49:03 -0500 Subject: [PATCH 059/109] docs(grpc-reflection): moved example to common directory and match grpc-go server --- examples/package.json | 1 + examples/reflection/server.js | 15 +++++++++++++++ packages/grpc-reflection/example/server.ts | 17 ----------------- 3 files changed, 16 insertions(+), 17 deletions(-) create mode 100644 examples/reflection/server.js delete mode 100644 packages/grpc-reflection/example/server.ts diff --git a/examples/package.json b/examples/package.json index aee948c3f..6857aa5d9 100644 --- a/examples/package.json +++ b/examples/package.json @@ -7,6 +7,7 @@ "google-protobuf": "^3.0.0", "@grpc/grpc-js": "^1.8.0", "@grpc/grpc-js-xds": "^1.8.0", + "@grpc/reflection": "^1.0.0", "lodash": "^4.6.1", "minimist": "^1.2.0" } diff --git a/examples/reflection/server.js b/examples/reflection/server.js new file mode 100644 index 000000000..0fa2a8a76 --- /dev/null +++ b/examples/reflection/server.js @@ -0,0 +1,15 @@ +var path = require('path'); +var grpc = require('@grpc/grpc-js'); +var protoLoader = require('@grpc/proto-loader'); +var reflection = require('@grpc/reflection'); + +var PROTO_PATH = path.join(__dirname, '../protos/helloworld.proto'); + +var server = new grpc.Server(); +var packageDefinition = protoLoader.loadSync(PROTO_PATH); +var reflection = new reflection.ReflectionService(packageDefinition); +reflection.addToServer(server); + +server.bindAsync('localhost:5000', grpc.ServerCredentials.createInsecure(), () => { + server.start(); +}); diff --git a/packages/grpc-reflection/example/server.ts b/packages/grpc-reflection/example/server.ts deleted file mode 100644 index 00a3e1150..000000000 --- a/packages/grpc-reflection/example/server.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as path from 'path'; -import * as grpc from '@grpc/grpc-js'; -import * as protoLoader from '@grpc/proto-loader'; - -import { ReflectionService } from '../src'; - -const PROTO_PATH = path.join(__dirname, '../proto/sample/sample.proto'); -const INCLUDE_PATH = path.join(__dirname, '../proto/sample/vendor'); - -const server = new grpc.Server(); -const packageDefinition = protoLoader.loadSync(PROTO_PATH, { includeDirs: [INCLUDE_PATH] }); -const reflection = new ReflectionService(packageDefinition, { services: ['sample.SampleService'] }); -reflection.addToServer(server); - -server.bindAsync('localhost:5000', grpc.ServerCredentials.createInsecure(), () => { - server.start(); -}); From 7a15a1cccb989b6352322951392ae2463ae22494 Mon Sep 17 00:00:00 2001 From: Justin Timmons Date: Wed, 15 Nov 2023 20:51:48 -0500 Subject: [PATCH 060/109] build(grpc-reflection): moved reflection tests to proper job and added test coverage --- gulpfile.ts | 4 ++-- package.json | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/gulpfile.ts b/gulpfile.ts index 3bdd6bb68..2c2c2e374 100644 --- a/gulpfile.ts +++ b/gulpfile.ts @@ -37,11 +37,11 @@ const clean = gulp.series(jsCore.clean, protobuf.clean, jsXds.clean); const cleanAll = gulp.series(jsXds.cleanAll, jsCore.cleanAll, internalTest.cleanAll, protobuf.cleanAll); -const nativeTestOnly = gulp.parallel(healthCheck.test, reflection.test); +const nativeTestOnly = gulp.parallel(healthCheck.test); const nativeTest = gulp.series(build, nativeTestOnly); -const testOnly = gulp.parallel(jsCore.test, nativeTestOnly, protobuf.test, jsXds.test); +const testOnly = gulp.parallel(jsCore.test, nativeTestOnly, protobuf.test, jsXds.test, reflection.test); const test = gulp.series(build, testOnly, internalTest.test); diff --git a/package.json b/package.json index 70a15fbbf..a1fd3d59e 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,8 @@ "include": [ "packages/grpc-health-check/health.js", "packages/grpc-js/build/src/*", - "packages/proto-loader/build/src/*" + "packages/proto-loader/build/src/*", + "packages/grpc-reflection/build/src/*" ], "cache": true, "all": true From bc8f2ead26b4f102f53eac70017c5d186272959c Mon Sep 17 00:00:00 2001 From: Justin Timmons Date: Wed, 15 Nov 2023 20:52:17 -0500 Subject: [PATCH 061/109] docs(grpc-reflection): fixed link to example image --- packages/grpc-reflection/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-reflection/README.md b/packages/grpc-reflection/README.md index cb2501877..47d790f5a 100644 --- a/packages/grpc-reflection/README.md +++ b/packages/grpc-reflection/README.md @@ -6,7 +6,7 @@ gRPC reflection API service for use with gRPC-node. This package provides an implementation of the [gRPC Server Reflection Protocol](https://github.com/grpc/grpc/blob/master/doc/server-reflection.md) service which can be added to an existing gRPC server. Adding this service to your server will allow clients [such as postman](https://blog.postman.com/postman-now-supports-grpc/) to dynamically load the API specification from your running application rather than needing to pass around and load proto files manually. -![example of reflection working with postman](https://gitlab.com/jtimmons/nestjs-grpc-reflection-module/-/raw/master/images/example.gif) +![example of reflection working with postman](./images/example.gif) ## Installation From 234f7f0a0cff6e00711bc36819c0d81fe768c7cf Mon Sep 17 00:00:00 2001 From: Justin Timmons Date: Sat, 18 Nov 2023 19:13:59 -0500 Subject: [PATCH 062/109] refactor(grpc-reflection): switch to using protobufjs library for message encoding/decoding --- packages/grpc-reflection/package.json | 5 +- .../common/protobuf-visitor.ts | 93 +++++++------- .../src/implementations/reflection-v1.ts | 113 ++++++++---------- .../test/test-reflection-v1-implementation.ts | 46 +++---- 4 files changed, 120 insertions(+), 137 deletions(-) diff --git a/packages/grpc-reflection/package.json b/packages/grpc-reflection/package.json index e89f71427..755ae5083 100644 --- a/packages/grpc-reflection/package.json +++ b/packages/grpc-reflection/package.json @@ -31,15 +31,14 @@ "generate-types": "proto-loader-gen-types --longs String --enums String --bytes Array --defaults --oneofs --includeComments --includeDirs proto/ -O src/generated grpc/reflection/v1/reflection.proto grpc/reflection/v1alpha/reflection.proto" }, "dependencies": { - "google-protobuf": "^3.21.2", - "@grpc/proto-loader": "^0.7.10" + "@grpc/proto-loader": "^0.7.10", + "protobufjs": "^7.2.5" }, "peerDependencies": { "@grpc/grpc-js": "^1.8.21" }, "devDependencies": { "@grpc/grpc-js": "file:../grpc-js", - "@types/google-protobuf": "^3.15.7", "copyfiles": "^2.4.1", "typescript": "^5.2.2" } diff --git a/packages/grpc-reflection/src/implementations/common/protobuf-visitor.ts b/packages/grpc-reflection/src/implementations/common/protobuf-visitor.ts index 556a8bf3e..451a2c0ea 100644 --- a/packages/grpc-reflection/src/implementations/common/protobuf-visitor.ts +++ b/packages/grpc-reflection/src/implementations/common/protobuf-visitor.ts @@ -1,24 +1,24 @@ import { - DescriptorProto, - EnumDescriptorProto, - EnumValueDescriptorProto, - FieldDescriptorProto, - FileDescriptorProto, - MethodDescriptorProto, - OneofDescriptorProto, - ServiceDescriptorProto, -} from 'google-protobuf/google/protobuf/descriptor_pb'; + IDescriptorProto, + IEnumDescriptorProto, + IEnumValueDescriptorProto, + IFieldDescriptorProto, + IFileDescriptorProto, + IMethodDescriptorProto, + IOneofDescriptorProto, + IServiceDescriptorProto, +} from 'protobufjs/ext/descriptor'; /** A set of functions for operating on protobuf objects as we visit them in a traversal */ interface Visitor { - field?: (fqn: string, file: FileDescriptorProto, field: FieldDescriptorProto) => void; - extension?: (fqn: string, file: FileDescriptorProto, extension: FieldDescriptorProto) => void; - oneOf?: (fqn: string, file: FileDescriptorProto, decl: OneofDescriptorProto) => void; - message?: (fqn: string, file: FileDescriptorProto, msg: DescriptorProto) => void; - enum?: (fqn: string, file: FileDescriptorProto, msg: EnumDescriptorProto) => void; - enumValue?: (fqn: string, file: FileDescriptorProto, msg: EnumValueDescriptorProto) => void; - service?: (fqn: string, file: FileDescriptorProto, msg: ServiceDescriptorProto) => void; - method?: (fqn: string, file: FileDescriptorProto, method: MethodDescriptorProto) => void; + field?: (fqn: string, file: IFileDescriptorProto, field: IFieldDescriptorProto) => void; + extension?: (fqn: string, file: IFileDescriptorProto, extension: IFieldDescriptorProto) => void; + oneOf?: (fqn: string, file: IFileDescriptorProto, decl: IOneofDescriptorProto) => void; + message?: (fqn: string, file: IFileDescriptorProto, msg: IDescriptorProto) => void; + enum?: (fqn: string, file: IFileDescriptorProto, msg: IEnumDescriptorProto) => void; + enumValue?: (fqn: string, file: IFileDescriptorProto, msg: IEnumValueDescriptorProto) => void; + service?: (fqn: string, file: IFileDescriptorProto, msg: IServiceDescriptorProto) => void; + method?: (fqn: string, file: IFileDescriptorProto, method: IMethodDescriptorProto) => void; } /** Visit each node in a protobuf file and perform an operation on it @@ -29,9 +29,9 @@ interface Visitor { * * @see Visitor for the interface to interact with the nodes */ -export const visit = (file: FileDescriptorProto, visitor: Visitor): void => { - const processField = (prefix: string, file: FileDescriptorProto, field: FieldDescriptorProto) => { - const fqn = `${prefix}.${field.getName()}`; +export const visit = (file: IFileDescriptorProto, visitor: Visitor): void => { + const processField = (prefix: string, file: IFileDescriptorProto, field: IFieldDescriptorProto) => { + const fqn = `${prefix}.${field.name}`; if (visitor.field) { visitor.field(fqn, file, field); } @@ -39,72 +39,71 @@ export const visit = (file: FileDescriptorProto, visitor: Visitor): void => { const processExtension = ( prefix: string, - file: FileDescriptorProto, - ext: FieldDescriptorProto, + file: IFileDescriptorProto, + ext: IFieldDescriptorProto, ) => { - const fqn = `${prefix}.${ext.getName()}`; + const fqn = `${prefix}.${ext.name}`; if (visitor.extension) { visitor.extension(fqn, file, ext); } }; - const processOneOf = (prefix: string, file: FileDescriptorProto, decl: OneofDescriptorProto) => { - const fqn = `${prefix}.${decl.getName()}`; + const processOneOf = (prefix: string, file: IFileDescriptorProto, decl: IOneofDescriptorProto) => { + const fqn = `${prefix}.${decl.name}`; if (visitor.oneOf) { visitor.oneOf(fqn, file, decl); } }; - const processEnum = (prefix: string, file: FileDescriptorProto, decl: EnumDescriptorProto) => { - const fqn = `${prefix}.${decl.getName()}`; + const processEnum = (prefix: string, file: IFileDescriptorProto, decl: IEnumDescriptorProto) => { + const fqn = `${prefix}.${decl.name}`; if (visitor.enum) { visitor.enum(fqn, file, decl); } - decl.getValueList().forEach((value) => { - const valueFqn = `${fqn}.${value.getName()}`; + decl.value?.forEach((value) => { + const valueFqn = `${fqn}.${value.name}`; if (visitor.enumValue) { visitor.enumValue(valueFqn, file, value); } }); }; - const processMessage = (prefix: string, file: FileDescriptorProto, msg: DescriptorProto) => { - const fqn = `${prefix}.${msg.getName()}`; + const processMessage = (prefix: string, file: IFileDescriptorProto, msg: IDescriptorProto) => { + const fqn = `${prefix}.${msg.name}`; if (visitor.message) { visitor.message(fqn, file, msg); } - msg.getNestedTypeList().forEach((type) => processMessage(fqn, file, type)); - msg.getEnumTypeList().forEach((type) => processEnum(fqn, file, type)); - msg.getFieldList().forEach((field) => processField(fqn, file, field)); - msg.getOneofDeclList().forEach((decl) => processOneOf(fqn, file, decl)); - msg.getExtensionList().forEach((ext) => processExtension(fqn, file, ext)); + msg.nestedType?.forEach((type) => processMessage(fqn, file, type)); + msg.enumType?.forEach((type) => processEnum(fqn, file, type)); + msg.field?.forEach((field) => processField(fqn, file, field)); + msg.oneofDecl?.forEach((decl) => processOneOf(fqn, file, decl)); + msg.extension?.forEach((ext) => processExtension(fqn, file, ext)); }; const processService = ( prefix: string, - file: FileDescriptorProto, - service: ServiceDescriptorProto, + file: IFileDescriptorProto, + service: IServiceDescriptorProto, ) => { - const fqn = `${prefix}.${service.getName()}`; + const fqn = `${prefix}.${service.name}`; if (visitor.service) { visitor.service(fqn, file, service); } - service.getMethodList().forEach((method) => { - const methodFqn = `${fqn}.${method.getName()}`; + service.method?.forEach((method) => { + const methodFqn = `${fqn}.${method.name}`; if (visitor.method) { visitor.method(methodFqn, file, method); } }); }; - const packageName = file.getPackage() || ''; - file.getEnumTypeList().forEach((type) => processEnum(packageName, file, type)); - file.getMessageTypeList().forEach((type) => processMessage(packageName, file, type)); - file.getServiceList().forEach((service) => processService(packageName, file, service)); - - file.getExtensionList().forEach((ext) => processExtension(packageName, file, ext)); + const packageName = file.package || ''; + file.enumType?.forEach((type) => processEnum(packageName, file, type)); + file.messageType?.forEach((type) => processMessage(packageName, file, type)); + file.service?.forEach((service) => processService(packageName, file, service)); + file.extension?.forEach((ext) => processExtension(packageName, file, ext)); }; diff --git a/packages/grpc-reflection/src/implementations/reflection-v1.ts b/packages/grpc-reflection/src/implementations/reflection-v1.ts index ac676e7ec..cf4b62818 100644 --- a/packages/grpc-reflection/src/implementations/reflection-v1.ts +++ b/packages/grpc-reflection/src/implementations/reflection-v1.ts @@ -1,8 +1,5 @@ import * as path from 'path'; -import { - FileDescriptorProto, - FileDescriptorSet, -} from 'google-protobuf/google/protobuf/descriptor_pb'; +import { FileDescriptorProto, IFileDescriptorProto } from 'protobufjs/ext/descriptor'; import * as grpc from '@grpc/grpc-js'; import * as protoLoader from '@grpc/proto-loader'; @@ -37,21 +34,21 @@ export class ReflectionError extends Error { */ export class ReflectionV1Implementation { - /** The full list of proto files (including imported deps) that the gRPC package includes */ - private readonly fileDescriptorSet = new FileDescriptorSet(); - /** An index of proto files by file name (eg. 'sample.proto') */ - private readonly fileNameIndex: Record = {}; + private readonly files: Record = {}; + + /** A graph of file dependencies */ + private readonly fileDependencies = new Map(); /** An index of proto files by type extension relationship * * extensionIndex[.][] contains a reference to the file containing an * extension for the type "." and field number "" */ - private readonly extensionIndex: Record> = {}; + private readonly extensions: Record> = {}; /** An index of fully qualified symbol names (eg. 'sample.Message') to the files that contain them */ - private readonly symbolMap: Record = {}; + private readonly symbols: Record = {}; /** Options that the user provided for this service */ private readonly options?: ReflectionServerOptions; @@ -60,29 +57,20 @@ export class ReflectionV1Implementation { this.options = options; Object.values(root).forEach(({ fileDescriptorProtos }) => { - // Add file descriptors to the FileDescriptorSet. - // We use the Array check here because a ServiceDefinition could have a method named the same thing - if (Array.isArray(fileDescriptorProtos)) { + if (Array.isArray(fileDescriptorProtos)) { // we use an array check to narrow the type fileDescriptorProtos.forEach((bin) => { - const proto = FileDescriptorProto.deserializeBinary(bin); - const isFileInSet = this.fileDescriptorSet - .getFileList() - .map((f) => f.getName()) - .includes(proto.getName()); - if (!isFileInSet) { - this.fileDescriptorSet.addFile(proto); + const proto = FileDescriptorProto.decode(bin) as IFileDescriptorProto; + + if (proto.name && !this.files[proto.name]) { + this.files[proto.name] = proto; } }); } }); - this.fileNameIndex = Object.fromEntries( - this.fileDescriptorSet.getFileList().map((f) => [f.getName(), f]), - ); - // Pass 1: Index Values - const index = (fqn: string, file: FileDescriptorProto) => (this.symbolMap[fqn] = file); - this.fileDescriptorSet.getFileList().forEach((file) => + const index = (fqn: string, file: IFileDescriptorProto) => (this.symbols[fqn] = file); + Object.values(this.files).forEach((file) => visit(file, { field: index, oneOf: index, @@ -94,36 +82,37 @@ export class ReflectionV1Implementation { extension: (fqn, file, ext) => { index(fqn, file); - const extendeeName = ext.getExtendee() || ''; - this.extensionIndex[extendeeName] = { - ...(this.extensionIndex[extendeeName] || {}), - [ext.getNumber() || -1]: file, + const extendeeName = ext.extendee || ''; + this.extensions[extendeeName] = { + ...(this.extensions[extendeeName] || {}), + [ext.number || -1]: file, }; }, }), ); // Pass 2: Link References To Values - const addReference = (ref: string, sourceFile: FileDescriptorProto, pkgScope: string) => { + // NOTE: this should be unnecessary after https://github.com/grpc/grpc-node/issues/2595 is resolved + const addReference = (ref: string, sourceFile: IFileDescriptorProto, pkgScope: string) => { if (!ref) { return; // nothing to do } - let referencedFile: FileDescriptorProto | null = null; + let referencedFile: IFileDescriptorProto | null = null; if (ref.startsWith('.')) { // absolute reference -- just remove the leading '.' and use the ref directly - referencedFile = this.symbolMap[ref.replace(/^\./, '')]; + referencedFile = this.symbols[ref.replace(/^\./, '')]; } else { // relative reference -- need to seek upwards up the current package scope until we find it let pkg = pkgScope; while (pkg && !referencedFile) { - referencedFile = this.symbolMap[`${pkg}.${ref}`]; + referencedFile = this.symbols[`${pkg}.${ref}`]; pkg = scope(pkg); } // if we didn't find anything then try just a FQN lookup if (!referencedFile) { - referencedFile = this.symbolMap[ref]; + referencedFile = this.symbols[ref]; } } @@ -132,19 +121,19 @@ export class ReflectionV1Implementation { return; } - const fname = referencedFile.getName(); - if (referencedFile !== sourceFile && fname) { - sourceFile.addDependency(fname); + if (referencedFile !== sourceFile) { + const existingDeps = this.fileDependencies.get(sourceFile) || []; + this.fileDependencies.set(sourceFile, [referencedFile, ...existingDeps]); } }; - this.fileDescriptorSet.getFileList().forEach((file) => + Object.values(this.files).forEach((file) => visit(file, { - field: (fqn, file, field) => addReference(field.getTypeName() || '', file, scope(fqn)), - extension: (fqn, file, ext) => addReference(ext.getTypeName() || '', file, scope(fqn)), + field: (fqn, file, field) => addReference(field.typeName || '', file, scope(fqn)), + extension: (fqn, file, ext) => addReference(ext.typeName || '', file, scope(fqn)), method: (fqn, file, method) => { - addReference(method.getInputType() || '', file, scope(fqn)); - addReference(method.getOutputType() || '', file, scope(fqn)); + addReference(method.inputType || '', file, scope(fqn)); + addReference(method.outputType || '', file, scope(fqn)); }, }), ); @@ -228,15 +217,15 @@ export class ReflectionV1Implementation { * @returns full-qualified service names (eg. 'sample.SampleService') */ listServices(listServices: string): ListServiceResponse__Output { - const services = this.fileDescriptorSet - .getFileList() + const services = Object.values(this.files) .map((file) => - file.getServiceList().map((service) => `${file.getPackage()}.${service.getName()}`), + file.service?.map((service) => `${file.package}.${service.name}`), ) - .flat(); + .flat() + .filter((service): service is string => !!service); const whitelist = new Set(this.options?.services ?? undefined); - const exposedServices = this.options?.services ? + const exposedServices = whitelist.size ? services.filter(service => whitelist.has(service)) : services; @@ -251,7 +240,7 @@ export class ReflectionV1Implementation { * @returns descriptors of the file which contains this symbol and its imports */ fileContainingSymbol(symbol: string): FileDescriptorResponse__Output { - const file = this.symbolMap[symbol]; + const file = this.symbols[symbol]; if (!file) { throw new ReflectionError(grpc.status.NOT_FOUND, `Symbol not found: ${symbol}`); @@ -260,7 +249,7 @@ export class ReflectionV1Implementation { const deps = this.getFileDependencies(file); return { - fileDescriptorProto: [file, ...deps].map((proto) => proto.serializeBinary()), + fileDescriptorProto: [file, ...deps].map((proto) => FileDescriptorProto.encode(proto).finish()), }; } @@ -269,7 +258,7 @@ export class ReflectionV1Implementation { * @returns descriptors of the file which contains this symbol and its imports */ fileByFilename(filename: string): FileDescriptorResponse__Output { - const file = this.fileNameIndex[filename]; + const file = this.files[filename]; if (!file) { throw new ReflectionError(grpc.status.NOT_FOUND, `Proto file not found: ${filename}`); @@ -278,7 +267,7 @@ export class ReflectionV1Implementation { const deps = this.getFileDependencies(file); return { - fileDescriptorProto: [file, ...deps].map((f) => f.serializeBinary()), + fileDescriptorProto: [file, ...deps].map((f) => FileDescriptorProto.encode(f).finish()), }; } @@ -287,7 +276,7 @@ export class ReflectionV1Implementation { * @returns descriptors of the file which contains this symbol and its imports */ fileContainingExtension(symbol: string, field: number): FileDescriptorResponse__Output { - const extensionsByFieldNumber = this.extensionIndex[symbol] || {}; + const extensionsByFieldNumber = this.extensions[symbol] || {}; const file = extensionsByFieldNumber[field]; if (!file) { @@ -300,16 +289,16 @@ export class ReflectionV1Implementation { const deps = this.getFileDependencies(file); return { - fileDescriptorProto: [file, ...deps].map((f) => f.serializeBinary()), + fileDescriptorProto: [file, ...deps].map((f) => FileDescriptorProto.encode(f).finish()), }; } allExtensionNumbersOfType(symbol: string): ExtensionNumberResponse__Output { - if (!(symbol in this.extensionIndex)) { + if (!(symbol in this.extensions)) { throw new ReflectionError(grpc.status.NOT_FOUND, `Extensions not found for symbol ${symbol}`); } - const fieldNumbers = Object.keys(this.extensionIndex[symbol]).map((key) => Number(key)); + const fieldNumbers = Object.keys(this.extensions[symbol]).map((key) => Number(key)); return { baseTypeName: symbol, @@ -317,9 +306,9 @@ export class ReflectionV1Implementation { }; } - private getFileDependencies(file: FileDescriptorProto): FileDescriptorProto[] { - const visited: Set = new Set(); - const toVisit: FileDescriptorProto[] = file.getDependencyList().map((dep) => this.fileNameIndex[dep]); + private getFileDependencies(file: IFileDescriptorProto): IFileDescriptorProto[] { + const visited: Set = new Set(); + const toVisit: IFileDescriptorProto[] = this.fileDependencies.get(file) || []; while (toVisit.length > 0) { const current = toVisit.pop(); @@ -329,11 +318,7 @@ export class ReflectionV1Implementation { } visited.add(current); - toVisit.push( - ...current.getDependencyList() - .map((dep) => this.fileNameIndex[dep]) - .filter((dep) => !visited.has(dep)) - ); + toVisit.push(...this.fileDependencies.get(current)?.filter((dep) => !visited.has(dep)) || []); } return Array.from(visited); diff --git a/packages/grpc-reflection/test/test-reflection-v1-implementation.ts b/packages/grpc-reflection/test/test-reflection-v1-implementation.ts index 81613ac69..552160f74 100644 --- a/packages/grpc-reflection/test/test-reflection-v1-implementation.ts +++ b/packages/grpc-reflection/test/test-reflection-v1-implementation.ts @@ -1,6 +1,6 @@ import * as assert from 'assert'; import * as path from 'path'; -import { FileDescriptorProto } from 'google-protobuf/google/protobuf/descriptor_pb'; +import { FileDescriptorProto, IFileDescriptorProto } from 'protobufjs/ext/descriptor'; import * as protoLoader from '@grpc/proto-loader'; import { ReflectionV1Implementation } from '../src/implementations/reflection-v1'; @@ -40,9 +40,9 @@ describe('GrpcReflectionService', () => { it('finds files with transitive dependencies', () => { const descriptors = reflectionService .fileByFilename('sample.proto') - .fileDescriptorProto.map(FileDescriptorProto.deserializeBinary); + .fileDescriptorProto.map(f => FileDescriptorProto.decode(f) as IFileDescriptorProto); - const names = descriptors.map((desc) => desc.getName()); + const names = descriptors.map((desc) => desc.name); assert.deepEqual( new Set(names), new Set(['sample.proto', 'vendor.proto', 'vendor_dependency.proto']) @@ -52,27 +52,27 @@ describe('GrpcReflectionService', () => { it('finds files with fewer transitive dependencies', () => { const descriptors = reflectionService .fileByFilename('vendor.proto') - .fileDescriptorProto.map(FileDescriptorProto.deserializeBinary); + .fileDescriptorProto.map(f => FileDescriptorProto.decode(f) as IFileDescriptorProto); - const names = descriptors.map((desc) => desc.getName()); + const names = descriptors.map((desc) => desc.name); assert.deepEqual(new Set(names), new Set(['vendor.proto', 'vendor_dependency.proto'])); }); it('finds files with no transitive dependencies', () => { const descriptors = reflectionService .fileByFilename('vendor_dependency.proto') - .fileDescriptorProto.map(FileDescriptorProto.deserializeBinary); + .fileDescriptorProto.map(f => FileDescriptorProto.decode(f) as IFileDescriptorProto); assert.equal(descriptors.length, 1); - assert.equal(descriptors[0].getName(), 'vendor_dependency.proto'); + assert.equal(descriptors[0].name, 'vendor_dependency.proto'); }); it('merges files based on package name', () => { const descriptors = reflectionService .fileByFilename('vendor.proto') - .fileDescriptorProto.map(FileDescriptorProto.deserializeBinary); + .fileDescriptorProto.map(f => FileDescriptorProto.decode(f) as IFileDescriptorProto); - const names = descriptors.map((desc) => desc.getName()); + const names = descriptors.map((desc) => desc.name); assert(!names.includes('common.proto')); // file merged into vendor.proto }); @@ -88,9 +88,9 @@ describe('GrpcReflectionService', () => { it('finds symbols and returns transitive file dependencies', () => { const descriptors = reflectionService .fileContainingSymbol('sample.HelloRequest') - .fileDescriptorProto.map(FileDescriptorProto.deserializeBinary); + .fileDescriptorProto.map(f => FileDescriptorProto.decode(f) as IFileDescriptorProto); - const names = descriptors.map((desc) => desc.getName()); + const names = descriptors.map((desc) => desc.name); assert.deepEqual( new Set(names), new Set(['sample.proto', 'vendor.proto', 'vendor_dependency.proto']), @@ -100,27 +100,27 @@ describe('GrpcReflectionService', () => { it('finds imported message types', () => { const descriptors = reflectionService .fileContainingSymbol('vendor.CommonMessage') - .fileDescriptorProto.map(FileDescriptorProto.deserializeBinary); + .fileDescriptorProto.map(f => FileDescriptorProto.decode(f) as IFileDescriptorProto); - const names = descriptors.map((desc) => desc.getName()); + const names = descriptors.map((desc) => desc.name); assert.deepEqual(new Set(names), new Set(['vendor.proto', 'vendor_dependency.proto'])); }); it('finds transitively imported message types', () => { const descriptors = reflectionService .fileContainingSymbol('vendor.dependency.DependentMessage') - .fileDescriptorProto.map(FileDescriptorProto.deserializeBinary); + .fileDescriptorProto.map(f => FileDescriptorProto.decode(f) as IFileDescriptorProto); assert.equal(descriptors.length, 1); - assert.equal(descriptors[0].getName(), 'vendor_dependency.proto'); + assert.equal(descriptors[0].name, 'vendor_dependency.proto'); }); it('finds nested message types', () => { const descriptors = reflectionService .fileContainingSymbol('sample.HelloRequest.HelloNested') - .fileDescriptorProto.map(FileDescriptorProto.deserializeBinary); + .fileDescriptorProto.map(f => FileDescriptorProto.decode(f) as IFileDescriptorProto); - const names = descriptors.map((desc) => desc.getName()); + const names = descriptors.map((desc) => desc.name); assert.deepEqual( new Set(names), new Set(['sample.proto', 'vendor.proto', 'vendor_dependency.proto']), @@ -130,9 +130,9 @@ describe('GrpcReflectionService', () => { it('merges files based on package name', () => { const descriptors = reflectionService .fileContainingSymbol('vendor.CommonMessage') - .fileDescriptorProto.map(FileDescriptorProto.deserializeBinary); + .fileDescriptorProto.map(f => FileDescriptorProto.decode(f) as IFileDescriptorProto); - const names = descriptors.map((desc) => desc.getName()); + const names = descriptors.map((desc) => desc.name); assert(!names.includes('common.proto')); // file merged into vendor.proto }); @@ -146,9 +146,9 @@ describe('GrpcReflectionService', () => { it('resolves references to method types', () => { const descriptors = reflectionService .fileContainingSymbol('sample.SampleService.Hello2') - .fileDescriptorProto.map(FileDescriptorProto.deserializeBinary); + .fileDescriptorProto.map(f => FileDescriptorProto.decode(f) as IFileDescriptorProto); - const names = descriptors.map((desc) => desc.getName()); + const names = descriptors.map((desc) => desc.name); assert.deepEqual( new Set(names), new Set(['sample.proto', 'vendor.proto', 'vendor_dependency.proto']), @@ -160,9 +160,9 @@ describe('GrpcReflectionService', () => { it('finds extensions and returns transitive file dependencies', () => { const descriptors = reflectionService .fileContainingExtension('.vendor.CommonMessage', 101) - .fileDescriptorProto.map(FileDescriptorProto.deserializeBinary); + .fileDescriptorProto.map(f => FileDescriptorProto.decode(f) as IFileDescriptorProto); - const names = descriptors.map((desc) => desc.getName()); + const names = descriptors.map((desc) => desc.name); assert.deepEqual(new Set(names), new Set(['vendor.proto', 'vendor_dependency.proto'])); }); From c53656d67bb3ba8a83fee5539dd34ea55b98d981 Mon Sep 17 00:00:00 2001 From: Justin Timmons Date: Sat, 18 Nov 2023 19:44:48 -0500 Subject: [PATCH 063/109] refactor(grpc-reflection): precompute service list and file encodings --- .../src/implementations/reflection-v1.ts | 50 +++++++++++-------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/packages/grpc-reflection/src/implementations/reflection-v1.ts b/packages/grpc-reflection/src/implementations/reflection-v1.ts index cf4b62818..913f93719 100644 --- a/packages/grpc-reflection/src/implementations/reflection-v1.ts +++ b/packages/grpc-reflection/src/implementations/reflection-v1.ts @@ -1,5 +1,9 @@ import * as path from 'path'; -import { FileDescriptorProto, IFileDescriptorProto } from 'protobufjs/ext/descriptor'; +import { + FileDescriptorProto, + IFileDescriptorProto, + IServiceDescriptorProto +} from 'protobufjs/ext/descriptor'; import * as grpc from '@grpc/grpc-js'; import * as protoLoader from '@grpc/proto-loader'; @@ -40,6 +44,9 @@ export class ReflectionV1Implementation { /** A graph of file dependencies */ private readonly fileDependencies = new Map(); + /** Pre-computed encoded-versions of each file */ + private readonly fileEncodings = new Map(); + /** An index of proto files by type extension relationship * * extensionIndex[.][] contains a reference to the file containing an @@ -50,12 +57,11 @@ export class ReflectionV1Implementation { /** An index of fully qualified symbol names (eg. 'sample.Message') to the files that contain them */ private readonly symbols: Record = {}; - /** Options that the user provided for this service */ - private readonly options?: ReflectionServerOptions; + /** An index of the services in the analyzed package(s) */ + private readonly services: Record = {}; - constructor(root: protoLoader.PackageDefinition, options?: ReflectionServerOptions) { - this.options = options; + constructor(root: protoLoader.PackageDefinition, options?: ReflectionServerOptions) { Object.values(root).forEach(({ fileDescriptorProtos }) => { if (Array.isArray(fileDescriptorProtos)) { // we use an array check to narrow the type fileDescriptorProtos.forEach((bin) => { @@ -69,16 +75,23 @@ export class ReflectionV1Implementation { }); // Pass 1: Index Values + const serviceWhitelist = new Set(options?.services); const index = (fqn: string, file: IFileDescriptorProto) => (this.symbols[fqn] = file); Object.values(this.files).forEach((file) => visit(file, { field: index, oneOf: index, message: index, - service: index, method: index, enum: index, enumValue: index, + service: (fqn, file, service) => { + index(fqn, file); + + if (options?.services === undefined || serviceWhitelist.has(fqn)) { + this.services[fqn] = service; + } + }, extension: (fqn, file, ext) => { index(fqn, file); @@ -137,6 +150,11 @@ export class ReflectionV1Implementation { }, }), ); + + // Pass 3: pre-compute file encoding since that can be slow and is done frequently + Object.values(this.files).forEach(file => { + this.fileEncodings.set(file, FileDescriptorProto.encode(file).finish()) + }); } addToServer(server: Pick) { @@ -217,19 +235,7 @@ export class ReflectionV1Implementation { * @returns full-qualified service names (eg. 'sample.SampleService') */ listServices(listServices: string): ListServiceResponse__Output { - const services = Object.values(this.files) - .map((file) => - file.service?.map((service) => `${file.package}.${service.name}`), - ) - .flat() - .filter((service): service is string => !!service); - - const whitelist = new Set(this.options?.services ?? undefined); - const exposedServices = whitelist.size ? - services.filter(service => whitelist.has(service)) - : services; - - return { service: exposedServices.map((service) => ({ name: service })) }; + return { service: Object.keys(this.services).map((service) => ({ name: service })) }; } /** Find the proto file(s) that declares the given fully-qualified symbol name @@ -249,7 +255,7 @@ export class ReflectionV1Implementation { const deps = this.getFileDependencies(file); return { - fileDescriptorProto: [file, ...deps].map((proto) => FileDescriptorProto.encode(proto).finish()), + fileDescriptorProto: [file, ...deps].map((file) => this.fileEncodings.get(file) || new Uint8Array()) }; } @@ -267,7 +273,7 @@ export class ReflectionV1Implementation { const deps = this.getFileDependencies(file); return { - fileDescriptorProto: [file, ...deps].map((f) => FileDescriptorProto.encode(f).finish()), + fileDescriptorProto: [file, ...deps].map((file) => this.fileEncodings.get(file) || new Uint8Array), }; } @@ -289,7 +295,7 @@ export class ReflectionV1Implementation { const deps = this.getFileDependencies(file); return { - fileDescriptorProto: [file, ...deps].map((f) => FileDescriptorProto.encode(f).finish()), + fileDescriptorProto: [file, ...deps].map((file) => this.fileEncodings.get(file) || new Uint8Array()), }; } From 8016d8758bda7cfdd87a3eaef535cdfc48174ad3 Mon Sep 17 00:00:00 2001 From: Justin Timmons Date: Mon, 27 Nov 2023 21:20:02 -0500 Subject: [PATCH 064/109] docs(grpc-reflection): updated reflection example to reflect on the reflection API itself --- examples/reflection/server.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/reflection/server.js b/examples/reflection/server.js index 0fa2a8a76..57ac01350 100644 --- a/examples/reflection/server.js +++ b/examples/reflection/server.js @@ -3,7 +3,11 @@ var grpc = require('@grpc/grpc-js'); var protoLoader = require('@grpc/proto-loader'); var reflection = require('@grpc/reflection'); -var PROTO_PATH = path.join(__dirname, '../protos/helloworld.proto'); +var PROTO_PATH = [ + path.join(__dirname, '../protos/helloworld.proto'), + require.resolve('@grpc/reflection/build/proto/grpc/reflection/v1/reflection.proto'), + require.resolve('@grpc/reflection/build/proto/grpc/reflection/v1alpha/reflection.proto') +]; var server = new grpc.Server(); var packageDefinition = protoLoader.loadSync(PROTO_PATH); From 674a6716d02af542bbe5622e95c91afb96c96203 Mon Sep 17 00:00:00 2001 From: Justin Timmons Date: Tue, 28 Nov 2023 21:13:52 -0500 Subject: [PATCH 065/109] Revert "docs(grpc-reflection): updated reflection example to reflect on the reflection API itself" This reverts commit 8016d8758bda7cfdd87a3eaef535cdfc48174ad3. --- examples/reflection/server.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/examples/reflection/server.js b/examples/reflection/server.js index 57ac01350..0fa2a8a76 100644 --- a/examples/reflection/server.js +++ b/examples/reflection/server.js @@ -3,11 +3,7 @@ var grpc = require('@grpc/grpc-js'); var protoLoader = require('@grpc/proto-loader'); var reflection = require('@grpc/reflection'); -var PROTO_PATH = [ - path.join(__dirname, '../protos/helloworld.proto'), - require.resolve('@grpc/reflection/build/proto/grpc/reflection/v1/reflection.proto'), - require.resolve('@grpc/reflection/build/proto/grpc/reflection/v1alpha/reflection.proto') -]; +var PROTO_PATH = path.join(__dirname, '../protos/helloworld.proto'); var server = new grpc.Server(); var packageDefinition = protoLoader.loadSync(PROTO_PATH); From 6a88cf21f47eb7e4b011a91edeb1a13dc2988af9 Mon Sep 17 00:00:00 2001 From: Justin Timmons Date: Tue, 28 Nov 2023 22:04:24 -0500 Subject: [PATCH 066/109] docs(grpc-reflection): added helloworld implementation for reflection example --- examples/reflection/server.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/reflection/server.js b/examples/reflection/server.js index 0fa2a8a76..83232e8e5 100644 --- a/examples/reflection/server.js +++ b/examples/reflection/server.js @@ -7,8 +7,13 @@ var PROTO_PATH = path.join(__dirname, '../protos/helloworld.proto'); var server = new grpc.Server(); var packageDefinition = protoLoader.loadSync(PROTO_PATH); +var proto = grpc.loadPackageDefinition(packageDefinition); var reflection = new reflection.ReflectionService(packageDefinition); + reflection.addToServer(server); +server.addService(proto.helloworld.Greeter.service, { + sayHello: (call, callback) => { callback(null, { message: 'Hello' }) } +}); server.bindAsync('localhost:5000', grpc.ServerCredentials.createInsecure(), () => { server.start(); From 3f2217e220adbbadaa6514fe00e7580598b83a62 Mon Sep 17 00:00:00 2001 From: Pitos Date: Thu, 14 Dec 2023 13:53:18 +0100 Subject: [PATCH 067/109] Fix issue #2631 --- packages/grpc-reflection/src/implementations/reflection-v1.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grpc-reflection/src/implementations/reflection-v1.ts b/packages/grpc-reflection/src/implementations/reflection-v1.ts index 913f93719..3858d7f42 100644 --- a/packages/grpc-reflection/src/implementations/reflection-v1.ts +++ b/packages/grpc-reflection/src/implementations/reflection-v1.ts @@ -314,8 +314,8 @@ export class ReflectionV1Implementation { private getFileDependencies(file: IFileDescriptorProto): IFileDescriptorProto[] { const visited: Set = new Set(); - const toVisit: IFileDescriptorProto[] = this.fileDependencies.get(file) || []; - + const toVisit: IFileDescriptorProto[] = [...(this.fileDependencies.get(file) || [])]; + while (toVisit.length > 0) { const current = toVisit.pop(); From 5fe8afc4e778f7d26933161e8e6568900c0bbda4 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 14 Dec 2023 10:38:11 -0500 Subject: [PATCH 068/109] grpc-reflection: Increment version to 1.0.1 --- packages/grpc-reflection/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-reflection/package.json b/packages/grpc-reflection/package.json index 755ae5083..8f0d8c934 100644 --- a/packages/grpc-reflection/package.json +++ b/packages/grpc-reflection/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/reflection", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Google Inc." }, From bda01f97f48b982237aa973ef336cdfcd9350864 Mon Sep 17 00:00:00 2001 From: Filippo Spinella Date: Thu, 14 Dec 2023 18:56:05 +0100 Subject: [PATCH 069/109] fix README --- packages/grpc-reflection/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grpc-reflection/README.md b/packages/grpc-reflection/README.md index 47d790f5a..c4aa03b63 100644 --- a/packages/grpc-reflection/README.md +++ b/packages/grpc-reflection/README.md @@ -21,12 +21,12 @@ npm install @grpc/reflection Any gRPC-node server can use `@grpc/reflection` to expose reflection information about their gRPC API. ```typescript -import { ReflectionServer } from '@grpc/reflection'; +import { ReflectionService } from '@grpc/reflection'; const pkg = protoLoader.load(...); // Load your gRPC package definition as normal // Create the reflection implementation based on your gRPC package and add it to your existing server -const reflection = new ReflectionServer(pkg); +const reflection = new ReflectionService(pkg); reflection.addToServer(server); ``` From 493f9bfa6733ce9807c8d1ed866eb2fae3ae0806 Mon Sep 17 00:00:00 2001 From: Xuan Wang Date: Mon, 18 Dec 2023 20:51:23 +0000 Subject: [PATCH 070/109] buildscripts: Use the Kokoro shared install lib from the new repo --- packages/grpc-js-xds/scripts/xds_k8s_lb.sh | 2 +- packages/grpc-js-xds/scripts/xds_k8s_url_map.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh b/packages/grpc-js-xds/scripts/xds_k8s_lb.sh index e4a0cf214..504c3ff37 100755 --- a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh +++ b/packages/grpc-js-xds/scripts/xds_k8s_lb.sh @@ -17,7 +17,7 @@ set -eo pipefail # Constants readonly GITHUB_REPOSITORY_NAME="grpc-node" -readonly TEST_DRIVER_INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/${TEST_DRIVER_REPO_OWNER:-grpc}/grpc/${TEST_DRIVER_BRANCH:-master}/tools/internal_ci/linux/grpc_xds_k8s_install_test_driver.sh" +readonly TEST_DRIVER_INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/${TEST_DRIVER_REPO_OWNER:-grpc}/psm-interop/${TEST_DRIVER_BRANCH:-main}/.kokoro/psm_interop_kokoro_lib.sh" ## xDS test client Docker images readonly SERVER_IMAGE_NAME="gcr.io/grpc-testing/xds-interop/java-server:558b5b0bfac8e21755c223063274a779b3898afe" readonly CLIENT_IMAGE_NAME="gcr.io/grpc-testing/xds-interop/node-client" diff --git a/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh b/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh index fc74718f2..9344d054b 100644 --- a/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh +++ b/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh @@ -17,7 +17,7 @@ set -eo pipefail # Constants readonly GITHUB_REPOSITORY_NAME="grpc-node" -readonly TEST_DRIVER_INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/${TEST_DRIVER_REPO_OWNER:-grpc}/grpc/${TEST_DRIVER_BRANCH:-master}/tools/internal_ci/linux/grpc_xds_k8s_install_test_driver.sh" +readonly TEST_DRIVER_INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/${TEST_DRIVER_REPO_OWNER:-grpc}/psm-interop/${TEST_DRIVER_BRANCH:-main}/.kokoro/psm_interop_kokoro_lib.sh" ## xDS test client Docker images readonly CLIENT_IMAGE_NAME="gcr.io/grpc-testing/xds-interop/node-client" readonly FORCE_IMAGE_BUILD="${FORCE_IMAGE_BUILD:-0}" From 3cdaebdd0c6347c55cf8a57235c3eec7ef067f00 Mon Sep 17 00:00:00 2001 From: "Chakhsu.Lau" Date: Thu, 4 Jan 2024 21:19:02 +0800 Subject: [PATCH 071/109] fix: export type VerifyOptions --- packages/grpc-js/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/grpc-js/src/index.ts b/packages/grpc-js/src/index.ts index d44a2dc6e..6733246ba 100644 --- a/packages/grpc-js/src/index.ts +++ b/packages/grpc-js/src/index.ts @@ -27,7 +27,7 @@ import { StatusObject } from './call-interface'; import { Channel, ChannelImplementation } from './channel'; import { CompressionAlgorithms } from './compression-algorithms'; import { ConnectivityState } from './connectivity-state'; -import { ChannelCredentials } from './channel-credentials'; +import { ChannelCredentials, VerifyOptions } from './channel-credentials'; import { CallOptions, Client, @@ -182,6 +182,7 @@ export { ServiceDefinition, UntypedHandleCall, UntypedServiceImplementation, + VerifyOptions }; /**** Server ****/ From f52d1429fb66ef34c0321d6422c3417972323144 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 16 Jan 2024 13:39:07 -0800 Subject: [PATCH 072/109] grpc-js: Implement server interceptors --- packages/grpc-js/src/call-interface.ts | 2 +- packages/grpc-js/src/index.ts | 15 +- packages/grpc-js/src/server-call.ts | 784 ++-------------- packages/grpc-js/src/server-interceptors.ts | 886 ++++++++++++++++++ packages/grpc-js/src/server.ts | 400 +++++--- packages/grpc-js/src/transport.ts | 4 +- packages/grpc-js/test/common.ts | 7 +- packages/grpc-js/test/test-channelz.ts | 4 +- .../grpc-js/test/test-server-deadlines.ts | 4 +- .../grpc-js/test/test-server-interceptors.ts | 296 ++++++ 10 files changed, 1544 insertions(+), 858 deletions(-) create mode 100644 packages/grpc-js/src/server-interceptors.ts create mode 100644 packages/grpc-js/test/test-server-interceptors.ts diff --git a/packages/grpc-js/src/call-interface.ts b/packages/grpc-js/src/call-interface.ts index 15035aeac..c0c63b957 100644 --- a/packages/grpc-js/src/call-interface.ts +++ b/packages/grpc-js/src/call-interface.ts @@ -37,7 +37,7 @@ export interface StatusObject { } export type PartialStatusObject = Pick & { - metadata: Metadata | null; + metadata?: Metadata | null | undefined; }; export const enum WriteFlags { diff --git a/packages/grpc-js/src/index.ts b/packages/grpc-js/src/index.ts index 50671b01c..b37f61103 100644 --- a/packages/grpc-js/src/index.ts +++ b/packages/grpc-js/src/index.ts @@ -49,6 +49,7 @@ import { import { Metadata, MetadataOptions, MetadataValue } from './metadata'; import { Server, + ServerOptions, UntypedHandleCall, UntypedServiceImplementation, } from './server'; @@ -226,7 +227,7 @@ export const setLogVerbosity = (verbosity: LogVerbosity): void => { logging.setLoggerVerbosity(verbosity); }; -export { Server }; +export { Server, ServerOptions }; export { ServerCredentials }; export { KeyCertPair }; @@ -264,6 +265,18 @@ export { addAdminServicesToServer } from './admin'; export { ServiceConfig, LoadBalancingConfig, MethodConfig, RetryPolicy } from './service-config'; +export { + ServerListener, + FullServerListener, + ServerListenerBuilder, + Responder, + FullResponder, + ResponderBuilder, + ServerInterceptingCallInterface, + ServerInterceptingCall, + ServerInterceptor +} from './server-interceptors'; + import * as experimental from './experimental'; export { experimental }; diff --git a/packages/grpc-js/src/server-call.ts b/packages/grpc-js/src/server-call.ts index 107c2e3ef..8fb1de2aa 100644 --- a/packages/grpc-js/src/server-call.ts +++ b/packages/grpc-js/src/server-call.ts @@ -16,66 +16,17 @@ */ import { EventEmitter } from 'events'; -import * as http2 from 'http2'; import { Duplex, Readable, Writable } from 'stream'; -import * as zlib from 'zlib'; -import { promisify } from 'util'; import { Status, - DEFAULT_MAX_SEND_MESSAGE_LENGTH, - DEFAULT_MAX_RECEIVE_MESSAGE_LENGTH, - LogVerbosity, } from './constants'; import { Deserialize, Serialize } from './make-client'; import { Metadata } from './metadata'; -import { StreamDecoder } from './stream-decoder'; import { ObjectReadable, ObjectWritable } from './object-stream'; -import { ChannelOptions } from './channel-options'; -import * as logging from './logging'; import { StatusObject, PartialStatusObject } from './call-interface'; import { Deadline } from './deadline'; -import { getErrorCode, getErrorMessage } from './error'; - -const TRACER_NAME = 'server_call'; -const unzip = promisify(zlib.unzip); -const inflate = promisify(zlib.inflate); - -function trace(text: string): void { - logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text); -} - -interface DeadlineUnitIndexSignature { - [name: string]: number; -} - -const GRPC_ACCEPT_ENCODING_HEADER = 'grpc-accept-encoding'; -const GRPC_ENCODING_HEADER = 'grpc-encoding'; -const GRPC_MESSAGE_HEADER = 'grpc-message'; -const GRPC_STATUS_HEADER = 'grpc-status'; -const GRPC_TIMEOUT_HEADER = 'grpc-timeout'; -const DEADLINE_REGEX = /(\d{1,8})\s*([HMSmun])/; -const deadlineUnitsToMs: DeadlineUnitIndexSignature = { - H: 3600000, - M: 60000, - S: 1000, - m: 1, - u: 0.001, - n: 0.000001, -}; -const defaultCompressionHeaders = { - // TODO(cjihrig): Remove these encoding headers from the default response - // once compression is integrated. - [GRPC_ACCEPT_ENCODING_HEADER]: 'identity,deflate,gzip', - [GRPC_ENCODING_HEADER]: 'identity', -}; -const defaultResponseHeaders = { - [http2.constants.HTTP2_HEADER_STATUS]: http2.constants.HTTP_STATUS_OK, - [http2.constants.HTTP2_HEADER_CONTENT_TYPE]: 'application/grpc+proto', -}; -const defaultResponseOptions = { - waitForTrailers: true, -} as http2.ServerStreamResponseOptions; +import { ServerInterceptingCallInterface } from './server-interceptors'; export type ServerStatusResponse = Partial; @@ -105,6 +56,38 @@ export type ServerDuplexStream = ServerSurfaceCall & ObjectReadable & ObjectWritable & { end: (metadata?: Metadata) => void }; +export function serverErrorToStatus(error: ServerErrorResponse | ServerStatusResponse, extraTrailers?: Metadata | undefined): PartialStatusObject { + const status: PartialStatusObject = { + code: Status.UNKNOWN, + details: 'message' in error ? error.message : 'Unknown Error', + metadata: + 'metadata' in error && error.metadata !== undefined + ? error.metadata + : null, + }; + + if ( + 'code' in error && + typeof error.code === 'number' && + Number.isInteger(error.code) + ) { + status.code = error.code; + + if ('details' in error && typeof error.details === 'string') { + status.details = error.details!; + } + } + + if (extraTrailers) { + if (status.metadata) { + status.metadata.merge(extraTrailers); + } else { + status.metadata = extraTrailers; + } + } + return status; +} + export class ServerUnaryCallImpl extends EventEmitter implements ServerUnaryCall @@ -112,13 +95,13 @@ export class ServerUnaryCallImpl cancelled: boolean; constructor( - private call: Http2ServerCallStream, + private path: string, + private call: ServerInterceptingCallInterface, public metadata: Metadata, public request: RequestType ) { super(); this.cancelled = false; - this.call.setupSurfaceCall(this); } getPeer(): string { @@ -134,7 +117,7 @@ export class ServerUnaryCallImpl } getPath(): string { - return this.call.getPath(); + return this.path; } } @@ -145,23 +128,16 @@ export class ServerReadableStreamImpl cancelled: boolean; constructor( - private call: Http2ServerCallStream, - public metadata: Metadata, - public deserialize: Deserialize, - encoding: string + private path: string, + private call: ServerInterceptingCallInterface, + public metadata: Metadata ) { super({ objectMode: true }); this.cancelled = false; - this.call.setupSurfaceCall(this); - this.call.setupReadable(this, encoding); } _read(size: number) { - if (!this.call.consumeUnpushedMessages(this)) { - return; - } - - this.call.resume(); + this.call.startRead(); } getPeer(): string { @@ -177,7 +153,7 @@ export class ServerReadableStreamImpl } getPath(): string { - return this.call.getPath(); + return this.path; } } @@ -187,20 +163,23 @@ export class ServerWritableStreamImpl { cancelled: boolean; private trailingMetadata: Metadata; + private pendingStatus: PartialStatusObject = { + code: Status.OK, + details: 'OK' + }; constructor( - private call: Http2ServerCallStream, + private path: string, + private call: ServerInterceptingCallInterface, public metadata: Metadata, - public serialize: Serialize, public request: RequestType ) { super({ objectMode: true }); this.cancelled = false; this.trailingMetadata = new Metadata(); - this.call.setupSurfaceCall(this); this.on('error', err => { - this.call.sendError(err); + this.pendingStatus = serverErrorToStatus(err); this.end(); }); } @@ -218,7 +197,7 @@ export class ServerWritableStreamImpl } getPath(): string { - return this.call.getPath(); + return this.path; } _write( @@ -227,28 +206,13 @@ export class ServerWritableStreamImpl // eslint-disable-next-line @typescript-eslint/no-explicit-any callback: (...args: any[]) => void ) { - try { - const response = this.call.serializeMessage(chunk); - - if (!this.call.write(response)) { - this.call.once('drain', callback); - return; - } - } catch (err) { - this.emit('error', { - details: getErrorMessage(err), - code: Status.INTERNAL, - }); - } - - callback(); + this.call.sendMessage(chunk, callback); } _final(callback: Function): void { this.call.sendStatus({ - code: Status.OK, - details: 'OK', - metadata: this.trailingMetadata, + ...this.pendingStatus, + metadata: this.pendingStatus.metadata ?? this.trailingMetadata, }); callback(null); } @@ -268,27 +232,23 @@ export class ServerDuplexStreamImpl implements ServerDuplexStream { cancelled: boolean; - /* This field appears to be unsued, but it is actually used in _final, which is assiged from - * ServerWritableStreamImpl.prototype._final below. */ - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore noUnusedLocals private trailingMetadata: Metadata; + private pendingStatus: PartialStatusObject = { + code: Status.OK, + details: 'OK' + }; constructor( - private call: Http2ServerCallStream, - public metadata: Metadata, - public serialize: Serialize, - public deserialize: Deserialize, - encoding: string + private path: string, + private call: ServerInterceptingCallInterface, + public metadata: Metadata ) { super({ objectMode: true }); this.cancelled = false; this.trailingMetadata = new Metadata(); - this.call.setupSurfaceCall(this); - this.call.setupReadable(this, encoding); this.on('error', err => { - this.call.sendError(err); + this.pendingStatus = serverErrorToStatus(err); this.end(); }); } @@ -306,7 +266,28 @@ export class ServerDuplexStreamImpl } getPath(): string { - return this.call.getPath(); + return this.path; + } + + _read(size: number) { + this.call.startRead(); + } + + _write( + chunk: ResponseType, + encoding: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + callback: (...args: any[]) => void + ) { + this.call.sendMessage(chunk, callback); + } + + _final(callback: Function): void { + this.call.sendStatus({ + ...this.pendingStatus, + metadata: this.pendingStatus.metadata ?? this.trailingMetadata, + }); + callback(null); } // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -319,13 +300,6 @@ export class ServerDuplexStreamImpl } } -ServerDuplexStreamImpl.prototype._read = - ServerReadableStreamImpl.prototype._read; -ServerDuplexStreamImpl.prototype._write = - ServerWritableStreamImpl.prototype._write; -ServerDuplexStreamImpl.prototype._final = - ServerWritableStreamImpl.prototype._final; - // Unary response callback signature. export type sendUnaryData = ( error: ServerErrorResponse | ServerStatusResponse | null, @@ -401,597 +375,3 @@ export type Handler = | BidiStreamingHandler; export type HandlerType = 'bidi' | 'clientStream' | 'serverStream' | 'unary'; - -// Internal class that wraps the HTTP2 request. -export class Http2ServerCallStream< - RequestType, - ResponseType -> extends EventEmitter { - cancelled = false; - deadlineTimer: NodeJS.Timeout | null = null; - private statusSent = false; - private deadline: Deadline = Infinity; - private wantTrailers = false; - private metadataSent = false; - private canPush = false; - private isPushPending = false; - private bufferedMessages: Array = []; - private messagesToPush: Array = []; - private maxSendMessageSize: number = DEFAULT_MAX_SEND_MESSAGE_LENGTH; - private maxReceiveMessageSize: number = DEFAULT_MAX_RECEIVE_MESSAGE_LENGTH; - - constructor( - private stream: http2.ServerHttp2Stream, - private handler: Handler, - options: ChannelOptions - ) { - super(); - - this.stream.once('error', (err: ServerErrorResponse) => { - /* We need an error handler to avoid uncaught error event exceptions, but - * there is nothing we can reasonably do here. Any error event should - * have a corresponding close event, which handles emitting the cancelled - * event. And the stream is now in a bad state, so we can't reasonably - * expect to be able to send an error over it. */ - }); - - this.stream.once('close', () => { - trace( - 'Request to method ' + - this.handler?.path + - ' stream closed with rstCode ' + - this.stream.rstCode - ); - - if (!this.statusSent) { - this.cancelled = true; - this.emit('cancelled', 'cancelled'); - this.emit('streamEnd', false); - this.sendStatus({ - code: Status.CANCELLED, - details: 'Cancelled by client', - metadata: null, - }); - if (this.deadlineTimer) clearTimeout(this.deadlineTimer); - } - }); - - this.stream.on('drain', () => { - this.emit('drain'); - }); - - if ('grpc.max_send_message_length' in options) { - this.maxSendMessageSize = options['grpc.max_send_message_length']!; - } - if ('grpc.max_receive_message_length' in options) { - this.maxReceiveMessageSize = options['grpc.max_receive_message_length']!; - } - } - - private checkCancelled(): boolean { - /* In some cases the stream can become destroyed before the close event - * fires. That creates a race condition that this check works around */ - if (this.stream.destroyed || this.stream.closed) { - this.cancelled = true; - } - return this.cancelled; - } - - private getDecompressedMessage( - message: Buffer, - encoding: string - ): Buffer | Promise { - if (encoding === 'deflate') { - return inflate(message.subarray(5)); - } else if (encoding === 'gzip') { - return unzip(message.subarray(5)); - } else if (encoding === 'identity') { - return message.subarray(5); - } - - return Promise.reject({ - code: Status.UNIMPLEMENTED, - details: `Received message compressed with unsupported encoding "${encoding}"`, - }); - } - - sendMetadata(customMetadata?: Metadata) { - if (this.checkCancelled()) { - return; - } - - if (this.metadataSent) { - return; - } - - this.metadataSent = true; - const custom = customMetadata ? customMetadata.toHttp2Headers() : null; - // TODO(cjihrig): Include compression headers. - const headers = { - ...defaultResponseHeaders, - ...defaultCompressionHeaders, - ...custom, - }; - this.stream.respond(headers, defaultResponseOptions); - } - - receiveMetadata(headers: http2.IncomingHttpHeaders) { - const metadata = Metadata.fromHttp2Headers(headers); - - if (logging.isTracerEnabled(TRACER_NAME)) { - trace( - 'Request to ' + - this.handler.path + - ' received headers ' + - JSON.stringify(metadata.toJSON()) - ); - } - - // TODO(cjihrig): Receive compression metadata. - - const timeoutHeader = metadata.get(GRPC_TIMEOUT_HEADER); - - if (timeoutHeader.length > 0) { - const match = timeoutHeader[0].toString().match(DEADLINE_REGEX); - - if (match === null) { - const err = new Error('Invalid deadline') as ServerErrorResponse; - err.code = Status.OUT_OF_RANGE; - this.sendError(err); - return metadata; - } - - const timeout = (+match[1] * deadlineUnitsToMs[match[2]]) | 0; - - const now = new Date(); - this.deadline = now.setMilliseconds(now.getMilliseconds() + timeout); - this.deadlineTimer = setTimeout(handleExpiredDeadline, timeout, this); - metadata.remove(GRPC_TIMEOUT_HEADER); - } - - // Remove several headers that should not be propagated to the application - metadata.remove(http2.constants.HTTP2_HEADER_ACCEPT_ENCODING); - metadata.remove(http2.constants.HTTP2_HEADER_TE); - metadata.remove(http2.constants.HTTP2_HEADER_CONTENT_TYPE); - metadata.remove('grpc-accept-encoding'); - - return metadata; - } - - receiveUnaryMessage(encoding: string): Promise { - return new Promise((resolve, reject) => { - const { stream } = this; - - let receivedLength = 0; - - // eslint-disable-next-line @typescript-eslint/no-this-alias - const call = this; - const body: Buffer[] = []; - const limit = this.maxReceiveMessageSize; - - this.stream.on('data', onData); - this.stream.on('end', onEnd); - this.stream.on('error', onEnd); - - function onData(chunk: Buffer) { - receivedLength += chunk.byteLength; - - if (limit !== -1 && receivedLength > limit) { - stream.removeListener('data', onData); - stream.removeListener('end', onEnd); - stream.removeListener('error', onEnd); - - reject({ - code: Status.RESOURCE_EXHAUSTED, - details: `Received message larger than max (${receivedLength} vs. ${limit})`, - }); - return; - } - - body.push(chunk); - } - - function onEnd(err?: Error) { - stream.removeListener('data', onData); - stream.removeListener('end', onEnd); - stream.removeListener('error', onEnd); - - if (err !== undefined) { - reject({ code: Status.INTERNAL, details: err.message }); - return; - } - - if (receivedLength === 0) { - reject({ - code: Status.INTERNAL, - details: 'received empty unary message', - }); - return; - } - - call.emit('receiveMessage'); - - const requestBytes = Buffer.concat(body, receivedLength); - const compressed = requestBytes.readUInt8(0) === 1; - const compressedMessageEncoding = compressed ? encoding : 'identity'; - const decompressedMessage = call.getDecompressedMessage( - requestBytes, - compressedMessageEncoding - ); - - if (Buffer.isBuffer(decompressedMessage)) { - resolve( - call.deserializeMessageWithInternalError(decompressedMessage) - ); - return; - } - - decompressedMessage.then( - decompressed => - resolve(call.deserializeMessageWithInternalError(decompressed)), - (err: any) => - reject( - err.code - ? err - : { - code: Status.INTERNAL, - details: `Received "grpc-encoding" header "${encoding}" but ${encoding} decompression failed`, - } - ) - ); - } - }); - } - - private async deserializeMessageWithInternalError(buffer: Buffer) { - try { - return this.deserializeMessage(buffer); - } catch (err) { - throw { - details: getErrorMessage(err), - code: Status.INTERNAL, - }; - } - } - - serializeMessage(value: ResponseType) { - const messageBuffer = this.handler.serialize(value); - - // TODO(cjihrig): Call compression aware serializeMessage(). - const byteLength = messageBuffer.byteLength; - const output = Buffer.allocUnsafe(byteLength + 5); - output.writeUInt8(0, 0); - output.writeUInt32BE(byteLength, 1); - messageBuffer.copy(output, 5); - return output; - } - - deserializeMessage(bytes: Buffer) { - return this.handler.deserialize(bytes); - } - - async sendUnaryMessage( - err: ServerErrorResponse | ServerStatusResponse | null, - value?: ResponseType | null, - metadata?: Metadata | null, - flags?: number - ) { - if (this.checkCancelled()) { - return; - } - - if (metadata === undefined) { - metadata = null; - } - - if (err) { - if (!Object.prototype.hasOwnProperty.call(err, 'metadata') && metadata) { - err.metadata = metadata; - } - this.sendError(err); - return; - } - - try { - const response = this.serializeMessage(value!); - - this.write(response); - this.sendStatus({ code: Status.OK, details: 'OK', metadata }); - } catch (err) { - this.sendError({ - details: getErrorMessage(err), - code: Status.INTERNAL, - }); - } - } - - sendStatus(statusObj: PartialStatusObject) { - this.emit('callEnd', statusObj.code); - this.emit('streamEnd', statusObj.code === Status.OK); - if (this.checkCancelled()) { - return; - } - - trace( - 'Request to method ' + - this.handler?.path + - ' ended with status code: ' + - Status[statusObj.code] + - ' details: ' + - statusObj.details - ); - - if (this.deadlineTimer) clearTimeout(this.deadlineTimer); - - if (this.stream.headersSent) { - if (!this.wantTrailers) { - this.wantTrailers = true; - this.stream.once('wantTrailers', () => { - const trailersToSend = { - [GRPC_STATUS_HEADER]: statusObj.code, - [GRPC_MESSAGE_HEADER]: encodeURI(statusObj.details), - ...statusObj.metadata?.toHttp2Headers(), - }; - - this.stream.sendTrailers(trailersToSend); - this.statusSent = true; - }); - this.stream.end(); - } - } else { - // Trailers-only response - const trailersToSend = { - [GRPC_STATUS_HEADER]: statusObj.code, - [GRPC_MESSAGE_HEADER]: encodeURI(statusObj.details), - ...defaultResponseHeaders, - ...statusObj.metadata?.toHttp2Headers(), - }; - this.stream.respond(trailersToSend, { endStream: true }); - this.statusSent = true; - } - } - - sendError(error: ServerErrorResponse | ServerStatusResponse) { - const status: PartialStatusObject = { - code: Status.UNKNOWN, - details: 'message' in error ? error.message : 'Unknown Error', - metadata: - 'metadata' in error && error.metadata !== undefined - ? error.metadata - : null, - }; - - if ( - 'code' in error && - typeof error.code === 'number' && - Number.isInteger(error.code) - ) { - status.code = error.code; - - if ('details' in error && typeof error.details === 'string') { - status.details = error.details!; - } - } - - this.sendStatus(status); - } - - write(chunk: Buffer) { - if (this.checkCancelled()) { - return; - } - - if ( - this.maxSendMessageSize !== -1 && - chunk.length > this.maxSendMessageSize - ) { - this.sendError({ - code: Status.RESOURCE_EXHAUSTED, - details: `Sent message larger than max (${chunk.length} vs. ${this.maxSendMessageSize})`, - }); - return; - } - - this.sendMetadata(); - this.emit('sendMessage'); - return this.stream.write(chunk); - } - - resume() { - this.stream.resume(); - } - - setupSurfaceCall(call: ServerSurfaceCall) { - this.once('cancelled', reason => { - call.cancelled = true; - call.emit('cancelled', reason); - }); - - this.once('callEnd', status => call.emit('callEnd', status)); - } - - setupReadable( - readable: - | ServerReadableStream - | ServerDuplexStream, - encoding: string - ) { - const decoder = new StreamDecoder(); - - let readsDone = false; - - let pendingMessageProcessing = false; - - let pushedEnd = false; - - const maybePushEnd = async () => { - if (!pushedEnd && readsDone && !pendingMessageProcessing) { - pushedEnd = true; - await this.pushOrBufferMessage(readable, null); - } - }; - - this.stream.on('data', async (data: Buffer) => { - const messages = decoder.write(data); - - pendingMessageProcessing = true; - this.stream.pause(); - for (const message of messages) { - if ( - this.maxReceiveMessageSize !== -1 && - message.length > this.maxReceiveMessageSize - ) { - this.sendError({ - code: Status.RESOURCE_EXHAUSTED, - details: `Received message larger than max (${message.length} vs. ${this.maxReceiveMessageSize})`, - }); - return; - } - this.emit('receiveMessage'); - - const compressed = message.readUInt8(0) === 1; - const compressedMessageEncoding = compressed ? encoding : 'identity'; - const decompressedMessage = await this.getDecompressedMessage( - message, - compressedMessageEncoding - ); - - // Encountered an error with decompression; it'll already have been propogated back - // Just return early - if (!decompressedMessage) return; - - await this.pushOrBufferMessage(readable, decompressedMessage); - } - pendingMessageProcessing = false; - this.stream.resume(); - await maybePushEnd(); - }); - - this.stream.once('end', async () => { - readsDone = true; - await maybePushEnd(); - }); - } - - consumeUnpushedMessages( - readable: - | ServerReadableStream - | ServerDuplexStream - ): boolean { - this.canPush = true; - - while (this.messagesToPush.length > 0) { - const nextMessage = this.messagesToPush.shift(); - const canPush = readable.push(nextMessage); - - if (nextMessage === null || canPush === false) { - this.canPush = false; - break; - } - } - - return this.canPush; - } - - private async pushOrBufferMessage( - readable: - | ServerReadableStream - | ServerDuplexStream, - messageBytes: Buffer | null - ): Promise { - if (this.isPushPending) { - this.bufferedMessages.push(messageBytes); - } else { - await this.pushMessage(readable, messageBytes); - } - } - - private async pushMessage( - readable: - | ServerReadableStream - | ServerDuplexStream, - messageBytes: Buffer | null - ) { - if (messageBytes === null) { - trace('Received end of stream'); - if (this.canPush) { - readable.push(null); - } else { - this.messagesToPush.push(null); - } - - return; - } - - trace('Received message of length ' + messageBytes.length); - - this.isPushPending = true; - - try { - const deserialized = await this.deserializeMessage(messageBytes); - - if (this.canPush) { - if (!readable.push(deserialized)) { - this.canPush = false; - this.stream.pause(); - } - } else { - this.messagesToPush.push(deserialized); - } - } catch (error) { - // Ignore any remaining messages when errors occur. - this.bufferedMessages.length = 0; - let code = getErrorCode(error); - if (code === null || code < Status.OK || code > Status.UNAUTHENTICATED) { - code = Status.INTERNAL; - } - - readable.emit('error', { - details: getErrorMessage(error), - code: code, - }); - } - - this.isPushPending = false; - - if (this.bufferedMessages.length > 0) { - await this.pushMessage( - readable, - this.bufferedMessages.shift() as Buffer | null - ); - } - } - - getPeer(): string { - const socket = this.stream.session?.socket; - if (socket?.remoteAddress) { - if (socket.remotePort) { - return `${socket.remoteAddress}:${socket.remotePort}`; - } else { - return socket.remoteAddress; - } - } else { - return 'unknown'; - } - } - - getDeadline(): Deadline { - return this.deadline; - } - - getPath(): string { - return this.handler.path; - } -} - -/* eslint-disable @typescript-eslint/no-explicit-any */ -type UntypedServerCall = Http2ServerCallStream; - -function handleExpiredDeadline(call: UntypedServerCall) { - const err = new Error('Deadline exceeded') as ServerErrorResponse; - err.code = Status.DEADLINE_EXCEEDED; - - call.sendError(err); - call.cancelled = true; - call.emit('cancelled', 'deadline'); -} diff --git a/packages/grpc-js/src/server-interceptors.ts b/packages/grpc-js/src/server-interceptors.ts new file mode 100644 index 000000000..c03f3028c --- /dev/null +++ b/packages/grpc-js/src/server-interceptors.ts @@ -0,0 +1,886 @@ +/* + * Copyright 2024 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 { PartialStatusObject} from "./call-interface"; +import { ServerMethodDefinition } from "./make-client"; +import { Metadata } from "./metadata"; +import { ChannelOptions } from "./channel-options"; +import { Handler, ServerErrorResponse } from "./server-call"; +import { Deadline } from "./deadline"; +import { DEFAULT_MAX_RECEIVE_MESSAGE_LENGTH, DEFAULT_MAX_SEND_MESSAGE_LENGTH, LogVerbosity, Status } from "./constants"; +import * as http2 from 'http2'; +import { getErrorMessage } from "./error"; +import * as zlib from 'zlib'; +import { promisify } from "util"; +import { StreamDecoder } from "./stream-decoder"; +import { CallEventTracker } from "./transport"; +import * as logging from './logging'; + +const unzip = promisify(zlib.unzip); +const inflate = promisify(zlib.inflate); + +const TRACER_NAME = 'server_call'; + +function trace(text: string) { + logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text); +} + +export interface ServerMetadataListener { + (metadata: Metadata, next: (metadata: Metadata) => void): void; +} + +export interface ServerMessageListener { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: any, next: (message: any) => void): void; +} + +export interface ServerHalfCloseListener { + (next: () => void): void; +} + +export interface ServerCancelListener { + (): void; +} + +export interface FullServerListener { + onReceiveMetadata: ServerMetadataListener; + onReceiveMessage: ServerMessageListener; + onReceiveHalfClose: ServerHalfCloseListener; + onCancel: ServerCancelListener; +} + +export type ServerListener = Partial; + +export class ServerListenerBuilder { + private metadata: ServerMetadataListener | undefined = undefined; + private message: ServerMessageListener | undefined = undefined; + private halfClose: ServerHalfCloseListener | undefined = undefined; + private cancel: ServerCancelListener | undefined = undefined; + + withOnReceiveMetadata(onReceiveMetadata: ServerMetadataListener): this { + this.metadata = onReceiveMetadata; + return this; + } + + withOnReceiveMessage(onReceiveMessage: ServerMessageListener): this { + this.message = onReceiveMessage; + return this; + } + + withOnReceiveHalfClose(onReceiveHalfClose: ServerHalfCloseListener): this { + this.halfClose = onReceiveHalfClose; + return this; + } + + withOnCancel(onCancel: ServerCancelListener): this { + this.cancel = onCancel; + return this; + } + + build(): ServerListener { + return { + onReceiveMetadata: this.metadata, + onReceiveMessage: this.message, + onReceiveHalfClose: this.halfClose, + onCancel: this.cancel + }; + } +} + +export interface InterceptingServerListener { + onReceiveMetadata(metadata: Metadata): void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onReceiveMessage(message: any): void; + onReceiveHalfClose(): void; + onCancel(): void; +} + +export function isInterceptingServerListener(listener: ServerListener | InterceptingServerListener): listener is InterceptingServerListener { + return listener.onReceiveMetadata !== undefined && listener.onReceiveMetadata.length === 1; +} + +class InterceptingServerListenerImpl implements InterceptingServerListener { + /** + * Once the call is cancelled, ignore all other events. + */ + private cancelled: boolean = false; + private processingMetadata: boolean = false; + private hasPendingMessage: boolean = false; + private pendingMessage: any = null; + private processingMessage: boolean = false; + private hasPendingHalfClose: boolean = false; + + constructor(private listener: FullServerListener, private nextListener: InterceptingServerListener) {} + + private processPendingMessage() { + if (this.hasPendingMessage) { + this.nextListener.onReceiveMessage(this.pendingMessage); + this.pendingMessage = null; + this.hasPendingMessage = false; + } + } + + private processPendingHalfClose() { + if (this.hasPendingHalfClose) { + this.nextListener.onReceiveHalfClose(); + this.hasPendingHalfClose = false; + } + } + + onReceiveMetadata(metadata: Metadata): void { + if (this.cancelled) { + return; + } + this.processingMetadata = true; + this.listener.onReceiveMetadata(metadata, interceptedMetadata => { + this.processingMetadata = false; + if (this.cancelled) { + return; + } + this.nextListener.onReceiveMetadata(interceptedMetadata); + this.processPendingMessage(); + this.processPendingHalfClose(); + }); + } + onReceiveMessage(message: any): void { + if (this.cancelled) { + return; + } + this.processingMessage = true; + this.listener.onReceiveMessage(message, msg => { + this.processingMessage = false; + if (this.cancelled) { + return; + } + if (this.processingMetadata) { + this.pendingMessage = msg; + this.hasPendingMessage = true; + } else { + this.nextListener.onReceiveMessage(msg); + this.processPendingHalfClose(); + } + }); + } + onReceiveHalfClose(): void { + if (this.cancelled) { + return; + } + this.listener.onReceiveHalfClose(() => { + if (this.cancelled) { + return; + } + if (this.processingMetadata || this.processingMessage) { + this.hasPendingHalfClose = true; + } else { + this.nextListener.onReceiveHalfClose(); + } + }); + } + onCancel(): void { + this.cancelled = true; + this.listener.onCancel(); + this.nextListener.onCancel(); + } + +} + +export interface StartResponder { + (next: (listener?: ServerListener) => void): void; +} + +export interface MetadataResponder { + (metadata: Metadata, next: (metadata: Metadata) => void): void; +} + +export interface MessageResponder { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: any, next: (message: any) => void): void; +} + +export interface StatusResponder { + (status: PartialStatusObject, next: (status: PartialStatusObject) => void): void; +} + +export interface FullResponder { + start: StartResponder; + sendMetadata: MetadataResponder; + sendMessage: MessageResponder; + sendStatus: StatusResponder; +} + +export type Responder = Partial; + +export class ResponderBuilder { + private start: StartResponder | undefined = undefined; + private metadata: MetadataResponder | undefined = undefined; + private message: MessageResponder | undefined = undefined; + private status: StatusResponder | undefined = undefined; + + withStart(start: StartResponder): this { + this.start = start; + return this; + } + + withSendMetadata(sendMetadata: MetadataResponder): this { + this.metadata = sendMetadata; + return this; + } + + withSendMessage(sendMessage: MessageResponder): this { + this.message = sendMessage; + return this; + } + + withSendStatus(sendStatus: StatusResponder): this { + this.status = sendStatus; + return this; + } + + build(): Responder { + return { + start: this.start, + sendMetadata: this.metadata, + sendMessage: this.message, + sendStatus: this.status + }; + } +} + +const defaultServerListener: FullServerListener = { + onReceiveMetadata: (metadata, next) => { + next(metadata); + }, + onReceiveMessage: (message, next) => { + next(message); + }, + onReceiveHalfClose: next => { + next(); + }, + onCancel: () => {} +}; + +const defaultResponder: FullResponder = { + start: (next) => { + next(); + }, + sendMetadata: (metadata, next) => { + next(metadata); + }, + sendMessage: (message, next) => { + next(message); + }, + sendStatus: (status, next) => { + next(status); + } +}; + +export interface ServerInterceptingCallInterface { + /** + * Register the listener to handle inbound events. + */ + start(listener: InterceptingServerListener): void; + /** + * Send response metadata. + */ + sendMetadata(metadata: Metadata): void; + /** + * Send a response message. + */ + sendMessage(message: any, callback: () => void): void; + /** + * End the call by sending this status. + */ + sendStatus(status: PartialStatusObject): void; + /** + * Start a single read, eventually triggering either listener.onReceiveMessage or listener.onReceiveHalfClose. + */ + startRead(): void; + /** + * Return the peer address of the client making the request, if known, or "unknown" otherwise + */ + getPeer(): string; + /** + * Return the call deadline set by the client. The value is Infinity if there is no deadline. + */ + getDeadline(): Deadline; +} + +export class ServerInterceptingCall implements ServerInterceptingCallInterface { + private responder: FullResponder; + private processingMetadata: boolean = false; + private processingMessage: boolean = false; + private pendingMessage: any = null; + private pendingMessageCallback: (() => void) | null = null; + private pendingStatus: PartialStatusObject | null = null; + constructor(private nextCall: ServerInterceptingCallInterface, responder?: Responder) { + this.responder = {...defaultResponder, ...responder}; + } + + private processPendingMessage() { + if (this.pendingMessageCallback) { + this.nextCall.sendMessage(this.pendingMessage, this.pendingMessageCallback); + this.pendingMessage = null; + this.pendingMessageCallback = null; + } + } + + private processPendingStatus() { + if (this.pendingStatus) { + this.nextCall.sendStatus(this.pendingStatus); + this.pendingStatus = null; + } + } + + start(listener: InterceptingServerListener): void { + this.responder.start(interceptedListener => { + const fullInterceptedListener: FullServerListener = {...defaultServerListener, ...interceptedListener}; + const finalInterceptingListener = new InterceptingServerListenerImpl(fullInterceptedListener, listener); + this.nextCall.start(finalInterceptingListener); + }); + } + sendMetadata(metadata: Metadata): void { + this.processingMetadata = true; + this.responder.sendMetadata(metadata, interceptedMetadata => { + this.processingMetadata = false; + this.nextCall.sendMetadata(interceptedMetadata); + this.processPendingMessage(); + this.processPendingStatus(); + }); + } + sendMessage(message: any, callback: () => void): void { + this.processingMessage = true; + this.responder.sendMessage(message, interceptedMessage => { + this.processingMessage = false; + if (this.processingMetadata) { + this.pendingMessage = interceptedMessage; + this.pendingMessageCallback = callback; + } else { + this.nextCall.sendMessage(interceptedMessage, callback); + } + }); + } + sendStatus(status: PartialStatusObject): void { + this.responder.sendStatus(status, interceptedStatus => { + if (this.processingMetadata || this.processingMessage) { + this.pendingStatus = interceptedStatus; + } else { + this.nextCall.sendStatus(interceptedStatus); + } + }); + } + startRead(): void { + this.nextCall.startRead(); + } + getPeer(): string { + return this.nextCall.getPeer(); + } + getDeadline(): Deadline { + return this.nextCall.getDeadline(); + } +} + +export interface ServerInterceptor { + (methodDescriptor: ServerMethodDefinition, call: ServerInterceptingCallInterface): ServerInterceptingCall; +} + +interface DeadlineUnitIndexSignature { + [name: string]: number; +} + +const GRPC_ACCEPT_ENCODING_HEADER = 'grpc-accept-encoding'; +const GRPC_ENCODING_HEADER = 'grpc-encoding'; +const GRPC_MESSAGE_HEADER = 'grpc-message'; +const GRPC_STATUS_HEADER = 'grpc-status'; +const GRPC_TIMEOUT_HEADER = 'grpc-timeout'; +const DEADLINE_REGEX = /(\d{1,8})\s*([HMSmun])/; +const deadlineUnitsToMs: DeadlineUnitIndexSignature = { + H: 3600000, + M: 60000, + S: 1000, + m: 1, + u: 0.001, + n: 0.000001, +}; + +const defaultCompressionHeaders = { + // TODO(cjihrig): Remove these encoding headers from the default response + // once compression is integrated. + [GRPC_ACCEPT_ENCODING_HEADER]: 'identity,deflate,gzip', + [GRPC_ENCODING_HEADER]: 'identity', +}; +const defaultResponseHeaders = { + [http2.constants.HTTP2_HEADER_STATUS]: http2.constants.HTTP_STATUS_OK, + [http2.constants.HTTP2_HEADER_CONTENT_TYPE]: 'application/grpc+proto', +}; +const defaultResponseOptions = { + waitForTrailers: true, +} as http2.ServerStreamResponseOptions; + +type ReadQueueEntryType = 'COMPRESSED' | 'READABLE' | 'HALF_CLOSE'; + +interface ReadQueueEntry { + type: ReadQueueEntryType; + compressedMessage: Buffer | null; + parsedMessage: any; +} + +export class BaseServerInterceptingCall implements ServerInterceptingCallInterface { + private listener: InterceptingServerListener | null = null; + private metadata: Metadata; + private deadlineTimer: NodeJS.Timeout | null = null; + private deadline: Deadline = Infinity; + private maxSendMessageSize: number = DEFAULT_MAX_SEND_MESSAGE_LENGTH; + private maxReceiveMessageSize: number = DEFAULT_MAX_RECEIVE_MESSAGE_LENGTH; + private cancelled = false; + private metadataSent = false; + private wantTrailers = false; + private cancelNotified = false; + private incomingEncoding: string = 'identity'; + private decoder = new StreamDecoder(); + private readQueue: ReadQueueEntry[] = []; + private isReadPending = false; + private receivedHalfClose = false; + private streamEnded = false; + + constructor( + private readonly stream: http2.ServerHttp2Stream, + headers: http2.IncomingHttpHeaders, + private readonly callEventTracker: CallEventTracker | null, + private readonly handler: Handler, + options: ChannelOptions + ) { + this.stream.once('error', (err: ServerErrorResponse) => { + /* We need an error handler to avoid uncaught error event exceptions, but + * there is nothing we can reasonably do here. Any error event should + * have a corresponding close event, which handles emitting the cancelled + * event. And the stream is now in a bad state, so we can't reasonably + * expect to be able to send an error over it. */ + }); + + this.stream.once('close', () => { + trace( + 'Request to method ' + + this.handler?.path + + ' stream closed with rstCode ' + + this.stream.rstCode + ); + + if (this.callEventTracker && !this.streamEnded) { + this.streamEnded = true; + this.callEventTracker.onStreamEnd(false); + this.callEventTracker.onCallEnd({ + code: Status.CANCELLED, + details: 'Stream closed before sending status', + metadata: null + }); + } + + this.notifyOnCancel(); + }); + + this.stream.on('data', (data: Buffer) => { + this.handleDataFrame(data); + }); + this.stream.pause(); + + this.stream.on('end', () => { + this.handleEndEvent(); + }); + + if ('grpc.max_send_message_length' in options) { + this.maxSendMessageSize = options['grpc.max_send_message_length']!; + } + if ('grpc.max_receive_message_length' in options) { + this.maxReceiveMessageSize = options['grpc.max_receive_message_length']!; + } + + const metadata = Metadata.fromHttp2Headers(headers); + + if (logging.isTracerEnabled(TRACER_NAME)) { + trace( + 'Request to ' + + this.handler.path + + ' received headers ' + + JSON.stringify(metadata.toJSON()) + ); + } + + const timeoutHeader = metadata.get(GRPC_TIMEOUT_HEADER); + + if (timeoutHeader.length > 0) { + this.handleTimeoutHeader(timeoutHeader[0] as string); + } + + const encodingHeader = metadata.get(GRPC_ENCODING_HEADER); + + if (encodingHeader.length > 0) { + this.incomingEncoding = encodingHeader[0] as string; + } + + // Remove several headers that should not be propagated to the application + metadata.remove(GRPC_TIMEOUT_HEADER); + metadata.remove(GRPC_ENCODING_HEADER); + metadata.remove(GRPC_ACCEPT_ENCODING_HEADER); + metadata.remove(http2.constants.HTTP2_HEADER_ACCEPT_ENCODING); + metadata.remove(http2.constants.HTTP2_HEADER_TE); + metadata.remove(http2.constants.HTTP2_HEADER_CONTENT_TYPE); + this.metadata = metadata; + } + + private handleTimeoutHeader(timeoutHeader: string) { + const match = timeoutHeader.toString().match(DEADLINE_REGEX); + + if (match === null) { + const status: PartialStatusObject = { + code: Status.INTERNAL, + details: `Invalid ${GRPC_TIMEOUT_HEADER} value "${timeoutHeader}"`, + metadata: null + }; + // Wait for the constructor to complete before sending the error. + process.nextTick(() => { + this.sendStatus(status); + }); + return; + } + + const timeout = (+match[1] * deadlineUnitsToMs[match[2]]) | 0; + + const now = new Date(); + this.deadline = now.setMilliseconds(now.getMilliseconds() + timeout); + this.deadlineTimer = setTimeout(() => { + const status: PartialStatusObject = { + code: Status.DEADLINE_EXCEEDED, + details: 'Deadline exceeded', + metadata: null + }; + this.sendStatus(status); + }, timeout); + + } + + private checkCancelled(): boolean { + /* In some cases the stream can become destroyed before the close event + * fires. That creates a race condition that this check works around */ + if (!this.cancelled && (this.stream.destroyed || this.stream.closed)) { + this.notifyOnCancel(); + this.cancelled = true; + } + return this.cancelled; + } + private notifyOnCancel() { + if (this.cancelNotified) { + return; + } + this.cancelNotified = true; + this.cancelled = true; + process.nextTick(() => { + this.listener?.onCancel(); + }); + if (this.deadlineTimer) { + clearTimeout(this.deadlineTimer); + } + // Flush incoming data frames + this.stream.resume(); + } + + /** + * A server handler can start sending messages without explicitly sending + * metadata. In that case, we need to send headers before sending any + * messages. This function does that if necessary. + */ + private maybeSendMetadata() { + if (!this.metadataSent) { + this.sendMetadata(new Metadata()); + } + } + + /** + * Serialize a message to a length-delimited byte string. + * @param value + * @returns + */ + private serializeMessage(value: any) { + const messageBuffer = this.handler.serialize(value); + const byteLength = messageBuffer.byteLength; + const output = Buffer.allocUnsafe(byteLength + 5); + /* Note: response compression is currently not supported, so this + * compressed bit is always 0. */ + output.writeUInt8(0, 0); + output.writeUInt32BE(byteLength, 1); + messageBuffer.copy(output, 5); + return output; + } + + private decompressMessage( + message: Buffer, + encoding: string + ): Buffer | Promise { + switch (encoding) { + case 'deflate': + return inflate(message.subarray(5)); + case 'gzip': + return unzip(message.subarray(5)); + case 'identity': + return message.subarray(5); + default: + return Promise.reject({ + code: Status.UNIMPLEMENTED, + details: `Received message compressed with unsupported encoding "${encoding}"`, + }); + } + } + + private async decompressAndMaybePush(queueEntry: ReadQueueEntry) { + if (queueEntry.type !== 'COMPRESSED') { + throw new Error(`Invalid queue entry type: ${queueEntry.type}`); + } + + const compressed = queueEntry.compressedMessage!.readUInt8(0) === 1; + const compressedMessageEncoding = compressed ? this.incomingEncoding : 'identity'; + const decompressedMessage = await this.decompressMessage(queueEntry.compressedMessage!, compressedMessageEncoding); + try { + queueEntry.parsedMessage = this.handler.deserialize(decompressedMessage); + } catch (err) { + this.sendStatus({ + code: Status.INTERNAL, + details: `Error deserializing request: ${(err as Error).message}` + }); + return; + } + queueEntry.type = 'READABLE'; + this.maybePushNextMessage(); + } + + private maybePushNextMessage() { + if (this.listener && this.isReadPending && this.readQueue.length > 0 && this.readQueue[0].type !== 'COMPRESSED') { + this.isReadPending = false; + const nextQueueEntry = this.readQueue.shift()!; + if (nextQueueEntry.type === 'READABLE') { + this.listener.onReceiveMessage(nextQueueEntry.parsedMessage); + } else { + // nextQueueEntry.type === 'HALF_CLOSE' + this.listener.onReceiveHalfClose(); + } + } + } + + private handleDataFrame(data: Buffer) { + if (this.checkCancelled()) { + return; + } + trace('Request to ' + this.handler.path + ' received data frame of size ' + data.length); + const rawMessages = this.decoder.write(data); + + for (const messageBytes of rawMessages) { + this.stream.pause(); + if (this.maxReceiveMessageSize !== -1 && messageBytes.length - 5 > this.maxReceiveMessageSize) { + this.sendStatus({ + code: Status.RESOURCE_EXHAUSTED, + details: `Received message larger than max (${messageBytes.length - 5} vs. ${this.maxReceiveMessageSize})`, + metadata: null + }); + return; + } + const queueEntry: ReadQueueEntry = { + type: 'COMPRESSED', + compressedMessage: messageBytes, + parsedMessage: null + }; + this.readQueue.push(queueEntry); + this.decompressAndMaybePush(queueEntry); + this.callEventTracker?.addMessageReceived(); + } + } + private handleEndEvent() { + this.readQueue.push({ + type: 'HALF_CLOSE', + compressedMessage: null, + parsedMessage: null + }); + this.receivedHalfClose = true; + this.maybePushNextMessage(); + } + start(listener: InterceptingServerListener): void { + trace('Request to ' + this.handler.path + ' start called'); + if (this.checkCancelled()) { + return; + } + this.listener = listener; + listener.onReceiveMetadata(this.metadata); + } + sendMetadata(metadata: Metadata): void { + if (this.checkCancelled()) { + return; + } + + if (this.metadataSent) { + return; + } + + this.metadataSent = true; + const custom = metadata ? metadata.toHttp2Headers() : null; + const headers = { + ...defaultResponseHeaders, + ...defaultCompressionHeaders, + ...custom, + }; + this.stream.respond(headers, defaultResponseOptions); + } + sendMessage(message: any, callback: () => void): void { + if (this.checkCancelled()) { + return; + } + let response: Buffer; + try { + response = this.serializeMessage(message); + } catch (e) { + this.sendStatus({ + code: Status.INTERNAL, + details: `Error serializing response: ${getErrorMessage(e)}`, + metadata: null + }); + return; + } + + if ( + this.maxSendMessageSize !== -1 && + response.length - 5 > this.maxSendMessageSize + ) { + this.sendStatus({ + code: Status.RESOURCE_EXHAUSTED, + details: `Sent message larger than max (${response.length} vs. ${this.maxSendMessageSize})`, + metadata: null + }); + return; + } + this.maybeSendMetadata(); + trace('Request to ' + this.handler.path + ' sent data frame of size ' + response.length); + this.stream.write(response, error => { + if (error) { + this.sendStatus({ + code: Status.INTERNAL, + details: `Error writing message: ${getErrorMessage(error)}`, + metadata: null + }); + return; + } + this.callEventTracker?.addMessageSent(); + callback(); + }); + } + sendStatus(status: PartialStatusObject): void { + if (this.checkCancelled()) { + return; + } + this.notifyOnCancel(); + + trace( + 'Request to method ' + + this.handler?.path + + ' ended with status code: ' + + Status[status.code] + + ' details: ' + + status.details + ); + + if (this.stream.headersSent) { + if (!this.wantTrailers) { + this.wantTrailers = true; + this.stream.once('wantTrailers', () => { + if (this.callEventTracker && !this.streamEnded) { + this.streamEnded = true; + this.callEventTracker.onStreamEnd(true); + this.callEventTracker.onCallEnd(status); + } + const trailersToSend = { + [GRPC_STATUS_HEADER]: status.code, + [GRPC_MESSAGE_HEADER]: encodeURI(status.details), + ...status.metadata?.toHttp2Headers(), + }; + + this.stream.sendTrailers(trailersToSend); + }); + this.stream.end(); + } + } else { + if (this.callEventTracker && !this.streamEnded) { + this.streamEnded = true; + this.callEventTracker.onStreamEnd(true); + this.callEventTracker.onCallEnd(status); + } + // Trailers-only response + const trailersToSend = { + [GRPC_STATUS_HEADER]: status.code, + [GRPC_MESSAGE_HEADER]: encodeURI(status.details), + ...defaultResponseHeaders, + ...status.metadata?.toHttp2Headers(), + }; + this.stream.respond(trailersToSend, { endStream: true }); + } + } + startRead(): void { + trace('Request to ' + this.handler.path + ' startRead called'); + if (this.checkCancelled()) { + return; + } + this.isReadPending = true; + if (this.readQueue.length === 0) { + if (!this.receivedHalfClose) { + this.stream.resume(); + } + } else { + this.maybePushNextMessage(); + } + } + getPeer(): string { + const socket = this.stream.session?.socket; + if (socket?.remoteAddress) { + if (socket.remotePort) { + return `${socket.remoteAddress}:${socket.remotePort}`; + } else { + return socket.remoteAddress; + } + } else { + return 'unknown'; + } + } + getDeadline(): Deadline { + return this.deadline; + } +} + +export function getServerInterceptingCall( + interceptors: ServerInterceptor[], + stream: http2.ServerHttp2Stream, + headers: http2.IncomingHttpHeaders, + callEventTracker: CallEventTracker | null, + handler: Handler, + options: ChannelOptions +) { + + const methodDefinition: ServerMethodDefinition = { + path: handler.path, + requestStream: handler.type === 'clientStream' || handler.type === 'bidi', + responseStream: handler.type === 'serverStream' || handler.type === 'bidi', + requestDeserialize: handler.deserialize, + responseSerialize: handler.serialize + } + const baseCall = new BaseServerInterceptingCall(stream, headers, callEventTracker, handler, options); + return interceptors.reduce((call: ServerInterceptingCallInterface, interceptor: ServerInterceptor) => { + return interceptor(methodDefinition, call); + }, baseCall); +} diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index adc7f299f..31851b832 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -28,20 +28,18 @@ import { HandleCall, Handler, HandlerType, - Http2ServerCallStream, sendUnaryData, ServerDuplexStream, ServerDuplexStreamImpl, ServerReadableStream, - ServerReadableStreamImpl, ServerStreamingHandler, ServerUnaryCall, - ServerUnaryCallImpl, ServerWritableStream, ServerWritableStreamImpl, UnaryHandler, ServerErrorResponse, ServerStatusResponse, + serverErrorToStatus, } from './server-call'; import { ServerCredentials } from './server-credentials'; import { ChannelOptions } from './channel-options'; @@ -72,6 +70,9 @@ import { unregisterChannelzRef, } from './channelz'; import { CipherNameAndProtocol, TLSSocket } from 'tls'; +import { ServerInterceptingCallInterface, ServerInterceptor, getServerInterceptingCall } from './server-interceptors'; +import { PartialStatusObject } from './call-interface'; +import { CallEventTracker } from './transport'; const UNLIMITED_CONNECTION_AGE_MS = ~(1 << 31); const KEEPALIVE_MAX_TIME_MS = ~(1 << 31); @@ -109,7 +110,7 @@ function deprecate(message: string) { function getUnimplementedStatusResponse( methodName: string -): Partial { +): PartialStatusObject { return { code: Status.UNIMPLEMENTED, details: `The server does not implement the method ${methodName}`, @@ -219,6 +220,10 @@ interface Http2ServerInfo { sessions: Set; } +export interface ServerOptions extends ChannelOptions { + interceptors?: ServerInterceptor[] +} + export class Server { private boundPorts: Map= new Map(); private http2Servers: Map = new Map(); @@ -234,7 +239,7 @@ export class Server { */ private started = false; private shutdown = false; - private options: ChannelOptions; + private options: ServerOptions; private serverAddressString = 'null'; // Channelz Info @@ -251,13 +256,15 @@ export class Server { private readonly keepaliveTimeMs: number; private readonly keepaliveTimeoutMs: number; + private readonly interceptors: ServerInterceptor[]; + /** * Options that will be used to construct all Http2Server instances for this * Server. */ private commonServerOptions: http2.ServerOptions; - constructor(options?: ChannelOptions) { + constructor(options?: ServerOptions) { this.options = options ?? {}; if (this.options['grpc.enable_channelz'] === 0) { this.channelzEnabled = false; @@ -296,6 +303,7 @@ export class Server { maxConcurrentStreams: this.options['grpc.max_concurrent_streams'], }; } + this.interceptors = this.options.interceptors ?? []; this.trace('Server constructed'); } @@ -1072,23 +1080,24 @@ export class Server { return handler; } - private _respondWithError>( - err: T, + private _respondWithError( + err: PartialStatusObject, stream: http2.ServerHttp2Stream, channelzSessionInfo: ChannelzSessionInfo | null = null ) { - const call = new Http2ServerCallStream(stream, null!, this.options); - - if (err.code === undefined) { - err.code = Status.INTERNAL; - } + const trailersToSend = { + 'grpc-status': err.code ?? Status.INTERNAL, + 'grpc-message': err.details, + [http2.constants.HTTP2_HEADER_STATUS]: http2.constants.HTTP_STATUS_OK, + [http2.constants.HTTP2_HEADER_CONTENT_TYPE]: 'application/grpc+proto', + ...err.metadata?.toHttp2Headers() + }; + stream.respond(trailersToSend, {endStream: true}); if (this.channelzEnabled) { this.callTracker.addCallFailed(); channelzSessionInfo?.streamTracker.addCallFailed(); } - - call.sendError(err); } private _channelzHandler( @@ -1120,39 +1129,44 @@ export class Server { return; } - const call = new Http2ServerCallStream(stream, handler, this.options); - - call.once('callEnd', (code: Status) => { - if (code === Status.OK) { - this.callTracker.addCallSucceeded(); - } else { - this.callTracker.addCallFailed(); - } - }); - - if (channelzSessionInfo) { - call.once('streamEnd', (success: boolean) => { - if (success) { - channelzSessionInfo.streamTracker.addCallSucceeded(); + let callEventTracker: CallEventTracker = { + addMessageSent: () => { + if (channelzSessionInfo) { + channelzSessionInfo.messagesSent += 1; + channelzSessionInfo.lastMessageSentTimestamp = new Date(); + } + }, + addMessageReceived: () => { + if (channelzSessionInfo) { + channelzSessionInfo.messagesReceived += 1; + channelzSessionInfo.lastMessageReceivedTimestamp = new Date(); + } + }, + onCallEnd: status => { + if (status.code === Status.OK) { + this.callTracker.addCallSucceeded(); } else { - channelzSessionInfo.streamTracker.addCallFailed(); + this.callTracker.addCallFailed(); } - }); - call.on('sendMessage', () => { - channelzSessionInfo.messagesSent += 1; - channelzSessionInfo.lastMessageSentTimestamp = new Date(); - }); - call.on('receiveMessage', () => { - channelzSessionInfo.messagesReceived += 1; - channelzSessionInfo.lastMessageReceivedTimestamp = new Date(); - }); + }, + onStreamEnd: success => { + if (channelzSessionInfo) { + if (success) { + channelzSessionInfo.streamTracker.addCallSucceeded(); + } else { + channelzSessionInfo.streamTracker.addCallFailed(); + } + } + } } - if (!this._runHandlerForCall(call, handler, headers)) { + const call = getServerInterceptingCall(this.interceptors, stream, headers, callEventTracker, handler, this.options); + + if (!this._runHandlerForCall(call, handler)) { this.callTracker.addCallFailed(); channelzSessionInfo?.streamTracker.addCallFailed(); - call.sendError({ + call.sendStatus({ code: Status.INTERNAL, details: `Unknown handler type: ${handler.type}`, }); @@ -1179,9 +1193,10 @@ export class Server { return; } - const call = new Http2ServerCallStream(stream, handler, this.options); - if (!this._runHandlerForCall(call, handler, headers)) { - call.sendError({ + const call = getServerInterceptingCall(this.interceptors, stream, headers, null, handler, this.options); + + if (!this._runHandlerForCall(call, handler)) { + call.sendStatus({ code: Status.INTERNAL, details: `Unknown handler type: ${handler.type}`, }); @@ -1189,38 +1204,27 @@ export class Server { } private _runHandlerForCall( - call: Http2ServerCallStream, - handler: Handler, - headers: http2.IncomingHttpHeaders + call: ServerInterceptingCallInterface, + handler: Handler ): boolean { - const metadata = call.receiveMetadata(headers); - const encoding = - (metadata.get('grpc-encoding')[0] as string | undefined) ?? 'identity'; - metadata.remove('grpc-encoding'); const { type } = handler; if (type === 'unary') { - handleUnary(call, handler as UntypedUnaryHandler, metadata, encoding); + handleUnary(call, handler as UntypedUnaryHandler); } else if (type === 'clientStream') { handleClientStreaming( call, - handler as UntypedClientStreamingHandler, - metadata, - encoding + handler as UntypedClientStreamingHandler ); } else if (type === 'serverStream') { handleServerStreaming( call, - handler as UntypedServerStreamingHandler, - metadata, - encoding + handler as UntypedServerStreamingHandler ); } else if (type === 'bidi') { handleBidiStreaming( call, - handler as UntypedBidiStreamingHandler, - metadata, - encoding + handler as UntypedBidiStreamingHandler ); } else { return false; @@ -1365,52 +1369,83 @@ export class Server { } async function handleUnary( - call: Http2ServerCallStream, - handler: UnaryHandler, - metadata: Metadata, - encoding: string + call: ServerInterceptingCallInterface, + handler: UnaryHandler ): Promise { - try { - const request = await call.receiveUnaryMessage(encoding); + let stream: ServerUnaryCall; - if (request === undefined || call.cancelled) { + function respond( + err: ServerErrorResponse | ServerStatusResponse | null, + value?: ResponseType | null, + trailer?: Metadata, + flags?: number + ) { + if (err) { + call.sendStatus(serverErrorToStatus(err, trailer)); return; } + call.sendMessage(value, () => { + call.sendStatus({ + code: Status.OK, + details: 'OK', + metadata: trailer ?? null + }); + }); + } - const emitter = new ServerUnaryCallImpl( - call, - metadata, - request - ); - - handler.func( - emitter, - ( - err: ServerErrorResponse | ServerStatusResponse | null, - value?: ResponseType | null, - trailer?: Metadata, - flags?: number - ) => { - call.sendUnaryMessage(err, value, trailer, flags); + let requestMetadata: Metadata; + let requestMessage: RequestType | null = null; + call.start({ + onReceiveMetadata(metadata) { + requestMetadata = metadata; + call.startRead(); + }, + onReceiveMessage(message) { + if (requestMessage) { + call.sendStatus({ + code: Status.UNIMPLEMENTED, + details: `Received a second request message for server streaming method ${handler.path}`, + metadata: null + }); + return; } - ); - } catch (err) { - call.sendError(err as ServerErrorResponse); - } + requestMessage = message; + call.startRead(); + }, + onReceiveHalfClose() { + if (!requestMessage) { + call.sendStatus({ + code: Status.UNIMPLEMENTED, + details: `Received no request message for server streaming method ${handler.path}`, + metadata: null + }); + return; + } + stream = new ServerWritableStreamImpl(handler.path, call, requestMetadata, requestMessage); + try { + handler.func(stream, respond); + } catch (err) { + call.sendStatus({ + code: Status.UNKNOWN, + details: `Server method handler threw error ${(err as Error).message}`, + metadata: null + }); + } + }, + onCancel() { + if (stream) { + stream.cancelled = true; + stream.emit('cancelled', 'cancelled'); + } + }, + }); } function handleClientStreaming( - call: Http2ServerCallStream, - handler: ClientStreamingHandler, - metadata: Metadata, - encoding: string + call: ServerInterceptingCallInterface, + handler: ClientStreamingHandler ): void { - const stream = new ServerReadableStreamImpl( - call, - metadata, - handler.deserialize, - encoding - ); + let stream: ServerReadableStream; function respond( err: ServerErrorResponse | ServerStatusResponse | null, @@ -1418,61 +1453,134 @@ function handleClientStreaming( trailer?: Metadata, flags?: number ) { - stream.destroy(); - call.sendUnaryMessage(err, value, trailer, flags); - } - - if (call.cancelled) { - return; - } - - stream.on('error', respond); - handler.func(stream, respond); -} - -async function handleServerStreaming( - call: Http2ServerCallStream, - handler: ServerStreamingHandler, - metadata: Metadata, - encoding: string -): Promise { - try { - const request = await call.receiveUnaryMessage(encoding); - - if (request === undefined || call.cancelled) { + if (err) { + call.sendStatus(serverErrorToStatus(err, trailer)); return; } + call.sendMessage(value, () => { + call.sendStatus({ + code: Status.OK, + details: 'OK', + metadata: trailer ?? null + }); + }); + } - const stream = new ServerWritableStreamImpl( - call, - metadata, - handler.serialize, - request - ); + call.start({ + onReceiveMetadata(metadata) { + stream = new ServerDuplexStreamImpl(handler.path, call, metadata); + try { + handler.func(stream, respond); + } catch (err) { + call.sendStatus({ + code: Status.UNKNOWN, + details: `Server method handler threw error ${(err as Error).message}`, + metadata: null + }); + } + }, + onReceiveMessage(message) { + stream.push(message); + }, + onReceiveHalfClose() { + stream.push(null); + }, + onCancel() { + if (stream) { + stream.cancelled = true; + stream.emit('cancelled', 'cancelled'); + stream.destroy(); + } + }, + }); +} - handler.func(stream); - } catch (err) { - call.sendError(err as ServerErrorResponse); - } +function handleServerStreaming( + call: ServerInterceptingCallInterface, + handler: ServerStreamingHandler +): void { + let stream: ServerWritableStream; + + let requestMetadata: Metadata; + let requestMessage: RequestType | null = null; + call.start({ + onReceiveMetadata(metadata) { + requestMetadata = metadata; + call.startRead(); + }, + onReceiveMessage(message) { + if (requestMessage) { + call.sendStatus({ + code: Status.UNIMPLEMENTED, + details: `Received a second request message for server streaming method ${handler.path}`, + metadata: null + }); + return; + } + requestMessage = message; + call.startRead(); + }, + onReceiveHalfClose() { + if (!requestMessage) { + call.sendStatus({ + code: Status.UNIMPLEMENTED, + details: `Received no request message for server streaming method ${handler.path}`, + metadata: null + }); + return; + } + stream = new ServerWritableStreamImpl(handler.path, call, requestMetadata, requestMessage); + try { + handler.func(stream); + } catch (err) { + call.sendStatus({ + code: Status.UNKNOWN, + details: `Server method handler threw error ${(err as Error).message}`, + metadata: null + }); + } + }, + onCancel() { + if (stream) { + stream.cancelled = true; + stream.emit('cancelled', 'cancelled'); + stream.destroy(); + } + }, + }); } function handleBidiStreaming( - call: Http2ServerCallStream, - handler: BidiStreamingHandler, - metadata: Metadata, - encoding: string + call: ServerInterceptingCallInterface, + handler: BidiStreamingHandler ): void { - const stream = new ServerDuplexStreamImpl( - call, - metadata, - handler.serialize, - handler.deserialize, - encoding - ); - - if (call.cancelled) { - return; - } - - handler.func(stream); + let stream: ServerDuplexStream; + + call.start({ + onReceiveMetadata(metadata) { + stream = new ServerDuplexStreamImpl(handler.path, call, metadata); + try { + handler.func(stream); + } catch (err) { + call.sendStatus({ + code: Status.UNKNOWN, + details: `Server method handler threw error ${(err as Error).message}`, + metadata: null + }); + } + }, + onReceiveMessage(message) { + stream.push(message); + }, + onReceiveHalfClose() { + stream.push(null); + }, + onCancel() { + if (stream) { + stream.cancelled = true; + stream.emit('cancelled', 'cancelled'); + stream.destroy(); + } + }, + }); } diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index 39ca69383..b21581375 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -23,7 +23,7 @@ import { PeerCertificate, TLSSocket, } from 'tls'; -import { StatusObject } from './call-interface'; +import { PartialStatusObject } from './call-interface'; import { ChannelCredentials } from './channel-credentials'; import { ChannelOptions } from './channel-options'; import { @@ -72,7 +72,7 @@ const KEEPALIVE_TIMEOUT_MS = 20000; export interface CallEventTracker { addMessageSent(): void; addMessageReceived(): void; - onCallEnd(status: StatusObject): void; + onCallEnd(status: PartialStatusObject): void; onStreamEnd(success: boolean): void; } diff --git a/packages/grpc-js/test/common.ts b/packages/grpc-js/test/common.ts index 20352fb2f..f6aeed103 100644 --- a/packages/grpc-js/test/common.ts +++ b/packages/grpc-js/test/common.ts @@ -72,7 +72,7 @@ const serviceImpl = { export class TestServer { private server: grpc.Server; public port: number | null = null; - constructor(public useTls: boolean, options?: grpc.ChannelOptions) { + constructor(public useTls: boolean, options?: grpc.ServerOptions) { this.server = new grpc.Server(options); this.server.addService(echoService.service, serviceImpl); } @@ -92,7 +92,6 @@ export class TestServer { return; } this.port = port; - this.server.start(); resolve(); }); }); @@ -130,6 +129,10 @@ export class TestClient { this.client.echo({}, callback); } + sendRequestWithMetadata(metadata: grpc.Metadata, callback: (error?: grpc.ServiceError) => void) { + this.client.echo({}, metadata, callback); + } + getChannelState() { return this.client.getChannel().getConnectivityState(false); } diff --git a/packages/grpc-js/test/test-channelz.ts b/packages/grpc-js/test/test-channelz.ts index e740c4329..5c3dc3d7d 100644 --- a/packages/grpc-js/test/test-channelz.ts +++ b/packages/grpc-js/test/test-channelz.ts @@ -495,12 +495,12 @@ describe('Channelz', () => { assert.strictEqual( +serverSocketResult.socket.data .streams_succeeded, - 0 + 1 ); assert.strictEqual( +serverSocketResult.socket.data .streams_failed, - 1 + 0 ); assert.strictEqual( +serverSocketResult.socket.data diff --git a/packages/grpc-js/test/test-server-deadlines.ts b/packages/grpc-js/test/test-server-deadlines.ts index 2a966e664..43e39808a 100644 --- a/packages/grpc-js/test/test-server-deadlines.ts +++ b/packages/grpc-js/test/test-server-deadlines.ts @@ -110,8 +110,8 @@ describe('Server deadlines', () => { {}, (error: any, response: any) => { assert(error); - assert.strictEqual(error.code, grpc.status.OUT_OF_RANGE); - assert.strictEqual(error.details, 'Invalid deadline'); + assert.strictEqual(error.code, grpc.status.INTERNAL); + assert.match(error.details, /^Invalid grpc-timeout value/); done(); } ); diff --git a/packages/grpc-js/test/test-server-interceptors.ts b/packages/grpc-js/test/test-server-interceptors.ts new file mode 100644 index 000000000..b6b5b55f2 --- /dev/null +++ b/packages/grpc-js/test/test-server-interceptors.ts @@ -0,0 +1,296 @@ +/* + * Copyright 2024 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 * as assert from 'assert'; +import * as path from 'path'; +import * as grpc from '../src'; +import { TestClient, loadProtoFile } from './common'; + +const protoFile = path.join(__dirname, 'fixtures', 'echo_service.proto'); +const echoService = loadProtoFile(protoFile) + .EchoService as grpc.ServiceClientConstructor; + +const AUTH_HEADER_KEY = 'auth'; +const AUTH_HEADER_ALLOWED_VALUE = 'allowed'; +const testAuthInterceptor: grpc.ServerInterceptor = (methodDescriptor, call) => { + return new grpc.ServerInterceptingCall(call, { + start: next => { + const authListener: grpc.ServerListener = { + onReceiveMetadata: (metadata, mdNext) => { + if (metadata.get(AUTH_HEADER_KEY)?.[0] !== AUTH_HEADER_ALLOWED_VALUE) { + call.sendStatus({ + code: grpc.status.UNAUTHENTICATED, + details: 'Auth metadata not correct' + }); + } else { + mdNext(metadata); + } + } + }; + next(authListener); + } + }); +}; + +let eventCounts = { + receiveMetadata: 0, + receiveMessage: 0, + receiveHalfClose: 0, + sendMetadata: 0, + sendMessage: 0, + sendStatus: 0 +}; + +function resetEventCounts() { + eventCounts = { + receiveMetadata: 0, + receiveMessage: 0, + receiveHalfClose: 0, + sendMetadata: 0, + sendMessage: 0, + sendStatus: 0 + }; +} + +/** + * Test interceptor to verify that interceptors see each expected event by + * counting each kind of event. + * @param methodDescription + * @param call + */ +const testLoggingInterceptor: grpc.ServerInterceptor = (methodDescription, call) => { + return new grpc.ServerInterceptingCall(call, { + start: next => { + next({ + onReceiveMetadata: (metadata, mdNext) => { + eventCounts.receiveMetadata += 1; + mdNext(metadata); + }, + onReceiveMessage: (message, messageNext) => { + eventCounts.receiveMessage += 1; + messageNext(message); + }, + onReceiveHalfClose: hcNext => { + eventCounts.receiveHalfClose += 1; + hcNext(); + } + }); + }, + sendMetadata: (metadata, mdNext) => { + eventCounts.sendMetadata += 1; + mdNext(metadata); + }, + sendMessage: (message, messageNext) => { + eventCounts.sendMessage += 1; + messageNext(message); + }, + sendStatus: (status, statusNext) => { + eventCounts.sendStatus += 1; + statusNext(status); + } + }); +}; + +const testHeaderInjectionInterceptor: grpc.ServerInterceptor = (methodDescriptor, call) => { + return new grpc.ServerInterceptingCall(call, { + start: next => { + const authListener: grpc.ServerListener = { + onReceiveMetadata: (metadata, mdNext) => { + metadata.set('injected-header', 'present'); + mdNext(metadata); + } + }; + next(authListener); + } + }); +}; + +describe('Server interceptors', () => { + describe('Auth-type interceptor', () => { + let server: grpc.Server; + let client: TestClient; + /* Tests that an interceptor can entirely prevent the handler from being + * invoked, based on the contents of the metadata. */ + before(done => { + server = new grpc.Server({interceptors: [testAuthInterceptor]}); + server.addService(echoService.service, { + echo: ( + call: grpc.ServerUnaryCall, + callback: grpc.sendUnaryData + ) => { + // A test will fail if a request makes it to the handler without the correct auth header + assert.strictEqual(call.metadata.get(AUTH_HEADER_KEY)?.[0], AUTH_HEADER_ALLOWED_VALUE); + callback(null, call.request); + }, + }); + server.bindAsync('localhost:0', grpc.ServerCredentials.createInsecure(), (error, port) => { + assert.ifError(error); + client = new TestClient(port, false); + done(); + }); + }); + after(done => { + client.close(); + server.tryShutdown(done); + }); + it('Should accept a request with the expected header', done => { + const requestMetadata = new grpc.Metadata(); + requestMetadata.set(AUTH_HEADER_KEY, AUTH_HEADER_ALLOWED_VALUE); + client.sendRequestWithMetadata(requestMetadata, done); + }); + it('Should reject a request without the expected header', done => { + const requestMetadata = new grpc.Metadata(); + requestMetadata.set(AUTH_HEADER_KEY, 'not allowed'); + client.sendRequestWithMetadata(requestMetadata, error => { + assert.strictEqual(error?.code, grpc.status.UNAUTHENTICATED); + done(); + }); + }); + }); + describe('Logging-type interceptor', () => { + let server: grpc.Server; + let client: TestClient; + before(done => { + server = new grpc.Server({interceptors: [testLoggingInterceptor]}); + server.addService(echoService.service, { + echo: ( + call: grpc.ServerUnaryCall, + callback: grpc.sendUnaryData + ) => { + call.sendMetadata(new grpc.Metadata()); + callback(null, call.request); + }, + }); + server.bindAsync('localhost:0', grpc.ServerCredentials.createInsecure(), (error, port) => { + assert.ifError(error); + client = new TestClient(port, false); + done(); + }); + }); + after(done => { + client.close(); + server.tryShutdown(done); + }); + beforeEach(() => { + resetEventCounts(); + }); + it('Should see every event once', done => { + client.sendRequest(error => { + assert.ifError(error); + assert.deepStrictEqual(eventCounts, { + receiveMetadata: 1, + receiveMessage: 1, + receiveHalfClose: 1, + sendMetadata: 1, + sendMessage: 1, + sendStatus: 1 + }); + done(); + }); + }); + }); + describe('Header injection interceptor', () => { + let server: grpc.Server; + let client: TestClient; + before(done => { + server = new grpc.Server({interceptors: [testHeaderInjectionInterceptor]}); + server.addService(echoService.service, { + echo: ( + call: grpc.ServerUnaryCall, + callback: grpc.sendUnaryData + ) => { + assert.strictEqual(call.metadata.get('injected-header')?.[0], 'present'); + callback(null, call.request); + }, + }); + server.bindAsync('localhost:0', grpc.ServerCredentials.createInsecure(), (error, port) => { + assert.ifError(error); + client = new TestClient(port, false); + done(); + }); + }); + after(done => { + client.close(); + server.tryShutdown(done); + }); + it('Should inject the header for the handler to see', done => { + client.sendRequest(done); + }); + }); + describe('Multiple interceptors', () => { + let server: grpc.Server; + let client: TestClient; + before(done => { + server = new grpc.Server({interceptors: [testAuthInterceptor, testLoggingInterceptor, testHeaderInjectionInterceptor]}); + server.addService(echoService.service, { + echo: ( + call: grpc.ServerUnaryCall, + callback: grpc.sendUnaryData + ) => { + assert.strictEqual(call.metadata.get(AUTH_HEADER_KEY)?.[0], AUTH_HEADER_ALLOWED_VALUE); + assert.strictEqual(call.metadata.get('injected-header')?.[0], 'present'); + call.sendMetadata(new grpc.Metadata()); + callback(null, call.request); + }, + }); + server.bindAsync('localhost:0', grpc.ServerCredentials.createInsecure(), (error, port) => { + assert.ifError(error); + client = new TestClient(port, false); + done(); + }); + }); + after(done => { + client.close(); + server.tryShutdown(done); + }); + beforeEach(() => { + resetEventCounts(); + }); + it('Should not log requests rejected by auth', done => { + const requestMetadata = new grpc.Metadata(); + requestMetadata.set(AUTH_HEADER_KEY, 'not allowed'); + client.sendRequestWithMetadata(requestMetadata, error => { + assert.strictEqual(error?.code, grpc.status.UNAUTHENTICATED); + assert.deepStrictEqual(eventCounts, { + receiveMetadata: 0, + receiveMessage: 0, + receiveHalfClose: 0, + sendMetadata: 0, + sendMessage: 0, + sendStatus: 0 + }); + done(); + }); + }); + it('Should log requests accepted by auth', done => { + const requestMetadata = new grpc.Metadata(); + requestMetadata.set(AUTH_HEADER_KEY, AUTH_HEADER_ALLOWED_VALUE); + client.sendRequestWithMetadata(requestMetadata, error => { + assert.ifError(error); + assert.deepStrictEqual(eventCounts, { + receiveMetadata: 1, + receiveMessage: 1, + receiveHalfClose: 1, + sendMetadata: 1, + sendMessage: 1, + sendStatus: 1 + }); + done(); + }); + }); + }); +}); From 24c258ad5807f1a12882788d9add6174c3c9f616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Cruz?= Date: Tue, 30 Jan 2024 14:53:34 +0000 Subject: [PATCH 073/109] grpc-health-check: Move `typescript` as a dev dependency --- packages/grpc-health-check/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/grpc-health-check/package.json b/packages/grpc-health-check/package.json index a7fe1c3fd..ecdaea579 100644 --- a/packages/grpc-health-check/package.json +++ b/packages/grpc-health-check/package.json @@ -21,8 +21,7 @@ "generate-test-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --includeDirs proto/ -O test/generated --grpcLib=@grpc/grpc-js health/v1/health.proto" }, "dependencies": { - "@grpc/proto-loader": "^0.7.10", - "typescript": "^5.2.2" + "@grpc/proto-loader": "^0.7.10" }, "files": [ "LICENSE", @@ -35,6 +34,7 @@ "types": "build/src/health.d.ts", "license": "Apache-2.0", "devDependencies": { - "@grpc/grpc-js": "file:../grpc-js" + "@grpc/grpc-js": "file:../grpc-js", + "typescript": "^5.2.2" } } From 7c9a5e71479292cf3dd766e796316bacfb89daee Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 31 Jan 2024 10:41:01 -0800 Subject: [PATCH 074/109] Make extra trailer behavior consistent with old code --- packages/grpc-js/src/server-call.ts | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/packages/grpc-js/src/server-call.ts b/packages/grpc-js/src/server-call.ts index 8fb1de2aa..edc38e983 100644 --- a/packages/grpc-js/src/server-call.ts +++ b/packages/grpc-js/src/server-call.ts @@ -56,14 +56,11 @@ export type ServerDuplexStream = ServerSurfaceCall & ObjectReadable & ObjectWritable & { end: (metadata?: Metadata) => void }; -export function serverErrorToStatus(error: ServerErrorResponse | ServerStatusResponse, extraTrailers?: Metadata | undefined): PartialStatusObject { +export function serverErrorToStatus(error: ServerErrorResponse | ServerStatusResponse, overrideTrailers?: Metadata | undefined): PartialStatusObject { const status: PartialStatusObject = { code: Status.UNKNOWN, details: 'message' in error ? error.message : 'Unknown Error', - metadata: - 'metadata' in error && error.metadata !== undefined - ? error.metadata - : null, + metadata: overrideTrailers ?? error.metadata ?? null }; if ( @@ -77,14 +74,6 @@ export function serverErrorToStatus(error: ServerErrorResponse | ServerStatusRes status.details = error.details!; } } - - if (extraTrailers) { - if (status.metadata) { - status.metadata.merge(extraTrailers); - } else { - status.metadata = extraTrailers; - } - } return status; } From 322b165c855468ef6d1a606f7e7083cf0ef04a56 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 1 Feb 2024 13:25:38 -0800 Subject: [PATCH 075/109] grpc-js-xds: De-experimentalize tested features and update feature list --- packages/grpc-js-xds/README.md | 5 ++++- packages/grpc-js-xds/gulpfile.ts | 5 ++--- packages/grpc-js-xds/src/environment.ts | 7 +++++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/grpc-js-xds/README.md b/packages/grpc-js-xds/README.md index c1db440cf..2e07675a6 100644 --- a/packages/grpc-js-xds/README.md +++ b/packages/grpc-js-xds/README.md @@ -30,5 +30,8 @@ const client = new MyServiceClient('xds:///example.com:123'); - [Client Status Discovery Service](https://github.com/grpc/proposal/blob/master/A40-csds-support.md) - [Outlier Detection](https://github.com/grpc/proposal/blob/master/A50-xds-outlier-detection.md) - [xDS Retry Support](https://github.com/grpc/proposal/blob/master/A44-xds-retry.md) - - [xDS Aggregate and Logical DNS Clusters](https://github.com/grpc/proposal/blob/master/A37-xds-aggregate-and-logical-dns-clusters.md)' + - [xDS Aggregate and Logical DNS Clusters](https://github.com/grpc/proposal/blob/master/A37-xds-aggregate-and-logical-dns-clusters.md) - [xDS Federation](https://github.com/grpc/proposal/blob/master/A47-xds-federation.md) (Currently experimental, enabled by environment variable `GRPC_EXPERIMENTAL_XDS_FEDERATION`) + - [xDS Custom Load Balancer Configuration](https://github.com/grpc/proposal/blob/master/A52-xds-custom-lb-policies.md) (Custom load balancer registration not currently supported) + - [xDS Ring Hash LB Policy](https://github.com/grpc/proposal/blob/master/A42-xds-ring-hash-lb-policy.md) + - [`pick_first` via xDS](https://github.com/grpc/proposal/blob/master/A62-pick-first.md#pick_first-via-xds-1) (Currently experimental, enabled by environment variable `GRPC_EXPERIMENTAL_PICKFIRST_LB_CONFIG`) diff --git a/packages/grpc-js-xds/gulpfile.ts b/packages/grpc-js-xds/gulpfile.ts index becf109a6..47ca71324 100644 --- a/packages/grpc-js-xds/gulpfile.ts +++ b/packages/grpc-js-xds/gulpfile.ts @@ -62,10 +62,9 @@ const compile = checkTask(() => execNpmCommand('compile')); const runTests = checkTask(() => { process.env.GRPC_EXPERIMENTAL_XDS_FEDERATION = 'true'; - process.env.GRPC_EXPERIMENTAL_XDS_CUSTOM_LB_CONFIG = 'true'; process.env.GRPC_EXPERIMENTAL_PICKFIRST_LB_CONFIG = 'true'; - if (Number(process.versions.node.split('.')[0]) > 14) { - process.env.GRPC_XDS_EXPERIMENTAL_ENABLE_RING_HASH = 'true'; + if (Number(process.versions.node.split('.')[0]) <= 14) { + process.env.GRPC_XDS_EXPERIMENTAL_ENABLE_RING_HASH = 'false'; } return gulp.src(`${outDir}/test/**/*.js`) .pipe(mocha({reporter: 'mocha-jenkins-reporter', diff --git a/packages/grpc-js-xds/src/environment.ts b/packages/grpc-js-xds/src/environment.ts index 02f14dabc..e32d788a6 100644 --- a/packages/grpc-js-xds/src/environment.ts +++ b/packages/grpc-js-xds/src/environment.ts @@ -15,10 +15,13 @@ * */ +/* Switches to enable or disable experimental features. If the default is + * 'true', the feature is enabled by default, if the default is 'false' the + * feature is disabled by default. */ export const EXPERIMENTAL_FAULT_INJECTION = (process.env.GRPC_XDS_EXPERIMENTAL_FAULT_INJECTION ?? 'true') === 'true'; export const EXPERIMENTAL_OUTLIER_DETECTION = (process.env.GRPC_EXPERIMENTAL_ENABLE_OUTLIER_DETECTION ?? 'true') === 'true'; export const EXPERIMENTAL_RETRY = (process.env.GRPC_XDS_EXPERIMENTAL_ENABLE_RETRY ?? 'true') === 'true'; export const EXPERIMENTAL_FEDERATION = (process.env.GRPC_EXPERIMENTAL_XDS_FEDERATION ?? 'false') === 'true'; -export const EXPERIMENTAL_CUSTOM_LB_CONFIG = (process.env.GRPC_EXPERIMENTAL_XDS_CUSTOM_LB_CONFIG ?? 'false') === 'true'; -export const EXPERIMENTAL_RING_HASH = (process.env.GRPC_XDS_EXPERIMENTAL_ENABLE_RING_HASH ?? 'false') === 'true'; +export const EXPERIMENTAL_CUSTOM_LB_CONFIG = (process.env.GRPC_EXPERIMENTAL_XDS_CUSTOM_LB_CONFIG ?? 'true') === 'true'; +export const EXPERIMENTAL_RING_HASH = (process.env.GRPC_XDS_EXPERIMENTAL_ENABLE_RING_HASH ?? 'true') === 'true'; export const EXPERIMENTAL_PICK_FIRST = (process.env.GRPC_EXPERIMENTAL_PICKFIRST_LB_CONFIG ?? 'false') === 'true'; From b1c45a819f117d38717a37e3e91be0ddefa8484c Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 1 Feb 2024 13:41:28 -0800 Subject: [PATCH 076/109] grpc-js/grpc-js-xds: Bump version to 1.10.0 --- packages/grpc-js-xds/README.md | 2 +- packages/grpc-js-xds/package.json | 4 ++-- packages/grpc-js/package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/grpc-js-xds/README.md b/packages/grpc-js-xds/README.md index c1db440cf..476dc077e 100644 --- a/packages/grpc-js-xds/README.md +++ b/packages/grpc-js-xds/README.md @@ -1,6 +1,6 @@ # @grpc/grpc-js xDS plugin -This package provides support for the `xds://` URL scheme to the `@grpc/grpc-js` library. The latest version of this package is compatible with `@grpc/grpc-js` version 1.9.x. +This package provides support for the `xds://` URL scheme to the `@grpc/grpc-js` library. The latest version of this package is compatible with `@grpc/grpc-js` version 1.10.x. ## Installation diff --git a/packages/grpc-js-xds/package.json b/packages/grpc-js-xds/package.json index c2f324b99..1286a16a8 100644 --- a/packages/grpc-js-xds/package.json +++ b/packages/grpc-js-xds/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js-xds", - "version": "1.9.2", + "version": "1.10.0", "description": "Plugin for @grpc/grpc-js. Adds the xds:// URL scheme and associated features.", "main": "build/src/index.js", "scripts": { @@ -50,7 +50,7 @@ "xxhash-wasm": "^1.0.2" }, "peerDependencies": { - "@grpc/grpc-js": "~1.9.0" + "@grpc/grpc-js": "~1.10.0" }, "engines": { "node": ">=10.10.0" diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 8d8f4fd90..701df6723 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.9.14", + "version": "1.10.0", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", From 429a66d1cbb06d8decfe9c5f08c07a951c3c895b Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 14 Feb 2024 11:05:26 -0800 Subject: [PATCH 077/109] grpc-js: round_robin: always have children reconnect immediately --- packages/grpc-js-xds/test/backend.ts | 7 ++--- packages/grpc-js-xds/test/test-core.ts | 28 +++++++++++++++++++ packages/grpc-js/package.json | 2 +- .../grpc-js/src/load-balancer-pick-first.ts | 4 +++ .../grpc-js/src/load-balancer-round-robin.ts | 9 ++++++ 5 files changed, 45 insertions(+), 5 deletions(-) diff --git a/packages/grpc-js-xds/test/backend.ts b/packages/grpc-js-xds/test/backend.ts index 01474284b..59c23ad7d 100644 --- a/packages/grpc-js-xds/test/backend.ts +++ b/packages/grpc-js-xds/test/backend.ts @@ -15,7 +15,7 @@ * */ -import { loadPackageDefinition, sendUnaryData, Server, ServerCredentials, ServerUnaryCall, UntypedServiceImplementation } from "@grpc/grpc-js"; +import { loadPackageDefinition, sendUnaryData, Server, ServerCredentials, ServerOptions, ServerUnaryCall, UntypedServiceImplementation } from "@grpc/grpc-js"; import { loadSync } from "@grpc/proto-loader"; import { ProtoGrpcType } from "./generated/echo"; import { EchoRequest__Output } from "./generated/grpc/testing/EchoRequest"; @@ -43,7 +43,7 @@ export class Backend { private receivedCallCount = 0; private callListeners: (() => void)[] = []; private port: number | null = null; - constructor() { + constructor(private serverOptions?: ServerOptions) { } Echo(call: ServerUnaryCall, callback: sendUnaryData) { // call.request.params is currently ignored @@ -76,13 +76,12 @@ export class Backend { if (this.server) { throw new Error("Backend already running"); } - this.server = new Server(); + this.server = new Server(this.serverOptions); this.server.addService(loadedProtos.grpc.testing.EchoTestService.service, this as unknown as UntypedServiceImplementation); const boundPort = this.port ?? 0; this.server.bindAsync(`localhost:${boundPort}`, ServerCredentials.createInsecure(), (error, port) => { if (!error) { this.port = port; - this.server!.start(); } callback(error, port); }) diff --git a/packages/grpc-js-xds/test/test-core.ts b/packages/grpc-js-xds/test/test-core.ts index f48ab6c11..5d71ff8b8 100644 --- a/packages/grpc-js-xds/test/test-core.ts +++ b/packages/grpc-js-xds/test/test-core.ts @@ -91,4 +91,32 @@ describe('core xDS functionality', () => { }); }, reason => done(reason)); }); + it('should handle connections aging out', function(done) { + this.timeout(5000); + const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [new Backend({'grpc.max_connection_age_ms': 1000})], locality:{region: 'region1'}}]); + const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: cluster}]); + routeGroup.startAllBackends().then(() => { + xdsServer.setEdsResource(cluster.getEndpointConfig()); + xdsServer.setCdsResource(cluster.getClusterConfig()); + xdsServer.setRdsResource(routeGroup.getRouteConfiguration()); + xdsServer.setLdsResource(routeGroup.getListener()); + xdsServer.addResponseListener((typeUrl, responseState) => { + if (responseState.state === 'NACKED') { + client.stopCalls(); + assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`); + } + }) + client = XdsTestClient.createFromServer('listener1', xdsServer); + client.sendOneCall(error => { + assert.ifError(error); + // Make another call after the max_connection_age_ms expires + setTimeout(() => { + client.sendOneCall(error => { + done(error); + }) + }, 1100); + }); + }, reason => done(reason)); + + }) }); diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 701df6723..d1cb3d561 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.10.0", + "version": "1.10.1", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/load-balancer-pick-first.ts b/packages/grpc-js/src/load-balancer-pick-first.ts index c9224de6b..02796fea0 100644 --- a/packages/grpc-js/src/load-balancer-pick-first.ts +++ b/packages/grpc-js/src/load-balancer-pick-first.ts @@ -605,6 +605,10 @@ export class LeafLoadBalancer { return this.endpoint; } + exitIdle() { + this.pickFirstBalancer.exitIdle(); + } + destroy() { this.pickFirstBalancer.destroy(); } diff --git a/packages/grpc-js/src/load-balancer-round-robin.ts b/packages/grpc-js/src/load-balancer-round-robin.ts index 5ed26c9a5..7c38569e5 100644 --- a/packages/grpc-js/src/load-balancer-round-robin.ts +++ b/packages/grpc-js/src/load-balancer-round-robin.ts @@ -161,6 +161,15 @@ export class RoundRobinLoadBalancer implements LoadBalancer { } else { this.updateState(ConnectivityState.IDLE, new QueuePicker(this)); } + /* round_robin should keep all children connected, this is how we do that. + * We can't do this more efficiently in the individual child's updateState + * callback because that doesn't have a reference to which child the state + * change is associated with. */ + for (const child of this.children) { + if (child.getConnectivityState() === ConnectivityState.IDLE) { + child.exitIdle(); + } + } } private updateState(newState: ConnectivityState, picker: Picker) { From 6c2bc599e55dc5b374603c2e5527b0165064b693 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 27 Feb 2024 12:51:38 -0800 Subject: [PATCH 078/109] grpc-js: Run code formatter, fix one lint error --- packages/grpc-js/src/backoff-timeout.ts | 4 +- packages/grpc-js/src/index.ts | 11 +- packages/grpc-js/src/internal-channel.ts | 8 +- .../grpc-js/src/load-balancer-pick-first.ts | 23 +- .../grpc-js/src/load-balancer-round-robin.ts | 4 +- packages/grpc-js/src/load-balancing-call.ts | 4 +- packages/grpc-js/src/logging.ts | 13 +- packages/grpc-js/src/resolver-dns.ts | 9 +- .../grpc-js/src/resolving-load-balancer.ts | 7 +- packages/grpc-js/src/server-call.ts | 15 +- packages/grpc-js/src/server-interceptors.ts | 186 +++++++---- packages/grpc-js/src/server.ts | 305 +++++++++++------- packages/grpc-js/src/service-config.ts | 15 +- packages/grpc-js/src/transport.ts | 7 +- packages/grpc-js/test/common.ts | 5 +- packages/grpc-js/test/test-confg-parsing.ts | 89 ++--- packages/grpc-js/test/test-pick-first.ts | 73 +++-- .../grpc-js/test/test-server-interceptors.ts | 135 +++++--- packages/grpc-js/test/test-server.ts | 139 +++++--- 19 files changed, 698 insertions(+), 354 deletions(-) diff --git a/packages/grpc-js/src/backoff-timeout.ts b/packages/grpc-js/src/backoff-timeout.ts index 78318d1e8..10d347e79 100644 --- a/packages/grpc-js/src/backoff-timeout.ts +++ b/packages/grpc-js/src/backoff-timeout.ts @@ -106,7 +106,9 @@ export class BackoffTimeout { private runTimer(delay: number) { this.endTime = this.startTime; - this.endTime.setMilliseconds(this.endTime.getMilliseconds() + this.nextDelay); + this.endTime.setMilliseconds( + this.endTime.getMilliseconds() + this.nextDelay + ); clearTimeout(this.timerId); this.timerId = setTimeout(() => { this.callback(); diff --git a/packages/grpc-js/src/index.ts b/packages/grpc-js/src/index.ts index b37f61103..c766a3718 100644 --- a/packages/grpc-js/src/index.ts +++ b/packages/grpc-js/src/index.ts @@ -183,7 +183,7 @@ export { ServiceDefinition, UntypedHandleCall, UntypedServiceImplementation, - VerifyOptions + VerifyOptions, }; /**** Server ****/ @@ -263,7 +263,12 @@ export { getChannelzServiceDefinition, getChannelzHandlers } from './channelz'; export { addAdminServicesToServer } from './admin'; -export { ServiceConfig, LoadBalancingConfig, MethodConfig, RetryPolicy } from './service-config'; +export { + ServiceConfig, + LoadBalancingConfig, + MethodConfig, + RetryPolicy, +} from './service-config'; export { ServerListener, @@ -274,7 +279,7 @@ export { ResponderBuilder, ServerInterceptingCallInterface, ServerInterceptingCall, - ServerInterceptor + ServerInterceptor, } from './server-interceptors'; import * as experimental from './experimental'; diff --git a/packages/grpc-js/src/internal-channel.ts b/packages/grpc-js/src/internal-channel.ts index be140522b..823c935af 100644 --- a/packages/grpc-js/src/internal-channel.ts +++ b/packages/grpc-js/src/internal-channel.ts @@ -583,7 +583,8 @@ export class InternalChannel { return; } const now = new Date(); - const timeSinceLastActivity = now.valueOf() - this.lastActivityTimestamp.valueOf(); + const timeSinceLastActivity = + now.valueOf() - this.lastActivityTimestamp.valueOf(); if (timeSinceLastActivity >= this.idleTimeoutMs) { this.trace( 'Idle timer triggered after ' + @@ -603,7 +604,10 @@ export class InternalChannel { } private maybeStartIdleTimer() { - if (this.connectivityState !== ConnectivityState.SHUTDOWN && !this.idleTimer) { + if ( + this.connectivityState !== ConnectivityState.SHUTDOWN && + !this.idleTimer + ) { this.startIdleTimeout(this.idleTimeoutMs); } } diff --git a/packages/grpc-js/src/load-balancer-pick-first.ts b/packages/grpc-js/src/load-balancer-pick-first.ts index 02796fea0..29bbfbf07 100644 --- a/packages/grpc-js/src/load-balancer-pick-first.ts +++ b/packages/grpc-js/src/load-balancer-pick-first.ts @@ -198,7 +198,12 @@ export class PickFirstLoadBalancer implements LoadBalancer { keepaliveTime, errorMessage ) => { - this.onSubchannelStateUpdate(subchannel, previousState, newState, errorMessage); + this.onSubchannelStateUpdate( + subchannel, + previousState, + newState, + errorMessage + ); }; private pickedSubchannelHealthListener: HealthListener = () => @@ -275,7 +280,9 @@ export class PickFirstLoadBalancer implements LoadBalancer { if (this.stickyTransientFailureMode) { this.updateState( ConnectivityState.TRANSIENT_FAILURE, - new UnavailablePicker({details: `No connection established. Last error: ${this.lastError}`}) + new UnavailablePicker({ + details: `No connection established. Last error: ${this.lastError}`, + }) ); } else { this.updateState(ConnectivityState.CONNECTING, new QueuePicker(this)); @@ -441,7 +448,12 @@ export class PickFirstLoadBalancer implements LoadBalancer { private resetSubchannelList() { for (const child of this.children) { - if (!(this.currentPick && child.subchannel.realSubchannelEquals(this.currentPick))) { + if ( + !( + this.currentPick && + child.subchannel.realSubchannelEquals(this.currentPick) + ) + ) { /* The connectivity state listener is the same whether the subchannel * is in the list of children or it is the currentPick, so if it is in * both, removing it here would cause problems. In particular, that @@ -523,7 +535,10 @@ export class PickFirstLoadBalancer implements LoadBalancer { } exitIdle() { - if (this.currentState === ConnectivityState.IDLE && this.latestAddressList) { + if ( + this.currentState === ConnectivityState.IDLE && + this.latestAddressList + ) { this.connectToAddressList(this.latestAddressList); } } diff --git a/packages/grpc-js/src/load-balancer-round-robin.ts b/packages/grpc-js/src/load-balancer-round-robin.ts index 7c38569e5..7e70c554f 100644 --- a/packages/grpc-js/src/load-balancer-round-robin.ts +++ b/packages/grpc-js/src/load-balancer-round-robin.ts @@ -156,7 +156,9 @@ export class RoundRobinLoadBalancer implements LoadBalancer { ) { this.updateState( ConnectivityState.TRANSIENT_FAILURE, - new UnavailablePicker({details: `No connection established. Last error: ${this.lastError}`}) + new UnavailablePicker({ + details: `No connection established. Last error: ${this.lastError}`, + }) ); } else { this.updateState(ConnectivityState.IDLE, new QueuePicker(this)); diff --git a/packages/grpc-js/src/load-balancing-call.ts b/packages/grpc-js/src/load-balancing-call.ts index 87ef02497..25a36553a 100644 --- a/packages/grpc-js/src/load-balancing-call.ts +++ b/packages/grpc-js/src/load-balancing-call.ts @@ -145,7 +145,9 @@ export class LoadBalancingCall implements Call { * metadata generation finished, we shouldn't do anything with * it. */ if (this.ended) { - this.trace('Credentials metadata generation finished after call ended'); + this.trace( + 'Credentials metadata generation finished after call ended' + ); return; } finalMetadata.merge(credsMetadata); diff --git a/packages/grpc-js/src/logging.ts b/packages/grpc-js/src/logging.ts index e1b396fff..2279d3b65 100644 --- a/packages/grpc-js/src/logging.ts +++ b/packages/grpc-js/src/logging.ts @@ -112,7 +112,18 @@ export function trace( text: string ): void { if (isTracerEnabled(tracer)) { - log(severity, new Date().toISOString() + ' | v' + clientVersion + ' ' + pid + ' | ' + tracer + ' | ' + text); + log( + severity, + new Date().toISOString() + + ' | v' + + clientVersion + + ' ' + + pid + + ' | ' + + tracer + + ' | ' + + text + ); } } diff --git a/packages/grpc-js/src/resolver-dns.ts b/packages/grpc-js/src/resolver-dns.ts index 978f1442a..6652839b0 100644 --- a/packages/grpc-js/src/resolver-dns.ts +++ b/packages/grpc-js/src/resolver-dns.ts @@ -335,9 +335,14 @@ class DnsResolver implements Resolver { if (this.pendingLookupPromise === null) { if (this.isNextResolutionTimerRunning || this.backoff.isRunning()) { if (this.isNextResolutionTimerRunning) { - trace('resolution update delayed by "min time between resolutions" rate limit'); + trace( + 'resolution update delayed by "min time between resolutions" rate limit' + ); } else { - trace('resolution update delayed by backoff timer until ' + this.backoff.getEndTime().toISOString()); + trace( + 'resolution update delayed by backoff timer until ' + + this.backoff.getEndTime().toISOString() + ); } this.continueResolving = true; } else { diff --git a/packages/grpc-js/src/resolving-load-balancer.ts b/packages/grpc-js/src/resolving-load-balancer.ts index a8de2019a..82c4ff436 100644 --- a/packages/grpc-js/src/resolving-load-balancer.ts +++ b/packages/grpc-js/src/resolving-load-balancer.ts @@ -223,8 +223,11 @@ export class ResolvingLoadBalancer implements LoadBalancer { * In that case, the backoff timer callback will call * updateResolution */ if (this.backoffTimeout.isRunning()) { - trace('requestReresolution delayed by backoff timer until ' + this.backoffTimeout.getEndTime().toISOString()); - this.continueResolving = true; + trace( + 'requestReresolution delayed by backoff timer until ' + + this.backoffTimeout.getEndTime().toISOString() + ); + this.continueResolving = true; } else { this.updateResolution(); } diff --git a/packages/grpc-js/src/server-call.ts b/packages/grpc-js/src/server-call.ts index edc38e983..95393fba9 100644 --- a/packages/grpc-js/src/server-call.ts +++ b/packages/grpc-js/src/server-call.ts @@ -18,9 +18,7 @@ import { EventEmitter } from 'events'; import { Duplex, Readable, Writable } from 'stream'; -import { - Status, -} from './constants'; +import { Status } from './constants'; import { Deserialize, Serialize } from './make-client'; import { Metadata } from './metadata'; import { ObjectReadable, ObjectWritable } from './object-stream'; @@ -56,11 +54,14 @@ export type ServerDuplexStream = ServerSurfaceCall & ObjectReadable & ObjectWritable & { end: (metadata?: Metadata) => void }; -export function serverErrorToStatus(error: ServerErrorResponse | ServerStatusResponse, overrideTrailers?: Metadata | undefined): PartialStatusObject { +export function serverErrorToStatus( + error: ServerErrorResponse | ServerStatusResponse, + overrideTrailers?: Metadata | undefined +): PartialStatusObject { const status: PartialStatusObject = { code: Status.UNKNOWN, details: 'message' in error ? error.message : 'Unknown Error', - metadata: overrideTrailers ?? error.metadata ?? null + metadata: overrideTrailers ?? error.metadata ?? null, }; if ( @@ -154,7 +155,7 @@ export class ServerWritableStreamImpl private trailingMetadata: Metadata; private pendingStatus: PartialStatusObject = { code: Status.OK, - details: 'OK' + details: 'OK', }; constructor( @@ -224,7 +225,7 @@ export class ServerDuplexStreamImpl private trailingMetadata: Metadata; private pendingStatus: PartialStatusObject = { code: Status.OK, - details: 'OK' + details: 'OK', }; constructor( diff --git a/packages/grpc-js/src/server-interceptors.ts b/packages/grpc-js/src/server-interceptors.ts index c03f3028c..60c5c8865 100644 --- a/packages/grpc-js/src/server-interceptors.ts +++ b/packages/grpc-js/src/server-interceptors.ts @@ -15,19 +15,24 @@ * */ -import { PartialStatusObject} from "./call-interface"; -import { ServerMethodDefinition } from "./make-client"; -import { Metadata } from "./metadata"; -import { ChannelOptions } from "./channel-options"; -import { Handler, ServerErrorResponse } from "./server-call"; -import { Deadline } from "./deadline"; -import { DEFAULT_MAX_RECEIVE_MESSAGE_LENGTH, DEFAULT_MAX_SEND_MESSAGE_LENGTH, LogVerbosity, Status } from "./constants"; +import { PartialStatusObject } from './call-interface'; +import { ServerMethodDefinition } from './make-client'; +import { Metadata } from './metadata'; +import { ChannelOptions } from './channel-options'; +import { Handler, ServerErrorResponse } from './server-call'; +import { Deadline } from './deadline'; +import { + DEFAULT_MAX_RECEIVE_MESSAGE_LENGTH, + DEFAULT_MAX_SEND_MESSAGE_LENGTH, + LogVerbosity, + Status, +} from './constants'; import * as http2 from 'http2'; -import { getErrorMessage } from "./error"; +import { getErrorMessage } from './error'; import * as zlib from 'zlib'; -import { promisify } from "util"; -import { StreamDecoder } from "./stream-decoder"; -import { CallEventTracker } from "./transport"; +import { promisify } from 'util'; +import { StreamDecoder } from './stream-decoder'; +import { CallEventTracker } from './transport'; import * as logging from './logging'; const unzip = promisify(zlib.unzip); @@ -96,7 +101,7 @@ export class ServerListenerBuilder { onReceiveMetadata: this.metadata, onReceiveMessage: this.message, onReceiveHalfClose: this.halfClose, - onCancel: this.cancel + onCancel: this.cancel, }; } } @@ -109,22 +114,30 @@ export interface InterceptingServerListener { onCancel(): void; } -export function isInterceptingServerListener(listener: ServerListener | InterceptingServerListener): listener is InterceptingServerListener { - return listener.onReceiveMetadata !== undefined && listener.onReceiveMetadata.length === 1; +export function isInterceptingServerListener( + listener: ServerListener | InterceptingServerListener +): listener is InterceptingServerListener { + return ( + listener.onReceiveMetadata !== undefined && + listener.onReceiveMetadata.length === 1 + ); } class InterceptingServerListenerImpl implements InterceptingServerListener { /** * Once the call is cancelled, ignore all other events. */ - private cancelled: boolean = false; - private processingMetadata: boolean = false; - private hasPendingMessage: boolean = false; + private cancelled = false; + private processingMetadata = false; + private hasPendingMessage = false; private pendingMessage: any = null; - private processingMessage: boolean = false; - private hasPendingHalfClose: boolean = false; + private processingMessage = false; + private hasPendingHalfClose = false; - constructor(private listener: FullServerListener, private nextListener: InterceptingServerListener) {} + constructor( + private listener: FullServerListener, + private nextListener: InterceptingServerListener + ) {} private processPendingMessage() { if (this.hasPendingMessage) { @@ -195,7 +208,6 @@ class InterceptingServerListenerImpl implements InterceptingServerListener { this.listener.onCancel(); this.nextListener.onCancel(); } - } export interface StartResponder { @@ -212,7 +224,10 @@ export interface MessageResponder { } export interface StatusResponder { - (status: PartialStatusObject, next: (status: PartialStatusObject) => void): void; + ( + status: PartialStatusObject, + next: (status: PartialStatusObject) => void + ): void; } export interface FullResponder { @@ -255,7 +270,7 @@ export class ResponderBuilder { start: this.start, sendMetadata: this.metadata, sendMessage: this.message, - sendStatus: this.status + sendStatus: this.status, }; } } @@ -270,11 +285,11 @@ const defaultServerListener: FullServerListener = { onReceiveHalfClose: next => { next(); }, - onCancel: () => {} + onCancel: () => {}, }; const defaultResponder: FullResponder = { - start: (next) => { + start: next => { next(); }, sendMetadata: (metadata, next) => { @@ -285,7 +300,7 @@ const defaultResponder: FullResponder = { }, sendStatus: (status, next) => { next(status); - } + }, }; export interface ServerInterceptingCallInterface { @@ -321,18 +336,24 @@ export interface ServerInterceptingCallInterface { export class ServerInterceptingCall implements ServerInterceptingCallInterface { private responder: FullResponder; - private processingMetadata: boolean = false; - private processingMessage: boolean = false; + private processingMetadata = false; + private processingMessage = false; private pendingMessage: any = null; private pendingMessageCallback: (() => void) | null = null; private pendingStatus: PartialStatusObject | null = null; - constructor(private nextCall: ServerInterceptingCallInterface, responder?: Responder) { - this.responder = {...defaultResponder, ...responder}; + constructor( + private nextCall: ServerInterceptingCallInterface, + responder?: Responder + ) { + this.responder = { ...defaultResponder, ...responder }; } private processPendingMessage() { if (this.pendingMessageCallback) { - this.nextCall.sendMessage(this.pendingMessage, this.pendingMessageCallback); + this.nextCall.sendMessage( + this.pendingMessage, + this.pendingMessageCallback + ); this.pendingMessage = null; this.pendingMessageCallback = null; } @@ -347,8 +368,14 @@ export class ServerInterceptingCall implements ServerInterceptingCallInterface { start(listener: InterceptingServerListener): void { this.responder.start(interceptedListener => { - const fullInterceptedListener: FullServerListener = {...defaultServerListener, ...interceptedListener}; - const finalInterceptingListener = new InterceptingServerListenerImpl(fullInterceptedListener, listener); + const fullInterceptedListener: FullServerListener = { + ...defaultServerListener, + ...interceptedListener, + }; + const finalInterceptingListener = new InterceptingServerListenerImpl( + fullInterceptedListener, + listener + ); this.nextCall.start(finalInterceptingListener); }); } @@ -394,7 +421,10 @@ export class ServerInterceptingCall implements ServerInterceptingCallInterface { } export interface ServerInterceptor { - (methodDescriptor: ServerMethodDefinition, call: ServerInterceptingCallInterface): ServerInterceptingCall; + ( + methodDescriptor: ServerMethodDefinition, + call: ServerInterceptingCallInterface + ): ServerInterceptingCall; } interface DeadlineUnitIndexSignature { @@ -438,7 +468,9 @@ interface ReadQueueEntry { parsedMessage: any; } -export class BaseServerInterceptingCall implements ServerInterceptingCallInterface { +export class BaseServerInterceptingCall + implements ServerInterceptingCallInterface +{ private listener: InterceptingServerListener | null = null; private metadata: Metadata; private deadlineTimer: NodeJS.Timeout | null = null; @@ -449,7 +481,7 @@ export class BaseServerInterceptingCall implements ServerInterceptingCallInterfa private metadataSent = false; private wantTrailers = false; private cancelNotified = false; - private incomingEncoding: string = 'identity'; + private incomingEncoding = 'identity'; private decoder = new StreamDecoder(); private readQueue: ReadQueueEntry[] = []; private isReadPending = false; @@ -485,7 +517,7 @@ export class BaseServerInterceptingCall implements ServerInterceptingCallInterfa this.callEventTracker.onCallEnd({ code: Status.CANCELLED, details: 'Stream closed before sending status', - metadata: null + metadata: null, }); } @@ -548,7 +580,7 @@ export class BaseServerInterceptingCall implements ServerInterceptingCallInterfa const status: PartialStatusObject = { code: Status.INTERNAL, details: `Invalid ${GRPC_TIMEOUT_HEADER} value "${timeoutHeader}"`, - metadata: null + metadata: null, }; // Wait for the constructor to complete before sending the error. process.nextTick(() => { @@ -565,11 +597,10 @@ export class BaseServerInterceptingCall implements ServerInterceptingCallInterfa const status: PartialStatusObject = { code: Status.DEADLINE_EXCEEDED, details: 'Deadline exceeded', - metadata: null + metadata: null, }; this.sendStatus(status); }, timeout); - } private checkCancelled(): boolean { @@ -650,14 +681,19 @@ export class BaseServerInterceptingCall implements ServerInterceptingCallInterfa } const compressed = queueEntry.compressedMessage!.readUInt8(0) === 1; - const compressedMessageEncoding = compressed ? this.incomingEncoding : 'identity'; - const decompressedMessage = await this.decompressMessage(queueEntry.compressedMessage!, compressedMessageEncoding); + const compressedMessageEncoding = compressed + ? this.incomingEncoding + : 'identity'; + const decompressedMessage = await this.decompressMessage( + queueEntry.compressedMessage!, + compressedMessageEncoding + ); try { queueEntry.parsedMessage = this.handler.deserialize(decompressedMessage); } catch (err) { this.sendStatus({ code: Status.INTERNAL, - details: `Error deserializing request: ${(err as Error).message}` + details: `Error deserializing request: ${(err as Error).message}`, }); return; } @@ -666,7 +702,12 @@ export class BaseServerInterceptingCall implements ServerInterceptingCallInterfa } private maybePushNextMessage() { - if (this.listener && this.isReadPending && this.readQueue.length > 0 && this.readQueue[0].type !== 'COMPRESSED') { + if ( + this.listener && + this.isReadPending && + this.readQueue.length > 0 && + this.readQueue[0].type !== 'COMPRESSED' + ) { this.isReadPending = false; const nextQueueEntry = this.readQueue.shift()!; if (nextQueueEntry.type === 'READABLE') { @@ -682,23 +723,33 @@ export class BaseServerInterceptingCall implements ServerInterceptingCallInterfa if (this.checkCancelled()) { return; } - trace('Request to ' + this.handler.path + ' received data frame of size ' + data.length); + trace( + 'Request to ' + + this.handler.path + + ' received data frame of size ' + + data.length + ); const rawMessages = this.decoder.write(data); for (const messageBytes of rawMessages) { this.stream.pause(); - if (this.maxReceiveMessageSize !== -1 && messageBytes.length - 5 > this.maxReceiveMessageSize) { + if ( + this.maxReceiveMessageSize !== -1 && + messageBytes.length - 5 > this.maxReceiveMessageSize + ) { this.sendStatus({ code: Status.RESOURCE_EXHAUSTED, - details: `Received message larger than max (${messageBytes.length - 5} vs. ${this.maxReceiveMessageSize})`, - metadata: null + details: `Received message larger than max (${ + messageBytes.length - 5 + } vs. ${this.maxReceiveMessageSize})`, + metadata: null, }); return; } const queueEntry: ReadQueueEntry = { type: 'COMPRESSED', compressedMessage: messageBytes, - parsedMessage: null + parsedMessage: null, }; this.readQueue.push(queueEntry); this.decompressAndMaybePush(queueEntry); @@ -709,7 +760,7 @@ export class BaseServerInterceptingCall implements ServerInterceptingCallInterfa this.readQueue.push({ type: 'HALF_CLOSE', compressedMessage: null, - parsedMessage: null + parsedMessage: null, }); this.receivedHalfClose = true; this.maybePushNextMessage(); @@ -751,7 +802,7 @@ export class BaseServerInterceptingCall implements ServerInterceptingCallInterfa this.sendStatus({ code: Status.INTERNAL, details: `Error serializing response: ${getErrorMessage(e)}`, - metadata: null + metadata: null, }); return; } @@ -763,18 +814,23 @@ export class BaseServerInterceptingCall implements ServerInterceptingCallInterfa this.sendStatus({ code: Status.RESOURCE_EXHAUSTED, details: `Sent message larger than max (${response.length} vs. ${this.maxSendMessageSize})`, - metadata: null + metadata: null, }); return; } this.maybeSendMetadata(); - trace('Request to ' + this.handler.path + ' sent data frame of size ' + response.length); + trace( + 'Request to ' + + this.handler.path + + ' sent data frame of size ' + + response.length + ); this.stream.write(response, error => { if (error) { this.sendStatus({ code: Status.INTERNAL, details: `Error writing message: ${getErrorMessage(error)}`, - metadata: null + metadata: null, }); return; } @@ -871,16 +927,24 @@ export function getServerInterceptingCall( handler: Handler, options: ChannelOptions ) { - const methodDefinition: ServerMethodDefinition = { path: handler.path, requestStream: handler.type === 'clientStream' || handler.type === 'bidi', responseStream: handler.type === 'serverStream' || handler.type === 'bidi', requestDeserialize: handler.deserialize, - responseSerialize: handler.serialize - } - const baseCall = new BaseServerInterceptingCall(stream, headers, callEventTracker, handler, options); - return interceptors.reduce((call: ServerInterceptingCallInterface, interceptor: ServerInterceptor) => { - return interceptor(methodDefinition, call); - }, baseCall); + responseSerialize: handler.serialize, + }; + const baseCall = new BaseServerInterceptingCall( + stream, + headers, + callEventTracker, + handler, + options + ); + return interceptors.reduce( + (call: ServerInterceptingCallInterface, interceptor: ServerInterceptor) => { + return interceptor(methodDefinition, call); + }, + baseCall + ); } diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index 31851b832..46bd22ead 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -55,7 +55,13 @@ import { subchannelAddressToString, stringToSubchannelAddress, } from './subchannel-address'; -import { GrpcUri, combineHostPort, parseUri, splitHostPort, uriToString } from './uri-parser'; +import { + GrpcUri, + combineHostPort, + parseUri, + splitHostPort, + uriToString, +} from './uri-parser'; import { ChannelzCallTracker, ChannelzChildrenTracker, @@ -70,7 +76,11 @@ import { unregisterChannelzRef, } from './channelz'; import { CipherNameAndProtocol, TLSSocket } from 'tls'; -import { ServerInterceptingCallInterface, ServerInterceptor, getServerInterceptingCall } from './server-interceptors'; +import { + ServerInterceptingCallInterface, + ServerInterceptor, + getServerInterceptingCall, +} from './server-interceptors'; import { PartialStatusObject } from './call-interface'; import { CallEventTracker } from './transport'; @@ -103,9 +113,15 @@ function noop(): void {} * @returns */ function deprecate(message: string) { - return function (target: (this: This, ...args: Args) => Return, context: ClassMethodDecoratorContext Return>) { + return function ( + target: (this: This, ...args: Args) => Return, + context: ClassMethodDecoratorContext< + This, + (this: This, ...args: Args) => Return + > + ) { return util.deprecate(target, message); - } + }; } function getUnimplementedStatusResponse( @@ -209,7 +225,7 @@ interface BoundPort { * that expands to multiple addresses will result in multiple listening * servers. */ - listeningServers: Set + listeningServers: Set; } /** @@ -221,11 +237,11 @@ interface Http2ServerInfo { } export interface ServerOptions extends ChannelOptions { - interceptors?: ServerInterceptor[] + interceptors?: ServerInterceptor[]; } export class Server { - private boundPorts: Map= new Map(); + private boundPorts: Map = new Map(); private http2Servers: Map = new Map(); private handlers: Map = new Map< @@ -526,10 +542,11 @@ export class Server { return http2Server; } - private bindOneAddress(address: SubchannelAddress, boundPortObject: BoundPort): Promise { - this.trace( - 'Attempting to bind ' + subchannelAddressToString(address) - ); + private bindOneAddress( + address: SubchannelAddress, + boundPortObject: BoundPort + ): Promise { + this.trace('Attempting to bind ' + subchannelAddressToString(address)); const http2Server = this.createHttp2Server(boundPortObject.credentials); return new Promise((resolve, reject) => { const onError = (err: Error) => { @@ -541,7 +558,7 @@ export class Server { ); resolve({ port: 'port' in address ? address.port : 1, - error: err.message + error: err.message, }); }; @@ -561,13 +578,15 @@ export class Server { }; } - const channelzRef = this.registerListenerToChannelz(boundSubchannelAddress); + const channelzRef = this.registerListenerToChannelz( + boundSubchannelAddress + ); if (this.channelzEnabled) { this.listenerChildrenTracker.refChild(channelzRef); } this.http2Servers.set(http2Server, { channelzRef: channelzRef, - sessions: new Set() + sessions: new Set(), }); boundPortObject.listeningServers.add(http2Server); this.trace( @@ -575,62 +594,86 @@ export class Server { subchannelAddressToString(boundSubchannelAddress) ); resolve({ - port: 'port' in boundSubchannelAddress - ? boundSubchannelAddress.port - : 1 + port: + 'port' in boundSubchannelAddress ? boundSubchannelAddress.port : 1, }); http2Server.removeListener('error', onError); }); }); } - private async bindManyPorts(addressList: SubchannelAddress[], boundPortObject: BoundPort): Promise { + private async bindManyPorts( + addressList: SubchannelAddress[], + boundPortObject: BoundPort + ): Promise { if (addressList.length === 0) { return { count: 0, port: 0, - errors: [] + errors: [], }; } if (isTcpSubchannelAddress(addressList[0]) && addressList[0].port === 0) { /* If binding to port 0, first try to bind the first address, then bind * the rest of the address list to the specific port that it binds. */ - const firstAddressResult = await this.bindOneAddress(addressList[0], boundPortObject); + const firstAddressResult = await this.bindOneAddress( + addressList[0], + boundPortObject + ); if (firstAddressResult.error) { /* If the first address fails to bind, try the same operation starting * from the second item in the list. */ - const restAddressResult = await this.bindManyPorts(addressList.slice(1), boundPortObject); + const restAddressResult = await this.bindManyPorts( + addressList.slice(1), + boundPortObject + ); return { ...restAddressResult, - errors: [firstAddressResult.error, ...restAddressResult.errors] + errors: [firstAddressResult.error, ...restAddressResult.errors], }; } else { - const restAddresses = addressList.slice(1).map(address => isTcpSubchannelAddress(address) ? {host: address.host, port: firstAddressResult.port} : address) - const restAddressResult = await Promise.all(restAddresses.map(address => this.bindOneAddress(address, boundPortObject))); + const restAddresses = addressList + .slice(1) + .map(address => + isTcpSubchannelAddress(address) + ? { host: address.host, port: firstAddressResult.port } + : address + ); + const restAddressResult = await Promise.all( + restAddresses.map(address => + this.bindOneAddress(address, boundPortObject) + ) + ); const allResults = [firstAddressResult, ...restAddressResult]; return { count: allResults.filter(result => result.error === undefined).length, port: firstAddressResult.port, - errors: allResults.filter(result => result.error).map(result => result.error!) + errors: allResults + .filter(result => result.error) + .map(result => result.error!), }; } } else { - const allResults = await Promise.all(addressList.map(address => this.bindOneAddress(address, boundPortObject))); + const allResults = await Promise.all( + addressList.map(address => + this.bindOneAddress(address, boundPortObject) + ) + ); return { count: allResults.filter(result => result.error === undefined).length, port: allResults[0].port, - errors: allResults.filter(result => result.error).map(result => result.error!) + errors: allResults + .filter(result => result.error) + .map(result => result.error!), }; } } - private async bindAddressList(addressList: SubchannelAddress[], boundPortObject: BoundPort): Promise { - let bindResult: BindResult; - try { - bindResult = await this.bindManyPorts(addressList, boundPortObject); - } catch (error) { - throw error; - } + private async bindAddressList( + addressList: SubchannelAddress[], + boundPortObject: BoundPort + ): Promise { + const bindResult = await this.bindManyPorts(addressList, boundPortObject); if (bindResult.count > 0) { if (bindResult.count < addressList.length) { logging.log( @@ -642,7 +685,9 @@ export class Server { } else { const errorString = `No address added out of total ${addressList.length} resolved`; logging.log(LogVerbosity.ERROR, errorString); - throw new Error(`${errorString} errors: [${bindResult.errors.join(',')}]`); + throw new Error( + `${errorString} errors: [${bindResult.errors.join(',')}]` + ); } } @@ -660,9 +705,7 @@ export class Server { ...endpointList.map(endpoint => endpoint.addresses) ); if (addressList.length === 0) { - reject( - new Error(`No addresses resolved for port ${port}`) - ); + reject(new Error(`No addresses resolved for port ${port}`)); return; } resolve(addressList); @@ -676,7 +719,10 @@ export class Server { }); } - private async bindPort(port: GrpcUri, boundPortObject: BoundPort): Promise { + private async bindPort( + port: GrpcUri, + boundPortObject: BoundPort + ): Promise { const addressList = await this.resolvePort(port); if (boundPortObject.cancelled) { this.completeUnbind(boundPortObject); @@ -691,7 +737,6 @@ export class Server { } private normalizePort(port: string): GrpcUri { - const initialPortUri = parseUri(port); if (initialPortUri === null) { throw new Error(`Could not parse port "${port}"`); @@ -736,14 +781,20 @@ export class Server { let boundPortObject = this.boundPorts.get(uriToString(portUri)); if (boundPortObject) { if (!creds._equals(boundPortObject.credentials)) { - deferredCallback(new Error(`${port} already bound with incompatible credentials`), 0); + deferredCallback( + new Error(`${port} already bound with incompatible credentials`), + 0 + ); return; } /* If that operation has previously been cancelled by an unbind call, * uncancel it. */ boundPortObject.cancelled = false; if (boundPortObject.completionPromise) { - boundPortObject.completionPromise.then(portNum => callback(null, portNum), error => callback(error as Error, 0)); + boundPortObject.completionPromise.then( + portNum => callback(null, portNum), + error => callback(error as Error, 0) + ); } else { deferredCallback(null, boundPortObject.portNumber); } @@ -756,7 +807,7 @@ export class Server { cancelled: false, portNumber: 0, credentials: creds, - listeningServers: new Set() + listeningServers: new Set(), }; const splitPort = splitHostPort(portUri.path); const completionPromise = this.bindPort(portUri, boundPortObject); @@ -765,34 +816,42 @@ export class Server { * bind operation completes and we have a specific port number. Otherwise, * populate it immediately. */ if (splitPort?.port === 0) { - completionPromise.then(portNum => { - const finalUri: GrpcUri = { - scheme: portUri.scheme, - authority: portUri.authority, - path: combineHostPort({host: splitPort.host, port: portNum}) - }; - boundPortObject!.mapKey = uriToString(finalUri); - boundPortObject!.completionPromise = null; - boundPortObject!.portNumber = portNum; - this.boundPorts.set(boundPortObject!.mapKey, boundPortObject!); - callback(null, portNum); - }, error => { - callback(error, 0); - }) + completionPromise.then( + portNum => { + const finalUri: GrpcUri = { + scheme: portUri.scheme, + authority: portUri.authority, + path: combineHostPort({ host: splitPort.host, port: portNum }), + }; + boundPortObject!.mapKey = uriToString(finalUri); + boundPortObject!.completionPromise = null; + boundPortObject!.portNumber = portNum; + this.boundPorts.set(boundPortObject!.mapKey, boundPortObject!); + callback(null, portNum); + }, + error => { + callback(error, 0); + } + ); } else { this.boundPorts.set(boundPortObject.mapKey, boundPortObject); - completionPromise.then(portNum => { - boundPortObject!.completionPromise = null; - boundPortObject!.portNumber = portNum; - callback(null, portNum); - }, error => { - callback(error, 0); - }); + completionPromise.then( + portNum => { + boundPortObject!.completionPromise = null; + boundPortObject!.portNumber = portNum; + callback(null, portNum); + }, + error => { + callback(error, 0); + } + ); } } private closeServer(server: AnyHttp2Server, callback?: () => void) { - this.trace('Closing server with address ' + JSON.stringify(server.address())); + this.trace( + 'Closing server with address ' + JSON.stringify(server.address()) + ); const serverInfo = this.http2Servers.get(server); server.close(() => { if (this.channelzEnabled && serverInfo) { @@ -802,10 +861,12 @@ export class Server { this.http2Servers.delete(server); callback?.(); }); - } - private closeSession(session: http2.ServerHttp2Session, callback?: () => void) { + private closeSession( + session: http2.ServerHttp2Session, + callback?: () => void + ) { this.trace('Closing session initiated by ' + session.socket?.remoteAddress); const sessionInfo = this.sessions.get(session); const closeCallback = () => { @@ -854,7 +915,12 @@ export class Server { } const boundPortObject = this.boundPorts.get(uriToString(portUri)); if (boundPortObject) { - this.trace('unbinding ' + boundPortObject.mapKey + ' originally bound as ' + uriToString(boundPortObject.originalUri)); + this.trace( + 'unbinding ' + + boundPortObject.mapKey + + ' originally bound as ' + + uriToString(boundPortObject.originalUri) + ); /* If the bind operation is pending, the cancelled flag will trigger * the unbind operation later. */ if (boundPortObject.completionPromise) { @@ -964,13 +1030,13 @@ export class Server { /** * @deprecated No longer needed as of version 1.10.x */ - @deprecate('Calling start() is no longer necessary. It can be safely omitted.') + @deprecate( + 'Calling start() is no longer necessary. It can be safely omitted.' + ) start(): void { if ( this.http2Servers.size === 0 || - [...this.http2Servers.keys()].every( - server => !server.listening - ) + [...this.http2Servers.keys()].every(server => !server.listening) ) { throw new Error('server must be bound in order to start'); } @@ -1090,9 +1156,9 @@ export class Server { 'grpc-message': err.details, [http2.constants.HTTP2_HEADER_STATUS]: http2.constants.HTTP_STATUS_OK, [http2.constants.HTTP2_HEADER_CONTENT_TYPE]: 'application/grpc+proto', - ...err.metadata?.toHttp2Headers() + ...err.metadata?.toHttp2Headers(), }; - stream.respond(trailersToSend, {endStream: true}); + stream.respond(trailersToSend, { endStream: true }); if (this.channelzEnabled) { this.callTracker.addCallFailed(); @@ -1129,7 +1195,7 @@ export class Server { return; } - let callEventTracker: CallEventTracker = { + const callEventTracker: CallEventTracker = { addMessageSent: () => { if (channelzSessionInfo) { channelzSessionInfo.messagesSent += 1; @@ -1157,10 +1223,17 @@ export class Server { channelzSessionInfo.streamTracker.addCallFailed(); } } - } - } + }, + }; - const call = getServerInterceptingCall(this.interceptors, stream, headers, callEventTracker, handler, this.options); + const call = getServerInterceptingCall( + this.interceptors, + stream, + headers, + callEventTracker, + handler, + this.options + ); if (!this._runHandlerForCall(call, handler)) { this.callTracker.addCallFailed(); @@ -1193,7 +1266,14 @@ export class Server { return; } - const call = getServerInterceptingCall(this.interceptors, stream, headers, null, handler, this.options); + const call = getServerInterceptingCall( + this.interceptors, + stream, + headers, + null, + handler, + this.options + ); if (!this._runHandlerForCall(call, handler)) { call.sendStatus({ @@ -1207,25 +1287,15 @@ export class Server { call: ServerInterceptingCallInterface, handler: Handler ): boolean { - const { type } = handler; if (type === 'unary') { handleUnary(call, handler as UntypedUnaryHandler); } else if (type === 'clientStream') { - handleClientStreaming( - call, - handler as UntypedClientStreamingHandler - ); + handleClientStreaming(call, handler as UntypedClientStreamingHandler); } else if (type === 'serverStream') { - handleServerStreaming( - call, - handler as UntypedServerStreamingHandler - ); + handleServerStreaming(call, handler as UntypedServerStreamingHandler); } else if (type === 'bidi') { - handleBidiStreaming( - call, - handler as UntypedBidiStreamingHandler - ); + handleBidiStreaming(call, handler as UntypedBidiStreamingHandler); } else { return false; } @@ -1257,7 +1327,6 @@ export class Server { http2Server.on('stream', handler.bind(this)); http2Server.on('session', session => { - const channelzRef = registerChannelzSocket( session.socket.remoteAddress ?? 'unknown', this.getChannelzSessionInfoGetter(session), @@ -1388,7 +1457,7 @@ async function handleUnary( call.sendStatus({ code: Status.OK, details: 'OK', - metadata: trailer ?? null + metadata: trailer ?? null, }); }); } @@ -1405,7 +1474,7 @@ async function handleUnary( call.sendStatus({ code: Status.UNIMPLEMENTED, details: `Received a second request message for server streaming method ${handler.path}`, - metadata: null + metadata: null, }); return; } @@ -1417,18 +1486,25 @@ async function handleUnary( call.sendStatus({ code: Status.UNIMPLEMENTED, details: `Received no request message for server streaming method ${handler.path}`, - metadata: null + metadata: null, }); return; } - stream = new ServerWritableStreamImpl(handler.path, call, requestMetadata, requestMessage); + stream = new ServerWritableStreamImpl( + handler.path, + call, + requestMetadata, + requestMessage + ); try { handler.func(stream, respond); } catch (err) { call.sendStatus({ code: Status.UNKNOWN, - details: `Server method handler threw error ${(err as Error).message}`, - metadata: null + details: `Server method handler threw error ${ + (err as Error).message + }`, + metadata: null, }); } }, @@ -1461,7 +1537,7 @@ function handleClientStreaming( call.sendStatus({ code: Status.OK, details: 'OK', - metadata: trailer ?? null + metadata: trailer ?? null, }); }); } @@ -1474,8 +1550,10 @@ function handleClientStreaming( } catch (err) { call.sendStatus({ code: Status.UNKNOWN, - details: `Server method handler threw error ${(err as Error).message}`, - metadata: null + details: `Server method handler threw error ${ + (err as Error).message + }`, + metadata: null, }); } }, @@ -1513,7 +1591,7 @@ function handleServerStreaming( call.sendStatus({ code: Status.UNIMPLEMENTED, details: `Received a second request message for server streaming method ${handler.path}`, - metadata: null + metadata: null, }); return; } @@ -1525,18 +1603,25 @@ function handleServerStreaming( call.sendStatus({ code: Status.UNIMPLEMENTED, details: `Received no request message for server streaming method ${handler.path}`, - metadata: null + metadata: null, }); return; } - stream = new ServerWritableStreamImpl(handler.path, call, requestMetadata, requestMessage); + stream = new ServerWritableStreamImpl( + handler.path, + call, + requestMetadata, + requestMessage + ); try { handler.func(stream); } catch (err) { call.sendStatus({ code: Status.UNKNOWN, - details: `Server method handler threw error ${(err as Error).message}`, - metadata: null + details: `Server method handler threw error ${ + (err as Error).message + }`, + metadata: null, }); } }, @@ -1564,8 +1649,10 @@ function handleBidiStreaming( } catch (err) { call.sendStatus({ code: Status.UNKNOWN, - details: `Server method handler threw error ${(err as Error).message}`, - metadata: null + details: `Server method handler threw error ${ + (err as Error).message + }`, + metadata: null, }); } }, diff --git a/packages/grpc-js/src/service-config.ts b/packages/grpc-js/src/service-config.ts index 5c2ca0d06..b0d0d5576 100644 --- a/packages/grpc-js/src/service-config.ts +++ b/packages/grpc-js/src/service-config.ts @@ -356,17 +356,23 @@ export function validateRetryThrottling(obj: any): RetryThrottling { function validateLoadBalancingConfig(obj: any): LoadBalancingConfig { if (!(typeof obj === 'object' && obj !== null)) { - throw new Error(`Invalid loadBalancingConfig: unexpected type ${typeof obj}`); + throw new Error( + `Invalid loadBalancingConfig: unexpected type ${typeof obj}` + ); } const keys = Object.keys(obj); if (keys.length > 1) { - throw new Error(`Invalid loadBalancingConfig: unexpected multiple keys ${keys}`); + throw new Error( + `Invalid loadBalancingConfig: unexpected multiple keys ${keys}` + ); } if (keys.length === 0) { - throw new Error('Invalid loadBalancingConfig: load balancing policy name required'); + throw new Error( + 'Invalid loadBalancingConfig: load balancing policy name required' + ); } return { - [keys[0]]: obj[keys[0]] + [keys[0]]: obj[keys[0]], }; } @@ -385,7 +391,6 @@ export function validateServiceConfig(obj: any): ServiceConfig { if ('loadBalancingConfig' in obj) { if (Array.isArray(obj.loadBalancingConfig)) { for (const config of obj.loadBalancingConfig) { - result.loadBalancingConfig.push(validateLoadBalancingConfig(config)); } } else { diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index b21581375..c4941b068 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -205,7 +205,12 @@ class Http2Transport implements Transport { ) { tooManyPings = true; } - this.trace('connection closed by GOAWAY with code ' + errorCode + ' and data ' + opaqueData?.toString()); + this.trace( + 'connection closed by GOAWAY with code ' + + errorCode + + ' and data ' + + opaqueData?.toString() + ); this.reportDisconnectToOwner(tooManyPings); } ); diff --git a/packages/grpc-js/test/common.ts b/packages/grpc-js/test/common.ts index f6aeed103..88aa129aa 100644 --- a/packages/grpc-js/test/common.ts +++ b/packages/grpc-js/test/common.ts @@ -129,7 +129,10 @@ export class TestClient { this.client.echo({}, callback); } - sendRequestWithMetadata(metadata: grpc.Metadata, callback: (error?: grpc.ServiceError) => void) { + sendRequestWithMetadata( + metadata: grpc.Metadata, + callback: (error?: grpc.ServiceError) => void + ) { this.client.echo({}, metadata, callback); } diff --git a/packages/grpc-js/test/test-confg-parsing.ts b/packages/grpc-js/test/test-confg-parsing.ts index 569b83f5e..b5b9832a7 100644 --- a/packages/grpc-js/test/test-confg-parsing.ts +++ b/packages/grpc-js/test/test-confg-parsing.ts @@ -28,7 +28,7 @@ import parseLoadBalancingConfig = experimental.parseLoadBalancingConfig; */ interface TestCase { name: string; - input: object, + input: object; output?: object; error?: RegExp; } @@ -40,52 +40,52 @@ interface TestCase { * Note: some tests have an expected output that is different from the output, * but all non-error tests additionally verify that parsing the output again * produces the same output. */ -const allTestCases: {[lbPolicyName: string]: TestCase[]} = { +const allTestCases: { [lbPolicyName: string]: TestCase[] } = { pick_first: [ { name: 'no fields set', input: {}, output: { - shuffleAddressList: false - } + shuffleAddressList: false, + }, }, { name: 'shuffleAddressList set', input: { - shuffleAddressList: true - } - } + shuffleAddressList: true, + }, + }, ], round_robin: [ { name: 'no fields set', - input: {} - } + input: {}, + }, ], outlier_detection: [ { name: 'only required fields set', input: { - child_policy: [{round_robin: {}}] + child_policy: [{ round_robin: {} }], }, output: { interval: { seconds: 10, - nanos: 0 + nanos: 0, }, base_ejection_time: { seconds: 30, - nanos: 0 + nanos: 0, }, max_ejection_time: { seconds: 300, - nanos: 0 + nanos: 0, }, max_ejection_percent: 10, success_rate_ejection: undefined, failure_percentage_ejection: undefined, - child_policy: [{round_robin: {}}] - } + child_policy: [{ round_robin: {} }], + }, }, { name: 'all optional fields undefined', @@ -96,53 +96,53 @@ const allTestCases: {[lbPolicyName: string]: TestCase[]} = { max_ejection_percent: undefined, success_rate_ejection: undefined, failure_percentage_ejection: undefined, - child_policy: [{round_robin: {}}] + child_policy: [{ round_robin: {} }], }, output: { interval: { seconds: 10, - nanos: 0 + nanos: 0, }, base_ejection_time: { seconds: 30, - nanos: 0 + nanos: 0, }, max_ejection_time: { seconds: 300, - nanos: 0 + nanos: 0, }, max_ejection_percent: 10, success_rate_ejection: undefined, failure_percentage_ejection: undefined, - child_policy: [{round_robin: {}}] - } + child_policy: [{ round_robin: {} }], + }, }, { name: 'empty ejection configs', input: { success_rate_ejection: {}, failure_percentage_ejection: {}, - child_policy: [{round_robin: {}}] + child_policy: [{ round_robin: {} }], }, output: { interval: { seconds: 10, - nanos: 0 + nanos: 0, }, base_ejection_time: { seconds: 30, - nanos: 0 + nanos: 0, }, max_ejection_time: { seconds: 300, - nanos: 0 + nanos: 0, }, max_ejection_percent: 10, success_rate_ejection: { stdev_factor: 1900, enforcement_percentage: 100, minimum_hosts: 5, - request_volume: 100 + request_volume: 100, }, failure_percentage_ejection: { threshold: 85, @@ -150,30 +150,30 @@ const allTestCases: {[lbPolicyName: string]: TestCase[]} = { minimum_hosts: 5, request_volume: 50, }, - child_policy: [{round_robin: {}}] - } + child_policy: [{ round_robin: {} }], + }, }, { name: 'all fields populated', input: { interval: { seconds: 20, - nanos: 0 + nanos: 0, }, base_ejection_time: { seconds: 40, - nanos: 0 + nanos: 0, }, max_ejection_time: { seconds: 400, - nanos: 0 + nanos: 0, }, max_ejection_percent: 20, success_rate_ejection: { stdev_factor: 1800, enforcement_percentage: 90, minimum_hosts: 4, - request_volume: 200 + request_volume: 200, }, failure_percentage_ejection: { threshold: 95, @@ -181,29 +181,34 @@ const allTestCases: {[lbPolicyName: string]: TestCase[]} = { minimum_hosts: 4, request_volume: 60, }, - child_policy: [{round_robin: {}}] - - } - } - ] -} + child_policy: [{ round_robin: {} }], + }, + }, + ], +}; describe('Load balancing policy config parsing', () => { for (const [lbPolicyName, testCases] of Object.entries(allTestCases)) { describe(lbPolicyName, () => { for (const testCase of testCases) { it(testCase.name, () => { - const lbConfigInput = {[lbPolicyName]: testCase.input}; + const lbConfigInput = { [lbPolicyName]: testCase.input }; if (testCase.error) { assert.throws(() => { parseLoadBalancingConfig(lbConfigInput); }, testCase.error); } else { const expectedOutput = testCase.output ?? testCase.input; - const parsedJson = parseLoadBalancingConfig(lbConfigInput).toJsonObject(); - assert.deepStrictEqual(parsedJson, {[lbPolicyName]: expectedOutput}); + const parsedJson = + parseLoadBalancingConfig(lbConfigInput).toJsonObject(); + assert.deepStrictEqual(parsedJson, { + [lbPolicyName]: expectedOutput, + }); // Test idempotency - assert.deepStrictEqual(parseLoadBalancingConfig(parsedJson).toJsonObject(), parsedJson); + assert.deepStrictEqual( + parseLoadBalancingConfig(parsedJson).toJsonObject(), + parsedJson + ); } }); } diff --git a/packages/grpc-js/test/test-pick-first.ts b/packages/grpc-js/test/test-pick-first.ts index df7a3c741..4c2c319e1 100644 --- a/packages/grpc-js/test/test-pick-first.ts +++ b/packages/grpc-js/test/test-pick-first.ts @@ -561,28 +561,43 @@ describe('pick_first load balancing policy', () => { }, updateState: updateStateCallBackForExpectedStateSequence( [ConnectivityState.CONNECTING, ConnectivityState.TRANSIENT_FAILURE], - err => setImmediate(() => { - assert.strictEqual(reresolutionRequestCount, targetReresolutionRequestCount); - done(err); - }) + err => + setImmediate(() => { + assert.strictEqual( + reresolutionRequestCount, + targetReresolutionRequestCount + ); + done(err); + }) ), requestReresolution: () => { reresolutionRequestCount += 1; - } + }, } ); const pickFirst = new PickFirstLoadBalancer(channelControlHelper, {}); - pickFirst.updateAddressList([{ addresses: [{ host: 'localhost', port: 1 }]}], config); + pickFirst.updateAddressList( + [{ addresses: [{ host: 'localhost', port: 1 }] }], + config + ); process.nextTick(() => { subchannels[0].transitionToState(ConnectivityState.TRANSIENT_FAILURE); process.nextTick(() => { - pickFirst.updateAddressList([{ addresses: [{ host: 'localhost', port: 2 }]}], config); + pickFirst.updateAddressList( + [{ addresses: [{ host: 'localhost', port: 2 }] }], + config + ); process.nextTick(() => { subchannels[1].transitionToState(ConnectivityState.TRANSIENT_FAILURE); process.nextTick(() => { - pickFirst.updateAddressList([{ addresses: [{ host: 'localhost', port: 3 }]}], config); + pickFirst.updateAddressList( + [{ addresses: [{ host: 'localhost', port: 3 }] }], + config + ); process.nextTick(() => { - subchannels[2].transitionToState(ConnectivityState.TRANSIENT_FAILURE); + subchannels[2].transitionToState( + ConnectivityState.TRANSIENT_FAILURE + ); }); }); }); @@ -606,22 +621,35 @@ describe('pick_first load balancing policy', () => { }, updateState: updateStateCallBackForExpectedStateSequence( [ConnectivityState.TRANSIENT_FAILURE], - err => setImmediate(() => { - assert.strictEqual(reresolutionRequestCount, targetReresolutionRequestCount); - done(err); - }) + err => + setImmediate(() => { + assert.strictEqual( + reresolutionRequestCount, + targetReresolutionRequestCount + ); + done(err); + }) ), requestReresolution: () => { reresolutionRequestCount += 1; - } + }, } ); const pickFirst = new PickFirstLoadBalancer(channelControlHelper, {}); - pickFirst.updateAddressList([{ addresses: [{ host: 'localhost', port: 1 }]}], config); + pickFirst.updateAddressList( + [{ addresses: [{ host: 'localhost', port: 1 }] }], + config + ); process.nextTick(() => { - pickFirst.updateAddressList([{ addresses: [{ host: 'localhost', port: 2 }]}], config); + pickFirst.updateAddressList( + [{ addresses: [{ host: 'localhost', port: 2 }] }], + config + ); process.nextTick(() => { - pickFirst.updateAddressList([{ addresses: [{ host: 'localhost', port: 2 }]}], config); + pickFirst.updateAddressList( + [{ addresses: [{ host: 'localhost', port: 2 }] }], + config + ); }); }); }); @@ -639,13 +667,20 @@ describe('pick_first load balancing policy', () => { return subchannel; }, updateState: updateStateCallBackForExpectedStateSequence( - [ConnectivityState.READY, ConnectivityState.IDLE, ConnectivityState.READY], + [ + ConnectivityState.READY, + ConnectivityState.IDLE, + ConnectivityState.READY, + ], done ), } ); const pickFirst = new PickFirstLoadBalancer(channelControlHelper, {}); - pickFirst.updateAddressList([{ addresses: [{ host: 'localhost', port: 1 }]}], config); + pickFirst.updateAddressList( + [{ addresses: [{ host: 'localhost', port: 1 }] }], + config + ); process.nextTick(() => { subchannels[0].transitionToState(ConnectivityState.IDLE); process.nextTick(() => { diff --git a/packages/grpc-js/test/test-server-interceptors.ts b/packages/grpc-js/test/test-server-interceptors.ts index b6b5b55f2..5e93d32d2 100644 --- a/packages/grpc-js/test/test-server-interceptors.ts +++ b/packages/grpc-js/test/test-server-interceptors.ts @@ -26,23 +26,28 @@ const echoService = loadProtoFile(protoFile) const AUTH_HEADER_KEY = 'auth'; const AUTH_HEADER_ALLOWED_VALUE = 'allowed'; -const testAuthInterceptor: grpc.ServerInterceptor = (methodDescriptor, call) => { +const testAuthInterceptor: grpc.ServerInterceptor = ( + methodDescriptor, + call +) => { return new grpc.ServerInterceptingCall(call, { start: next => { const authListener: grpc.ServerListener = { onReceiveMetadata: (metadata, mdNext) => { - if (metadata.get(AUTH_HEADER_KEY)?.[0] !== AUTH_HEADER_ALLOWED_VALUE) { + if ( + metadata.get(AUTH_HEADER_KEY)?.[0] !== AUTH_HEADER_ALLOWED_VALUE + ) { call.sendStatus({ code: grpc.status.UNAUTHENTICATED, - details: 'Auth metadata not correct' + details: 'Auth metadata not correct', }); } else { mdNext(metadata); } - } + }, }; next(authListener); - } + }, }); }; @@ -52,7 +57,7 @@ let eventCounts = { receiveHalfClose: 0, sendMetadata: 0, sendMessage: 0, - sendStatus: 0 + sendStatus: 0, }; function resetEventCounts() { @@ -62,7 +67,7 @@ function resetEventCounts() { receiveHalfClose: 0, sendMetadata: 0, sendMessage: 0, - sendStatus: 0 + sendStatus: 0, }; } @@ -72,7 +77,10 @@ function resetEventCounts() { * @param methodDescription * @param call */ -const testLoggingInterceptor: grpc.ServerInterceptor = (methodDescription, call) => { +const testLoggingInterceptor: grpc.ServerInterceptor = ( + methodDescription, + call +) => { return new grpc.ServerInterceptingCall(call, { start: next => { next({ @@ -87,7 +95,7 @@ const testLoggingInterceptor: grpc.ServerInterceptor = (methodDescription, call) onReceiveHalfClose: hcNext => { eventCounts.receiveHalfClose += 1; hcNext(); - } + }, }); }, sendMetadata: (metadata, mdNext) => { @@ -101,21 +109,24 @@ const testLoggingInterceptor: grpc.ServerInterceptor = (methodDescription, call) sendStatus: (status, statusNext) => { eventCounts.sendStatus += 1; statusNext(status); - } + }, }); }; -const testHeaderInjectionInterceptor: grpc.ServerInterceptor = (methodDescriptor, call) => { +const testHeaderInjectionInterceptor: grpc.ServerInterceptor = ( + methodDescriptor, + call +) => { return new grpc.ServerInterceptingCall(call, { start: next => { const authListener: grpc.ServerListener = { onReceiveMetadata: (metadata, mdNext) => { metadata.set('injected-header', 'present'); mdNext(metadata); - } + }, }; next(authListener); - } + }, }); }; @@ -126,22 +137,29 @@ describe('Server interceptors', () => { /* Tests that an interceptor can entirely prevent the handler from being * invoked, based on the contents of the metadata. */ before(done => { - server = new grpc.Server({interceptors: [testAuthInterceptor]}); + server = new grpc.Server({ interceptors: [testAuthInterceptor] }); server.addService(echoService.service, { echo: ( call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData ) => { // A test will fail if a request makes it to the handler without the correct auth header - assert.strictEqual(call.metadata.get(AUTH_HEADER_KEY)?.[0], AUTH_HEADER_ALLOWED_VALUE); + assert.strictEqual( + call.metadata.get(AUTH_HEADER_KEY)?.[0], + AUTH_HEADER_ALLOWED_VALUE + ); callback(null, call.request); }, }); - server.bindAsync('localhost:0', grpc.ServerCredentials.createInsecure(), (error, port) => { - assert.ifError(error); - client = new TestClient(port, false); - done(); - }); + server.bindAsync( + 'localhost:0', + grpc.ServerCredentials.createInsecure(), + (error, port) => { + assert.ifError(error); + client = new TestClient(port, false); + done(); + } + ); }); after(done => { client.close(); @@ -165,7 +183,7 @@ describe('Server interceptors', () => { let server: grpc.Server; let client: TestClient; before(done => { - server = new grpc.Server({interceptors: [testLoggingInterceptor]}); + server = new grpc.Server({ interceptors: [testLoggingInterceptor] }); server.addService(echoService.service, { echo: ( call: grpc.ServerUnaryCall, @@ -175,11 +193,15 @@ describe('Server interceptors', () => { callback(null, call.request); }, }); - server.bindAsync('localhost:0', grpc.ServerCredentials.createInsecure(), (error, port) => { - assert.ifError(error); - client = new TestClient(port, false); - done(); - }); + server.bindAsync( + 'localhost:0', + grpc.ServerCredentials.createInsecure(), + (error, port) => { + assert.ifError(error); + client = new TestClient(port, false); + done(); + } + ); }); after(done => { client.close(); @@ -197,7 +219,7 @@ describe('Server interceptors', () => { receiveHalfClose: 1, sendMetadata: 1, sendMessage: 1, - sendStatus: 1 + sendStatus: 1, }); done(); }); @@ -207,21 +229,30 @@ describe('Server interceptors', () => { let server: grpc.Server; let client: TestClient; before(done => { - server = new grpc.Server({interceptors: [testHeaderInjectionInterceptor]}); + server = new grpc.Server({ + interceptors: [testHeaderInjectionInterceptor], + }); server.addService(echoService.service, { echo: ( call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData ) => { - assert.strictEqual(call.metadata.get('injected-header')?.[0], 'present'); + assert.strictEqual( + call.metadata.get('injected-header')?.[0], + 'present' + ); callback(null, call.request); }, }); - server.bindAsync('localhost:0', grpc.ServerCredentials.createInsecure(), (error, port) => { - assert.ifError(error); - client = new TestClient(port, false); - done(); - }); + server.bindAsync( + 'localhost:0', + grpc.ServerCredentials.createInsecure(), + (error, port) => { + assert.ifError(error); + client = new TestClient(port, false); + done(); + } + ); }); after(done => { client.close(); @@ -235,23 +266,39 @@ describe('Server interceptors', () => { let server: grpc.Server; let client: TestClient; before(done => { - server = new grpc.Server({interceptors: [testAuthInterceptor, testLoggingInterceptor, testHeaderInjectionInterceptor]}); + server = new grpc.Server({ + interceptors: [ + testAuthInterceptor, + testLoggingInterceptor, + testHeaderInjectionInterceptor, + ], + }); server.addService(echoService.service, { echo: ( call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData ) => { - assert.strictEqual(call.metadata.get(AUTH_HEADER_KEY)?.[0], AUTH_HEADER_ALLOWED_VALUE); - assert.strictEqual(call.metadata.get('injected-header')?.[0], 'present'); + assert.strictEqual( + call.metadata.get(AUTH_HEADER_KEY)?.[0], + AUTH_HEADER_ALLOWED_VALUE + ); + assert.strictEqual( + call.metadata.get('injected-header')?.[0], + 'present' + ); call.sendMetadata(new grpc.Metadata()); callback(null, call.request); }, }); - server.bindAsync('localhost:0', grpc.ServerCredentials.createInsecure(), (error, port) => { - assert.ifError(error); - client = new TestClient(port, false); - done(); - }); + server.bindAsync( + 'localhost:0', + grpc.ServerCredentials.createInsecure(), + (error, port) => { + assert.ifError(error); + client = new TestClient(port, false); + done(); + } + ); }); after(done => { client.close(); @@ -271,7 +318,7 @@ describe('Server interceptors', () => { receiveHalfClose: 0, sendMetadata: 0, sendMessage: 0, - sendStatus: 0 + sendStatus: 0, }); done(); }); @@ -287,7 +334,7 @@ describe('Server interceptors', () => { receiveHalfClose: 1, sendMetadata: 1, sendMessage: 1, - sendStatus: 1 + sendStatus: 1, }); done(); }); diff --git a/packages/grpc-js/test/test-server.ts b/packages/grpc-js/test/test-server.ts index 48b305ef4..dbcdad469 100644 --- a/packages/grpc-js/test/test-server.ts +++ b/packages/grpc-js/test/test-server.ts @@ -149,14 +149,22 @@ describe('Server', () => { }); it('succeeds when called with an already bound port', done => { - server.bindAsync('localhost:0', ServerCredentials.createInsecure(), (err, port) => { - assert.ifError(err); - server.bindAsync(`localhost:${port}`, ServerCredentials.createInsecure(), (err2, port2) => { - assert.ifError(err2); - assert.strictEqual(port, port2); - done(); - }); - }); + server.bindAsync( + 'localhost:0', + ServerCredentials.createInsecure(), + (err, port) => { + assert.ifError(err); + server.bindAsync( + `localhost:${port}`, + ServerCredentials.createInsecure(), + (err2, port2) => { + assert.ifError(err2); + assert.strictEqual(port, port2); + done(); + } + ); + } + ); }); it('fails when called on a bound port with different credentials', done => { @@ -165,15 +173,19 @@ describe('Server', () => { [{ private_key: key, cert_chain: cert }], true ); - server.bindAsync('localhost:0', ServerCredentials.createInsecure(), (err, port) => { - assert.ifError(err); - server.bindAsync(`localhost:${port}`, secureCreds, (err2, port2) => { - assert(err2 !== null); - assert.match(err2.message, /credentials/); - done(); - }) - }); - }) + server.bindAsync( + 'localhost:0', + ServerCredentials.createInsecure(), + (err, port) => { + assert.ifError(err); + server.bindAsync(`localhost:${port}`, secureCreds, (err2, port2) => { + assert(err2 !== null); + assert.match(err2.message, /credentials/); + done(); + }); + } + ); + }); }); describe('unbind', () => { @@ -188,42 +200,73 @@ describe('Server', () => { assert.throws(() => { server.unbind('localhost:0'); }, /port 0/); - server.bindAsync('localhost:0', ServerCredentials.createInsecure(), (err, port) => { - assert.ifError(err); - assert.notStrictEqual(port, 0); - assert.throws(() => { - server.unbind('localhost:0'); - }, /port 0/); - done(); - }) + server.bindAsync( + 'localhost:0', + ServerCredentials.createInsecure(), + (err, port) => { + assert.ifError(err); + assert.notStrictEqual(port, 0); + assert.throws(() => { + server.unbind('localhost:0'); + }, /port 0/); + done(); + } + ); }); it('successfully unbinds a bound ephemeral port', done => { - server.bindAsync('localhost:0', ServerCredentials.createInsecure(), (err, port) => { - client = new grpc.Client(`localhost:${port}`, grpc.credentials.createInsecure()); - client.makeUnaryRequest('/math.Math/Div', x => x, x => x, Buffer.from('abc'), (callError1, result) => { - assert(callError1); - // UNIMPLEMENTED means that the request reached the call handling code - assert.strictEqual(callError1.code, grpc.status.UNIMPLEMENTED); - server.unbind(`localhost:${port}`); - const deadline = new Date(); - deadline.setSeconds(deadline.getSeconds() + 1); - client!.makeUnaryRequest('/math.Math/Div', x => x, x => x, Buffer.from('abc'), {deadline: deadline}, (callError2, result) => { - assert(callError2); - // DEADLINE_EXCEEDED means that the server is unreachable - assert(callError2.code === grpc.status.DEADLINE_EXCEEDED || callError2.code === grpc.status.UNAVAILABLE); - done(); - }); - }); - }) + server.bindAsync( + 'localhost:0', + ServerCredentials.createInsecure(), + (err, port) => { + client = new grpc.Client( + `localhost:${port}`, + grpc.credentials.createInsecure() + ); + client.makeUnaryRequest( + '/math.Math/Div', + x => x, + x => x, + Buffer.from('abc'), + (callError1, result) => { + assert(callError1); + // UNIMPLEMENTED means that the request reached the call handling code + assert.strictEqual(callError1.code, grpc.status.UNIMPLEMENTED); + server.unbind(`localhost:${port}`); + const deadline = new Date(); + deadline.setSeconds(deadline.getSeconds() + 1); + client!.makeUnaryRequest( + '/math.Math/Div', + x => x, + x => x, + Buffer.from('abc'), + { deadline: deadline }, + (callError2, result) => { + assert(callError2); + // DEADLINE_EXCEEDED means that the server is unreachable + assert( + callError2.code === grpc.status.DEADLINE_EXCEEDED || + callError2.code === grpc.status.UNAVAILABLE + ); + done(); + } + ); + } + ); + } + ); }); it('cancels a bindAsync in progress', done => { - server.bindAsync('localhost:50051', ServerCredentials.createInsecure(), (err, port) => { - assert(err); - assert.match(err.message, /cancelled by unbind/); - done(); - }); + server.bindAsync( + 'localhost:50051', + ServerCredentials.createInsecure(), + (err, port) => { + assert(err); + assert.match(err.message, /cancelled by unbind/); + done(); + } + ); server.unbind('localhost:50051'); }); }); @@ -282,7 +325,7 @@ describe('Server', () => { call.on('data', () => { server.drain(`localhost:${portNumber!}`, 100); }); - call.write({value: 'abc'}); + call.write({ value: 'abc' }); }); }); From e0b900dd6903922b66bbfd2e17f00450d12f66b6 Mon Sep 17 00:00:00 2001 From: AVVS Date: Tue, 27 Feb 2024 12:27:25 -0800 Subject: [PATCH 079/109] feat: channelz improvements, idle timeout implementation --- packages/grpc-js/.eslintrc | 1 + packages/grpc-js/gulpfile.ts | 33 +- packages/grpc-js/package.json | 37 +- packages/grpc-js/src/channel.ts | 2 +- packages/grpc-js/src/channelz.ts | 468 ++++++------ .../src/load-balancer-child-handler.ts | 2 +- packages/grpc-js/src/load-balancer.ts | 2 +- packages/grpc-js/src/server-call.ts | 18 +- packages/grpc-js/src/server.ts | 678 ++++++++++++------ packages/grpc-js/src/subchannel-interface.ts | 2 +- packages/grpc-js/src/subchannel.ts | 38 +- packages/grpc-js/src/transport.ts | 16 +- packages/grpc-js/test/common.ts | 4 +- 13 files changed, 784 insertions(+), 517 deletions(-) diff --git a/packages/grpc-js/.eslintrc b/packages/grpc-js/.eslintrc index 2f6bfd62d..9a72b31de 100644 --- a/packages/grpc-js/.eslintrc +++ b/packages/grpc-js/.eslintrc @@ -50,6 +50,7 @@ "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/ban-types": "off", "@typescript-eslint/camelcase": "off", + "@typescript-eslint/no-explicit-any": "off", "node/no-missing-import": "off", "node/no-empty-function": "off", "node/no-unsupported-features/es-syntax": "off", diff --git a/packages/grpc-js/gulpfile.ts b/packages/grpc-js/gulpfile.ts index d85900364..e4e9071ff 100644 --- a/packages/grpc-js/gulpfile.ts +++ b/packages/grpc-js/gulpfile.ts @@ -35,14 +35,17 @@ const pkgPath = path.resolve(jsCoreDir, 'package.json'); const supportedVersionRange = require(pkgPath).engines.node; const versionNotSupported = () => { console.log(`Skipping grpc-js task for Node ${process.version}`); - return () => { return Promise.resolve(); }; + return () => { + return Promise.resolve(); + }; }; const identity = (value: any): any => value; -const checkTask = semver.satisfies(process.version, supportedVersionRange) ? - identity : versionNotSupported; +const checkTask = semver.satisfies(process.version, supportedVersionRange) + ? identity + : versionNotSupported; const execNpmVerb = (verb: string, ...args: string[]) => - execa('npm', [verb, ...args], {cwd: jsCoreDir, stdio: 'inherit'}); + execa('npm', [verb, ...args], { cwd: jsCoreDir, stdio: 'inherit' }); const execNpmCommand = execNpmVerb.bind(null, 'run'); const install = checkTask(() => execNpmVerb('install', '--unsafe-perm')); @@ -64,22 +67,20 @@ const cleanAll = gulp.parallel(clean); */ const compile = checkTask(() => execNpmCommand('compile')); -const copyTestFixtures = checkTask(() => ncpP(`${jsCoreDir}/test/fixtures`, `${outDir}/test/fixtures`)); +const copyTestFixtures = checkTask(() => + ncpP(`${jsCoreDir}/test/fixtures`, `${outDir}/test/fixtures`) +); const runTests = checkTask(() => { process.env.GRPC_EXPERIMENTAL_ENABLE_OUTLIER_DETECTION = 'true'; - return gulp.src(`${outDir}/test/**/*.js`) - .pipe(mocha({reporter: 'mocha-jenkins-reporter', - require: ['ts-node/register']})); + return gulp.src(`${outDir}/test/**/*.js`).pipe( + mocha({ + reporter: 'mocha-jenkins-reporter', + require: ['ts-node/register'], + }) + ); }); const test = gulp.series(install, copyTestFixtures, runTests); -export { - install, - lint, - clean, - cleanAll, - compile, - test -} +export { install, lint, clean, cleanAll, compile, test }; diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index d1cb3d561..34d8b558b 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -6,7 +6,7 @@ "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", "main": "build/src/index.js", "engines": { - "node": "^8.13.0 || >=10.10.0" + "node": ">=12.10.0" }, "keywords": [], "author": { @@ -15,17 +15,18 @@ "types": "build/src/index.d.ts", "license": "Apache-2.0", "devDependencies": { - "@types/gulp": "^4.0.6", - "@types/gulp-mocha": "0.0.32", - "@types/lodash": "^4.14.186", - "@types/mocha": "^5.2.6", - "@types/ncp": "^2.0.1", - "@types/pify": "^3.0.2", - "@types/semver": "^7.3.9", - "@typescript-eslint/eslint-plugin": "^5.59.11", - "@typescript-eslint/parser": "^5.59.11", - "@typescript-eslint/typescript-estree": "^5.59.11", - "clang-format": "^1.0.55", + "@types/gulp": "^4.0.17", + "@types/gulp-mocha": "0.0.37", + "@types/lodash": "^4.14.202", + "@types/mocha": "^10.0.6", + "@types/ncp": "^2.0.8", + "@types/pify": "^5.0.4", + "@types/semver": "^7.5.8", + "@types/node": ">=20.11.20", + "@typescript-eslint/eslint-plugin": "^7.1.0", + "@typescript-eslint/parser": "^7.1.0", + "@typescript-eslint/typescript-estree": "^7.1.0", + "clang-format": "^1.8.0", "eslint": "^8.42.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-node": "^11.1.0", @@ -33,16 +34,16 @@ "execa": "^2.0.3", "gulp": "^4.0.2", "gulp-mocha": "^6.0.0", - "lodash": "^4.17.4", + "lodash": "^4.17.21", "madge": "^5.0.1", "mocha-jenkins-reporter": "^0.4.1", "ncp": "^2.0.0", "pify": "^4.0.1", "prettier": "^2.8.8", "rimraf": "^3.0.2", - "semver": "^7.3.5", - "ts-node": "^10.9.1", - "typescript": "^5.1.3" + "semver": "^7.6.0", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" }, "contributors": [ { @@ -65,8 +66,8 @@ "generate-test-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --include-dirs test/fixtures/ -O test/generated/ --grpcLib ../../src/index test_service.proto" }, "dependencies": { - "@grpc/proto-loader": "^0.7.8", - "@types/node": ">=12.12.47" + "@grpc/proto-loader": "^0.7.10", + "@js-sdsl/ordered-map": "^4.4.2" }, "files": [ "src/**/*.ts", diff --git a/packages/grpc-js/src/channel.ts b/packages/grpc-js/src/channel.ts index 7ce5a15f7..514920c8f 100644 --- a/packages/grpc-js/src/channel.ts +++ b/packages/grpc-js/src/channel.ts @@ -20,7 +20,7 @@ import { ChannelOptions } from './channel-options'; import { ServerSurfaceCall } from './server-call'; import { ConnectivityState } from './connectivity-state'; -import { ChannelRef } from './channelz'; +import type { ChannelRef } from './channelz'; import { Call } from './call-interface'; import { InternalChannel } from './internal-channel'; import { Deadline } from './deadline'; diff --git a/packages/grpc-js/src/channelz.ts b/packages/grpc-js/src/channelz.ts index 1e2627a97..6d70b7543 100644 --- a/packages/grpc-js/src/channelz.ts +++ b/packages/grpc-js/src/channelz.ts @@ -16,6 +16,7 @@ */ import { isIPv4, isIPv6 } from 'net'; +import { OrderedMap, type OrderedMapIterator } from '@js-sdsl/ordered-map'; import { ConnectivityState } from './connectivity-state'; import { Status } from './constants'; import { Timestamp } from './generated/google/protobuf/Timestamp'; @@ -66,24 +67,25 @@ export type TraceSeverity = | 'CT_ERROR'; export interface ChannelRef { - kind: 'channel'; + kind: EntityTypes.channel; id: number; name: string; } export interface SubchannelRef { - kind: 'subchannel'; + kind: EntityTypes.subchannel; id: number; name: string; } export interface ServerRef { - kind: 'server'; + kind: EntityTypes.server; id: number; + name: string; } export interface SocketRef { - kind: 'socket'; + kind: EntityTypes.socket; id: number; name: string; } @@ -131,6 +133,21 @@ interface TraceEvent { */ const TARGET_RETAINED_TRACES = 32; +export class ChannelzTraceStub { + readonly events: TraceEvent[] = []; + readonly creationTimestamp: Date = new Date(); + readonly eventsLogged = 0; + + addTrace(): void {} + getTraceMessage(): ChannelTrace { + return { + creation_timestamp: dateToProtoTimestamp(this.creationTimestamp), + num_events_logged: this.eventsLogged, + events: [], + }; + } +} + export class ChannelzTrace { events: TraceEvent[] = []; creationTimestamp: Date; @@ -182,105 +199,64 @@ export class ChannelzTrace { } export class ChannelzChildrenTracker { - private channelChildren: Map = - new Map(); - private subchannelChildren: Map< + private channelChildren = new OrderedMap< + number, + { ref: ChannelRef; count: number } + >(); + private subchannelChildren = new OrderedMap< number, { ref: SubchannelRef; count: number } - > = new Map(); - private socketChildren: Map = - new Map(); + >(); + private socketChildren = new OrderedMap< + number, + { ref: SocketRef; count: number } + >(); + private trackerMap = { + [EntityTypes.channel]: this.channelChildren, + [EntityTypes.subchannel]: this.subchannelChildren, + [EntityTypes.socket]: this.socketChildren, + } as const; refChild(child: ChannelRef | SubchannelRef | SocketRef) { - switch (child.kind) { - case 'channel': { - const trackedChild = this.channelChildren.get(child.id) ?? { - ref: child, - count: 0, - }; - trackedChild.count += 1; - this.channelChildren.set(child.id, trackedChild); - break; - } - case 'subchannel': { - const trackedChild = this.subchannelChildren.get(child.id) ?? { - ref: child, - count: 0, - }; - trackedChild.count += 1; - this.subchannelChildren.set(child.id, trackedChild); - break; - } - case 'socket': { - const trackedChild = this.socketChildren.get(child.id) ?? { - ref: child, - count: 0, - }; - trackedChild.count += 1; - this.socketChildren.set(child.id, trackedChild); - break; - } + const tracker = this.trackerMap[child.kind]; + const trackedChild = tracker.getElementByKey(child.id); + + if (trackedChild === undefined) { + tracker.setElement(child.id, { + // @ts-expect-error union issues + ref: child, + count: 1, + }); + } else { + trackedChild.count += 1; } } unrefChild(child: ChannelRef | SubchannelRef | SocketRef) { - switch (child.kind) { - case 'channel': { - const trackedChild = this.channelChildren.get(child.id); - if (trackedChild !== undefined) { - trackedChild.count -= 1; - if (trackedChild.count === 0) { - this.channelChildren.delete(child.id); - } else { - this.channelChildren.set(child.id, trackedChild); - } - } - break; - } - case 'subchannel': { - const trackedChild = this.subchannelChildren.get(child.id); - if (trackedChild !== undefined) { - trackedChild.count -= 1; - if (trackedChild.count === 0) { - this.subchannelChildren.delete(child.id); - } else { - this.subchannelChildren.set(child.id, trackedChild); - } - } - break; - } - case 'socket': { - const trackedChild = this.socketChildren.get(child.id); - if (trackedChild !== undefined) { - trackedChild.count -= 1; - if (trackedChild.count === 0) { - this.socketChildren.delete(child.id); - } else { - this.socketChildren.set(child.id, trackedChild); - } - } - break; + const tracker = this.trackerMap[child.kind]; + const trackedChild = tracker.getElementByKey(child.id); + if (trackedChild !== undefined) { + trackedChild.count -= 1; + if (trackedChild.count === 0) { + tracker.eraseElementByKey(child.id); } } } getChildLists(): ChannelzChildren { - const channels: ChannelRef[] = []; - for (const { ref } of this.channelChildren.values()) { - channels.push(ref); - } - const subchannels: SubchannelRef[] = []; - for (const { ref } of this.subchannelChildren.values()) { - subchannels.push(ref); - } - const sockets: SocketRef[] = []; - for (const { ref } of this.socketChildren.values()) { - sockets.push(ref); - } - return { channels, subchannels, sockets }; + return { + channels: this.channelChildren, + subchannels: this.subchannelChildren, + sockets: this.socketChildren, + }; } } +export class ChannelzChildrenTrackerStub extends ChannelzChildrenTracker { + override refChild(): void {} + override unrefChild(): void {} +} + export class ChannelzCallTracker { callsStarted = 0; callsSucceeded = 0; @@ -299,17 +275,23 @@ export class ChannelzCallTracker { } } +export class ChannelzCallTrackerStub extends ChannelzCallTracker { + override addCallStarted() {} + override addCallSucceeded() {} + override addCallFailed() {} +} + export interface ChannelzChildren { - channels: ChannelRef[]; - subchannels: SubchannelRef[]; - sockets: SocketRef[]; + channels: OrderedMap; + subchannels: OrderedMap; + sockets: OrderedMap; } export interface ChannelInfo { target: string; state: ConnectivityState; - trace: ChannelzTrace; - callTracker: ChannelzCallTracker; + trace: ChannelzTrace | ChannelzTraceStub; + callTracker: ChannelzCallTracker | ChannelzCallTrackerStub; children: ChannelzChildren; } @@ -348,105 +330,102 @@ export interface SocketInfo { remoteFlowControlWindow: number | null; } -interface ChannelEntry { +type ChannelEntry = { ref: ChannelRef; getInfo(): ChannelInfo; -} +}; -interface SubchannelEntry { +type SubchannelEntry = { ref: SubchannelRef; getInfo(): SubchannelInfo; -} +}; -interface ServerEntry { +type ServerEntry = { ref: ServerRef; getInfo(): ServerInfo; -} +}; -interface SocketEntry { +type SocketEntry = { ref: SocketRef; getInfo(): SocketInfo; -} - -let nextId = 1; - -function getNextId(): number { - return nextId++; -} - -const channels: (ChannelEntry | undefined)[] = []; -const subchannels: (SubchannelEntry | undefined)[] = []; -const servers: (ServerEntry | undefined)[] = []; -const sockets: (SocketEntry | undefined)[] = []; - -export function registerChannelzChannel( - name: string, - getInfo: () => ChannelInfo, - channelzEnabled: boolean -): ChannelRef { - const id = getNextId(); - const ref: ChannelRef = { id, name, kind: 'channel' }; - if (channelzEnabled) { - channels[id] = { ref, getInfo }; - } - return ref; -} - -export function registerChannelzSubchannel( - name: string, - getInfo: () => SubchannelInfo, - channelzEnabled: boolean -): SubchannelRef { - const id = getNextId(); - const ref: SubchannelRef = { id, name, kind: 'subchannel' }; - if (channelzEnabled) { - subchannels[id] = { ref, getInfo }; +}; + +export const enum EntityTypes { + channel = 'channel', + subchannel = 'subchannel', + server = 'server', + socket = 'socket', +} + +const entityMaps = { + [EntityTypes.channel]: new OrderedMap(), + [EntityTypes.subchannel]: new OrderedMap(), + [EntityTypes.server]: new OrderedMap(), + [EntityTypes.socket]: new OrderedMap(), +} as const; + +export type RefByType = T extends EntityTypes.channel + ? ChannelRef + : T extends EntityTypes.server + ? ServerRef + : T extends EntityTypes.socket + ? SocketRef + : T extends EntityTypes.subchannel + ? SubchannelRef + : never; + +export type EntryByType = T extends EntityTypes.channel + ? ChannelEntry + : T extends EntityTypes.server + ? ServerEntry + : T extends EntityTypes.socket + ? SocketEntry + : T extends EntityTypes.subchannel + ? SubchannelEntry + : never; + +export type InfoByType = T extends EntityTypes.channel + ? ChannelInfo + : T extends EntityTypes.subchannel + ? SubchannelInfo + : T extends EntityTypes.server + ? ServerInfo + : T extends EntityTypes.socket + ? SocketInfo + : never; + +const generateRegisterFn = (kind: R) => { + let nextId = 1; + function getNextId(): number { + return nextId++; } - return ref; -} -export function registerChannelzServer( - getInfo: () => ServerInfo, - channelzEnabled: boolean -): ServerRef { - const id = getNextId(); - const ref: ServerRef = { id, kind: 'server' }; - if (channelzEnabled) { - servers[id] = { ref, getInfo }; - } - return ref; -} - -export function registerChannelzSocket( - name: string, - getInfo: () => SocketInfo, - channelzEnabled: boolean -): SocketRef { - const id = getNextId(); - const ref: SocketRef = { id, name, kind: 'socket' }; - if (channelzEnabled) { - sockets[id] = { ref, getInfo }; - } - return ref; -} + return ( + name: string, + getInfo: () => InfoByType, + channelzEnabled: boolean + ): RefByType => { + const id = getNextId(); + const ref = { id, name, kind } as RefByType; + if (channelzEnabled) { + // @ts-expect-error typing issues + entityMaps[kind].setElement(id, { ref, getInfo }); + } + return ref; + }; +}; + +export const registerChannelzChannel = generateRegisterFn(EntityTypes.channel); +export const registerChannelzSubchannel = generateRegisterFn( + EntityTypes.subchannel +); +export const registerChannelzServer = generateRegisterFn(EntityTypes.server); +export const registerChannelzSocket = generateRegisterFn(EntityTypes.socket); export function unregisterChannelzRef( ref: ChannelRef | SubchannelRef | ServerRef | SocketRef ) { - switch (ref.kind) { - case 'channel': - delete channels[ref.id]; - return; - case 'subchannel': - delete subchannels[ref.id]; - return; - case 'server': - delete servers[ref.id]; - return; - case 'socket': - delete sockets[ref.id]; - return; - } + entityMaps[ref.kind].eraseElementByKey(ref.id); } /** @@ -556,6 +535,17 @@ function dateToProtoTimestamp(date?: Date | null): Timestamp | null { function getChannelMessage(channelEntry: ChannelEntry): ChannelMessage { const resolvedInfo = channelEntry.getInfo(); + const channelRef: ChannelRefMessage[] = []; + const subchannelRef: SubchannelRefMessage[] = []; + + resolvedInfo.children.channels.forEach(el => { + channelRef.push(channelRefToMessage(el[1].ref)); + }); + + resolvedInfo.children.subchannels.forEach(el => { + subchannelRef.push(subchannelRefToMessage(el[1].ref)); + }); + return { ref: channelRefToMessage(channelEntry.ref), data: { @@ -569,12 +559,8 @@ function getChannelMessage(channelEntry: ChannelEntry): ChannelMessage { ), trace: resolvedInfo.trace.getTraceMessage(), }, - channel_ref: resolvedInfo.children.channels.map(ref => - channelRefToMessage(ref) - ), - subchannel_ref: resolvedInfo.children.subchannels.map(ref => - subchannelRefToMessage(ref) - ), + channel_ref: channelRef, + subchannel_ref: subchannelRef, }; } @@ -582,8 +568,9 @@ function GetChannel( call: ServerUnaryCall, callback: sendUnaryData ): void { - const channelId = Number.parseInt(call.request.channel_id); - const channelEntry = channels[channelId]; + const channelId = parseInt(call.request.channel_id, 10); + const channelEntry = + entityMaps[EntityTypes.channel].getElementByKey(channelId); if (channelEntry === undefined) { callback({ code: Status.NOT_FOUND, @@ -598,27 +585,34 @@ function GetTopChannels( call: ServerUnaryCall, callback: sendUnaryData ): void { - const maxResults = Number.parseInt(call.request.max_results); + const maxResults = parseInt(call.request.max_results, 10) || 100; const resultList: ChannelMessage[] = []; - let i = Number.parseInt(call.request.start_channel_id); - for (; i < channels.length; i++) { - const channelEntry = channels[i]; - if (channelEntry === undefined) { - continue; - } - resultList.push(getChannelMessage(channelEntry)); - if (resultList.length >= maxResults) { - break; - } + const startId = parseInt(call.request.start_channel_id, 10); + const channelEntries = entityMaps[EntityTypes.channel]; + + let i: OrderedMapIterator; + for ( + i = channelEntries.lowerBound(startId); + !i.equals(channelEntries.end()) && resultList.length < maxResults; + i = i.next() + ) { + resultList.push(getChannelMessage(i.pointer[1])); } + callback(null, { channel: resultList, - end: i >= servers.length, + end: i.equals(channelEntries.end()), }); } function getServerMessage(serverEntry: ServerEntry): ServerMessage { const resolvedInfo = serverEntry.getInfo(); + const listenSocket: SocketRefMessage[] = []; + + resolvedInfo.listenerChildren.sockets.forEach(el => { + listenSocket.push(socketRefToMessage(el[1].ref)); + }); + return { ref: serverRefToMessage(serverEntry.ref), data: { @@ -630,9 +624,7 @@ function getServerMessage(serverEntry: ServerEntry): ServerMessage { ), trace: resolvedInfo.trace.getTraceMessage(), }, - listen_socket: resolvedInfo.listenerChildren.sockets.map(ref => - socketRefToMessage(ref) - ), + listen_socket: listenSocket, }; } @@ -640,8 +632,9 @@ function GetServer( call: ServerUnaryCall, callback: sendUnaryData ): void { - const serverId = Number.parseInt(call.request.server_id); - const serverEntry = servers[serverId]; + const serverId = parseInt(call.request.server_id, 10); + const serverEntries = entityMaps[EntityTypes.server]; + const serverEntry = serverEntries.getElementByKey(serverId); if (serverEntry === undefined) { callback({ code: Status.NOT_FOUND, @@ -656,22 +649,23 @@ function GetServers( call: ServerUnaryCall, callback: sendUnaryData ): void { - const maxResults = Number.parseInt(call.request.max_results); + const maxResults = parseInt(call.request.max_results, 10) || 100; + const startId = parseInt(call.request.start_server_id, 10); + const serverEntries = entityMaps[EntityTypes.server]; const resultList: ServerMessage[] = []; - let i = Number.parseInt(call.request.start_server_id); - for (; i < servers.length; i++) { - const serverEntry = servers[i]; - if (serverEntry === undefined) { - continue; - } - resultList.push(getServerMessage(serverEntry)); - if (resultList.length >= maxResults) { - break; - } + + let i: OrderedMapIterator; + for ( + i = serverEntries.lowerBound(startId); + !i.equals(serverEntries.end()) && resultList.length < maxResults; + i = i.next() + ) { + resultList.push(getServerMessage(i.pointer[1])); } + callback(null, { server: resultList, - end: i >= servers.length, + end: i.equals(serverEntries.end()), }); } @@ -679,8 +673,9 @@ function GetSubchannel( call: ServerUnaryCall, callback: sendUnaryData ): void { - const subchannelId = Number.parseInt(call.request.subchannel_id); - const subchannelEntry = subchannels[subchannelId]; + const subchannelId = parseInt(call.request.subchannel_id, 10); + const subchannelEntry = + entityMaps[EntityTypes.subchannel].getElementByKey(subchannelId); if (subchannelEntry === undefined) { callback({ code: Status.NOT_FOUND, @@ -689,6 +684,12 @@ function GetSubchannel( return; } const resolvedInfo = subchannelEntry.getInfo(); + const listenSocket: SocketRefMessage[] = []; + + resolvedInfo.children.sockets.forEach(el => { + listenSocket.push(socketRefToMessage(el[1].ref)); + }); + const subchannelMessage: SubchannelMessage = { ref: subchannelRefToMessage(subchannelEntry.ref), data: { @@ -702,9 +703,7 @@ function GetSubchannel( ), trace: resolvedInfo.trace.getTraceMessage(), }, - socket_ref: resolvedInfo.children.sockets.map(ref => - socketRefToMessage(ref) - ), + socket_ref: listenSocket, }; callback(null, { subchannel: subchannelMessage }); } @@ -735,8 +734,8 @@ function GetSocket( call: ServerUnaryCall, callback: sendUnaryData ): void { - const socketId = Number.parseInt(call.request.socket_id); - const socketEntry = sockets[socketId]; + const socketId = parseInt(call.request.socket_id, 10); + const socketEntry = entityMaps[EntityTypes.socket].getElementByKey(socketId); if (socketEntry === undefined) { callback({ code: Status.NOT_FOUND, @@ -809,8 +808,9 @@ function GetServerSockets( >, callback: sendUnaryData ): void { - const serverId = Number.parseInt(call.request.server_id); - const serverEntry = servers[serverId]; + const serverId = parseInt(call.request.server_id, 10); + const serverEntry = entityMaps[EntityTypes.server].getElementByKey(serverId); + if (serverEntry === undefined) { callback({ code: Status.NOT_FOUND, @@ -818,28 +818,28 @@ function GetServerSockets( }); return; } - const startId = Number.parseInt(call.request.start_socket_id); - const maxResults = Number.parseInt(call.request.max_results); + + const startId = parseInt(call.request.start_socket_id, 10); + const maxResults = parseInt(call.request.max_results, 10) || 100; const resolvedInfo = serverEntry.getInfo(); // If we wanted to include listener sockets in the result, this line would // instead say // const allSockets = resolvedInfo.listenerChildren.sockets.concat(resolvedInfo.sessionChildren.sockets).sort((ref1, ref2) => ref1.id - ref2.id); - const allSockets = resolvedInfo.sessionChildren.sockets.sort( - (ref1, ref2) => ref1.id - ref2.id - ); + const allSockets = resolvedInfo.sessionChildren.sockets; const resultList: SocketRefMessage[] = []; - let i = 0; - for (; i < allSockets.length; i++) { - if (allSockets[i].id >= startId) { - resultList.push(socketRefToMessage(allSockets[i])); - if (resultList.length >= maxResults) { - break; - } - } + + let i: OrderedMapIterator; + for ( + i = allSockets.lowerBound(startId); + !i.equals(allSockets.end()) && resultList.length < maxResults; + i = i.next() + ) { + resultList.push(socketRefToMessage(i.pointer[1].ref)); } + callback(null, { socket_ref: resultList, - end: i >= allSockets.length, + end: i.equals(allSockets.end()), }); } diff --git a/packages/grpc-js/src/load-balancer-child-handler.ts b/packages/grpc-js/src/load-balancer-child-handler.ts index a29d6c92b..352ea7b81 100644 --- a/packages/grpc-js/src/load-balancer-child-handler.ts +++ b/packages/grpc-js/src/load-balancer-child-handler.ts @@ -25,7 +25,7 @@ import { Endpoint, SubchannelAddress } from './subchannel-address'; import { ChannelOptions } from './channel-options'; import { ConnectivityState } from './connectivity-state'; import { Picker } from './picker'; -import { ChannelRef, SubchannelRef } from './channelz'; +import type { ChannelRef, SubchannelRef } from './channelz'; import { SubchannelInterface } from './subchannel-interface'; const TYPE_NAME = 'child_load_balancer_helper'; diff --git a/packages/grpc-js/src/load-balancer.ts b/packages/grpc-js/src/load-balancer.ts index f8071317a..fb353a59a 100644 --- a/packages/grpc-js/src/load-balancer.ts +++ b/packages/grpc-js/src/load-balancer.ts @@ -19,7 +19,7 @@ import { ChannelOptions } from './channel-options'; import { Endpoint, SubchannelAddress } from './subchannel-address'; import { ConnectivityState } from './connectivity-state'; import { Picker } from './picker'; -import { ChannelRef, SubchannelRef } from './channelz'; +import type { ChannelRef, SubchannelRef } from './channelz'; import { SubchannelInterface } from './subchannel-interface'; import { LoadBalancingConfig } from './service-config'; import { log } from './logging'; diff --git a/packages/grpc-js/src/server-call.ts b/packages/grpc-js/src/server-call.ts index 95393fba9..d5aababfa 100644 --- a/packages/grpc-js/src/server-call.ts +++ b/packages/grpc-js/src/server-call.ts @@ -19,12 +19,12 @@ import { EventEmitter } from 'events'; import { Duplex, Readable, Writable } from 'stream'; import { Status } from './constants'; -import { Deserialize, Serialize } from './make-client'; +import type { Deserialize, Serialize } from './make-client'; import { Metadata } from './metadata'; -import { ObjectReadable, ObjectWritable } from './object-stream'; -import { StatusObject, PartialStatusObject } from './call-interface'; -import { Deadline } from './deadline'; -import { ServerInterceptingCallInterface } from './server-interceptors'; +import type { ObjectReadable, ObjectWritable } from './object-stream'; +import type { StatusObject, PartialStatusObject } from './call-interface'; +import type { Deadline } from './deadline'; +import type { ServerInterceptingCallInterface } from './server-interceptors'; export type ServerStatusResponse = Partial; @@ -330,7 +330,7 @@ export interface UnaryHandler { func: handleUnaryCall; serialize: Serialize; deserialize: Deserialize; - type: HandlerType; + type: 'unary'; path: string; } @@ -338,7 +338,7 @@ export interface ClientStreamingHandler { func: handleClientStreamingCall; serialize: Serialize; deserialize: Deserialize; - type: HandlerType; + type: 'clientStream'; path: string; } @@ -346,7 +346,7 @@ export interface ServerStreamingHandler { func: handleServerStreamingCall; serialize: Serialize; deserialize: Deserialize; - type: HandlerType; + type: 'serverStream'; path: string; } @@ -354,7 +354,7 @@ export interface BidiStreamingHandler { func: handleBidiStreamingCall; serialize: Serialize; deserialize: Deserialize; - type: HandlerType; + type: 'bidi'; path: string; } diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index 46bd22ead..0a5bc0e9b 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -64,8 +64,11 @@ import { } from './uri-parser'; import { ChannelzCallTracker, + ChannelzCallTrackerStub, ChannelzChildrenTracker, + ChannelzChildrenTrackerStub, ChannelzTrace, + ChannelzTraceStub, registerChannelzServer, registerChannelzSocket, ServerInfo, @@ -87,6 +90,7 @@ import { CallEventTracker } from './transport'; const UNLIMITED_CONNECTION_AGE_MS = ~(1 << 31); const KEEPALIVE_MAX_TIME_MS = ~(1 << 31); const KEEPALIVE_TIMEOUT_MS = 20000; +const MAX_CONNECTION_IDLE_MS = 30 * 60 * 1e3; // 30 min const { HTTP2_HEADER_PATH } = http2.constants; @@ -177,9 +181,10 @@ function getDefaultHandler(handlerType: HandlerType, methodName: string) { interface ChannelzSessionInfo { ref: SocketRef; - streamTracker: ChannelzCallTracker; + streamTracker: ChannelzCallTracker | ChannelzCallTrackerStub; messagesSent: number; messagesReceived: number; + keepAlivesSent: number; lastMessageSentTimestamp: Date | null; lastMessageReceivedTimestamp: Date | null; } @@ -243,6 +248,13 @@ export interface ServerOptions extends ChannelOptions { export class Server { private boundPorts: Map = new Map(); private http2Servers: Map = new Map(); + private sessionIdleTimeouts = new Map< + http2.Http2Session, + { + activeStreams: number; + timeout: NodeJS.Timeout | null; + } + >(); private handlers: Map = new Map< string, @@ -261,10 +273,14 @@ export class Server { // Channelz Info private readonly channelzEnabled: boolean = true; private channelzRef: ServerRef; - private channelzTrace = new ChannelzTrace(); - private callTracker = new ChannelzCallTracker(); - private listenerChildrenTracker = new ChannelzChildrenTracker(); - private sessionChildrenTracker = new ChannelzChildrenTracker(); + private channelzTrace: ChannelzTrace | ChannelzTraceStub; + private callTracker: ChannelzCallTracker | ChannelzCallTrackerStub; + private listenerChildrenTracker: + | ChannelzChildrenTracker + | ChannelzChildrenTrackerStub; + private sessionChildrenTracker: + | ChannelzChildrenTracker + | ChannelzChildrenTrackerStub; private readonly maxConnectionAgeMs: number; private readonly maxConnectionAgeGraceMs: number; @@ -272,6 +288,8 @@ export class Server { private readonly keepaliveTimeMs: number; private readonly keepaliveTimeoutMs: number; + private readonly sessionIdleTimeout: number; + private readonly interceptors: ServerInterceptor[]; /** @@ -284,14 +302,24 @@ export class Server { this.options = options ?? {}; if (this.options['grpc.enable_channelz'] === 0) { this.channelzEnabled = false; + this.channelzTrace = new ChannelzTraceStub(); + this.callTracker = new ChannelzCallTrackerStub(); + this.listenerChildrenTracker = new ChannelzChildrenTrackerStub(); + this.sessionChildrenTracker = new ChannelzChildrenTrackerStub(); + } else { + this.channelzTrace = new ChannelzTrace(); + this.callTracker = new ChannelzCallTracker(); + this.listenerChildrenTracker = new ChannelzChildrenTracker(); + this.sessionChildrenTracker = new ChannelzChildrenTracker(); } + this.channelzRef = registerChannelzServer( + 'server', () => this.getChannelzInfo(), this.channelzEnabled ); - if (this.channelzEnabled) { - this.channelzTrace.addTrace('CT_INFO', 'Server created'); - } + + this.channelzTrace.addTrace('CT_INFO', 'Server created'); this.maxConnectionAgeMs = this.options['grpc.max_connection_age_ms'] ?? UNLIMITED_CONNECTION_AGE_MS; this.maxConnectionAgeGraceMs = @@ -301,6 +329,9 @@ export class Server { this.options['grpc.keepalive_time_ms'] ?? KEEPALIVE_MAX_TIME_MS; this.keepaliveTimeoutMs = this.options['grpc.keepalive_timeout_ms'] ?? KEEPALIVE_TIMEOUT_MS; + this.sessionIdleTimeout = + this.options['grpc.max_connection_idle'] ?? MAX_CONNECTION_IDLE_MS; + this.commonServerOptions = { maxSendHeaderBlockLength: Number.MAX_SAFE_INTEGER, }; @@ -382,7 +413,7 @@ export class Server { streamsFailed: sessionInfo.streamTracker.callsFailed, messagesSent: sessionInfo.messagesSent, messagesReceived: sessionInfo.messagesReceived, - keepAlivesSent: 0, + keepAlivesSent: sessionInfo.keepAlivesSent, lastLocalStreamCreatedTimestamp: null, lastRemoteStreamCreatedTimestamp: sessionInfo.streamTracker.lastCallStartedTimestamp, @@ -581,9 +612,8 @@ export class Server { const channelzRef = this.registerListenerToChannelz( boundSubchannelAddress ); - if (this.channelzEnabled) { - this.listenerChildrenTracker.refChild(channelzRef); - } + this.listenerChildrenTracker.refChild(channelzRef); + this.http2Servers.set(http2Server, { channelzRef: channelzRef, sessions: new Set(), @@ -854,7 +884,7 @@ export class Server { ); const serverInfo = this.http2Servers.get(server); server.close(() => { - if (this.channelzEnabled && serverInfo) { + if (serverInfo) { this.listenerChildrenTracker.unrefChild(serverInfo.channelzRef); unregisterChannelzRef(serverInfo.channelzRef); } @@ -870,15 +900,15 @@ export class Server { this.trace('Closing session initiated by ' + session.socket?.remoteAddress); const sessionInfo = this.sessions.get(session); const closeCallback = () => { - if (this.channelzEnabled && sessionInfo) { + if (sessionInfo) { this.sessionChildrenTracker.unrefChild(sessionInfo.ref); unregisterChannelzRef(sessionInfo.ref); + this.sessions.delete(session); } - this.sessions.delete(session); callback?.(); }; if (session.closed) { - process.nextTick(closeCallback); + queueMicrotask(closeCallback); } else { session.close(closeCallback); } @@ -956,14 +986,13 @@ export class Server { const allSessions: Set = new Set(); for (const http2Server of boundPortObject.listeningServers) { const serverEntry = this.http2Servers.get(http2Server); - if (!serverEntry) { - continue; - } - for (const session of serverEntry.sessions) { - allSessions.add(session); - this.closeSession(session, () => { - allSessions.delete(session); - }); + if (serverEntry) { + for (const session of serverEntry.sessions) { + allSessions.add(session); + this.closeSession(session, () => { + allSessions.delete(session); + }); + } } } /* After the grace time ends, send another goaway to all remaining sessions @@ -995,9 +1024,7 @@ export class Server { session.destroy(http2.constants.NGHTTP2_CANCEL as any); }); this.sessions.clear(); - if (this.channelzEnabled) { - unregisterChannelzRef(this.channelzRef); - } + unregisterChannelzRef(this.channelzRef); this.shutdown = true; } @@ -1049,9 +1076,7 @@ export class Server { tryShutdown(callback: (error?: Error) => void): void { const wrappedCallback = (error?: Error) => { - if (this.channelzEnabled) { - unregisterChannelzRef(this.channelzRef); - } + unregisterChannelzRef(this.channelzRef); callback(error); }; let pendingChecks = 0; @@ -1065,24 +1090,26 @@ export class Server { } this.shutdown = true; - for (const server of this.http2Servers.keys()) { + for (const [serverKey, server] of this.http2Servers.entries()) { pendingChecks++; - const serverString = this.http2Servers.get(server)!.channelzRef.name; + const serverString = server.channelzRef.name; this.trace('Waiting for server ' + serverString + ' to close'); - this.closeServer(server, () => { + this.closeServer(serverKey, () => { this.trace('Server ' + serverString + ' finished closing'); maybeCallback(); }); + + for (const session of server.sessions.keys()) { + pendingChecks++; + const sessionString = session.socket?.remoteAddress; + this.trace('Waiting for session ' + sessionString + ' to close'); + this.closeSession(session, () => { + this.trace('Session ' + sessionString + ' finished closing'); + maybeCallback(); + }); + } } - for (const session of this.sessions.keys()) { - pendingChecks++; - const sessionString = session.socket?.remoteAddress; - this.trace('Waiting for session ' + sessionString + ' to close'); - this.closeSession(session, () => { - this.trace('Session ' + sessionString + ' finished closing'); - maybeCallback(); - }); - } + if (pendingChecks === 0) { wrappedCallback(); } @@ -1160,213 +1187,161 @@ export class Server { }; stream.respond(trailersToSend, { endStream: true }); - if (this.channelzEnabled) { - this.callTracker.addCallFailed(); - channelzSessionInfo?.streamTracker.addCallFailed(); - } + this.callTracker.addCallFailed(); + channelzSessionInfo?.streamTracker.addCallFailed(); } - private _channelzHandler( - stream: http2.ServerHttp2Stream, - headers: http2.IncomingHttpHeaders + private _sessionHandler( + http2Server: http2.Http2Server | http2.Http2SecureServer ) { - const channelzSessionInfo = this.sessions.get( - stream.session as http2.ServerHttp2Session - ); + return (session: http2.ServerHttp2Session) => { + this.http2Servers.get(http2Server)?.sessions.add(session); + + let connectionAgeTimer: NodeJS.Timeout | null = null; + let connectionAgeGraceTimer: NodeJS.Timeout | null = null; + let sessionClosedByServer = false; - this.callTracker.addCallStarted(); - channelzSessionInfo?.streamTracker.addCallStarted(); + const idleTimeoutObj = this.enableIdleTimeout(session); - if (!this._verifyContentType(stream, headers)) { - this.callTracker.addCallFailed(); - channelzSessionInfo?.streamTracker.addCallFailed(); - return; - } + if (this.maxConnectionAgeMs !== UNLIMITED_CONNECTION_AGE_MS) { + // Apply a random jitter within a +/-10% range + const jitterMagnitude = this.maxConnectionAgeMs / 10; + const jitter = Math.random() * jitterMagnitude * 2 - jitterMagnitude; - const path = headers[HTTP2_HEADER_PATH] as string; + connectionAgeTimer = setTimeout(() => { + sessionClosedByServer = true; - const handler = this._retrieveHandler(path); - if (!handler) { - this._respondWithError( - getUnimplementedStatusResponse(path), - stream, - channelzSessionInfo - ); - return; - } + this.trace( + `Connection dropped by max connection age: ${session.socket?.remoteAddress}` + ); - const callEventTracker: CallEventTracker = { - addMessageSent: () => { - if (channelzSessionInfo) { - channelzSessionInfo.messagesSent += 1; - channelzSessionInfo.lastMessageSentTimestamp = new Date(); - } - }, - addMessageReceived: () => { - if (channelzSessionInfo) { - channelzSessionInfo.messagesReceived += 1; - channelzSessionInfo.lastMessageReceivedTimestamp = new Date(); - } - }, - onCallEnd: status => { - if (status.code === Status.OK) { - this.callTracker.addCallSucceeded(); - } else { - this.callTracker.addCallFailed(); - } - }, - onStreamEnd: success => { - if (channelzSessionInfo) { - if (success) { - channelzSessionInfo.streamTracker.addCallSucceeded(); - } else { - channelzSessionInfo.streamTracker.addCallFailed(); + try { + session.goaway( + http2.constants.NGHTTP2_NO_ERROR, + ~(1 << 31), + Buffer.from('max_age') + ); + } catch (e) { + // The goaway can't be sent because the session is already closed + session.destroy(); + return; } - } - }, - }; + session.close(); - const call = getServerInterceptingCall( - this.interceptors, - stream, - headers, - callEventTracker, - handler, - this.options - ); + /* Allow a grace period after sending the GOAWAY before forcibly + * closing the connection. */ + if (this.maxConnectionAgeGraceMs !== UNLIMITED_CONNECTION_AGE_MS) { + connectionAgeGraceTimer = setTimeout(() => { + session.destroy(); + }, this.maxConnectionAgeGraceMs).unref?.(); + } + }, this.maxConnectionAgeMs + jitter).unref?.(); + } - if (!this._runHandlerForCall(call, handler)) { - this.callTracker.addCallFailed(); - channelzSessionInfo?.streamTracker.addCallFailed(); + const keeapliveTimeTimer: NodeJS.Timeout | null = setInterval(() => { + const timeoutTImer = setTimeout(() => { + sessionClosedByServer = true; + session.close(); + }, this.keepaliveTimeoutMs).unref?.(); - call.sendStatus({ - code: Status.INTERNAL, - details: `Unknown handler type: ${handler.type}`, - }); - } - } + try { + session.ping( + (err: Error | null, duration: number, payload: Buffer) => { + clearTimeout(timeoutTImer); - private _streamHandler( - stream: http2.ServerHttp2Stream, - headers: http2.IncomingHttpHeaders - ) { - if (this._verifyContentType(stream, headers) !== true) { - return; - } + if (err) { + sessionClosedByServer = true; + this.trace( + `Connection dropped due to error of a ping frame ${err.message} return in ${duration}` + ); + session.close(); + } + } + ); + } catch (e) { + // The ping can't be sent because the session is already closed + session.destroy(); + } + }, this.keepaliveTimeMs).unref?.(); - const path = headers[HTTP2_HEADER_PATH] as string; + session.on('close', () => { + if (!sessionClosedByServer) { + this.trace( + `Connection dropped by client ${session.socket?.remoteAddress}` + ); + } - const handler = this._retrieveHandler(path); - if (!handler) { - this._respondWithError( - getUnimplementedStatusResponse(path), - stream, - null - ); - return; - } + if (connectionAgeTimer) { + clearTimeout(connectionAgeTimer); + } - const call = getServerInterceptingCall( - this.interceptors, - stream, - headers, - null, - handler, - this.options - ); + if (connectionAgeGraceTimer) { + clearTimeout(connectionAgeGraceTimer); + } - if (!this._runHandlerForCall(call, handler)) { - call.sendStatus({ - code: Status.INTERNAL, - details: `Unknown handler type: ${handler.type}`, - }); - } - } + if (keeapliveTimeTimer) { + clearTimeout(keeapliveTimeTimer); + } - private _runHandlerForCall( - call: ServerInterceptingCallInterface, - handler: Handler - ): boolean { - const { type } = handler; - if (type === 'unary') { - handleUnary(call, handler as UntypedUnaryHandler); - } else if (type === 'clientStream') { - handleClientStreaming(call, handler as UntypedClientStreamingHandler); - } else if (type === 'serverStream') { - handleServerStreaming(call, handler as UntypedServerStreamingHandler); - } else if (type === 'bidi') { - handleBidiStreaming(call, handler as UntypedBidiStreamingHandler); - } else { - return false; - } + clearTimeout(idleTimeoutObj.timeout); + this.sessionIdleTimeouts.delete(session); - return true; + this.http2Servers.get(http2Server)?.sessions.delete(session); + }); + }; } - private _setupHandlers( + private _channelzSessionHandler( http2Server: http2.Http2Server | http2.Http2SecureServer - ): void { - if (http2Server === null) { - return; - } - - const serverAddress = http2Server.address(); - let serverAddressString = 'null'; - if (serverAddress) { - if (typeof serverAddress === 'string') { - serverAddressString = serverAddress; - } else { - serverAddressString = serverAddress.address + ':' + serverAddress.port; - } - } - this.serverAddressString = serverAddressString; - - const handler = this.channelzEnabled - ? this._channelzHandler - : this._streamHandler; - - http2Server.on('stream', handler.bind(this)); - http2Server.on('session', session => { + ) { + return (session: http2.ServerHttp2Session) => { const channelzRef = registerChannelzSocket( - session.socket.remoteAddress ?? 'unknown', + session.socket?.remoteAddress ?? 'unknown', this.getChannelzSessionInfoGetter(session), this.channelzEnabled ); const channelzSessionInfo: ChannelzSessionInfo = { ref: channelzRef, - streamTracker: new ChannelzCallTracker(), + streamTracker: this.channelzEnabled + ? new ChannelzCallTracker() + : new ChannelzCallTrackerStub(), messagesSent: 0, messagesReceived: 0, + keepAlivesSent: 0, lastMessageSentTimestamp: null, lastMessageReceivedTimestamp: null, }; this.http2Servers.get(http2Server)?.sessions.add(session); this.sessions.set(session, channelzSessionInfo); - const clientAddress = session.socket.remoteAddress; - if (this.channelzEnabled) { - this.channelzTrace.addTrace( - 'CT_INFO', - 'Connection established by client ' + clientAddress - ); - this.sessionChildrenTracker.refChild(channelzRef); - } + const clientAddress = `${session.socket.remoteAddress}:${session.socket.remotePort}`; + + this.channelzTrace.addTrace( + 'CT_INFO', + 'Connection established by client ' + clientAddress + ); + this.trace('Connection established by client ' + clientAddress); + this.sessionChildrenTracker.refChild(channelzRef); + let connectionAgeTimer: NodeJS.Timeout | null = null; let connectionAgeGraceTimer: NodeJS.Timeout | null = null; let sessionClosedByServer = false; + + const idleTimeoutObj = this.enableIdleTimeout(session); + if (this.maxConnectionAgeMs !== UNLIMITED_CONNECTION_AGE_MS) { // Apply a random jitter within a +/-10% range const jitterMagnitude = this.maxConnectionAgeMs / 10; const jitter = Math.random() * jitterMagnitude * 2 - jitterMagnitude; + connectionAgeTimer = setTimeout(() => { sessionClosedByServer = true; - if (this.channelzEnabled) { - this.channelzTrace.addTrace( - 'CT_INFO', - 'Connection dropped by max connection age from ' + clientAddress - ); - } + this.channelzTrace.addTrace( + 'CT_INFO', + 'Connection dropped by max connection age from ' + clientAddress + ); + try { session.goaway( http2.constants.NGHTTP2_NO_ERROR, @@ -1379,6 +1354,7 @@ export class Server { return; } session.close(); + /* Allow a grace period after sending the GOAWAY before forcibly * closing the connection. */ if (this.maxConnectionAgeGraceMs !== UNLIMITED_CONNECTION_AGE_MS) { @@ -1388,52 +1364,316 @@ export class Server { } }, this.maxConnectionAgeMs + jitter).unref?.(); } + const keeapliveTimeTimer: NodeJS.Timeout | null = setInterval(() => { const timeoutTImer = setTimeout(() => { sessionClosedByServer = true; - if (this.channelzEnabled) { - this.channelzTrace.addTrace( - 'CT_INFO', - 'Connection dropped by keepalive timeout from ' + clientAddress - ); - } + this.channelzTrace.addTrace( + 'CT_INFO', + 'Connection dropped by keepalive timeout from ' + clientAddress + ); + session.close(); }, this.keepaliveTimeoutMs).unref?.(); try { session.ping( (err: Error | null, duration: number, payload: Buffer) => { clearTimeout(timeoutTImer); + + if (err) { + sessionClosedByServer = true; + this.channelzTrace.addTrace( + 'CT_INFO', + `Connection dropped due to error of a ping frame ${err.message} return in ${duration}` + ); + + session.close(); + } } ); + channelzSessionInfo.keepAlivesSent += 1; } catch (e) { // The ping can't be sent because the session is already closed session.destroy(); } }, this.keepaliveTimeMs).unref?.(); + session.on('close', () => { - if (this.channelzEnabled) { - if (!sessionClosedByServer) { - this.channelzTrace.addTrace( - 'CT_INFO', - 'Connection dropped by client ' + clientAddress - ); - } - this.sessionChildrenTracker.unrefChild(channelzRef); - unregisterChannelzRef(channelzRef); + if (!sessionClosedByServer) { + this.channelzTrace.addTrace( + 'CT_INFO', + 'Connection dropped by client ' + clientAddress + ); } + this.trace( + `DROPPING ${channelzRef.name} - ${channelzRef.kind} - ${channelzRef.id}` + ); + this.sessionChildrenTracker.unrefChild(channelzRef); + unregisterChannelzRef(channelzRef); + if (connectionAgeTimer) { clearTimeout(connectionAgeTimer); } + if (connectionAgeGraceTimer) { clearTimeout(connectionAgeGraceTimer); } + if (keeapliveTimeTimer) { clearTimeout(keeapliveTimeTimer); } + + clearTimeout(idleTimeoutObj.timeout); + this.sessionIdleTimeouts.delete(session); + this.http2Servers.get(http2Server)?.sessions.delete(session); this.sessions.delete(session); }); - }); + }; + } + + private _channelzHandler(http2Server: http2.Http2Server) { + return ( + stream: http2.ServerHttp2Stream, + headers: http2.IncomingHttpHeaders + ) => { + // for handling idle timeout + this.onStreamOpened(stream); + + const channelzSessionInfo = this.sessions.get( + stream.session as http2.ServerHttp2Session + ); + + this.callTracker.addCallStarted(); + channelzSessionInfo?.streamTracker.addCallStarted(); + + if (!this._verifyContentType(stream, headers)) { + this.callTracker.addCallFailed(); + channelzSessionInfo?.streamTracker.addCallFailed(); + return; + } + + const path = headers[HTTP2_HEADER_PATH] as string; + + const handler = this._retrieveHandler(path); + if (!handler) { + this._respondWithError( + getUnimplementedStatusResponse(path), + stream, + channelzSessionInfo + ); + return; + } + + const callEventTracker: CallEventTracker = { + addMessageSent: () => { + if (channelzSessionInfo) { + channelzSessionInfo.messagesSent += 1; + channelzSessionInfo.lastMessageSentTimestamp = new Date(); + } + }, + addMessageReceived: () => { + if (channelzSessionInfo) { + channelzSessionInfo.messagesReceived += 1; + channelzSessionInfo.lastMessageReceivedTimestamp = new Date(); + } + }, + onCallEnd: status => { + if (status.code === Status.OK) { + this.callTracker.addCallSucceeded(); + } else { + this.callTracker.addCallFailed(); + } + }, + onStreamEnd: success => { + if (channelzSessionInfo) { + if (success) { + channelzSessionInfo.streamTracker.addCallSucceeded(); + } else { + channelzSessionInfo.streamTracker.addCallFailed(); + } + } + }, + }; + + const call = getServerInterceptingCall( + this.interceptors, + stream, + headers, + callEventTracker, + handler, + this.options + ); + + if (!this._runHandlerForCall(call, handler)) { + this.callTracker.addCallFailed(); + channelzSessionInfo?.streamTracker.addCallFailed(); + + call.sendStatus({ + code: Status.INTERNAL, + details: `Unknown handler type: ${handler.type}`, + }); + } + }; + } + + private _streamHandler(http2Server: http2.Http2Server) { + return ( + stream: http2.ServerHttp2Stream, + headers: http2.IncomingHttpHeaders + ) => { + // for handling idle timeout + this.onStreamOpened(stream); + + if (this._verifyContentType(stream, headers) !== true) { + return; + } + + const path = headers[HTTP2_HEADER_PATH] as string; + + const handler = this._retrieveHandler(path); + if (!handler) { + this._respondWithError( + getUnimplementedStatusResponse(path), + stream, + null + ); + return; + } + + const call = getServerInterceptingCall( + this.interceptors, + stream, + headers, + null, + handler, + this.options + ); + + if (!this._runHandlerForCall(call, handler)) { + call.sendStatus({ + code: Status.INTERNAL, + details: `Unknown handler type: ${handler.type}`, + }); + } + }; + } + + private _runHandlerForCall( + call: ServerInterceptingCallInterface, + handler: + | UntypedUnaryHandler + | UntypedClientStreamingHandler + | UntypedServerStreamingHandler + | UntypedBidiStreamingHandler + ): boolean { + const { type } = handler; + if (type === 'unary') { + handleUnary(call, handler); + } else if (type === 'clientStream') { + handleClientStreaming(call, handler); + } else if (type === 'serverStream') { + handleServerStreaming(call, handler); + } else if (type === 'bidi') { + handleBidiStreaming(call, handler); + } else { + return false; + } + + return true; + } + + private _setupHandlers( + http2Server: http2.Http2Server | http2.Http2SecureServer + ): void { + if (http2Server === null) { + return; + } + + const serverAddress = http2Server.address(); + let serverAddressString = 'null'; + if (serverAddress) { + if (typeof serverAddress === 'string') { + serverAddressString = serverAddress; + } else { + serverAddressString = serverAddress.address + ':' + serverAddress.port; + } + } + this.serverAddressString = serverAddressString; + + const handler = this.channelzEnabled + ? this._channelzHandler(http2Server) + : this._streamHandler(http2Server); + + const sessionHandler = this.channelzEnabled + ? this._channelzSessionHandler(http2Server) + : this._sessionHandler(http2Server); + + http2Server.on('stream', handler); + http2Server.on('session', sessionHandler); + } + + private enableIdleTimeout(session: http2.ServerHttp2Session) { + const idleTimeoutObj = { + activeStreams: 0, + timeout: setTimeout( + this.onIdleTimeout, + this.sessionIdleTimeout, + this, + session + ).unref(), + }; + this.sessionIdleTimeouts.set(session, idleTimeoutObj); + + this.trace(`Enable idle timeout for ${session.socket?.remoteAddress}`); + + return idleTimeoutObj; + } + + private onIdleTimeout(ctx: Server, session: http2.ServerHttp2Session) { + ctx.trace(`Idle timeout for ${session.socket?.remoteAddress}`); + ctx.closeSession(session); + } + + private onStreamOpened(stream: http2.ServerHttp2Stream) { + const session = stream.session as http2.ServerHttp2Session; + this.trace(`Stream opened for ${session.socket?.remoteAddress}`); + const idleTimeoutObj = this.sessionIdleTimeouts.get(session); + if (idleTimeoutObj) { + idleTimeoutObj.activeStreams += 1; + if (idleTimeoutObj.timeout) { + clearTimeout(idleTimeoutObj.timeout); + idleTimeoutObj.timeout = null; + } + + this.trace( + `onStreamOpened: adding on stream close event for ${session.socket?.remoteAddress}` + ); + stream.once('close', () => this.onStreamClose(session)); + } else { + this.trace( + `onStreamOpened: missing stream for ${session.socket?.remoteAddress}` + ); + } + } + + private onStreamClose(session: http2.ServerHttp2Session) { + this.trace(`Stream closed for ${session.socket?.remoteAddress}`); + const idleTimeoutObj = this.sessionIdleTimeouts.get(session); + if (idleTimeoutObj) { + idleTimeoutObj.activeStreams -= 1; + if (idleTimeoutObj.activeStreams === 0) { + this.trace( + `onStreamClose: set idle timeout for ${this.sessionIdleTimeout}ms ${session.socket?.remoteAddress}` + ); + idleTimeoutObj.timeout = setTimeout( + this.onIdleTimeout, + this.sessionIdleTimeout, + this, + session + ).unref(); + } + } } } diff --git a/packages/grpc-js/src/subchannel-interface.ts b/packages/grpc-js/src/subchannel-interface.ts index c26669ba3..6c314189a 100644 --- a/packages/grpc-js/src/subchannel-interface.ts +++ b/packages/grpc-js/src/subchannel-interface.ts @@ -15,7 +15,7 @@ * */ -import { SubchannelRef } from './channelz'; +import type { SubchannelRef } from './channelz'; import { ConnectivityState } from './connectivity-state'; import { Subchannel } from './subchannel'; diff --git a/packages/grpc-js/src/subchannel.ts b/packages/grpc-js/src/subchannel.ts index 63e254cf3..95b600c4c 100644 --- a/packages/grpc-js/src/subchannel.ts +++ b/packages/grpc-js/src/subchannel.ts @@ -31,10 +31,13 @@ import { SubchannelRef, ChannelzTrace, ChannelzChildrenTracker, + ChannelzChildrenTrackerStub, SubchannelInfo, registerChannelzSubchannel, ChannelzCallTracker, + ChannelzCallTrackerStub, unregisterChannelzRef, + ChannelzTraceStub, } from './channelz'; import { ConnectivityStateListener, @@ -89,12 +92,15 @@ export class Subchannel { // Channelz info private readonly channelzEnabled: boolean = true; private channelzRef: SubchannelRef; - private channelzTrace: ChannelzTrace; - private callTracker = new ChannelzCallTracker(); - private childrenTracker = new ChannelzChildrenTracker(); + + private channelzTrace: ChannelzTrace | ChannelzTraceStub; + private callTracker: ChannelzCallTracker | ChannelzCallTrackerStub; + private childrenTracker: + | ChannelzChildrenTracker + | ChannelzChildrenTrackerStub; // Channelz socket info - private streamTracker = new ChannelzCallTracker(); + private streamTracker: ChannelzCallTracker | ChannelzCallTrackerStub; /** * A class representing a connection to a single backend. @@ -127,16 +133,24 @@ export class Subchannel { if (options['grpc.enable_channelz'] === 0) { this.channelzEnabled = false; + this.channelzTrace = new ChannelzTraceStub(); + this.callTracker = new ChannelzCallTrackerStub(); + this.childrenTracker = new ChannelzChildrenTrackerStub(); + this.streamTracker = new ChannelzCallTrackerStub(); + } else { + this.channelzTrace = new ChannelzTrace(); + this.callTracker = new ChannelzCallTracker(); + this.childrenTracker = new ChannelzChildrenTracker(); + this.streamTracker = new ChannelzCallTracker(); } - this.channelzTrace = new ChannelzTrace(); + this.channelzRef = registerChannelzSubchannel( this.subchannelAddressString, () => this.getChannelzInfo(), this.channelzEnabled ); - if (this.channelzEnabled) { - this.channelzTrace.addTrace('CT_INFO', 'Subchannel created'); - } + + this.channelzTrace.addTrace('CT_INFO', 'Subchannel created'); this.trace( 'Subchannel constructed with options ' + JSON.stringify(options, undefined, 2) @@ -338,12 +352,8 @@ export class Subchannel { this.refTrace('refcount ' + this.refcount + ' -> ' + (this.refcount - 1)); this.refcount -= 1; if (this.refcount === 0) { - if (this.channelzEnabled) { - this.channelzTrace.addTrace('CT_INFO', 'Shutting down'); - } - if (this.channelzEnabled) { - unregisterChannelzRef(this.channelzRef); - } + this.channelzTrace.addTrace('CT_INFO', 'Shutting down'); + unregisterChannelzRef(this.channelzRef); process.nextTick(() => { this.transitionToState( [ConnectivityState.CONNECTING, ConnectivityState.READY], diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index c4941b068..620488635 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -28,6 +28,7 @@ import { ChannelCredentials } from './channel-credentials'; import { ChannelOptions } from './channel-options'; import { ChannelzCallTracker, + ChannelzCallTrackerStub, registerChannelzSocket, SocketInfo, SocketRef, @@ -136,7 +137,7 @@ class Http2Transport implements Transport { // Channelz info private channelzRef: SocketRef; private readonly channelzEnabled: boolean = true; - private streamTracker = new ChannelzCallTracker(); + private streamTracker: ChannelzCallTracker | ChannelzCallTrackerStub; private keepalivesSent = 0; private messagesSent = 0; private messagesReceived = 0; @@ -159,12 +160,17 @@ class Http2Transport implements Transport { if (options['grpc.enable_channelz'] === 0) { this.channelzEnabled = false; + this.streamTracker = new ChannelzCallTrackerStub(); + } else { + this.streamTracker = new ChannelzCallTracker(); } + this.channelzRef = registerChannelzSocket( this.subchannelAddressString, () => this.getChannelzInfo(), this.channelzEnabled ); + // Build user-agent string. this.userAgent = [ options['grpc.primary_user_agent'], @@ -192,6 +198,7 @@ class Http2Transport implements Transport { this.stopKeepalivePings(); this.handleDisconnect(); }); + session.once( 'goaway', (errorCode: number, lastStreamID: number, opaqueData?: Buffer) => { @@ -214,11 +221,13 @@ class Http2Transport implements Transport { this.reportDisconnectToOwner(tooManyPings); } ); + session.once('error', error => { /* Do nothing here. Any error should also trigger a close event, which is * where we want to handle that. */ this.trace('connection closed with error ' + (error as Error).message); }); + if (logging.isTracerEnabled(TRACER_NAME)) { session.on('remoteSettings', (settings: http2.Settings) => { this.trace( @@ -237,6 +246,7 @@ class Http2Transport implements Transport { ); }); } + /* Start the keepalive timer last, because this can trigger trace logs, * which should only happen after everything else is set up. */ if (this.keepaliveWithoutCalls) { @@ -625,6 +635,7 @@ export class Http2SubchannelConnector implements SubchannelConnector { private session: http2.ClientHttp2Session | null = null; private isShutdown = false; constructor(private channelTarget: GrpcUri) {} + private trace(text: string) { logging.trace( LogVerbosity.DEBUG, @@ -632,6 +643,7 @@ export class Http2SubchannelConnector implements SubchannelConnector { uriToString(this.channelTarget) + ' ' + text ); } + private createSession( address: SubchannelAddress, credentials: ChannelCredentials, @@ -641,6 +653,7 @@ export class Http2SubchannelConnector implements SubchannelConnector { if (this.isShutdown) { return Promise.reject(); } + return new Promise((resolve, reject) => { let remoteName: string | null; if (proxyConnectionResult.realTarget) { @@ -767,6 +780,7 @@ export class Http2SubchannelConnector implements SubchannelConnector { }); }); } + connect( address: SubchannelAddress, credentials: ChannelCredentials, diff --git a/packages/grpc-js/test/common.ts b/packages/grpc-js/test/common.ts index 88aa129aa..eaa701f18 100644 --- a/packages/grpc-js/test/common.ts +++ b/packages/grpc-js/test/common.ts @@ -31,7 +31,7 @@ import { HealthListener, SubchannelInterface, } from '../src/subchannel-interface'; -import { SubchannelRef } from '../src/channelz'; +import { EntityTypes, SubchannelRef } from '../src/channelz'; import { Subchannel } from '../src/subchannel'; import { ConnectivityState } from '../src/connectivity-state'; @@ -196,7 +196,7 @@ export class MockSubchannel implements SubchannelInterface { unref(): void {} getChannelzRef(): SubchannelRef { return { - kind: 'subchannel', + kind: EntityTypes.subchannel, id: -1, name: this.address, }; From a4a676d3788976ef3e8bb1049f37b971eb8d8d4d Mon Sep 17 00:00:00 2001 From: AVVS Date: Tue, 27 Feb 2024 14:17:32 -0800 Subject: [PATCH 080/109] chore: move new functions towards the end of the class --- packages/grpc-js/src/server.ts | 362 ++++++++++++++++----------------- 1 file changed, 181 insertions(+), 181 deletions(-) diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index 0a5bc0e9b..1229b8c6c 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -1191,6 +1191,187 @@ export class Server { channelzSessionInfo?.streamTracker.addCallFailed(); } + private _channelzHandler(http2Server: http2.Http2Server) { + return ( + stream: http2.ServerHttp2Stream, + headers: http2.IncomingHttpHeaders + ) => { + // for handling idle timeout + this.onStreamOpened(stream); + + const channelzSessionInfo = this.sessions.get( + stream.session as http2.ServerHttp2Session + ); + + this.callTracker.addCallStarted(); + channelzSessionInfo?.streamTracker.addCallStarted(); + + if (!this._verifyContentType(stream, headers)) { + this.callTracker.addCallFailed(); + channelzSessionInfo?.streamTracker.addCallFailed(); + return; + } + + const path = headers[HTTP2_HEADER_PATH] as string; + + const handler = this._retrieveHandler(path); + if (!handler) { + this._respondWithError( + getUnimplementedStatusResponse(path), + stream, + channelzSessionInfo + ); + return; + } + + const callEventTracker: CallEventTracker = { + addMessageSent: () => { + if (channelzSessionInfo) { + channelzSessionInfo.messagesSent += 1; + channelzSessionInfo.lastMessageSentTimestamp = new Date(); + } + }, + addMessageReceived: () => { + if (channelzSessionInfo) { + channelzSessionInfo.messagesReceived += 1; + channelzSessionInfo.lastMessageReceivedTimestamp = new Date(); + } + }, + onCallEnd: status => { + if (status.code === Status.OK) { + this.callTracker.addCallSucceeded(); + } else { + this.callTracker.addCallFailed(); + } + }, + onStreamEnd: success => { + if (channelzSessionInfo) { + if (success) { + channelzSessionInfo.streamTracker.addCallSucceeded(); + } else { + channelzSessionInfo.streamTracker.addCallFailed(); + } + } + }, + }; + + const call = getServerInterceptingCall( + this.interceptors, + stream, + headers, + callEventTracker, + handler, + this.options + ); + + if (!this._runHandlerForCall(call, handler)) { + this.callTracker.addCallFailed(); + channelzSessionInfo?.streamTracker.addCallFailed(); + + call.sendStatus({ + code: Status.INTERNAL, + details: `Unknown handler type: ${handler.type}`, + }); + } + }; + } + + private _streamHandler(http2Server: http2.Http2Server) { + return ( + stream: http2.ServerHttp2Stream, + headers: http2.IncomingHttpHeaders + ) => { + // for handling idle timeout + this.onStreamOpened(stream); + + if (this._verifyContentType(stream, headers) !== true) { + return; + } + + const path = headers[HTTP2_HEADER_PATH] as string; + + const handler = this._retrieveHandler(path); + if (!handler) { + this._respondWithError( + getUnimplementedStatusResponse(path), + stream, + null + ); + return; + } + + const call = getServerInterceptingCall( + this.interceptors, + stream, + headers, + null, + handler, + this.options + ); + + if (!this._runHandlerForCall(call, handler)) { + call.sendStatus({ + code: Status.INTERNAL, + details: `Unknown handler type: ${handler.type}`, + }); + } + }; + } + + private _runHandlerForCall( + call: ServerInterceptingCallInterface, + handler: + | UntypedUnaryHandler + | UntypedClientStreamingHandler + | UntypedServerStreamingHandler + | UntypedBidiStreamingHandler + ): boolean { + const { type } = handler; + if (type === 'unary') { + handleUnary(call, handler); + } else if (type === 'clientStream') { + handleClientStreaming(call, handler); + } else if (type === 'serverStream') { + handleServerStreaming(call, handler); + } else if (type === 'bidi') { + handleBidiStreaming(call, handler); + } else { + return false; + } + + return true; + } + + private _setupHandlers( + http2Server: http2.Http2Server | http2.Http2SecureServer + ): void { + if (http2Server === null) { + return; + } + + const serverAddress = http2Server.address(); + let serverAddressString = 'null'; + if (serverAddress) { + if (typeof serverAddress === 'string') { + serverAddressString = serverAddress; + } else { + serverAddressString = serverAddress.address + ':' + serverAddress.port; + } + } + this.serverAddressString = serverAddressString; + + const handler = this.channelzEnabled + ? this._channelzHandler(http2Server) + : this._streamHandler(http2Server); + + const sessionHandler = this.channelzEnabled + ? this._channelzSessionHandler(http2Server) + : this._sessionHandler(http2Server); + + http2Server.on('stream', handler); + http2Server.on('session', sessionHandler); + } + private _sessionHandler( http2Server: http2.Http2Server | http2.Http2SecureServer ) { @@ -1432,187 +1613,6 @@ export class Server { }; } - private _channelzHandler(http2Server: http2.Http2Server) { - return ( - stream: http2.ServerHttp2Stream, - headers: http2.IncomingHttpHeaders - ) => { - // for handling idle timeout - this.onStreamOpened(stream); - - const channelzSessionInfo = this.sessions.get( - stream.session as http2.ServerHttp2Session - ); - - this.callTracker.addCallStarted(); - channelzSessionInfo?.streamTracker.addCallStarted(); - - if (!this._verifyContentType(stream, headers)) { - this.callTracker.addCallFailed(); - channelzSessionInfo?.streamTracker.addCallFailed(); - return; - } - - const path = headers[HTTP2_HEADER_PATH] as string; - - const handler = this._retrieveHandler(path); - if (!handler) { - this._respondWithError( - getUnimplementedStatusResponse(path), - stream, - channelzSessionInfo - ); - return; - } - - const callEventTracker: CallEventTracker = { - addMessageSent: () => { - if (channelzSessionInfo) { - channelzSessionInfo.messagesSent += 1; - channelzSessionInfo.lastMessageSentTimestamp = new Date(); - } - }, - addMessageReceived: () => { - if (channelzSessionInfo) { - channelzSessionInfo.messagesReceived += 1; - channelzSessionInfo.lastMessageReceivedTimestamp = new Date(); - } - }, - onCallEnd: status => { - if (status.code === Status.OK) { - this.callTracker.addCallSucceeded(); - } else { - this.callTracker.addCallFailed(); - } - }, - onStreamEnd: success => { - if (channelzSessionInfo) { - if (success) { - channelzSessionInfo.streamTracker.addCallSucceeded(); - } else { - channelzSessionInfo.streamTracker.addCallFailed(); - } - } - }, - }; - - const call = getServerInterceptingCall( - this.interceptors, - stream, - headers, - callEventTracker, - handler, - this.options - ); - - if (!this._runHandlerForCall(call, handler)) { - this.callTracker.addCallFailed(); - channelzSessionInfo?.streamTracker.addCallFailed(); - - call.sendStatus({ - code: Status.INTERNAL, - details: `Unknown handler type: ${handler.type}`, - }); - } - }; - } - - private _streamHandler(http2Server: http2.Http2Server) { - return ( - stream: http2.ServerHttp2Stream, - headers: http2.IncomingHttpHeaders - ) => { - // for handling idle timeout - this.onStreamOpened(stream); - - if (this._verifyContentType(stream, headers) !== true) { - return; - } - - const path = headers[HTTP2_HEADER_PATH] as string; - - const handler = this._retrieveHandler(path); - if (!handler) { - this._respondWithError( - getUnimplementedStatusResponse(path), - stream, - null - ); - return; - } - - const call = getServerInterceptingCall( - this.interceptors, - stream, - headers, - null, - handler, - this.options - ); - - if (!this._runHandlerForCall(call, handler)) { - call.sendStatus({ - code: Status.INTERNAL, - details: `Unknown handler type: ${handler.type}`, - }); - } - }; - } - - private _runHandlerForCall( - call: ServerInterceptingCallInterface, - handler: - | UntypedUnaryHandler - | UntypedClientStreamingHandler - | UntypedServerStreamingHandler - | UntypedBidiStreamingHandler - ): boolean { - const { type } = handler; - if (type === 'unary') { - handleUnary(call, handler); - } else if (type === 'clientStream') { - handleClientStreaming(call, handler); - } else if (type === 'serverStream') { - handleServerStreaming(call, handler); - } else if (type === 'bidi') { - handleBidiStreaming(call, handler); - } else { - return false; - } - - return true; - } - - private _setupHandlers( - http2Server: http2.Http2Server | http2.Http2SecureServer - ): void { - if (http2Server === null) { - return; - } - - const serverAddress = http2Server.address(); - let serverAddressString = 'null'; - if (serverAddress) { - if (typeof serverAddress === 'string') { - serverAddressString = serverAddress; - } else { - serverAddressString = serverAddress.address + ':' + serverAddress.port; - } - } - this.serverAddressString = serverAddressString; - - const handler = this.channelzEnabled - ? this._channelzHandler(http2Server) - : this._streamHandler(http2Server); - - const sessionHandler = this.channelzEnabled - ? this._channelzSessionHandler(http2Server) - : this._sessionHandler(http2Server); - - http2Server.on('stream', handler); - http2Server.on('session', sessionHandler); - } - private enableIdleTimeout(session: http2.ServerHttp2Session) { const idleTimeoutObj = { activeStreams: 0, From b8f157ed21add2daffe11e320781e345d7cc38fb Mon Sep 17 00:00:00 2001 From: AVVS Date: Tue, 27 Feb 2024 14:30:55 -0800 Subject: [PATCH 081/109] chore: revert interface -> type change in channelz --- packages/grpc-js/src/channelz.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/grpc-js/src/channelz.ts b/packages/grpc-js/src/channelz.ts index 6d70b7543..4eab762a8 100644 --- a/packages/grpc-js/src/channelz.ts +++ b/packages/grpc-js/src/channelz.ts @@ -330,25 +330,25 @@ export interface SocketInfo { remoteFlowControlWindow: number | null; } -type ChannelEntry = { +interface ChannelEntry { ref: ChannelRef; getInfo(): ChannelInfo; -}; +} -type SubchannelEntry = { +interface SubchannelEntry { ref: SubchannelRef; getInfo(): SubchannelInfo; -}; +} -type ServerEntry = { +interface ServerEntry { ref: ServerRef; getInfo(): ServerInfo; -}; +} -type SocketEntry = { +interface SocketEntry { ref: SocketRef; getInfo(): SocketInfo; -}; +} export const enum EntityTypes { channel = 'channel', From 0b79b7420a77564babca5e8718ef95c6aedbea71 Mon Sep 17 00:00:00 2001 From: AVVS Date: Tue, 27 Feb 2024 14:35:02 -0800 Subject: [PATCH 082/109] chore: cleanup traces --- packages/grpc-js/src/server.ts | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index 1229b8c6c..67b5fdf73 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -1586,9 +1586,7 @@ export class Server { 'Connection dropped by client ' + clientAddress ); } - this.trace( - `DROPPING ${channelzRef.name} - ${channelzRef.kind} - ${channelzRef.id}` - ); + this.sessionChildrenTracker.unrefChild(channelzRef); unregisterChannelzRef(channelzRef); @@ -1637,7 +1635,7 @@ export class Server { private onStreamOpened(stream: http2.ServerHttp2Stream) { const session = stream.session as http2.ServerHttp2Session; - this.trace(`Stream opened for ${session.socket?.remoteAddress}`); + const idleTimeoutObj = this.sessionIdleTimeouts.get(session); if (idleTimeoutObj) { idleTimeoutObj.activeStreams += 1; @@ -1646,26 +1644,16 @@ export class Server { idleTimeoutObj.timeout = null; } - this.trace( - `onStreamOpened: adding on stream close event for ${session.socket?.remoteAddress}` - ); stream.once('close', () => this.onStreamClose(session)); - } else { - this.trace( - `onStreamOpened: missing stream for ${session.socket?.remoteAddress}` - ); } } private onStreamClose(session: http2.ServerHttp2Session) { - this.trace(`Stream closed for ${session.socket?.remoteAddress}`); const idleTimeoutObj = this.sessionIdleTimeouts.get(session); + if (idleTimeoutObj) { idleTimeoutObj.activeStreams -= 1; if (idleTimeoutObj.activeStreams === 0) { - this.trace( - `onStreamClose: set idle timeout for ${this.sessionIdleTimeout}ms ${session.socket?.remoteAddress}` - ); idleTimeoutObj.timeout = setTimeout( this.onIdleTimeout, this.sessionIdleTimeout, From 74102fcc872759d18a62cf5098256eb521064b0c Mon Sep 17 00:00:00 2001 From: AVVS Date: Tue, 27 Feb 2024 14:39:24 -0800 Subject: [PATCH 083/109] chore: extraneous closure, dont need server ref --- packages/grpc-js/src/server.ts | 216 ++++++++++++++++----------------- 1 file changed, 106 insertions(+), 110 deletions(-) diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index 67b5fdf73..eed097f83 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -1191,131 +1191,127 @@ export class Server { channelzSessionInfo?.streamTracker.addCallFailed(); } - private _channelzHandler(http2Server: http2.Http2Server) { - return ( - stream: http2.ServerHttp2Stream, - headers: http2.IncomingHttpHeaders - ) => { - // for handling idle timeout - this.onStreamOpened(stream); - - const channelzSessionInfo = this.sessions.get( - stream.session as http2.ServerHttp2Session - ); + private _channelzHandler( + stream: http2.ServerHttp2Stream, + headers: http2.IncomingHttpHeaders + ) { + // for handling idle timeout + this.onStreamOpened(stream); - this.callTracker.addCallStarted(); - channelzSessionInfo?.streamTracker.addCallStarted(); + const channelzSessionInfo = this.sessions.get( + stream.session as http2.ServerHttp2Session + ); - if (!this._verifyContentType(stream, headers)) { - this.callTracker.addCallFailed(); - channelzSessionInfo?.streamTracker.addCallFailed(); - return; - } + this.callTracker.addCallStarted(); + channelzSessionInfo?.streamTracker.addCallStarted(); - const path = headers[HTTP2_HEADER_PATH] as string; + if (!this._verifyContentType(stream, headers)) { + this.callTracker.addCallFailed(); + channelzSessionInfo?.streamTracker.addCallFailed(); + return; + } - const handler = this._retrieveHandler(path); - if (!handler) { - this._respondWithError( - getUnimplementedStatusResponse(path), - stream, - channelzSessionInfo - ); - return; - } + const path = headers[HTTP2_HEADER_PATH] as string; - const callEventTracker: CallEventTracker = { - addMessageSent: () => { - if (channelzSessionInfo) { - channelzSessionInfo.messagesSent += 1; - channelzSessionInfo.lastMessageSentTimestamp = new Date(); - } - }, - addMessageReceived: () => { - if (channelzSessionInfo) { - channelzSessionInfo.messagesReceived += 1; - channelzSessionInfo.lastMessageReceivedTimestamp = new Date(); - } - }, - onCallEnd: status => { - if (status.code === Status.OK) { - this.callTracker.addCallSucceeded(); + const handler = this._retrieveHandler(path); + if (!handler) { + this._respondWithError( + getUnimplementedStatusResponse(path), + stream, + channelzSessionInfo + ); + return; + } + + const callEventTracker: CallEventTracker = { + addMessageSent: () => { + if (channelzSessionInfo) { + channelzSessionInfo.messagesSent += 1; + channelzSessionInfo.lastMessageSentTimestamp = new Date(); + } + }, + addMessageReceived: () => { + if (channelzSessionInfo) { + channelzSessionInfo.messagesReceived += 1; + channelzSessionInfo.lastMessageReceivedTimestamp = new Date(); + } + }, + onCallEnd: status => { + if (status.code === Status.OK) { + this.callTracker.addCallSucceeded(); + } else { + this.callTracker.addCallFailed(); + } + }, + onStreamEnd: success => { + if (channelzSessionInfo) { + if (success) { + channelzSessionInfo.streamTracker.addCallSucceeded(); } else { - this.callTracker.addCallFailed(); - } - }, - onStreamEnd: success => { - if (channelzSessionInfo) { - if (success) { - channelzSessionInfo.streamTracker.addCallSucceeded(); - } else { - channelzSessionInfo.streamTracker.addCallFailed(); - } + channelzSessionInfo.streamTracker.addCallFailed(); } - }, - }; + } + }, + }; - const call = getServerInterceptingCall( - this.interceptors, - stream, - headers, - callEventTracker, - handler, - this.options - ); + const call = getServerInterceptingCall( + this.interceptors, + stream, + headers, + callEventTracker, + handler, + this.options + ); - if (!this._runHandlerForCall(call, handler)) { - this.callTracker.addCallFailed(); - channelzSessionInfo?.streamTracker.addCallFailed(); + if (!this._runHandlerForCall(call, handler)) { + this.callTracker.addCallFailed(); + channelzSessionInfo?.streamTracker.addCallFailed(); - call.sendStatus({ - code: Status.INTERNAL, - details: `Unknown handler type: ${handler.type}`, - }); - } - }; + call.sendStatus({ + code: Status.INTERNAL, + details: `Unknown handler type: ${handler.type}`, + }); + } } - private _streamHandler(http2Server: http2.Http2Server) { - return ( - stream: http2.ServerHttp2Stream, - headers: http2.IncomingHttpHeaders - ) => { - // for handling idle timeout - this.onStreamOpened(stream); - - if (this._verifyContentType(stream, headers) !== true) { - return; - } + private _streamHandler( + stream: http2.ServerHttp2Stream, + headers: http2.IncomingHttpHeaders + ) { + // for handling idle timeout + this.onStreamOpened(stream); - const path = headers[HTTP2_HEADER_PATH] as string; + if (this._verifyContentType(stream, headers) !== true) { + return; + } - const handler = this._retrieveHandler(path); - if (!handler) { - this._respondWithError( - getUnimplementedStatusResponse(path), - stream, - null - ); - return; - } + const path = headers[HTTP2_HEADER_PATH] as string; - const call = getServerInterceptingCall( - this.interceptors, + const handler = this._retrieveHandler(path); + if (!handler) { + this._respondWithError( + getUnimplementedStatusResponse(path), stream, - headers, - null, - handler, - this.options + null ); + return; + } - if (!this._runHandlerForCall(call, handler)) { - call.sendStatus({ - code: Status.INTERNAL, - details: `Unknown handler type: ${handler.type}`, - }); - } - }; + const call = getServerInterceptingCall( + this.interceptors, + stream, + headers, + null, + handler, + this.options + ); + + if (!this._runHandlerForCall(call, handler)) { + call.sendStatus({ + code: Status.INTERNAL, + details: `Unknown handler type: ${handler.type}`, + }); + } } private _runHandlerForCall( @@ -1361,14 +1357,14 @@ export class Server { this.serverAddressString = serverAddressString; const handler = this.channelzEnabled - ? this._channelzHandler(http2Server) - : this._streamHandler(http2Server); + ? this._channelzHandler + : this._streamHandler; const sessionHandler = this.channelzEnabled ? this._channelzSessionHandler(http2Server) : this._sessionHandler(http2Server); - http2Server.on('stream', handler); + http2Server.on('stream', handler.bind(this)); http2Server.on('session', sessionHandler); } From 11a98b5f373ff32ba5f4aceabfa8a98197ec1675 Mon Sep 17 00:00:00 2001 From: AVVS Date: Tue, 27 Feb 2024 16:49:20 -0800 Subject: [PATCH 084/109] chore: updated docs, cached onStreamClose per session --- packages/grpc-js/README.md | 3 + packages/grpc-js/src/channel-options.ts | 1 + packages/grpc-js/src/server.ts | 188 ++++++++++++++---------- 3 files changed, 116 insertions(+), 76 deletions(-) diff --git a/packages/grpc-js/README.md b/packages/grpc-js/README.md index eb04ece2f..f3b682f3c 100644 --- a/packages/grpc-js/README.md +++ b/packages/grpc-js/README.md @@ -60,6 +60,9 @@ Many channel arguments supported in `grpc` are not supported in `@grpc/grpc-js`. - `grpc.enable_channelz` - `grpc.dns_min_time_between_resolutions_ms` - `grpc.enable_retries` + - `grpc.max_connection_age_ms` + - `grpc.max_connection_age_grace_ms` + - `grpc.max_connection_idle_ms` - `grpc.per_rpc_retry_buffer_size` - `grpc.retry_buffer_size` - `grpc.service_config_disable_resolution` diff --git a/packages/grpc-js/src/channel-options.ts b/packages/grpc-js/src/channel-options.ts index aa1e6c83e..6804852e2 100644 --- a/packages/grpc-js/src/channel-options.ts +++ b/packages/grpc-js/src/channel-options.ts @@ -54,6 +54,7 @@ export interface ChannelOptions { 'grpc.retry_buffer_size'?: number; 'grpc.max_connection_age_ms'?: number; 'grpc.max_connection_age_grace_ms'?: number; + 'grpc.max_connection_idle_ms'?: number; 'grpc-node.max_session_memory'?: number; 'grpc.service_config_disable_resolution'?: number; 'grpc.client_idle_timeout_ms'?: number; diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index eed097f83..8a3a29fd8 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -90,7 +90,7 @@ import { CallEventTracker } from './transport'; const UNLIMITED_CONNECTION_AGE_MS = ~(1 << 31); const KEEPALIVE_MAX_TIME_MS = ~(1 << 31); const KEEPALIVE_TIMEOUT_MS = 20000; -const MAX_CONNECTION_IDLE_MS = 30 * 60 * 1e3; // 30 min +const MAX_CONNECTION_IDLE_MS = ~(1 << 31); const { HTTP2_HEADER_PATH } = http2.constants; @@ -241,6 +241,12 @@ interface Http2ServerInfo { sessions: Set; } +interface SessionIdleTimeoutTracker { + activeStreams: number; + timeout: NodeJS.Timeout | null; + onClose: (session: http2.ServerHttp2Session) => void | null; +} + export interface ServerOptions extends ChannelOptions { interceptors?: ServerInterceptor[]; } @@ -249,11 +255,8 @@ export class Server { private boundPorts: Map = new Map(); private http2Servers: Map = new Map(); private sessionIdleTimeouts = new Map< - http2.Http2Session, - { - activeStreams: number; - timeout: NodeJS.Timeout | null; - } + http2.ServerHttp2Session, + SessionIdleTimeoutTracker >(); private handlers: Map = new Map< @@ -330,7 +333,7 @@ export class Server { this.keepaliveTimeoutMs = this.options['grpc.keepalive_timeout_ms'] ?? KEEPALIVE_TIMEOUT_MS; this.sessionIdleTimeout = - this.options['grpc.max_connection_idle'] ?? MAX_CONNECTION_IDLE_MS; + this.options['grpc.max_connection_idle_ms'] ?? MAX_CONNECTION_IDLE_MS; this.commonServerOptions = { maxSendHeaderBlockLength: Number.MAX_SAFE_INTEGER, @@ -903,7 +906,6 @@ export class Server { if (sessionInfo) { this.sessionChildrenTracker.unrefChild(sessionInfo.ref); unregisterChannelzRef(sessionInfo.ref); - this.sessions.delete(session); } callback?.(); }; @@ -1001,7 +1003,7 @@ export class Server { for (const session of allSessions) { session.destroy(http2.constants.NGHTTP2_CANCEL as any); } - }, graceTimeMs).unref?.(); + }, graceTimeMs).unref(); } forceShutdown(): void { @@ -1376,6 +1378,7 @@ export class Server { let connectionAgeTimer: NodeJS.Timeout | null = null; let connectionAgeGraceTimer: NodeJS.Timeout | null = null; + let keeapliveTimeTimer: NodeJS.Timeout | null = null; let sessionClosedByServer = false; const idleTimeoutObj = this.enableIdleTimeout(session); @@ -1389,7 +1392,8 @@ export class Server { sessionClosedByServer = true; this.trace( - `Connection dropped by max connection age: ${session.socket?.remoteAddress}` + 'Connection dropped by max connection age: ' + + session.socket?.remoteAddress ); try { @@ -1410,36 +1414,38 @@ export class Server { if (this.maxConnectionAgeGraceMs !== UNLIMITED_CONNECTION_AGE_MS) { connectionAgeGraceTimer = setTimeout(() => { session.destroy(); - }, this.maxConnectionAgeGraceMs).unref?.(); + }, this.maxConnectionAgeGraceMs).unref(); } - }, this.maxConnectionAgeMs + jitter).unref?.(); + }, this.maxConnectionAgeMs + jitter).unref(); } - const keeapliveTimeTimer: NodeJS.Timeout | null = setInterval(() => { - const timeoutTImer = setTimeout(() => { - sessionClosedByServer = true; - session.close(); - }, this.keepaliveTimeoutMs).unref?.(); - - try { - session.ping( - (err: Error | null, duration: number, payload: Buffer) => { - clearTimeout(timeoutTImer); - - if (err) { - sessionClosedByServer = true; - this.trace( - `Connection dropped due to error of a ping frame ${err.message} return in ${duration}` - ); - session.close(); + if (this.keepaliveTimeMs < KEEPALIVE_MAX_TIME_MS) { + keeapliveTimeTimer = setInterval(() => { + const timeoutTimer = setTimeout(() => { + sessionClosedByServer = true; + session.close(); + }, this.keepaliveTimeoutMs).unref(); + + try { + session.ping( + (err: Error | null, duration: number, payload: Buffer) => { + clearTimeout(timeoutTimer); + + if (err) { + sessionClosedByServer = true; + this.trace( + `Connection dropped due to error of a ping frame ${err.message} return in ${duration}` + ); + session.close(); + } } - } - ); - } catch (e) { - // The ping can't be sent because the session is already closed - session.destroy(); - } - }, this.keepaliveTimeMs).unref?.(); + ); + } catch (e) { + // The ping can't be sent because the session is already closed + session.destroy(); + } + }, this.keepaliveTimeMs).unref(); + } session.on('close', () => { if (!sessionClosedByServer) { @@ -1460,8 +1466,12 @@ export class Server { clearTimeout(keeapliveTimeTimer); } - clearTimeout(idleTimeoutObj.timeout); - this.sessionIdleTimeouts.delete(session); + if (idleTimeoutObj !== null) { + if (idleTimeoutObj.timeout !== null) { + clearTimeout(idleTimeoutObj.timeout); + } + this.sessionIdleTimeouts.delete(session); + } this.http2Servers.get(http2Server)?.sessions.delete(session); }); @@ -1503,6 +1513,7 @@ export class Server { let connectionAgeTimer: NodeJS.Timeout | null = null; let connectionAgeGraceTimer: NodeJS.Timeout | null = null; + let keeapliveTimeTimer: NodeJS.Timeout | null = null; let sessionClosedByServer = false; const idleTimeoutObj = this.enableIdleTimeout(session); @@ -1537,43 +1548,48 @@ export class Server { if (this.maxConnectionAgeGraceMs !== UNLIMITED_CONNECTION_AGE_MS) { connectionAgeGraceTimer = setTimeout(() => { session.destroy(); - }, this.maxConnectionAgeGraceMs).unref?.(); + }, this.maxConnectionAgeGraceMs).unref(); } - }, this.maxConnectionAgeMs + jitter).unref?.(); + }, this.maxConnectionAgeMs + jitter).unref(); } - const keeapliveTimeTimer: NodeJS.Timeout | null = setInterval(() => { - const timeoutTImer = setTimeout(() => { - sessionClosedByServer = true; - this.channelzTrace.addTrace( - 'CT_INFO', - 'Connection dropped by keepalive timeout from ' + clientAddress - ); + if (this.keepaliveTimeMs < KEEPALIVE_MAX_TIME_MS) { + keeapliveTimeTimer = setInterval(() => { + const timeoutTImer = setTimeout(() => { + sessionClosedByServer = true; + this.channelzTrace.addTrace( + 'CT_INFO', + 'Connection dropped by keepalive timeout from ' + clientAddress + ); - session.close(); - }, this.keepaliveTimeoutMs).unref?.(); - try { - session.ping( - (err: Error | null, duration: number, payload: Buffer) => { - clearTimeout(timeoutTImer); - - if (err) { - sessionClosedByServer = true; - this.channelzTrace.addTrace( - 'CT_INFO', - `Connection dropped due to error of a ping frame ${err.message} return in ${duration}` - ); - - session.close(); + session.close(); + }, this.keepaliveTimeoutMs).unref(); + try { + session.ping( + (err: Error | null, duration: number, payload: Buffer) => { + clearTimeout(timeoutTImer); + + if (err) { + sessionClosedByServer = true; + this.channelzTrace.addTrace( + 'CT_INFO', + 'Connection dropped due to error of a ping frame ' + + err.message + + ' return in ' + + duration + ); + + session.close(); + } } - } - ); - channelzSessionInfo.keepAlivesSent += 1; - } catch (e) { - // The ping can't be sent because the session is already closed - session.destroy(); - } - }, this.keepaliveTimeMs).unref?.(); + ); + channelzSessionInfo.keepAlivesSent += 1; + } catch (e) { + // The ping can't be sent because the session is already closed + session.destroy(); + } + }, this.keepaliveTimeMs).unref(); + } session.on('close', () => { if (!sessionClosedByServer) { @@ -1598,8 +1614,12 @@ export class Server { clearTimeout(keeapliveTimeTimer); } - clearTimeout(idleTimeoutObj.timeout); - this.sessionIdleTimeouts.delete(session); + if (idleTimeoutObj !== null) { + if (idleTimeoutObj.timeout !== null) { + clearTimeout(idleTimeoutObj.timeout); + } + this.sessionIdleTimeouts.delete(session); + } this.http2Servers.get(http2Server)?.sessions.delete(session); this.sessions.delete(session); @@ -1607,9 +1627,16 @@ export class Server { }; } - private enableIdleTimeout(session: http2.ServerHttp2Session) { + private enableIdleTimeout( + session: http2.ServerHttp2Session + ): SessionIdleTimeoutTracker | null { + if (this.sessionIdleTimeout >= MAX_CONNECTION_IDLE_MS) { + return null; + } + const idleTimeoutObj = { activeStreams: 0, + onClose: this.onStreamClose.bind(this, session), // so that we don't recreate it each time timeout: setTimeout( this.onIdleTimeout, this.sessionIdleTimeout, @@ -1619,13 +1646,22 @@ export class Server { }; this.sessionIdleTimeouts.set(session, idleTimeoutObj); - this.trace(`Enable idle timeout for ${session.socket?.remoteAddress}`); + const { socket } = session; + this.trace( + 'Enable idle timeout for ' + + socket.remoteAddress + + ':' + + socket.remotePort + ); return idleTimeoutObj; } private onIdleTimeout(ctx: Server, session: http2.ServerHttp2Session) { - ctx.trace(`Idle timeout for ${session.socket?.remoteAddress}`); + const { socket } = session; + ctx.trace( + 'Idle timeout for ' + socket?.remoteAddress + ':' + socket?.remotePort + ); ctx.closeSession(session); } @@ -1640,7 +1676,7 @@ export class Server { idleTimeoutObj.timeout = null; } - stream.once('close', () => this.onStreamClose(session)); + stream.once('close', idleTimeoutObj.onClose); } } From bedb5055e89b39125d9466ee6f3c504f130792f6 Mon Sep 17 00:00:00 2001 From: AVVS Date: Wed, 28 Feb 2024 13:36:24 -0800 Subject: [PATCH 085/109] refactor: no clearTimeout/null timers, use .refresh() + count refs --- packages/grpc-js/src/server.ts | 51 +++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index 8a3a29fd8..32c18ea94 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -243,7 +243,8 @@ interface Http2ServerInfo { interface SessionIdleTimeoutTracker { activeStreams: number; - timeout: NodeJS.Timeout | null; + lastIdle: number; + timeout: NodeJS.Timeout; onClose: (session: http2.ServerHttp2Session) => void | null; } @@ -292,6 +293,7 @@ export class Server { private readonly keepaliveTimeoutMs: number; private readonly sessionIdleTimeout: number; + private readonly sessionHalfIdleTimeout: number; private readonly interceptors: ServerInterceptor[]; @@ -334,6 +336,7 @@ export class Server { this.options['grpc.keepalive_timeout_ms'] ?? KEEPALIVE_TIMEOUT_MS; this.sessionIdleTimeout = this.options['grpc.max_connection_idle_ms'] ?? MAX_CONNECTION_IDLE_MS; + this.sessionHalfIdleTimeout = Math.ceil(this.sessionIdleTimeout / 2); this.commonServerOptions = { maxSendHeaderBlockLength: Number.MAX_SAFE_INTEGER, @@ -1467,9 +1470,7 @@ export class Server { } if (idleTimeoutObj !== null) { - if (idleTimeoutObj.timeout !== null) { - clearTimeout(idleTimeoutObj.timeout); - } + clearTimeout(idleTimeoutObj.timeout); this.sessionIdleTimeouts.delete(session); } @@ -1615,9 +1616,7 @@ export class Server { } if (idleTimeoutObj !== null) { - if (idleTimeoutObj.timeout !== null) { - clearTimeout(idleTimeoutObj.timeout); - } + clearTimeout(idleTimeoutObj.timeout); this.sessionIdleTimeouts.delete(session); } @@ -1634,12 +1633,14 @@ export class Server { return null; } - const idleTimeoutObj = { + const idleTimeoutObj: SessionIdleTimeoutTracker = { activeStreams: 0, + lastIdle: Date.now(), onClose: this.onStreamClose.bind(this, session), // so that we don't recreate it each time + // this is 50% of the actual timeout, we will check half-way through and .refresh() for a subsequent check timeout: setTimeout( this.onIdleTimeout, - this.sessionIdleTimeout, + this.sessionHalfIdleTimeout, this, session ).unref(), @@ -1660,9 +1661,25 @@ export class Server { private onIdleTimeout(ctx: Server, session: http2.ServerHttp2Session) { const { socket } = session; ctx.trace( - 'Idle timeout for ' + socket?.remoteAddress + ':' + socket?.remotePort + 'Session idle timeout checkpoint for ' + + socket?.remoteAddress + + ':' + + socket?.remotePort ); - ctx.closeSession(session); + + const sessionInfo = ctx.sessionIdleTimeouts.get(session); + // if it is called while we have activeStreams - timer will not be rescheduled + // until last active stream is closed, then it will call .refresh() on the timer + // important part is to not clearTimeout(timer) or it becomes unusable + // for future refreshes + if (sessionInfo && sessionInfo.activeStreams === 0) { + const idleFor = Date.now() - sessionInfo.lastIdle; + if (idleFor >= this.sessionIdleTimeout) { + ctx.closeSession(session); + } else { + sessionInfo.timeout.refresh(); + } + } } private onStreamOpened(stream: http2.ServerHttp2Stream) { @@ -1671,11 +1688,6 @@ export class Server { const idleTimeoutObj = this.sessionIdleTimeouts.get(session); if (idleTimeoutObj) { idleTimeoutObj.activeStreams += 1; - if (idleTimeoutObj.timeout) { - clearTimeout(idleTimeoutObj.timeout); - idleTimeoutObj.timeout = null; - } - stream.once('close', idleTimeoutObj.onClose); } } @@ -1686,12 +1698,7 @@ export class Server { if (idleTimeoutObj) { idleTimeoutObj.activeStreams -= 1; if (idleTimeoutObj.activeStreams === 0) { - idleTimeoutObj.timeout = setTimeout( - this.onIdleTimeout, - this.sessionIdleTimeout, - this, - session - ).unref(); + idleTimeoutObj.timeout.refresh(); } } } From b873dce908ddfefc42da43be8a612c4cdc2eefcc Mon Sep 17 00:00:00 2001 From: AVVS Date: Wed, 28 Feb 2024 14:26:42 -0800 Subject: [PATCH 086/109] chore: simplify idle timeout further, fix wrong ref --- packages/grpc-js/src/server.ts | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index 32c18ea94..46fecc2e3 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -293,7 +293,6 @@ export class Server { private readonly keepaliveTimeoutMs: number; private readonly sessionIdleTimeout: number; - private readonly sessionHalfIdleTimeout: number; private readonly interceptors: ServerInterceptor[]; @@ -336,7 +335,6 @@ export class Server { this.options['grpc.keepalive_timeout_ms'] ?? KEEPALIVE_TIMEOUT_MS; this.sessionIdleTimeout = this.options['grpc.max_connection_idle_ms'] ?? MAX_CONNECTION_IDLE_MS; - this.sessionHalfIdleTimeout = Math.ceil(this.sessionIdleTimeout / 2); this.commonServerOptions = { maxSendHeaderBlockLength: Number.MAX_SAFE_INTEGER, @@ -1636,11 +1634,10 @@ export class Server { const idleTimeoutObj: SessionIdleTimeoutTracker = { activeStreams: 0, lastIdle: Date.now(), - onClose: this.onStreamClose.bind(this, session), // so that we don't recreate it each time - // this is 50% of the actual timeout, we will check half-way through and .refresh() for a subsequent check + onClose: this.onStreamClose.bind(this, session), timeout: setTimeout( this.onIdleTimeout, - this.sessionHalfIdleTimeout, + this.sessionIdleTimeout, this, session ).unref(), @@ -1658,7 +1655,11 @@ export class Server { return idleTimeoutObj; } - private onIdleTimeout(ctx: Server, session: http2.ServerHttp2Session) { + private onIdleTimeout( + this: undefined, + ctx: Server, + session: http2.ServerHttp2Session + ) { const { socket } = session; ctx.trace( 'Session idle timeout checkpoint for ' + @@ -1668,17 +1669,17 @@ export class Server { ); const sessionInfo = ctx.sessionIdleTimeouts.get(session); + // if it is called while we have activeStreams - timer will not be rescheduled // until last active stream is closed, then it will call .refresh() on the timer // important part is to not clearTimeout(timer) or it becomes unusable // for future refreshes - if (sessionInfo && sessionInfo.activeStreams === 0) { - const idleFor = Date.now() - sessionInfo.lastIdle; - if (idleFor >= this.sessionIdleTimeout) { - ctx.closeSession(session); - } else { - sessionInfo.timeout.refresh(); - } + if ( + sessionInfo !== undefined && + sessionInfo.activeStreams === 0 && + Date.now() - sessionInfo.lastIdle >= ctx.sessionIdleTimeout + ) { + ctx.closeSession(session); } } @@ -1698,6 +1699,7 @@ export class Server { if (idleTimeoutObj) { idleTimeoutObj.activeStreams -= 1; if (idleTimeoutObj.activeStreams === 0) { + idleTimeoutObj.lastIdle = Date.now(); idleTimeoutObj.timeout.refresh(); } } From 62e8ea97e659ea3c98ab77a38702fb5e0b67fed8 Mon Sep 17 00:00:00 2001 From: AVVS Date: Sat, 2 Mar 2024 07:58:54 -0800 Subject: [PATCH 087/109] chore: tests & cleanup of unref?.() --- packages/grpc-js/package.json | 2 +- .../grpc-js/src/load-balancer-pick-first.ts | 3 +- packages/grpc-js/src/resolver-dns.ts | 3 +- .../grpc-js/src/resolving-load-balancer.ts | 1 + packages/grpc-js/src/server.ts | 207 ++++++++++-------- packages/grpc-js/src/transport.ts | 3 +- packages/grpc-js/test/common.ts | 21 ++ packages/grpc-js/test/test-idle-timer.ts | 86 ++++++++ 8 files changed, 235 insertions(+), 91 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 34d8b558b..1c7935473 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -20,9 +20,9 @@ "@types/lodash": "^4.14.202", "@types/mocha": "^10.0.6", "@types/ncp": "^2.0.8", + "@types/node": ">=20.11.20", "@types/pify": "^5.0.4", "@types/semver": "^7.5.8", - "@types/node": ">=20.11.20", "@typescript-eslint/eslint-plugin": "^7.1.0", "@typescript-eslint/parser": "^7.1.0", "@typescript-eslint/typescript-estree": "^7.1.0", diff --git a/packages/grpc-js/src/load-balancer-pick-first.ts b/packages/grpc-js/src/load-balancer-pick-first.ts index 29bbfbf07..f6c43b33d 100644 --- a/packages/grpc-js/src/load-balancer-pick-first.ts +++ b/packages/grpc-js/src/load-balancer-pick-first.ts @@ -415,7 +415,8 @@ export class PickFirstLoadBalancer implements LoadBalancer { } this.connectionDelayTimeout = setTimeout(() => { this.startNextSubchannelConnecting(subchannelIndex + 1); - }, CONNECTION_DELAY_INTERVAL_MS).unref?.(); + }, CONNECTION_DELAY_INTERVAL_MS); + this.connectionDelayTimeout.unref?.(); } private pickSubchannel(subchannel: SubchannelInterface) { diff --git a/packages/grpc-js/src/resolver-dns.ts b/packages/grpc-js/src/resolver-dns.ts index 6652839b0..6463c2656 100644 --- a/packages/grpc-js/src/resolver-dns.ts +++ b/packages/grpc-js/src/resolver-dns.ts @@ -309,7 +309,8 @@ class DnsResolver implements Resolver { if (this.continueResolving) { this.startResolutionWithBackoff(); } - }, this.minTimeBetweenResolutionsMs).unref?.(); + }, this.minTimeBetweenResolutionsMs); + this.nextResolutionTimer.unref?.(); this.isNextResolutionTimerRunning = true; } diff --git a/packages/grpc-js/src/resolving-load-balancer.ts b/packages/grpc-js/src/resolving-load-balancer.ts index 82c4ff436..72aef0dfd 100644 --- a/packages/grpc-js/src/resolving-load-balancer.ts +++ b/packages/grpc-js/src/resolving-load-balancer.ts @@ -212,6 +212,7 @@ export class ResolvingLoadBalancer implements LoadBalancer { methodConfig: [], }; } + this.updateState(ConnectivityState.IDLE, new QueuePicker(this)); this.childLoadBalancer = new ChildLoadBalancerHandler( { diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index 46fecc2e3..b0fd5e7d3 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -95,6 +95,7 @@ const MAX_CONNECTION_IDLE_MS = ~(1 << 31); const { HTTP2_HEADER_PATH } = http2.constants; const TRACER_NAME = 'server'; +const kMaxAge = Buffer.from('max_age'); type AnyHttp2Server = http2.Http2Server | http2.Http2SecureServer; @@ -369,65 +370,61 @@ export class Server { private getChannelzSessionInfoGetter( session: http2.ServerHttp2Session - ): () => SocketInfo { - return () => { - const sessionInfo = this.sessions.get(session)!; - const sessionSocket = session.socket; - const remoteAddress = sessionSocket.remoteAddress - ? stringToSubchannelAddress( - sessionSocket.remoteAddress, - sessionSocket.remotePort - ) - : null; - const localAddress = sessionSocket.localAddress - ? stringToSubchannelAddress( - sessionSocket.localAddress!, - sessionSocket.localPort - ) - : null; - let tlsInfo: TlsInfo | null; - if (session.encrypted) { - const tlsSocket: TLSSocket = sessionSocket as TLSSocket; - const cipherInfo: CipherNameAndProtocol & { standardName?: string } = - tlsSocket.getCipher(); - const certificate = tlsSocket.getCertificate(); - const peerCertificate = tlsSocket.getPeerCertificate(); - tlsInfo = { - cipherSuiteStandardName: cipherInfo.standardName ?? null, - cipherSuiteOtherName: cipherInfo.standardName - ? null - : cipherInfo.name, - localCertificate: - certificate && 'raw' in certificate ? certificate.raw : null, - remoteCertificate: - peerCertificate && 'raw' in peerCertificate - ? peerCertificate.raw - : null, - }; - } else { - tlsInfo = null; - } - const socketInfo: SocketInfo = { - remoteAddress: remoteAddress, - localAddress: localAddress, - security: tlsInfo, - remoteName: null, - streamsStarted: sessionInfo.streamTracker.callsStarted, - streamsSucceeded: sessionInfo.streamTracker.callsSucceeded, - streamsFailed: sessionInfo.streamTracker.callsFailed, - messagesSent: sessionInfo.messagesSent, - messagesReceived: sessionInfo.messagesReceived, - keepAlivesSent: sessionInfo.keepAlivesSent, - lastLocalStreamCreatedTimestamp: null, - lastRemoteStreamCreatedTimestamp: - sessionInfo.streamTracker.lastCallStartedTimestamp, - lastMessageSentTimestamp: sessionInfo.lastMessageSentTimestamp, - lastMessageReceivedTimestamp: sessionInfo.lastMessageReceivedTimestamp, - localFlowControlWindow: session.state.localWindowSize ?? null, - remoteFlowControlWindow: session.state.remoteWindowSize ?? null, + ): SocketInfo { + const sessionInfo = this.sessions.get(session)!; + const sessionSocket = session.socket; + const remoteAddress = sessionSocket.remoteAddress + ? stringToSubchannelAddress( + sessionSocket.remoteAddress, + sessionSocket.remotePort + ) + : null; + const localAddress = sessionSocket.localAddress + ? stringToSubchannelAddress( + sessionSocket.localAddress!, + sessionSocket.localPort + ) + : null; + let tlsInfo: TlsInfo | null; + if (session.encrypted) { + const tlsSocket: TLSSocket = sessionSocket as TLSSocket; + const cipherInfo: CipherNameAndProtocol & { standardName?: string } = + tlsSocket.getCipher(); + const certificate = tlsSocket.getCertificate(); + const peerCertificate = tlsSocket.getPeerCertificate(); + tlsInfo = { + cipherSuiteStandardName: cipherInfo.standardName ?? null, + cipherSuiteOtherName: cipherInfo.standardName ? null : cipherInfo.name, + localCertificate: + certificate && 'raw' in certificate ? certificate.raw : null, + remoteCertificate: + peerCertificate && 'raw' in peerCertificate + ? peerCertificate.raw + : null, }; - return socketInfo; + } else { + tlsInfo = null; + } + const socketInfo: SocketInfo = { + remoteAddress: remoteAddress, + localAddress: localAddress, + security: tlsInfo, + remoteName: null, + streamsStarted: sessionInfo.streamTracker.callsStarted, + streamsSucceeded: sessionInfo.streamTracker.callsSucceeded, + streamsFailed: sessionInfo.streamTracker.callsFailed, + messagesSent: sessionInfo.messagesSent, + messagesReceived: sessionInfo.messagesReceived, + keepAlivesSent: sessionInfo.keepAlivesSent, + lastLocalStreamCreatedTimestamp: null, + lastRemoteStreamCreatedTimestamp: + sessionInfo.streamTracker.lastCallStartedTimestamp, + lastMessageSentTimestamp: sessionInfo.lastMessageSentTimestamp, + lastMessageReceivedTimestamp: sessionInfo.lastMessageReceivedTimestamp, + localFlowControlWindow: session.state.localWindowSize ?? null, + remoteFlowControlWindow: session.state.remoteWindowSize ?? null, }; + return socketInfo; } private trace(text: string): void { @@ -1004,7 +1001,7 @@ export class Server { for (const session of allSessions) { session.destroy(http2.constants.NGHTTP2_CANCEL as any); } - }, graceTimeMs).unref(); + }, graceTimeMs).unref?.(); } forceShutdown(): void { @@ -1380,6 +1377,7 @@ export class Server { let connectionAgeTimer: NodeJS.Timeout | null = null; let connectionAgeGraceTimer: NodeJS.Timeout | null = null; let keeapliveTimeTimer: NodeJS.Timeout | null = null; + let keepaliveTimeoutTimer: NodeJS.Timeout | null = null; let sessionClosedByServer = false; const idleTimeoutObj = this.enableIdleTimeout(session); @@ -1401,7 +1399,7 @@ export class Server { session.goaway( http2.constants.NGHTTP2_NO_ERROR, ~(1 << 31), - Buffer.from('max_age') + kMaxAge ); } catch (e) { // The goaway can't be sent because the session is already closed @@ -1415,37 +1413,47 @@ export class Server { if (this.maxConnectionAgeGraceMs !== UNLIMITED_CONNECTION_AGE_MS) { connectionAgeGraceTimer = setTimeout(() => { session.destroy(); - }, this.maxConnectionAgeGraceMs).unref(); + }, this.maxConnectionAgeGraceMs); + connectionAgeGraceTimer.unref?.(); } - }, this.maxConnectionAgeMs + jitter).unref(); + }, this.maxConnectionAgeMs + jitter); + connectionAgeTimer.unref?.(); } if (this.keepaliveTimeMs < KEEPALIVE_MAX_TIME_MS) { keeapliveTimeTimer = setInterval(() => { - const timeoutTimer = setTimeout(() => { + keepaliveTimeoutTimer = setTimeout(() => { sessionClosedByServer = true; session.close(); - }, this.keepaliveTimeoutMs).unref(); + }, this.keepaliveTimeoutMs); + keepaliveTimeoutTimer.unref?.(); try { session.ping( (err: Error | null, duration: number, payload: Buffer) => { - clearTimeout(timeoutTimer); + if (keepaliveTimeoutTimer) { + clearTimeout(keepaliveTimeoutTimer); + } if (err) { sessionClosedByServer = true; this.trace( - `Connection dropped due to error of a ping frame ${err.message} return in ${duration}` + 'Connection dropped due to error of a ping frame ' + + err.message + + ' return in ' + + duration ); session.close(); } } ); } catch (e) { + clearTimeout(keepaliveTimeoutTimer); // The ping can't be sent because the session is already closed session.destroy(); } - }, this.keepaliveTimeMs).unref(); + }, this.keepaliveTimeMs); + keeapliveTimeTimer.unref?.(); } session.on('close', () => { @@ -1464,7 +1472,10 @@ export class Server { } if (keeapliveTimeTimer) { - clearTimeout(keeapliveTimeTimer); + clearInterval(keeapliveTimeTimer); + if (keepaliveTimeoutTimer) { + clearTimeout(keepaliveTimeoutTimer); + } } if (idleTimeoutObj !== null) { @@ -1483,15 +1494,13 @@ export class Server { return (session: http2.ServerHttp2Session) => { const channelzRef = registerChannelzSocket( session.socket?.remoteAddress ?? 'unknown', - this.getChannelzSessionInfoGetter(session), + this.getChannelzSessionInfoGetter.bind(this, session), this.channelzEnabled ); const channelzSessionInfo: ChannelzSessionInfo = { ref: channelzRef, - streamTracker: this.channelzEnabled - ? new ChannelzCallTracker() - : new ChannelzCallTrackerStub(), + streamTracker: new ChannelzCallTracker(), messagesSent: 0, messagesReceived: 0, keepAlivesSent: 0, @@ -1513,6 +1522,7 @@ export class Server { let connectionAgeTimer: NodeJS.Timeout | null = null; let connectionAgeGraceTimer: NodeJS.Timeout | null = null; let keeapliveTimeTimer: NodeJS.Timeout | null = null; + let keepaliveTimeoutTimer: NodeJS.Timeout | null = null; let sessionClosedByServer = false; const idleTimeoutObj = this.enableIdleTimeout(session); @@ -1533,7 +1543,7 @@ export class Server { session.goaway( http2.constants.NGHTTP2_NO_ERROR, ~(1 << 31), - Buffer.from('max_age') + kMaxAge ); } catch (e) { // The goaway can't be sent because the session is already closed @@ -1547,14 +1557,16 @@ export class Server { if (this.maxConnectionAgeGraceMs !== UNLIMITED_CONNECTION_AGE_MS) { connectionAgeGraceTimer = setTimeout(() => { session.destroy(); - }, this.maxConnectionAgeGraceMs).unref(); + }, this.maxConnectionAgeGraceMs); + connectionAgeGraceTimer.unref?.(); } - }, this.maxConnectionAgeMs + jitter).unref(); + }, this.maxConnectionAgeMs + jitter); + connectionAgeTimer.unref?.(); } if (this.keepaliveTimeMs < KEEPALIVE_MAX_TIME_MS) { keeapliveTimeTimer = setInterval(() => { - const timeoutTImer = setTimeout(() => { + keepaliveTimeoutTimer = setTimeout(() => { sessionClosedByServer = true; this.channelzTrace.addTrace( 'CT_INFO', @@ -1562,11 +1574,15 @@ export class Server { ); session.close(); - }, this.keepaliveTimeoutMs).unref(); + }, this.keepaliveTimeoutMs); + keepaliveTimeoutTimer.unref?.(); + try { session.ping( (err: Error | null, duration: number, payload: Buffer) => { - clearTimeout(timeoutTImer); + if (keepaliveTimeoutTimer) { + clearTimeout(keepaliveTimeoutTimer); + } if (err) { sessionClosedByServer = true; @@ -1584,10 +1600,12 @@ export class Server { ); channelzSessionInfo.keepAlivesSent += 1; } catch (e) { + clearTimeout(keepaliveTimeoutTimer); // The ping can't be sent because the session is already closed session.destroy(); } - }, this.keepaliveTimeMs).unref(); + }, this.keepaliveTimeMs); + keeapliveTimeTimer.unref?.(); } session.on('close', () => { @@ -1610,7 +1628,10 @@ export class Server { } if (keeapliveTimeTimer) { - clearTimeout(keeapliveTimeTimer); + clearInterval(keeapliveTimeTimer); + if (keepaliveTimeoutTimer) { + clearTimeout(keepaliveTimeoutTimer); + } } if (idleTimeoutObj !== null) { @@ -1640,8 +1661,9 @@ export class Server { this.sessionIdleTimeout, this, session - ).unref(), + ), }; + idleTimeoutObj.timeout.unref?.(); this.sessionIdleTimeouts.set(session, idleTimeoutObj); const { socket } = session; @@ -1661,13 +1683,6 @@ export class Server { session: http2.ServerHttp2Session ) { const { socket } = session; - ctx.trace( - 'Session idle timeout checkpoint for ' + - socket?.remoteAddress + - ':' + - socket?.remotePort - ); - const sessionInfo = ctx.sessionIdleTimeouts.get(session); // if it is called while we have activeStreams - timer will not be rescheduled @@ -1679,6 +1694,15 @@ export class Server { sessionInfo.activeStreams === 0 && Date.now() - sessionInfo.lastIdle >= ctx.sessionIdleTimeout ) { + ctx.trace( + 'Session idle timeout triggered for ' + + socket?.remoteAddress + + ':' + + socket?.remotePort + + ' last idle at ' + + sessionInfo.lastIdle + ); + ctx.closeSession(session); } } @@ -1701,6 +1725,15 @@ export class Server { if (idleTimeoutObj.activeStreams === 0) { idleTimeoutObj.lastIdle = Date.now(); idleTimeoutObj.timeout.refresh(); + + this.trace( + 'Session onStreamClose' + + session.socket?.remoteAddress + + ':' + + session.socket?.remotePort + + ' at ' + + idleTimeoutObj.lastIdle + ); } } } diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index 620488635..71d0f26b3 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -477,7 +477,8 @@ class Http2Transport implements Transport { ); this.keepaliveTimerId = setTimeout(() => { this.maybeSendPing(); - }, this.keepaliveTimeMs).unref?.(); + }, this.keepaliveTimeMs); + this.keepaliveTimerId.unref?.(); } /* Otherwise, there is already either a keepalive timer or a ping pending, * wait for those to resolve. */ diff --git a/packages/grpc-js/test/common.ts b/packages/grpc-js/test/common.ts index eaa701f18..fcdbb4500 100644 --- a/packages/grpc-js/test/common.ts +++ b/packages/grpc-js/test/common.ts @@ -140,6 +140,27 @@ export class TestClient { return this.client.getChannel().getConnectivityState(false); } + waitForClientState( + deadline: grpc.Deadline, + state: ConnectivityState, + callback: (error?: Error) => void + ) { + this.client + .getChannel() + .watchConnectivityState(this.getChannelState(), deadline, err => { + if (err) { + return callback(err); + } + + const currentState = this.getChannelState(); + if (currentState === state) { + callback(); + } else { + return this.waitForClientState(deadline, currentState, callback); + } + }); + } + close() { this.client.close(); } diff --git a/packages/grpc-js/test/test-idle-timer.ts b/packages/grpc-js/test/test-idle-timer.ts index 3fdeb1f64..a8f457e3f 100644 --- a/packages/grpc-js/test/test-idle-timer.ts +++ b/packages/grpc-js/test/test-idle-timer.ts @@ -128,3 +128,89 @@ describe('Channel idle timer', () => { }); }); }); + +describe('Server idle timer', () => { + let server: TestServer; + let client: TestClient | null = null; + before(() => { + server = new TestServer(false, { + 'grpc.max_connection_idle_ms': 500, // small for testing purposes + }); + return server.start(); + }); + afterEach(() => { + if (client) { + client.close(); + client = null; + } + }); + after(() => { + server.shutdown(); + }); + + it('Should go idle after the specified time after a request ends', function (done) { + this.timeout(5000); + client = TestClient.createFromServer(server); + client.sendRequest(error => { + assert.ifError(error); + assert.strictEqual( + client!.getChannelState(), + grpc.connectivityState.READY + ); + client?.waitForClientState( + Date.now() + 600, + grpc.connectivityState.IDLE, + done + ); + }); + }); + + it('Should be able to make a request after going idle', function (done) { + this.timeout(5000); + client = TestClient.createFromServer(server); + client.sendRequest(error => { + assert.ifError(error); + assert.strictEqual( + client!.getChannelState(), + grpc.connectivityState.READY + ); + + client!.waitForClientState( + Date.now() + 600, + grpc.connectivityState.IDLE, + err => { + if (err) return done(err); + + assert.strictEqual( + client!.getChannelState(), + grpc.connectivityState.IDLE + ); + client!.sendRequest(error => { + assert.ifError(error); + done(); + }); + } + ); + }); + }); + + it('Should go idle after the specified time after waitForReady ends', function (done) { + this.timeout(5000); + client = TestClient.createFromServer(server); + const deadline = new Date(); + deadline.setSeconds(deadline.getSeconds() + 3); + client.waitForReady(deadline, error => { + assert.ifError(error); + assert.strictEqual( + client!.getChannelState(), + grpc.connectivityState.READY + ); + + client!.waitForClientState( + Date.now() + 600, + grpc.connectivityState.IDLE, + done + ); + }); + }); +}); From 4a3fefa2b34c96d2f05eb263bbf800bb0dcd36ee Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 4 Mar 2024 09:33:41 -0800 Subject: [PATCH 088/109] grpc-js: pick_first: Don't automatically reconnect after connection drop --- packages/grpc-js/src/load-balancer-pick-first.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/grpc-js/src/load-balancer-pick-first.ts b/packages/grpc-js/src/load-balancer-pick-first.ts index 29bbfbf07..d625339f4 100644 --- a/packages/grpc-js/src/load-balancer-pick-first.ts +++ b/packages/grpc-js/src/load-balancer-pick-first.ts @@ -348,7 +348,6 @@ export class PickFirstLoadBalancer implements LoadBalancer { if (newState !== ConnectivityState.READY) { this.removeCurrentPick(); this.calculateAndReportNewState(); - this.requestReresolution(); } return; } From cf321a80b1918affaa41da9fe7e6424876ca5b48 Mon Sep 17 00:00:00 2001 From: AVVS Date: Mon, 4 Mar 2024 18:25:23 -0800 Subject: [PATCH 089/109] chore: use iterators for tracking map, const for default values --- packages/grpc-js/src/channelz.ts | 61 ++++++++++++++++++-------------- packages/grpc-js/src/server.ts | 4 +-- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/packages/grpc-js/src/channelz.ts b/packages/grpc-js/src/channelz.ts index 4eab762a8..697ea5d8a 100644 --- a/packages/grpc-js/src/channelz.ts +++ b/packages/grpc-js/src/channelz.ts @@ -133,6 +133,11 @@ interface TraceEvent { */ const TARGET_RETAINED_TRACES = 32; +/** + * Default number of sockets/servers/channels/subchannels to return + */ +const DEFAULT_MAX_RESULTS = 100; + export class ChannelzTraceStub { readonly events: TraceEvent[] = []; readonly creationTimestamp: Date = new Date(); @@ -198,19 +203,15 @@ export class ChannelzTrace { } } +type RefOrderedMap = OrderedMap< + number, + { ref: { id: number; kind: EntityTypes; name: string }; count: number } +>; + export class ChannelzChildrenTracker { - private channelChildren = new OrderedMap< - number, - { ref: ChannelRef; count: number } - >(); - private subchannelChildren = new OrderedMap< - number, - { ref: SubchannelRef; count: number } - >(); - private socketChildren = new OrderedMap< - number, - { ref: SocketRef; count: number } - >(); + private channelChildren: RefOrderedMap = new OrderedMap(); + private subchannelChildren: RefOrderedMap = new OrderedMap(); + private socketChildren: RefOrderedMap = new OrderedMap(); private trackerMap = { [EntityTypes.channel]: this.channelChildren, [EntityTypes.subchannel]: this.subchannelChildren, @@ -219,16 +220,19 @@ export class ChannelzChildrenTracker { refChild(child: ChannelRef | SubchannelRef | SocketRef) { const tracker = this.trackerMap[child.kind]; - const trackedChild = tracker.getElementByKey(child.id); - - if (trackedChild === undefined) { - tracker.setElement(child.id, { - // @ts-expect-error union issues - ref: child, - count: 1, - }); + const trackedChild = tracker.find(child.id); + + if (trackedChild.equals(tracker.end())) { + tracker.setElement( + child.id, + { + ref: child, + count: 1, + }, + trackedChild + ); } else { - trackedChild.count += 1; + trackedChild.pointer[1].count += 1; } } @@ -245,9 +249,9 @@ export class ChannelzChildrenTracker { getChildLists(): ChannelzChildren { return { - channels: this.channelChildren, - subchannels: this.subchannelChildren, - sockets: this.socketChildren, + channels: this.channelChildren as ChannelzChildren['channels'], + subchannels: this.subchannelChildren as ChannelzChildren['subchannels'], + sockets: this.socketChildren as ChannelzChildren['sockets'], }; } } @@ -585,7 +589,8 @@ function GetTopChannels( call: ServerUnaryCall, callback: sendUnaryData ): void { - const maxResults = parseInt(call.request.max_results, 10) || 100; + const maxResults = + parseInt(call.request.max_results, 10) || DEFAULT_MAX_RESULTS; const resultList: ChannelMessage[] = []; const startId = parseInt(call.request.start_channel_id, 10); const channelEntries = entityMaps[EntityTypes.channel]; @@ -649,7 +654,8 @@ function GetServers( call: ServerUnaryCall, callback: sendUnaryData ): void { - const maxResults = parseInt(call.request.max_results, 10) || 100; + const maxResults = + parseInt(call.request.max_results, 10) || DEFAULT_MAX_RESULTS; const startId = parseInt(call.request.start_server_id, 10); const serverEntries = entityMaps[EntityTypes.server]; const resultList: ServerMessage[] = []; @@ -820,7 +826,8 @@ function GetServerSockets( } const startId = parseInt(call.request.start_socket_id, 10); - const maxResults = parseInt(call.request.max_results, 10) || 100; + const maxResults = + parseInt(call.request.max_results, 10) || DEFAULT_MAX_RESULTS; const resolvedInfo = serverEntry.getInfo(); // If we wanted to include listener sockets in the result, this line would // instead say diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index b0fd5e7d3..feb511b41 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -368,7 +368,7 @@ export class Server { }; } - private getChannelzSessionInfoGetter( + private getChannelzSessionInfo( session: http2.ServerHttp2Session ): SocketInfo { const sessionInfo = this.sessions.get(session)!; @@ -1494,7 +1494,7 @@ export class Server { return (session: http2.ServerHttp2Session) => { const channelzRef = registerChannelzSocket( session.socket?.remoteAddress ?? 'unknown', - this.getChannelzSessionInfoGetter.bind(this, session), + this.getChannelzSessionInfo.bind(this, session), this.channelzEnabled ); From 07ee52acb04625f08092da59ad823c8651f41bee Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 5 Mar 2024 10:26:39 -0800 Subject: [PATCH 090/109] grpc-js: Rearrange some function calls to revert event order changes --- packages/grpc-js/src/server-call.ts | 4 ++-- packages/grpc-js/src/server-interceptors.ts | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/grpc-js/src/server-call.ts b/packages/grpc-js/src/server-call.ts index 95393fba9..77fee7df2 100644 --- a/packages/grpc-js/src/server-call.ts +++ b/packages/grpc-js/src/server-call.ts @@ -200,11 +200,11 @@ export class ServerWritableStreamImpl } _final(callback: Function): void { + callback(null); this.call.sendStatus({ ...this.pendingStatus, metadata: this.pendingStatus.metadata ?? this.trailingMetadata, }); - callback(null); } // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -273,11 +273,11 @@ export class ServerDuplexStreamImpl } _final(callback: Function): void { + callback(null); this.call.sendStatus({ ...this.pendingStatus, metadata: this.pendingStatus.metadata ?? this.trailingMetadata, }); - callback(null); } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/grpc-js/src/server-interceptors.ts b/packages/grpc-js/src/server-interceptors.ts index 60c5c8865..9dfbbb2ff 100644 --- a/packages/grpc-js/src/server-interceptors.ts +++ b/packages/grpc-js/src/server-interceptors.ts @@ -842,7 +842,6 @@ export class BaseServerInterceptingCall if (this.checkCancelled()) { return; } - this.notifyOnCancel(); trace( 'Request to method ' + @@ -869,8 +868,11 @@ export class BaseServerInterceptingCall }; this.stream.sendTrailers(trailersToSend); + this.notifyOnCancel(); }); this.stream.end(); + } else { + this.notifyOnCancel(); } } else { if (this.callEventTracker && !this.streamEnded) { @@ -886,6 +888,7 @@ export class BaseServerInterceptingCall ...status.metadata?.toHttp2Headers(), }; this.stream.respond(trailersToSend, { endStream: true }); + this.notifyOnCancel(); } } startRead(): void { From 74ddb3bd6fb0d37b716bf6b4f0eb694b8db761ec Mon Sep 17 00:00:00 2001 From: AVVS Date: Tue, 5 Mar 2024 15:34:29 -0800 Subject: [PATCH 091/109] chore: address ts errors --- packages/grpc-js/src/channelz.ts | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/grpc-js/src/channelz.ts b/packages/grpc-js/src/channelz.ts index 697ea5d8a..c207e567c 100644 --- a/packages/grpc-js/src/channelz.ts +++ b/packages/grpc-js/src/channelz.ts @@ -66,28 +66,26 @@ export type TraceSeverity = | 'CT_WARNING' | 'CT_ERROR'; -export interface ChannelRef { - kind: EntityTypes.channel; +interface Ref { + kind: EntityTypes; id: number; name: string; } -export interface SubchannelRef { +export interface ChannelRef extends Ref { + kind: EntityTypes.channel; +} + +export interface SubchannelRef extends Ref { kind: EntityTypes.subchannel; - id: number; - name: string; } -export interface ServerRef { +export interface ServerRef extends Ref { kind: EntityTypes.server; - id: number; - name: string; } -export interface SocketRef { +export interface SocketRef extends Ref { kind: EntityTypes.socket; - id: number; - name: string; } function channelRefToMessage(ref: ChannelRef): ChannelRefMessage { @@ -361,6 +359,8 @@ export const enum EntityTypes { socket = 'socket', } +type EntryOrderedMap = OrderedMap any }>; + const entityMaps = { [EntityTypes.channel]: new OrderedMap(), [EntityTypes.subchannel]: new OrderedMap(), @@ -404,6 +404,8 @@ const generateRegisterFn = (kind: R) => { return nextId++; } + const entityMap: EntryOrderedMap = entityMaps[kind]; + return ( name: string, getInfo: () => InfoByType, @@ -412,8 +414,7 @@ const generateRegisterFn = (kind: R) => { const id = getNextId(); const ref = { id, name, kind } as RefByType; if (channelzEnabled) { - // @ts-expect-error typing issues - entityMaps[kind].setElement(id, { ref, getInfo }); + entityMap.setElement(id, { ref, getInfo }); } return ref; }; From 4d235c339b5dc60efa58d398139c5f57d30ec023 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 7 Mar 2024 09:24:04 -0800 Subject: [PATCH 092/109] grpc-js: Bump to 1.10.2 --- packages/grpc-js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 1c7935473..839735f6f 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.10.1", + "version": "1.10.2", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", From d0c20268878f01b3083b3543065b8ea6de50ac26 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 15 Mar 2024 09:23:08 -0700 Subject: [PATCH 093/109] Revert "grpc-js: pick_first: Don't automatically reconnect after connection drop" This reverts commit 4a3fefa2b34c96d2f05eb263bbf800bb0dcd36ee. --- packages/grpc-js/src/load-balancer-pick-first.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/grpc-js/src/load-balancer-pick-first.ts b/packages/grpc-js/src/load-balancer-pick-first.ts index f5339aed8..f6c43b33d 100644 --- a/packages/grpc-js/src/load-balancer-pick-first.ts +++ b/packages/grpc-js/src/load-balancer-pick-first.ts @@ -348,6 +348,7 @@ export class PickFirstLoadBalancer implements LoadBalancer { if (newState !== ConnectivityState.READY) { this.removeCurrentPick(); this.calculateAndReportNewState(); + this.requestReresolution(); } return; } From a8c6c33daa560b1b249f9c6edf975fdbc2d547a0 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 15 Mar 2024 09:24:01 -0700 Subject: [PATCH 094/109] grpc-js: Bump version to 1.10.3 --- packages/grpc-js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 839735f6f..b2395b59a 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.10.2", + "version": "1.10.3", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", From d7d171776d686fbecdbf06e83ad278681c86cbda Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 15 Mar 2024 15:16:58 -0700 Subject: [PATCH 095/109] grpc-js: Add more info to deadline exceeded errors --- packages/grpc-js/src/call-interface.ts | 4 +++ packages/grpc-js/src/deadline.ts | 11 ++++++ packages/grpc-js/src/internal-channel.ts | 2 +- packages/grpc-js/src/load-balancing-call.ts | 28 ++++++++++++++-- packages/grpc-js/src/resolving-call.ts | 37 +++++++++++++++++++-- packages/grpc-js/src/retrying-call.ts | 24 +++++++++++-- packages/grpc-js/src/subchannel-address.ts | 8 +++-- packages/grpc-js/src/subchannel-call.ts | 4 +++ 8 files changed, 109 insertions(+), 9 deletions(-) diff --git a/packages/grpc-js/src/call-interface.ts b/packages/grpc-js/src/call-interface.ts index c0c63b957..c93c504f6 100644 --- a/packages/grpc-js/src/call-interface.ts +++ b/packages/grpc-js/src/call-interface.ts @@ -171,3 +171,7 @@ export interface Call { getCallNumber(): number; setCredentials(credentials: CallCredentials): void; } + +export interface DeadlineInfoProvider { + getDeadlineInfo(): string[]; +} diff --git a/packages/grpc-js/src/deadline.ts b/packages/grpc-js/src/deadline.ts index 8f8fe67b7..de05e381e 100644 --- a/packages/grpc-js/src/deadline.ts +++ b/packages/grpc-js/src/deadline.ts @@ -93,3 +93,14 @@ export function deadlineToString(deadline: Deadline): string { } } } + +/** + * Calculate the difference between two dates as a number of seconds and format + * it as a string. + * @param startDate + * @param endDate + * @returns + */ +export function formatDateDifference(startDate: Date, endDate: Date): string { + return ((endDate.getTime() - startDate.getTime()) / 1000).toFixed(3) + 's'; +} diff --git a/packages/grpc-js/src/internal-channel.ts b/packages/grpc-js/src/internal-channel.ts index 823c935af..469ace557 100644 --- a/packages/grpc-js/src/internal-channel.ts +++ b/packages/grpc-js/src/internal-channel.ts @@ -684,7 +684,7 @@ export class InternalChannel { host: string, credentials: CallCredentials, deadline: Deadline - ): Call { + ): LoadBalancingCall | RetryingCall { // Create a RetryingCall if retries are enabled if (this.options['grpc.enable_retries'] === 0) { return this.createLoadBalancingCall( diff --git a/packages/grpc-js/src/load-balancing-call.ts b/packages/grpc-js/src/load-balancing-call.ts index 25a36553a..9940500bc 100644 --- a/packages/grpc-js/src/load-balancing-call.ts +++ b/packages/grpc-js/src/load-balancing-call.ts @@ -18,6 +18,7 @@ import { CallCredentials } from './call-credentials'; import { Call, + DeadlineInfoProvider, InterceptingListener, MessageContext, StatusObject, @@ -25,7 +26,7 @@ import { import { SubchannelCall } from './subchannel-call'; import { ConnectivityState } from './connectivity-state'; import { LogVerbosity, Status } from './constants'; -import { Deadline, getDeadlineTimeoutString } from './deadline'; +import { Deadline, formatDateDifference, getDeadlineTimeoutString } from './deadline'; import { InternalChannel } from './internal-channel'; import { Metadata } from './metadata'; import { PickResultType } from './picker'; @@ -48,7 +49,7 @@ export interface LoadBalancingCallInterceptingListener onReceiveStatus(status: StatusObjectWithProgress): void; } -export class LoadBalancingCall implements Call { +export class LoadBalancingCall implements Call, DeadlineInfoProvider { private child: SubchannelCall | null = null; private readPending = false; private pendingMessage: { context: MessageContext; message: Buffer } | null = @@ -59,6 +60,8 @@ export class LoadBalancingCall implements Call { private metadata: Metadata | null = null; private listener: InterceptingListener | null = null; private onCallEnded: ((statusCode: Status) => void) | null = null; + private startTime: Date; + private childStartTime: Date | null = null; constructor( private readonly channel: InternalChannel, private readonly callConfig: CallConfig, @@ -80,6 +83,26 @@ export class LoadBalancingCall implements Call { /* Currently, call credentials are only allowed on HTTPS connections, so we * can assume that the scheme is "https" */ this.serviceUrl = `https://${hostname}/${serviceName}`; + this.startTime = new Date(); + } + getDeadlineInfo(): string[] { + const deadlineInfo: string[] = []; + if (this.childStartTime) { + if (this.childStartTime > this.startTime) { + if (this.metadata?.getOptions().waitForReady) { + deadlineInfo.push('wait_for_ready'); + } + deadlineInfo.push(`LB pick: ${formatDateDifference(this.startTime, this.childStartTime)}`); + } + deadlineInfo.push(...this.child!.getDeadlineInfo()); + return deadlineInfo; + } else { + if (this.metadata?.getOptions().waitForReady) { + deadlineInfo.push('wait_for_ready'); + } + deadlineInfo.push('Waiting for LB pick'); + } + return deadlineInfo; } private trace(text: string): void { @@ -209,6 +232,7 @@ export class LoadBalancingCall implements Call { } }, }); + this.childStartTime = new Date(); } catch (error) { this.trace( 'Failed to start call on picked subchannel ' + diff --git a/packages/grpc-js/src/resolving-call.ts b/packages/grpc-js/src/resolving-call.ts index 723533dba..2c81e7883 100644 --- a/packages/grpc-js/src/resolving-call.ts +++ b/packages/grpc-js/src/resolving-call.ts @@ -19,6 +19,7 @@ import { CallCredentials } from './call-credentials'; import { Call, CallStreamOptions, + DeadlineInfoProvider, InterceptingListener, MessageContext, StatusObject, @@ -27,6 +28,7 @@ import { LogVerbosity, Propagate, Status } from './constants'; import { Deadline, deadlineToString, + formatDateDifference, getRelativeTimeout, minDeadline, } from './deadline'; @@ -39,7 +41,7 @@ import { restrictControlPlaneStatusCode } from './control-plane-status'; const TRACER_NAME = 'resolving_call'; export class ResolvingCall implements Call { - private child: Call | null = null; + private child: (Call & DeadlineInfoProvider) | null = null; private readPending = false; private pendingMessage: { context: MessageContext; message: Buffer } | null = null; @@ -56,6 +58,10 @@ export class ResolvingCall implements Call { private deadlineTimer: NodeJS.Timeout = setTimeout(() => {}, 0); private filterStack: FilterStack | null = null; + private deadlineStartTime: Date | null = null; + private configReceivedTime: Date | null = null; + private childStartTime: Date | null = null; + constructor( private readonly channel: InternalChannel, private readonly method: string, @@ -97,12 +103,37 @@ export class ResolvingCall implements Call { private runDeadlineTimer() { clearTimeout(this.deadlineTimer); + this.deadlineStartTime = new Date(); this.trace('Deadline: ' + deadlineToString(this.deadline)); const timeout = getRelativeTimeout(this.deadline); if (timeout !== Infinity) { this.trace('Deadline will be reached in ' + timeout + 'ms'); const handleDeadline = () => { - this.cancelWithStatus(Status.DEADLINE_EXCEEDED, 'Deadline exceeded'); + if (!this.deadlineStartTime) { + this.cancelWithStatus(Status.DEADLINE_EXCEEDED, 'Deadline exceeded'); + return; + } + const deadlineInfo: string[] = []; + const deadlineEndTime = new Date(); + deadlineInfo.push(`Deadline exceeded after ${formatDateDifference(this.deadlineStartTime, deadlineEndTime)}`); + if (this.configReceivedTime) { + if (this.configReceivedTime > this.deadlineStartTime) { + deadlineInfo.push(`name resolution: ${formatDateDifference(this.deadlineStartTime, this.configReceivedTime)}`); + } + if (this.childStartTime) { + if (this.childStartTime > this.configReceivedTime) { + deadlineInfo.push(`metadata filters: ${formatDateDifference(this.configReceivedTime, this.childStartTime)}`); + } + } else { + deadlineInfo.push('waiting for metadata filters'); + } + } else { + deadlineInfo.push('waiting for name resolution'); + } + if (this.child) { + deadlineInfo.push(...this.child.getDeadlineInfo()); + } + this.cancelWithStatus(Status.DEADLINE_EXCEEDED, deadlineInfo.join(',')); }; if (timeout <= 0) { process.nextTick(handleDeadline); @@ -176,6 +207,7 @@ export class ResolvingCall implements Call { return; } // configResult.type === 'SUCCESS' + this.configReceivedTime = new Date(); const config = configResult.config; if (config.status !== Status.OK) { const { code, details } = restrictControlPlaneStatusCode( @@ -215,6 +247,7 @@ export class ResolvingCall implements Call { this.deadline ); this.trace('Created child [' + this.child.getCallNumber() + ']'); + this.childStartTime = new Date(); this.child.start(filteredMetadata, { onReceiveMetadata: metadata => { this.trace('Received metadata'); diff --git a/packages/grpc-js/src/retrying-call.ts b/packages/grpc-js/src/retrying-call.ts index e6e1cbb44..14969916e 100644 --- a/packages/grpc-js/src/retrying-call.ts +++ b/packages/grpc-js/src/retrying-call.ts @@ -17,12 +17,13 @@ import { CallCredentials } from './call-credentials'; import { LogVerbosity, Status } from './constants'; -import { Deadline } from './deadline'; +import { Deadline, formatDateDifference } from './deadline'; import { Metadata } from './metadata'; import { CallConfig } from './resolver'; import * as logging from './logging'; import { Call, + DeadlineInfoProvider, InterceptingListener, MessageContext, StatusObject, @@ -121,6 +122,7 @@ interface UnderlyingCall { state: UnderlyingCallState; call: LoadBalancingCall; nextMessageToSend: number; + startTime: Date; } /** @@ -170,7 +172,7 @@ interface WriteBufferEntry { const PREVIONS_RPC_ATTEMPTS_METADATA_KEY = 'grpc-previous-rpc-attempts'; -export class RetryingCall implements Call { +export class RetryingCall implements Call, DeadlineInfoProvider { private state: RetryingCallState; private listener: InterceptingListener | null = null; private initialMetadata: Metadata | null = null; @@ -198,6 +200,7 @@ export class RetryingCall implements Call { private committedCallIndex: number | null = null; private initialRetryBackoffSec = 0; private nextRetryBackoffSec = 0; + private startTime: Date; constructor( private readonly channel: InternalChannel, private readonly callConfig: CallConfig, @@ -223,6 +226,22 @@ export class RetryingCall implements Call { } else { this.state = 'TRANSPARENT_ONLY'; } + this.startTime = new Date(); + } + getDeadlineInfo(): string[] { + if (this.underlyingCalls.length === 0) { + return []; + } + const deadlineInfo: string[] = []; + const latestCall = this.underlyingCalls[this.underlyingCalls.length - 1]; + if (this.underlyingCalls.length > 1) { + deadlineInfo.push(`previous attempts: ${this.underlyingCalls.length - 1}`); + } + if (latestCall.startTime > this.startTime) { + deadlineInfo.push(`time to current attempt start: ${formatDateDifference(this.startTime, latestCall.startTime)}`); + } + deadlineInfo.push(...latestCall.call.getDeadlineInfo()); + return deadlineInfo; } getCallNumber(): number { return this.callNumber; @@ -628,6 +647,7 @@ export class RetryingCall implements Call { state: 'ACTIVE', call: child, nextMessageToSend: 0, + startTime: new Date() }); const previousAttempts = this.attempts - 1; const initialMetadata = this.initialMetadata!.clone(); diff --git a/packages/grpc-js/src/subchannel-address.ts b/packages/grpc-js/src/subchannel-address.ts index 70a7962f7..7e4f3e475 100644 --- a/packages/grpc-js/src/subchannel-address.ts +++ b/packages/grpc-js/src/subchannel-address.ts @@ -15,7 +15,7 @@ * */ -import { isIP } from 'net'; +import { isIP, isIPv6 } from 'net'; export interface TcpSubchannelAddress { port: number; @@ -63,7 +63,11 @@ export function subchannelAddressEqual( export function subchannelAddressToString(address: SubchannelAddress): string { if (isTcpSubchannelAddress(address)) { - return address.host + ':' + address.port; + if (isIPv6(address.host)) { + return '[' + address.host + ']:' + address.port; + } else { + return address.host + ':' + address.port; + } } else { return address.path; } diff --git a/packages/grpc-js/src/subchannel-call.ts b/packages/grpc-js/src/subchannel-call.ts index 3b9b6152f..33cf544d8 100644 --- a/packages/grpc-js/src/subchannel-call.ts +++ b/packages/grpc-js/src/subchannel-call.ts @@ -70,6 +70,7 @@ export interface SubchannelCall { startRead(): void; halfClose(): void; getCallNumber(): number; + getDeadlineInfo(): string[]; } export interface StatusObjectWithRstCode extends StatusObject { @@ -288,6 +289,9 @@ export class Http2SubchannelCall implements SubchannelCall { this.callEventTracker.onStreamEnd(false); }); } + getDeadlineInfo(): string[] { + return [`remote_addr=${this.getPeer()}`]; + } public onDisconnect() { this.endCall({ From 14f1d02c9af266104e896c70fccc144b724461f6 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 20 Mar 2024 15:44:18 -0700 Subject: [PATCH 096/109] grpc-js: Avoid sending redundant RST_STREAMs from the client --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/subchannel-call.ts | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index b2395b59a..a8a9f2d06 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.10.3", + "version": "1.10.4", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/subchannel-call.ts b/packages/grpc-js/src/subchannel-call.ts index 3b9b6152f..fd1cfb454 100644 --- a/packages/grpc-js/src/subchannel-call.ts +++ b/packages/grpc-js/src/subchannel-call.ts @@ -105,6 +105,8 @@ export class Http2SubchannelCall implements SubchannelCall { private internalError: SystemError | null = null; + private serverEndedCall = false; + constructor( private readonly http2Stream: http2.ClientHttp2Stream, private readonly callEventTracker: CallEventTracker, @@ -182,6 +184,7 @@ export class Http2SubchannelCall implements SubchannelCall { this.maybeOutputStatus(); }); http2Stream.on('close', () => { + this.serverEndedCall = true; /* Use process.next tick to ensure that this code happens after any * "error" event that may be emitted at about the same time, so that * we can bubble up the error message from that event. */ @@ -400,6 +403,7 @@ export class Http2SubchannelCall implements SubchannelCall { } private handleTrailers(headers: http2.IncomingHttpHeaders) { + this.serverEndedCall = true; this.callEventTracker.onStreamEnd(true); let headersString = ''; for (const header of Object.keys(headers)) { @@ -445,7 +449,15 @@ export class Http2SubchannelCall implements SubchannelCall { private destroyHttp2Stream() { // The http2 stream could already have been destroyed if cancelWithStatus // is called in response to an internal http2 error. - if (!this.http2Stream.destroyed) { + if (this.http2Stream.destroyed) { + return; + } + /* If the server ended the call, sending an RST_STREAM is redundant, so we + * just half close on the client side instead to finish closing the stream. + */ + if (this.serverEndedCall) { + this.http2Stream.end(); + } else { /* If the call has ended with an OK status, communicate that when closing * the stream, partly to avoid a situation in which we detect an error * RST_STREAM as a result after we have the status */ From f4330f72c940645f910a7a78b3231ceab9cc2619 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 21 Mar 2024 09:49:58 -0700 Subject: [PATCH 097/109] Use call start times in some trace logs --- packages/grpc-js/src/load-balancing-call.ts | 3 ++- packages/grpc-js/src/retrying-call.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/grpc-js/src/load-balancing-call.ts b/packages/grpc-js/src/load-balancing-call.ts index 9940500bc..764769753 100644 --- a/packages/grpc-js/src/load-balancing-call.ts +++ b/packages/grpc-js/src/load-balancing-call.ts @@ -121,7 +121,8 @@ export class LoadBalancingCall implements Call, DeadlineInfoProvider { status.code + ' details="' + status.details + - '"' + '" start time=' + + this.startTime.toISOString() ); const finalStatus = { ...status, progress }; this.listener?.onReceiveStatus(finalStatus); diff --git a/packages/grpc-js/src/retrying-call.ts b/packages/grpc-js/src/retrying-call.ts index 14969916e..1c5ffaa4f 100644 --- a/packages/grpc-js/src/retrying-call.ts +++ b/packages/grpc-js/src/retrying-call.ts @@ -261,7 +261,8 @@ export class RetryingCall implements Call, DeadlineInfoProvider { statusObject.code + ' details="' + statusObject.details + - '"' + '" start time=' + + this.startTime.toISOString() ); this.bufferTracker.freeAll(this.callNumber); this.writeBufferOffset = this.writeBufferOffset + this.writeBuffer.length; From 9948aea5a536c9796ecf54814cab81aeb9c9ff3c Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 21 Mar 2024 14:58:37 -0700 Subject: [PATCH 098/109] grpc-js: Ensure server interceptors work with builder utility classes --- packages/grpc-js/src/server-interceptors.ts | 20 +++++++++-- .../grpc-js/test/test-server-interceptors.ts | 35 +++++++++---------- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/packages/grpc-js/src/server-interceptors.ts b/packages/grpc-js/src/server-interceptors.ts index 9dfbbb2ff..c9cfe416b 100644 --- a/packages/grpc-js/src/server-interceptors.ts +++ b/packages/grpc-js/src/server-interceptors.ts @@ -345,7 +345,12 @@ export class ServerInterceptingCall implements ServerInterceptingCallInterface { private nextCall: ServerInterceptingCallInterface, responder?: Responder ) { - this.responder = { ...defaultResponder, ...responder }; + this.responder = { + start: responder?.start ?? defaultResponder.start, + sendMetadata: responder?.sendMetadata ?? defaultResponder.sendMetadata, + sendMessage: responder?.sendMessage ?? defaultResponder.sendMessage, + sendStatus: responder?.sendStatus ?? defaultResponder.sendStatus, + }; } private processPendingMessage() { @@ -369,8 +374,17 @@ export class ServerInterceptingCall implements ServerInterceptingCallInterface { start(listener: InterceptingServerListener): void { this.responder.start(interceptedListener => { const fullInterceptedListener: FullServerListener = { - ...defaultServerListener, - ...interceptedListener, + onReceiveMetadata: + interceptedListener?.onReceiveMetadata ?? + defaultServerListener.onReceiveMetadata, + onReceiveMessage: + interceptedListener?.onReceiveMessage ?? + defaultServerListener.onReceiveMessage, + onReceiveHalfClose: + interceptedListener?.onReceiveHalfClose ?? + defaultServerListener.onReceiveHalfClose, + onCancel: + interceptedListener?.onCancel ?? defaultServerListener.onCancel, }; const finalInterceptingListener = new InterceptingServerListenerImpl( fullInterceptedListener, diff --git a/packages/grpc-js/test/test-server-interceptors.ts b/packages/grpc-js/test/test-server-interceptors.ts index 5e93d32d2..e94169721 100644 --- a/packages/grpc-js/test/test-server-interceptors.ts +++ b/packages/grpc-js/test/test-server-interceptors.ts @@ -30,25 +30,22 @@ const testAuthInterceptor: grpc.ServerInterceptor = ( methodDescriptor, call ) => { - return new grpc.ServerInterceptingCall(call, { - start: next => { - const authListener: grpc.ServerListener = { - onReceiveMetadata: (metadata, mdNext) => { - if ( - metadata.get(AUTH_HEADER_KEY)?.[0] !== AUTH_HEADER_ALLOWED_VALUE - ) { - call.sendStatus({ - code: grpc.status.UNAUTHENTICATED, - details: 'Auth metadata not correct', - }); - } else { - mdNext(metadata); - } - }, - }; - next(authListener); - }, - }); + const authListener = (new grpc.ServerListenerBuilder()) + .withOnReceiveMetadata((metadata, mdNext) => { + if ( + metadata.get(AUTH_HEADER_KEY)?.[0] !== AUTH_HEADER_ALLOWED_VALUE + ) { + call.sendStatus({ + code: grpc.status.UNAUTHENTICATED, + details: 'Auth metadata not correct', + }); + } else { + mdNext(metadata); + } + }).build(); + const responder = (new grpc.ResponderBuilder()) + .withStart(next => next(authListener)).build(); + return new grpc.ServerInterceptingCall(call, responder); }; let eventCounts = { From e1f831a57bff9e5bce48aa80edb5ae527c397ca6 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 1 Apr 2024 09:54:06 -0700 Subject: [PATCH 099/109] grpc-js: Call custom checkServerIdentity when target name override is set --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/transport.ts | 8 +++-- .../grpc-js/test/test-channel-credentials.ts | 30 +++++++++++++++++-- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index a8a9f2d06..09ae217d8 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.10.4", + "version": "1.10.5", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index 71d0f26b3..66a5d4556 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -694,11 +694,13 @@ export class Http2SubchannelConnector implements SubchannelConnector { if (options['grpc.ssl_target_name_override']) { const sslTargetNameOverride = options['grpc.ssl_target_name_override']!; + const originalCheckServerIdentity = + connectionOptions.checkServerIdentity ?? checkServerIdentity; connectionOptions.checkServerIdentity = ( host: string, cert: PeerCertificate ): Error | undefined => { - return checkServerIdentity(sslTargetNameOverride, cert); + return originalCheckServerIdentity(sslTargetNameOverride, cert); }; connectionOptions.servername = sslTargetNameOverride; } else { @@ -804,11 +806,13 @@ export class Http2SubchannelConnector implements SubchannelConnector { // This option is used for testing only. if (options['grpc.ssl_target_name_override']) { const sslTargetNameOverride = options['grpc.ssl_target_name_override']!; + const originalCheckServerIdentity = + connectionOptions.checkServerIdentity ?? checkServerIdentity; connectionOptions.checkServerIdentity = ( host: string, cert: PeerCertificate ): Error | undefined => { - return checkServerIdentity(sslTargetNameOverride, cert); + return originalCheckServerIdentity(sslTargetNameOverride, cert); }; connectionOptions.servername = sslTargetNameOverride; } else { diff --git a/packages/grpc-js/test/test-channel-credentials.ts b/packages/grpc-js/test/test-channel-credentials.ts index b05b0d048..b5c011581 100644 --- a/packages/grpc-js/test/test-channel-credentials.ts +++ b/packages/grpc-js/test/test-channel-credentials.ts @@ -150,8 +150,12 @@ describe('ChannelCredentials Implementation', () => { describe('ChannelCredentials usage', () => { let client: ServiceClient; let server: grpc.Server; + let portNum: number; + let caCert: Buffer; + const hostnameOverride = 'foo.test.google.fr'; before(async () => { const { ca, key, cert } = await pFixtures; + caCert = ca; const serverCreds = grpc.ServerCredentials.createSsl(null, [ { private_key: key, cert_chain: cert }, ]); @@ -178,9 +182,10 @@ describe('ChannelCredentials usage', () => { reject(err); return; } + portNum = port; client = new echoService(`localhost:${port}`, combinedCreds, { - 'grpc.ssl_target_name_override': 'foo.test.google.fr', - 'grpc.default_authority': 'foo.test.google.fr', + 'grpc.ssl_target_name_override': hostnameOverride, + 'grpc.default_authority': hostnameOverride, }); server.start(); resolve(); @@ -207,4 +212,25 @@ describe('ChannelCredentials usage', () => { ); assert2.afterMustCallsSatisfied(done); }); + + it('Should call the checkServerIdentity callback', done => { + const channelCreds = ChannelCredentials.createSsl(caCert, null, null, { + checkServerIdentity: assert2.mustCall((hostname, cert) => { + assert.strictEqual(hostname, hostnameOverride); + return undefined; + }), + }); + const client = new echoService(`localhost:${portNum}`, channelCreds, { + 'grpc.ssl_target_name_override': hostnameOverride, + 'grpc.default_authority': hostnameOverride, + }); + client.echo( + { value: 'test value', value2: 3 }, + assert2.mustCall((error: ServiceError, response: any) => { + assert.ifError(error); + assert.deepStrictEqual(response, { value: 'test value', value2: 3 }); + }) + ); + assert2.afterMustCallsSatisfied(done); + }); }); From 213230c73b9300ae9fff6b8f2e66f9913439dde7 Mon Sep 17 00:00:00 2001 From: David Fiala Date: Wed, 27 Mar 2024 16:39:45 -0700 Subject: [PATCH 100/109] Resolve exception when Error.stackTraceLimit is undefined Some applications may explicitly set Error.stackTraceLimit = undefined. In this case it is not safe to assume new Error().stack is available. --- packages/grpc-js/src/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js/src/client.ts b/packages/grpc-js/src/client.ts index e122f6cf4..995d5b328 100644 --- a/packages/grpc-js/src/client.ts +++ b/packages/grpc-js/src/client.ts @@ -110,7 +110,7 @@ export type ClientOptions = Partial & { }; function getErrorStackString(error: Error): string { - return error.stack!.split('\n').slice(1).join('\n'); + return error.stack?.split('\n').slice(1).join('\n') || 'no stack trace available'; } /** From 0d9a8c1dcf0f46eda09540518c50d28519201d58 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 3 Apr 2024 09:40:22 -0700 Subject: [PATCH 101/109] grpc-js: Fix check for whether to send a trailers-only response --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/server-interceptors.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 09ae217d8..3b8ccf2c1 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.10.5", + "version": "1.10.6", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/server-interceptors.ts b/packages/grpc-js/src/server-interceptors.ts index c9cfe416b..b62d55108 100644 --- a/packages/grpc-js/src/server-interceptors.ts +++ b/packages/grpc-js/src/server-interceptors.ts @@ -866,7 +866,7 @@ export class BaseServerInterceptingCall status.details ); - if (this.stream.headersSent) { + if (this.metadataSent) { if (!this.wantTrailers) { this.wantTrailers = true; this.stream.once('wantTrailers', () => { From 2af21a55f366416030ec2e58b8b7c16f033a9e8e Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 8 Apr 2024 17:47:26 -0700 Subject: [PATCH 102/109] Merge pull request #2712 from sergiitk/psm-interop-pkg-dev PSM Interop: Migrate to Artifact Registry --- packages/grpc-js-xds/scripts/xds_k8s_lb.sh | 7 ++++--- packages/grpc-js-xds/scripts/xds_k8s_url_map.sh | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh b/packages/grpc-js-xds/scripts/xds_k8s_lb.sh index 504c3ff37..c900a4ea5 100755 --- a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh +++ b/packages/grpc-js-xds/scripts/xds_k8s_lb.sh @@ -19,8 +19,9 @@ set -eo pipefail readonly GITHUB_REPOSITORY_NAME="grpc-node" readonly TEST_DRIVER_INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/${TEST_DRIVER_REPO_OWNER:-grpc}/psm-interop/${TEST_DRIVER_BRANCH:-main}/.kokoro/psm_interop_kokoro_lib.sh" ## xDS test client Docker images -readonly SERVER_IMAGE_NAME="gcr.io/grpc-testing/xds-interop/java-server:558b5b0bfac8e21755c223063274a779b3898afe" -readonly CLIENT_IMAGE_NAME="gcr.io/grpc-testing/xds-interop/node-client" +readonly DOCKER_REGISTRY="us-docker.pkg.dev" +readonly SERVER_IMAGE_NAME="us-docker.pkg.dev/grpc-testing/psm-interop/java-server:canonical" +readonly CLIENT_IMAGE_NAME="us-docker.pkg.dev/grpc-testing/psm-interop/node-client" readonly FORCE_IMAGE_BUILD="${FORCE_IMAGE_BUILD:-0}" readonly BUILD_APP_PATH="packages/grpc-js-xds/interop/Dockerfile" readonly LANGUAGE_NAME="Node" @@ -46,7 +47,7 @@ build_test_app_docker_images() { -t "${CLIENT_IMAGE_NAME}:${GIT_COMMIT}" \ . - gcloud -q auth configure-docker + gcloud -q auth configure-docker "${DOCKER_REGISTRY}" docker push "${CLIENT_IMAGE_NAME}:${GIT_COMMIT}" if is_version_branch "${TESTING_VERSION}"; then tag_and_push_docker_image "${CLIENT_IMAGE_NAME}" "${GIT_COMMIT}" "${TESTING_VERSION}" diff --git a/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh b/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh index 9344d054b..d6e2c7ed4 100644 --- a/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh +++ b/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh @@ -19,7 +19,8 @@ set -eo pipefail readonly GITHUB_REPOSITORY_NAME="grpc-node" readonly TEST_DRIVER_INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/${TEST_DRIVER_REPO_OWNER:-grpc}/psm-interop/${TEST_DRIVER_BRANCH:-main}/.kokoro/psm_interop_kokoro_lib.sh" ## xDS test client Docker images -readonly CLIENT_IMAGE_NAME="gcr.io/grpc-testing/xds-interop/node-client" +readonly DOCKER_REGISTRY="us-docker.pkg.dev" +readonly CLIENT_IMAGE_NAME="us-docker.pkg.dev/grpc-testing/psm-interop/node-client" readonly FORCE_IMAGE_BUILD="${FORCE_IMAGE_BUILD:-0}" readonly BUILD_APP_PATH="packages/grpc-js-xds/interop/Dockerfile" readonly LANGUAGE_NAME="Node" @@ -45,7 +46,7 @@ build_test_app_docker_images() { -t "${CLIENT_IMAGE_NAME}:${GIT_COMMIT}" \ . - gcloud -q auth configure-docker + gcloud -q auth configure-docker "${DOCKER_REGISTRY}" docker push "${CLIENT_IMAGE_NAME}:${GIT_COMMIT}" if is_version_branch "${TESTING_VERSION}"; then tag_and_push_docker_image "${CLIENT_IMAGE_NAME}" "${GIT_COMMIT}" "${TESTING_VERSION}" From 8754ccb7dbefa0345a5805092a275f7e6c4cf0d2 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 11 Apr 2024 10:56:18 -0700 Subject: [PATCH 103/109] grpc-js: Improve reporting of HTTP error codes --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/subchannel-call.ts | 121 ++++++++++++++---------- 2 files changed, 73 insertions(+), 50 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 3b8ccf2c1..c9999465c 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.10.6", + "version": "1.10.7", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/subchannel-call.ts b/packages/grpc-js/src/subchannel-call.ts index d54a6bcbf..0ce7d72cb 100644 --- a/packages/grpc-js/src/subchannel-call.ts +++ b/packages/grpc-js/src/subchannel-call.ts @@ -82,6 +82,39 @@ export interface SubchannelCallInterceptingListener onReceiveStatus(status: StatusObjectWithRstCode): void; } +function mapHttpStatusCode(code: number): StatusObject { + const details = `Received HTTP status code ${code}`; + let mappedStatusCode: number; + switch (code) { + // TODO(murgatroid99): handle 100 and 101 + case 400: + mappedStatusCode = Status.INTERNAL; + break; + case 401: + mappedStatusCode = Status.UNAUTHENTICATED; + break; + case 403: + mappedStatusCode = Status.PERMISSION_DENIED; + break; + case 404: + mappedStatusCode = Status.UNIMPLEMENTED; + break; + case 429: + case 502: + case 503: + case 504: + mappedStatusCode = Status.UNAVAILABLE; + break; + default: + mappedStatusCode = Status.UNKNOWN; + } + return { + code: mappedStatusCode, + details: details, + metadata: new Metadata() + }; +} + export class Http2SubchannelCall implements SubchannelCall { private decoder = new StreamDecoder(); @@ -98,8 +131,7 @@ export class Http2SubchannelCall implements SubchannelCall { private unpushedReadMessages: Buffer[] = []; - // Status code mapped from :status. To be used if grpc-status is not received - private mappedStatusCode: Status = Status.UNKNOWN; + private httpStatusCode: number | undefined; // This is populated (non-null) if and only if the call has ended private finalStatus: StatusObject | null = null; @@ -121,29 +153,7 @@ export class Http2SubchannelCall implements SubchannelCall { headersString += '\t\t' + header + ': ' + headers[header] + '\n'; } this.trace('Received server headers:\n' + headersString); - switch (headers[':status']) { - // TODO(murgatroid99): handle 100 and 101 - case 400: - this.mappedStatusCode = Status.INTERNAL; - break; - case 401: - this.mappedStatusCode = Status.UNAUTHENTICATED; - break; - case 403: - this.mappedStatusCode = Status.PERMISSION_DENIED; - break; - case 404: - this.mappedStatusCode = Status.UNIMPLEMENTED; - break; - case 429: - case 502: - case 503: - case 504: - this.mappedStatusCode = Status.UNAVAILABLE; - break; - default: - this.mappedStatusCode = Status.UNKNOWN; - } + this.httpStatusCode = headers[':status']; if (flags & http2.constants.NGHTTP2_FLAG_END_STREAM) { this.handleTrailers(headers); @@ -208,8 +218,14 @@ export class Http2SubchannelCall implements SubchannelCall { if (this.finalStatus !== null) { return; } - code = Status.INTERNAL; - details = `Received RST_STREAM with code ${http2Stream.rstCode}`; + if (this.httpStatusCode && this.httpStatusCode !== 200) { + const mappedStatus = mapHttpStatusCode(this.httpStatusCode); + code = mappedStatus.code; + details = mappedStatus.details; + } else { + code = Status.INTERNAL; + details = `Received RST_STREAM with code ${http2Stream.rstCode} (Call ended without gRPC status)`; + } break; case http2.constants.NGHTTP2_REFUSED_STREAM: code = Status.UNAVAILABLE; @@ -421,31 +437,38 @@ export class Http2SubchannelCall implements SubchannelCall { metadata = new Metadata(); } const metadataMap = metadata.getMap(); - let code: Status = this.mappedStatusCode; - if ( - code === Status.UNKNOWN && - typeof metadataMap['grpc-status'] === 'string' - ) { - const receivedStatus = Number(metadataMap['grpc-status']); - if (receivedStatus in Status) { - code = receivedStatus; - this.trace('received status code ' + receivedStatus + ' from server'); - } + let status: StatusObject; + if (typeof metadataMap['grpc-status'] === 'string') { + const receivedStatus: Status = Number(metadataMap['grpc-status']); + this.trace('received status code ' + receivedStatus + ' from server'); metadata.remove('grpc-status'); - } - let details = ''; - if (typeof metadataMap['grpc-message'] === 'string') { - try { - details = decodeURI(metadataMap['grpc-message']); - } catch (e) { - details = metadataMap['grpc-message']; + let details = ''; + if (typeof metadataMap['grpc-message'] === 'string') { + try { + details = decodeURI(metadataMap['grpc-message']); + } catch (e) { + details = metadataMap['grpc-message']; + } + metadata.remove('grpc-message'); + this.trace( + 'received status details string "' + details + '" from server' + ); } - metadata.remove('grpc-message'); - this.trace( - 'received status details string "' + details + '" from server' - ); + status = { + code: receivedStatus, + details: details, + metadata: metadata + }; + } else if (this.httpStatusCode) { + status = mapHttpStatusCode(this.httpStatusCode); + status.metadata = metadata; + } else { + status = { + code: Status.UNKNOWN, + details: 'No status information received', + metadata: metadata + }; } - const status: StatusObject = { code, details, metadata }; // This is a no-op if the call was already ended when handling headers. this.endCall(status); } From e4f2ecd0537191c3d4c6f27b2faa38daeda7059c Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 30 Apr 2024 15:49:20 -0700 Subject: [PATCH 104/109] grpc-js(-xds): Pick up proto-loader update --- .../grpc/testing/ClientConfigureRequest.ts | 30 +- .../generated/grpc/testing/GrpclbRouteType.ts | 45 +- .../LoadBalancerAccumulatedStatsResponse.ts | 8 +- .../grpc/testing/LoadBalancerStatsResponse.ts | 2 +- .../grpc/testing/LoadBalancerStatsService.ts | 38 +- .../interop/generated/grpc/testing/Payload.ts | 6 +- .../generated/grpc/testing/PayloadType.ts | 21 +- .../grpc/testing/ReconnectService.ts | 38 +- .../grpc/testing/ResponseParameters.ts | 4 +- .../generated/grpc/testing/SimpleRequest.ts | 22 +- .../generated/grpc/testing/SimpleResponse.ts | 10 +- .../grpc/testing/StreamingInputCallRequest.ts | 8 +- .../testing/StreamingOutputCallRequest.ts | 14 +- .../testing/StreamingOutputCallResponse.ts | 4 +- .../generated/grpc/testing/TestService.ts | 92 +-- .../grpc/testing/UnimplementedService.ts | 21 +- .../XdsUpdateClientConfigureService.ts | 21 +- .../grpc/testing/XdsUpdateHealthService.ts | 38 +- .../grpc-js-xds/interop/generated/test.ts | 26 +- .../grpc-js-xds/interop/xds-interop-client.ts | 2 +- packages/grpc-js-xds/package.json | 6 +- packages/grpc-js-xds/src/generated/cluster.ts | 9 + .../envoy/admin/v3/BootstrapConfigDump.ts | 32 - .../envoy/admin/v3/ClientResourceStatus.ts | 57 +- .../envoy/admin/v3/ClustersConfigDump.ts | 6 +- .../generated/envoy/admin/v3/ConfigDump.ts | 65 -- .../envoy/admin/v3/EcdsConfigDump.ts | 6 +- .../envoy/admin/v3/EndpointsConfigDump.ts | 6 +- .../envoy/admin/v3/ListenersConfigDump.ts | 6 +- .../envoy/admin/v3/RoutesConfigDump.ts | 6 +- .../envoy/admin/v3/ScopedRoutesConfigDump.ts | 6 +- .../envoy/admin/v3/SecretsConfigDump.ts | 162 ----- .../config/accesslog/v3/ComparisonFilter.ts | 33 +- .../config/accesslog/v3/GrpcStatusFilter.ts | 80 ++- .../config/accesslog/v3/LogTypeFilter.ts | 6 +- .../envoy/config/bootstrap/v3/Admin.ts | 75 -- .../envoy/config/bootstrap/v3/Bootstrap.ts | 642 ------------------ .../config/bootstrap/v3/ClusterManager.ts | 99 --- .../config/bootstrap/v3/CustomInlineHeader.ts | 85 --- .../envoy/config/bootstrap/v3/FatalAction.ts | 39 -- .../config/bootstrap/v3/LayeredRuntime.ts | 25 - .../envoy/config/bootstrap/v3/Runtime.ts | 77 --- .../envoy/config/bootstrap/v3/RuntimeLayer.ts | 142 ---- .../envoy/config/bootstrap/v3/Watchdog.ts | 141 ---- .../envoy/config/bootstrap/v3/Watchdogs.ts | 35 - .../config/cluster/v3/CircuitBreakers.ts | 6 +- .../envoy/config/cluster/v3/Cluster.ts | 469 +++++++++++-- .../config/cluster/v3/UpstreamBindConfig.ts | 25 - .../envoy/config/core/v3/ApiConfigSource.ts | 76 ++- .../envoy/config/core/v3/ApiVersion.ts | 41 +- .../envoy/config/core/v3/BindConfig.ts | 2 + .../envoy/config/core/v3/ConfigSource.ts | 8 +- .../envoy/config/core/v3/Extension.ts | 2 + .../envoy/config/core/v3/HeaderValueOption.ts | 46 +- .../envoy/config/core/v3/HealthCheck.ts | 15 +- .../envoy/config/core/v3/HealthStatus.ts | 61 +- .../envoy/config/core/v3/HealthStatusSet.ts | 6 +- .../config/core/v3/Http2ProtocolOptions.ts | 2 + .../config/core/v3/HttpProtocolOptions.ts | 51 +- .../generated/envoy/config/core/v3/Node.ts | 2 + .../config/core/v3/ProxyProtocolConfig.ts | 26 +- .../core/v3/ProxyProtocolPassThroughTLVs.ts | 26 +- .../envoy/config/core/v3/RequestMethod.ts | 54 +- .../envoy/config/core/v3/RoutingPriority.ts | 34 +- .../envoy/config/core/v3/SelfConfigSource.ts | 6 +- .../envoy/config/core/v3/SocketAddress.ts | 20 +- .../envoy/config/core/v3/SocketOption.ts | 33 +- .../core/v3/SubstitutionFormatString.ts | 2 + .../envoy/config/core/v3/TrafficDirection.ts | 35 +- .../envoy/config/core/v3/UdpSocketConfig.ts | 1 - .../envoy/config/endpoint/v3/LbEndpoint.ts | 6 +- .../envoy/config/listener/v3/FilterChain.ts | 2 + .../config/listener/v3/FilterChainMatch.ts | 33 +- .../envoy/config/listener/v3/Listener.ts | 39 +- .../envoy/config/metrics/v3/DogStatsdSink.ts | 65 -- .../metrics/v3/HistogramBucketSettings.ts | 35 - .../envoy/config/metrics/v3/HystrixSink.ts | 61 -- .../envoy/config/metrics/v3/StatsConfig.ts | 148 ---- .../envoy/config/metrics/v3/StatsMatcher.ts | 47 -- .../envoy/config/metrics/v3/StatsSink.ts | 43 -- .../envoy/config/metrics/v3/StatsdSink.ts | 103 --- .../envoy/config/metrics/v3/TagSpecifier.ts | 174 ----- .../config/overload/v3/BufferFactoryConfig.ts | 50 -- .../config/overload/v3/OverloadAction.ts | 42 -- .../config/overload/v3/OverloadManager.ts | 44 -- .../config/overload/v3/ResourceMonitor.ts | 33 - .../v3/ScaleTimersOverloadActionConfig.ts | 89 --- .../envoy/config/overload/v3/ScaledTrigger.ts | 28 - .../config/overload/v3/ThresholdTrigger.ts | 18 - .../envoy/config/overload/v3/Trigger.ts | 24 - .../envoy/config/route/v3/HeaderMatcher.ts | 11 +- .../envoy/config/route/v3/RateLimit.ts | 28 +- .../envoy/config/route/v3/RedirectAction.ts | 47 +- .../envoy/config/route/v3/RetryPolicy.ts | 20 +- .../envoy/config/route/v3/RouteAction.ts | 86 ++- .../envoy/config/route/v3/VirtualHost.ts | 37 +- .../envoy/config/route/v3/WeightedCluster.ts | 2 + .../data/accesslog/v3/AccessLogCommon.ts | 8 +- .../envoy/data/accesslog/v3/AccessLogType.ts | 52 +- .../data/accesslog/v3/HTTPAccessLogEntry.ts | 38 +- .../accesslog/v3/HTTPRequestProperties.ts | 6 +- .../envoy/data/accesslog/v3/ResponseFlags.ts | 29 +- .../envoy/data/accesslog/v3/TLSProperties.ts | 32 +- .../filters/common/fault/v3/FaultDelay.ts | 15 +- .../v3/HttpConnectionManager.ts | 234 ++++++- .../common/v3/LocalityLbConfig.ts | 1 - .../ring_hash/v3/RingHash.ts | 46 +- .../v3/CertificateProviderPluginInstance.ts | 52 -- .../tls/v3/CertificateValidationContext.ts | 372 ---------- .../transport_sockets/tls/v3/GenericSecret.ts | 17 - .../tls/v3/PrivateKeyProvider.ts | 39 -- .../tls/v3/SdsSecretConfig.ts | 23 - .../transport_sockets/tls/v3/Secret.ts | 36 - .../tls/v3/TlsCertificate.ts | 127 ---- .../transport_sockets/tls/v3/TlsParameters.ts | 211 ------ .../tls/v3/TlsSessionTicketKeys.ts | 61 -- .../envoy/service/status/v3/ClientConfig.ts | 14 +- .../service/status/v3/ClientConfigStatus.ts | 45 +- .../envoy/service/status/v3/ConfigStatus.ts | 52 +- .../envoy/service/status/v3/PerXdsConfig.ts | 14 +- .../envoy/type/matcher/v3/RegexMatcher.ts | 4 + .../envoy/type/v3/CodecClientType.ts | 25 +- .../envoy/type/v3/FractionalPercent.ts | 45 +- .../google/protobuf/FieldDescriptorProto.ts | 108 ++- .../generated/google/protobuf/FieldOptions.ts | 48 +- .../generated/google/protobuf/FileOptions.ts | 30 +- .../generated/google/protobuf/NullValue.ts | 12 +- .../src/generated/google/protobuf/Value.ts | 6 +- .../udpa/annotations/PackageVersionStatus.ts | 37 +- .../udpa/annotations/StatusAnnotation.ts | 6 +- .../src/generated/validate/FieldRules.ts | 1 - .../src/generated/validate/KnownRegex.ts | 32 +- .../src/generated/validate/StringRules.ts | 6 +- .../annotations/v3/PackageVersionStatus.ts | 37 +- .../xds/annotations/v3/StatusAnnotation.ts | 6 +- .../generated/xds/core/v3/ResourceLocator.ts | 24 +- packages/grpc-js/package.json | 2 +- 137 files changed, 2344 insertions(+), 4269 deletions(-) delete mode 100644 packages/grpc-js-xds/src/generated/envoy/admin/v3/BootstrapConfigDump.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/admin/v3/ConfigDump.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/admin/v3/SecretsConfigDump.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/Admin.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/Bootstrap.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/ClusterManager.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/CustomInlineHeader.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/FatalAction.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/LayeredRuntime.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/Runtime.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/RuntimeLayer.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/Watchdog.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/Watchdogs.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/UpstreamBindConfig.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/DogStatsdSink.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/HistogramBucketSettings.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/HystrixSink.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/StatsConfig.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/StatsMatcher.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/StatsSink.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/StatsdSink.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/TagSpecifier.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/config/overload/v3/BufferFactoryConfig.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/config/overload/v3/OverloadAction.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/config/overload/v3/OverloadManager.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/config/overload/v3/ResourceMonitor.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/config/overload/v3/ScaleTimersOverloadActionConfig.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/config/overload/v3/ScaledTrigger.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/config/overload/v3/ThresholdTrigger.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/config/overload/v3/Trigger.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/CertificateProviderPluginInstance.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/CertificateValidationContext.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/GenericSecret.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/PrivateKeyProvider.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/SdsSecretConfig.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/Secret.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/TlsCertificate.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/TlsParameters.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/TlsSessionTicketKeys.ts diff --git a/packages/grpc-js-xds/interop/generated/grpc/testing/ClientConfigureRequest.ts b/packages/grpc-js-xds/interop/generated/grpc/testing/ClientConfigureRequest.ts index 7f07e8966..4128fdda4 100644 --- a/packages/grpc-js-xds/interop/generated/grpc/testing/ClientConfigureRequest.ts +++ b/packages/grpc-js-xds/interop/generated/grpc/testing/ClientConfigureRequest.ts @@ -5,7 +5,7 @@ * Metadata to be attached for the given type of RPCs. */ export interface _grpc_testing_ClientConfigureRequest_Metadata { - 'type'?: (_grpc_testing_ClientConfigureRequest_RpcType | keyof typeof _grpc_testing_ClientConfigureRequest_RpcType); + 'type'?: (_grpc_testing_ClientConfigureRequest_RpcType); 'key'?: (string); 'value'?: (string); } @@ -14,7 +14,7 @@ export interface _grpc_testing_ClientConfigureRequest_Metadata { * Metadata to be attached for the given type of RPCs. */ export interface _grpc_testing_ClientConfigureRequest_Metadata__Output { - 'type': (keyof typeof _grpc_testing_ClientConfigureRequest_RpcType); + 'type': (_grpc_testing_ClientConfigureRequest_RpcType__Output); 'key': (string); 'value': (string); } @@ -24,10 +24,24 @@ export interface _grpc_testing_ClientConfigureRequest_Metadata__Output { /** * Type of RPCs to send. */ -export enum _grpc_testing_ClientConfigureRequest_RpcType { - EMPTY_CALL = 0, - UNARY_CALL = 1, -} +export const _grpc_testing_ClientConfigureRequest_RpcType = { + EMPTY_CALL: 'EMPTY_CALL', + UNARY_CALL: 'UNARY_CALL', +} as const; + +/** + * Type of RPCs to send. + */ +export type _grpc_testing_ClientConfigureRequest_RpcType = + | 'EMPTY_CALL' + | 0 + | 'UNARY_CALL' + | 1 + +/** + * Type of RPCs to send. + */ +export type _grpc_testing_ClientConfigureRequest_RpcType__Output = typeof _grpc_testing_ClientConfigureRequest_RpcType[keyof typeof _grpc_testing_ClientConfigureRequest_RpcType] /** * Configurations for a test client. @@ -36,7 +50,7 @@ export interface ClientConfigureRequest { /** * The types of RPCs the client sends. */ - 'types'?: (_grpc_testing_ClientConfigureRequest_RpcType | keyof typeof _grpc_testing_ClientConfigureRequest_RpcType)[]; + 'types'?: (_grpc_testing_ClientConfigureRequest_RpcType)[]; /** * The collection of custom metadata to be attached to RPCs sent by the client. */ @@ -55,7 +69,7 @@ export interface ClientConfigureRequest__Output { /** * The types of RPCs the client sends. */ - 'types': (keyof typeof _grpc_testing_ClientConfigureRequest_RpcType)[]; + 'types': (_grpc_testing_ClientConfigureRequest_RpcType__Output)[]; /** * The collection of custom metadata to be attached to RPCs sent by the client. */ diff --git a/packages/grpc-js-xds/interop/generated/grpc/testing/GrpclbRouteType.ts b/packages/grpc-js-xds/interop/generated/grpc/testing/GrpclbRouteType.ts index 8ab0146b7..667442b41 100644 --- a/packages/grpc-js-xds/interop/generated/grpc/testing/GrpclbRouteType.ts +++ b/packages/grpc-js-xds/interop/generated/grpc/testing/GrpclbRouteType.ts @@ -8,17 +8,52 @@ * the address of this server from the gRPCLB server BalanceLoad RPC). Exactly * how this detection is done is context and server dependent. */ -export enum GrpclbRouteType { +export const GrpclbRouteType = { /** * Server didn't detect the route that a client took to reach it. */ - GRPCLB_ROUTE_TYPE_UNKNOWN = 0, + GRPCLB_ROUTE_TYPE_UNKNOWN: 'GRPCLB_ROUTE_TYPE_UNKNOWN', /** * Indicates that a client reached a server via gRPCLB fallback. */ - GRPCLB_ROUTE_TYPE_FALLBACK = 1, + GRPCLB_ROUTE_TYPE_FALLBACK: 'GRPCLB_ROUTE_TYPE_FALLBACK', /** * Indicates that a client reached a server as a gRPCLB-given backend. */ - GRPCLB_ROUTE_TYPE_BACKEND = 2, -} + GRPCLB_ROUTE_TYPE_BACKEND: 'GRPCLB_ROUTE_TYPE_BACKEND', +} as const; + +/** + * The type of route that a client took to reach a server w.r.t. gRPCLB. + * The server must fill in "fallback" if it detects that the RPC reached + * the server via the "gRPCLB fallback" path, and "backend" if it detects + * that the RPC reached the server via "gRPCLB backend" path (i.e. if it got + * the address of this server from the gRPCLB server BalanceLoad RPC). Exactly + * how this detection is done is context and server dependent. + */ +export type GrpclbRouteType = + /** + * Server didn't detect the route that a client took to reach it. + */ + | 'GRPCLB_ROUTE_TYPE_UNKNOWN' + | 0 + /** + * Indicates that a client reached a server via gRPCLB fallback. + */ + | 'GRPCLB_ROUTE_TYPE_FALLBACK' + | 1 + /** + * Indicates that a client reached a server as a gRPCLB-given backend. + */ + | 'GRPCLB_ROUTE_TYPE_BACKEND' + | 2 + +/** + * The type of route that a client took to reach a server w.r.t. gRPCLB. + * The server must fill in "fallback" if it detects that the RPC reached + * the server via the "gRPCLB fallback" path, and "backend" if it detects + * that the RPC reached the server via "gRPCLB backend" path (i.e. if it got + * the address of this server from the gRPCLB server BalanceLoad RPC). Exactly + * how this detection is done is context and server dependent. + */ +export type GrpclbRouteType__Output = typeof GrpclbRouteType[keyof typeof GrpclbRouteType] diff --git a/packages/grpc-js-xds/interop/generated/grpc/testing/LoadBalancerAccumulatedStatsResponse.ts b/packages/grpc-js-xds/interop/generated/grpc/testing/LoadBalancerAccumulatedStatsResponse.ts index 91157ac4e..000ef9ecf 100644 --- a/packages/grpc-js-xds/interop/generated/grpc/testing/LoadBalancerAccumulatedStatsResponse.ts +++ b/packages/grpc-js-xds/interop/generated/grpc/testing/LoadBalancerAccumulatedStatsResponse.ts @@ -32,16 +32,19 @@ export interface LoadBalancerAccumulatedStatsResponse { /** * The total number of RPCs have ever issued for each type. * Deprecated: use stats_per_method.rpcs_started instead. + * @deprecated */ 'num_rpcs_started_by_method'?: ({[key: string]: number}); /** * The total number of RPCs have ever completed successfully for each type. * Deprecated: use stats_per_method.result instead. + * @deprecated */ 'num_rpcs_succeeded_by_method'?: ({[key: string]: number}); /** * The total number of RPCs have ever failed for each type. * Deprecated: use stats_per_method.result instead. + * @deprecated */ 'num_rpcs_failed_by_method'?: ({[key: string]: number}); /** @@ -58,21 +61,24 @@ export interface LoadBalancerAccumulatedStatsResponse__Output { /** * The total number of RPCs have ever issued for each type. * Deprecated: use stats_per_method.rpcs_started instead. + * @deprecated */ 'num_rpcs_started_by_method': ({[key: string]: number}); /** * The total number of RPCs have ever completed successfully for each type. * Deprecated: use stats_per_method.result instead. + * @deprecated */ 'num_rpcs_succeeded_by_method': ({[key: string]: number}); /** * The total number of RPCs have ever failed for each type. * Deprecated: use stats_per_method.result instead. + * @deprecated */ 'num_rpcs_failed_by_method': ({[key: string]: number}); /** * Per-method RPC statistics. The key is the RpcType in string form; e.g. * 'EMPTY_CALL' or 'UNARY_CALL' */ - 'stats_per_method'?: ({[key: string]: _grpc_testing_LoadBalancerAccumulatedStatsResponse_MethodStats__Output}); + 'stats_per_method': ({[key: string]: _grpc_testing_LoadBalancerAccumulatedStatsResponse_MethodStats__Output}); } diff --git a/packages/grpc-js-xds/interop/generated/grpc/testing/LoadBalancerStatsResponse.ts b/packages/grpc-js-xds/interop/generated/grpc/testing/LoadBalancerStatsResponse.ts index 184a6e258..ab33612c3 100644 --- a/packages/grpc-js-xds/interop/generated/grpc/testing/LoadBalancerStatsResponse.ts +++ b/packages/grpc-js-xds/interop/generated/grpc/testing/LoadBalancerStatsResponse.ts @@ -36,5 +36,5 @@ export interface LoadBalancerStatsResponse__Output { * The number of RPCs that failed to record a remote peer. */ 'num_failures': (number); - 'rpcs_by_method'?: ({[key: string]: _grpc_testing_LoadBalancerStatsResponse_RpcsByPeer__Output}); + 'rpcs_by_method': ({[key: string]: _grpc_testing_LoadBalancerStatsResponse_RpcsByPeer__Output}); } diff --git a/packages/grpc-js-xds/interop/generated/grpc/testing/LoadBalancerStatsService.ts b/packages/grpc-js-xds/interop/generated/grpc/testing/LoadBalancerStatsService.ts index 26cfee9d7..9d11d9418 100644 --- a/packages/grpc-js-xds/interop/generated/grpc/testing/LoadBalancerStatsService.ts +++ b/packages/grpc-js-xds/interop/generated/grpc/testing/LoadBalancerStatsService.ts @@ -1,6 +1,7 @@ // Original file: proto/grpc/testing/test.proto import type * as grpc from '@grpc/grpc-js' +import type { MethodDefinition } from '@grpc/proto-loader' import type { LoadBalancerAccumulatedStatsRequest as _grpc_testing_LoadBalancerAccumulatedStatsRequest, LoadBalancerAccumulatedStatsRequest__Output as _grpc_testing_LoadBalancerAccumulatedStatsRequest__Output } from '../../grpc/testing/LoadBalancerAccumulatedStatsRequest'; import type { LoadBalancerAccumulatedStatsResponse as _grpc_testing_LoadBalancerAccumulatedStatsResponse, LoadBalancerAccumulatedStatsResponse__Output as _grpc_testing_LoadBalancerAccumulatedStatsResponse__Output } from '../../grpc/testing/LoadBalancerAccumulatedStatsResponse'; import type { LoadBalancerStatsRequest as _grpc_testing_LoadBalancerStatsRequest, LoadBalancerStatsRequest__Output as _grpc_testing_LoadBalancerStatsRequest__Output } from '../../grpc/testing/LoadBalancerStatsRequest'; @@ -13,32 +14,32 @@ export interface LoadBalancerStatsServiceClient extends grpc.Client { /** * Gets the accumulated stats for RPCs sent by a test client. */ - GetClientAccumulatedStats(argument: _grpc_testing_LoadBalancerAccumulatedStatsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_LoadBalancerAccumulatedStatsResponse__Output) => void): grpc.ClientUnaryCall; - GetClientAccumulatedStats(argument: _grpc_testing_LoadBalancerAccumulatedStatsRequest, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_testing_LoadBalancerAccumulatedStatsResponse__Output) => void): grpc.ClientUnaryCall; - GetClientAccumulatedStats(argument: _grpc_testing_LoadBalancerAccumulatedStatsRequest, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_LoadBalancerAccumulatedStatsResponse__Output) => void): grpc.ClientUnaryCall; - GetClientAccumulatedStats(argument: _grpc_testing_LoadBalancerAccumulatedStatsRequest, callback: (error?: grpc.ServiceError, result?: _grpc_testing_LoadBalancerAccumulatedStatsResponse__Output) => void): grpc.ClientUnaryCall; + GetClientAccumulatedStats(argument: _grpc_testing_LoadBalancerAccumulatedStatsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_LoadBalancerAccumulatedStatsResponse__Output>): grpc.ClientUnaryCall; + GetClientAccumulatedStats(argument: _grpc_testing_LoadBalancerAccumulatedStatsRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_LoadBalancerAccumulatedStatsResponse__Output>): grpc.ClientUnaryCall; + GetClientAccumulatedStats(argument: _grpc_testing_LoadBalancerAccumulatedStatsRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_LoadBalancerAccumulatedStatsResponse__Output>): grpc.ClientUnaryCall; + GetClientAccumulatedStats(argument: _grpc_testing_LoadBalancerAccumulatedStatsRequest, callback: grpc.requestCallback<_grpc_testing_LoadBalancerAccumulatedStatsResponse__Output>): grpc.ClientUnaryCall; /** * Gets the accumulated stats for RPCs sent by a test client. */ - getClientAccumulatedStats(argument: _grpc_testing_LoadBalancerAccumulatedStatsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_LoadBalancerAccumulatedStatsResponse__Output) => void): grpc.ClientUnaryCall; - getClientAccumulatedStats(argument: _grpc_testing_LoadBalancerAccumulatedStatsRequest, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_testing_LoadBalancerAccumulatedStatsResponse__Output) => void): grpc.ClientUnaryCall; - getClientAccumulatedStats(argument: _grpc_testing_LoadBalancerAccumulatedStatsRequest, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_LoadBalancerAccumulatedStatsResponse__Output) => void): grpc.ClientUnaryCall; - getClientAccumulatedStats(argument: _grpc_testing_LoadBalancerAccumulatedStatsRequest, callback: (error?: grpc.ServiceError, result?: _grpc_testing_LoadBalancerAccumulatedStatsResponse__Output) => void): grpc.ClientUnaryCall; + getClientAccumulatedStats(argument: _grpc_testing_LoadBalancerAccumulatedStatsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_LoadBalancerAccumulatedStatsResponse__Output>): grpc.ClientUnaryCall; + getClientAccumulatedStats(argument: _grpc_testing_LoadBalancerAccumulatedStatsRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_LoadBalancerAccumulatedStatsResponse__Output>): grpc.ClientUnaryCall; + getClientAccumulatedStats(argument: _grpc_testing_LoadBalancerAccumulatedStatsRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_LoadBalancerAccumulatedStatsResponse__Output>): grpc.ClientUnaryCall; + getClientAccumulatedStats(argument: _grpc_testing_LoadBalancerAccumulatedStatsRequest, callback: grpc.requestCallback<_grpc_testing_LoadBalancerAccumulatedStatsResponse__Output>): grpc.ClientUnaryCall; /** * Gets the backend distribution for RPCs sent by a test client. */ - GetClientStats(argument: _grpc_testing_LoadBalancerStatsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_LoadBalancerStatsResponse__Output) => void): grpc.ClientUnaryCall; - GetClientStats(argument: _grpc_testing_LoadBalancerStatsRequest, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_testing_LoadBalancerStatsResponse__Output) => void): grpc.ClientUnaryCall; - GetClientStats(argument: _grpc_testing_LoadBalancerStatsRequest, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_LoadBalancerStatsResponse__Output) => void): grpc.ClientUnaryCall; - GetClientStats(argument: _grpc_testing_LoadBalancerStatsRequest, callback: (error?: grpc.ServiceError, result?: _grpc_testing_LoadBalancerStatsResponse__Output) => void): grpc.ClientUnaryCall; + GetClientStats(argument: _grpc_testing_LoadBalancerStatsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_LoadBalancerStatsResponse__Output>): grpc.ClientUnaryCall; + GetClientStats(argument: _grpc_testing_LoadBalancerStatsRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_LoadBalancerStatsResponse__Output>): grpc.ClientUnaryCall; + GetClientStats(argument: _grpc_testing_LoadBalancerStatsRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_LoadBalancerStatsResponse__Output>): grpc.ClientUnaryCall; + GetClientStats(argument: _grpc_testing_LoadBalancerStatsRequest, callback: grpc.requestCallback<_grpc_testing_LoadBalancerStatsResponse__Output>): grpc.ClientUnaryCall; /** * Gets the backend distribution for RPCs sent by a test client. */ - getClientStats(argument: _grpc_testing_LoadBalancerStatsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_LoadBalancerStatsResponse__Output) => void): grpc.ClientUnaryCall; - getClientStats(argument: _grpc_testing_LoadBalancerStatsRequest, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_testing_LoadBalancerStatsResponse__Output) => void): grpc.ClientUnaryCall; - getClientStats(argument: _grpc_testing_LoadBalancerStatsRequest, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_LoadBalancerStatsResponse__Output) => void): grpc.ClientUnaryCall; - getClientStats(argument: _grpc_testing_LoadBalancerStatsRequest, callback: (error?: grpc.ServiceError, result?: _grpc_testing_LoadBalancerStatsResponse__Output) => void): grpc.ClientUnaryCall; + getClientStats(argument: _grpc_testing_LoadBalancerStatsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_LoadBalancerStatsResponse__Output>): grpc.ClientUnaryCall; + getClientStats(argument: _grpc_testing_LoadBalancerStatsRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_LoadBalancerStatsResponse__Output>): grpc.ClientUnaryCall; + getClientStats(argument: _grpc_testing_LoadBalancerStatsRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_LoadBalancerStatsResponse__Output>): grpc.ClientUnaryCall; + getClientStats(argument: _grpc_testing_LoadBalancerStatsRequest, callback: grpc.requestCallback<_grpc_testing_LoadBalancerStatsResponse__Output>): grpc.ClientUnaryCall; } @@ -57,3 +58,8 @@ export interface LoadBalancerStatsServiceHandlers extends grpc.UntypedServiceImp GetClientStats: grpc.handleUnaryCall<_grpc_testing_LoadBalancerStatsRequest__Output, _grpc_testing_LoadBalancerStatsResponse>; } + +export interface LoadBalancerStatsServiceDefinition extends grpc.ServiceDefinition { + GetClientAccumulatedStats: MethodDefinition<_grpc_testing_LoadBalancerAccumulatedStatsRequest, _grpc_testing_LoadBalancerAccumulatedStatsResponse, _grpc_testing_LoadBalancerAccumulatedStatsRequest__Output, _grpc_testing_LoadBalancerAccumulatedStatsResponse__Output> + GetClientStats: MethodDefinition<_grpc_testing_LoadBalancerStatsRequest, _grpc_testing_LoadBalancerStatsResponse, _grpc_testing_LoadBalancerStatsRequest__Output, _grpc_testing_LoadBalancerStatsResponse__Output> +} diff --git a/packages/grpc-js-xds/interop/generated/grpc/testing/Payload.ts b/packages/grpc-js-xds/interop/generated/grpc/testing/Payload.ts index 79102d2bf..17eb9e60a 100644 --- a/packages/grpc-js-xds/interop/generated/grpc/testing/Payload.ts +++ b/packages/grpc-js-xds/interop/generated/grpc/testing/Payload.ts @@ -1,6 +1,6 @@ // Original file: proto/grpc/testing/messages.proto -import type { PayloadType as _grpc_testing_PayloadType } from '../../grpc/testing/PayloadType'; +import type { PayloadType as _grpc_testing_PayloadType, PayloadType__Output as _grpc_testing_PayloadType__Output } from '../../grpc/testing/PayloadType'; /** * A block of data, to simply increase gRPC message size. @@ -9,7 +9,7 @@ export interface Payload { /** * The type of data in body. */ - 'type'?: (_grpc_testing_PayloadType | keyof typeof _grpc_testing_PayloadType); + 'type'?: (_grpc_testing_PayloadType); /** * Primary contents of payload. */ @@ -23,7 +23,7 @@ export interface Payload__Output { /** * The type of data in body. */ - 'type': (keyof typeof _grpc_testing_PayloadType); + 'type': (_grpc_testing_PayloadType__Output); /** * Primary contents of payload. */ diff --git a/packages/grpc-js-xds/interop/generated/grpc/testing/PayloadType.ts b/packages/grpc-js-xds/interop/generated/grpc/testing/PayloadType.ts index 3cf9d375a..64e526090 100644 --- a/packages/grpc-js-xds/interop/generated/grpc/testing/PayloadType.ts +++ b/packages/grpc-js-xds/interop/generated/grpc/testing/PayloadType.ts @@ -3,9 +3,24 @@ /** * The type of payload that should be returned. */ -export enum PayloadType { +export const PayloadType = { /** * Compressable text format. */ - COMPRESSABLE = 0, -} + COMPRESSABLE: 'COMPRESSABLE', +} as const; + +/** + * The type of payload that should be returned. + */ +export type PayloadType = + /** + * Compressable text format. + */ + | 'COMPRESSABLE' + | 0 + +/** + * The type of payload that should be returned. + */ +export type PayloadType__Output = typeof PayloadType[keyof typeof PayloadType] diff --git a/packages/grpc-js-xds/interop/generated/grpc/testing/ReconnectService.ts b/packages/grpc-js-xds/interop/generated/grpc/testing/ReconnectService.ts index e489e2849..2e3f25680 100644 --- a/packages/grpc-js-xds/interop/generated/grpc/testing/ReconnectService.ts +++ b/packages/grpc-js-xds/interop/generated/grpc/testing/ReconnectService.ts @@ -1,6 +1,7 @@ // Original file: proto/grpc/testing/test.proto import type * as grpc from '@grpc/grpc-js' +import type { MethodDefinition } from '@grpc/proto-loader' import type { Empty as _grpc_testing_Empty, Empty__Output as _grpc_testing_Empty__Output } from '../../grpc/testing/Empty'; import type { ReconnectInfo as _grpc_testing_ReconnectInfo, ReconnectInfo__Output as _grpc_testing_ReconnectInfo__Output } from '../../grpc/testing/ReconnectInfo'; import type { ReconnectParams as _grpc_testing_ReconnectParams, ReconnectParams__Output as _grpc_testing_ReconnectParams__Output } from '../../grpc/testing/ReconnectParams'; @@ -9,23 +10,23 @@ import type { ReconnectParams as _grpc_testing_ReconnectParams, ReconnectParams_ * A service used to control reconnect server. */ export interface ReconnectServiceClient extends grpc.Client { - Start(argument: _grpc_testing_ReconnectParams, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - Start(argument: _grpc_testing_ReconnectParams, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - Start(argument: _grpc_testing_ReconnectParams, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - Start(argument: _grpc_testing_ReconnectParams, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - start(argument: _grpc_testing_ReconnectParams, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - start(argument: _grpc_testing_ReconnectParams, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - start(argument: _grpc_testing_ReconnectParams, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - start(argument: _grpc_testing_ReconnectParams, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; + Start(argument: _grpc_testing_ReconnectParams, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + Start(argument: _grpc_testing_ReconnectParams, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + Start(argument: _grpc_testing_ReconnectParams, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + Start(argument: _grpc_testing_ReconnectParams, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + start(argument: _grpc_testing_ReconnectParams, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + start(argument: _grpc_testing_ReconnectParams, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + start(argument: _grpc_testing_ReconnectParams, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + start(argument: _grpc_testing_ReconnectParams, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; - Stop(argument: _grpc_testing_Empty, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_ReconnectInfo__Output) => void): grpc.ClientUnaryCall; - Stop(argument: _grpc_testing_Empty, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_testing_ReconnectInfo__Output) => void): grpc.ClientUnaryCall; - Stop(argument: _grpc_testing_Empty, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_ReconnectInfo__Output) => void): grpc.ClientUnaryCall; - Stop(argument: _grpc_testing_Empty, callback: (error?: grpc.ServiceError, result?: _grpc_testing_ReconnectInfo__Output) => void): grpc.ClientUnaryCall; - stop(argument: _grpc_testing_Empty, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_ReconnectInfo__Output) => void): grpc.ClientUnaryCall; - stop(argument: _grpc_testing_Empty, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_testing_ReconnectInfo__Output) => void): grpc.ClientUnaryCall; - stop(argument: _grpc_testing_Empty, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_ReconnectInfo__Output) => void): grpc.ClientUnaryCall; - stop(argument: _grpc_testing_Empty, callback: (error?: grpc.ServiceError, result?: _grpc_testing_ReconnectInfo__Output) => void): grpc.ClientUnaryCall; + Stop(argument: _grpc_testing_Empty, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_ReconnectInfo__Output>): grpc.ClientUnaryCall; + Stop(argument: _grpc_testing_Empty, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_ReconnectInfo__Output>): grpc.ClientUnaryCall; + Stop(argument: _grpc_testing_Empty, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_ReconnectInfo__Output>): grpc.ClientUnaryCall; + Stop(argument: _grpc_testing_Empty, callback: grpc.requestCallback<_grpc_testing_ReconnectInfo__Output>): grpc.ClientUnaryCall; + stop(argument: _grpc_testing_Empty, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_ReconnectInfo__Output>): grpc.ClientUnaryCall; + stop(argument: _grpc_testing_Empty, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_ReconnectInfo__Output>): grpc.ClientUnaryCall; + stop(argument: _grpc_testing_Empty, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_ReconnectInfo__Output>): grpc.ClientUnaryCall; + stop(argument: _grpc_testing_Empty, callback: grpc.requestCallback<_grpc_testing_ReconnectInfo__Output>): grpc.ClientUnaryCall; } @@ -38,3 +39,8 @@ export interface ReconnectServiceHandlers extends grpc.UntypedServiceImplementat Stop: grpc.handleUnaryCall<_grpc_testing_Empty__Output, _grpc_testing_ReconnectInfo>; } + +export interface ReconnectServiceDefinition extends grpc.ServiceDefinition { + Start: MethodDefinition<_grpc_testing_ReconnectParams, _grpc_testing_Empty, _grpc_testing_ReconnectParams__Output, _grpc_testing_Empty__Output> + Stop: MethodDefinition<_grpc_testing_Empty, _grpc_testing_ReconnectInfo, _grpc_testing_Empty__Output, _grpc_testing_ReconnectInfo__Output> +} diff --git a/packages/grpc-js-xds/interop/generated/grpc/testing/ResponseParameters.ts b/packages/grpc-js-xds/interop/generated/grpc/testing/ResponseParameters.ts index 04ca94ced..15f2f01f4 100644 --- a/packages/grpc-js-xds/interop/generated/grpc/testing/ResponseParameters.ts +++ b/packages/grpc-js-xds/interop/generated/grpc/testing/ResponseParameters.ts @@ -21,7 +21,7 @@ export interface ResponseParameters { * implement the full compression tests by introspecting the call to verify * the response's compression status. */ - 'compressed'?: (_grpc_testing_BoolValue); + 'compressed'?: (_grpc_testing_BoolValue | null); } /** @@ -43,5 +43,5 @@ export interface ResponseParameters__Output { * implement the full compression tests by introspecting the call to verify * the response's compression status. */ - 'compressed'?: (_grpc_testing_BoolValue__Output); + 'compressed': (_grpc_testing_BoolValue__Output | null); } diff --git a/packages/grpc-js-xds/interop/generated/grpc/testing/SimpleRequest.ts b/packages/grpc-js-xds/interop/generated/grpc/testing/SimpleRequest.ts index 056eb10b2..21843af69 100644 --- a/packages/grpc-js-xds/interop/generated/grpc/testing/SimpleRequest.ts +++ b/packages/grpc-js-xds/interop/generated/grpc/testing/SimpleRequest.ts @@ -1,6 +1,6 @@ // Original file: proto/grpc/testing/messages.proto -import type { PayloadType as _grpc_testing_PayloadType } from '../../grpc/testing/PayloadType'; +import type { PayloadType as _grpc_testing_PayloadType, PayloadType__Output as _grpc_testing_PayloadType__Output } from '../../grpc/testing/PayloadType'; import type { Payload as _grpc_testing_Payload, Payload__Output as _grpc_testing_Payload__Output } from '../../grpc/testing/Payload'; import type { BoolValue as _grpc_testing_BoolValue, BoolValue__Output as _grpc_testing_BoolValue__Output } from '../../grpc/testing/BoolValue'; import type { EchoStatus as _grpc_testing_EchoStatus, EchoStatus__Output as _grpc_testing_EchoStatus__Output } from '../../grpc/testing/EchoStatus'; @@ -13,7 +13,7 @@ export interface SimpleRequest { * Desired payload type in the response from the server. * If response_type is RANDOM, server randomly chooses one from other formats. */ - 'response_type'?: (_grpc_testing_PayloadType | keyof typeof _grpc_testing_PayloadType); + 'response_type'?: (_grpc_testing_PayloadType); /** * Desired payload size in the response from the server. */ @@ -21,7 +21,7 @@ export interface SimpleRequest { /** * Optional input payload sent along with the request. */ - 'payload'?: (_grpc_testing_Payload); + 'payload'?: (_grpc_testing_Payload | null); /** * Whether SimpleResponse should include username. */ @@ -36,15 +36,15 @@ export interface SimpleRequest { * implement the full compression tests by introspecting the call to verify * the response's compression status. */ - 'response_compressed'?: (_grpc_testing_BoolValue); + 'response_compressed'?: (_grpc_testing_BoolValue | null); /** * Whether server should return a given status */ - 'response_status'?: (_grpc_testing_EchoStatus); + 'response_status'?: (_grpc_testing_EchoStatus | null); /** * Whether the server should expect this request to be compressed. */ - 'expect_compressed'?: (_grpc_testing_BoolValue); + 'expect_compressed'?: (_grpc_testing_BoolValue | null); /** * Whether SimpleResponse should include server_id. */ @@ -63,7 +63,7 @@ export interface SimpleRequest__Output { * Desired payload type in the response from the server. * If response_type is RANDOM, server randomly chooses one from other formats. */ - 'response_type': (keyof typeof _grpc_testing_PayloadType); + 'response_type': (_grpc_testing_PayloadType__Output); /** * Desired payload size in the response from the server. */ @@ -71,7 +71,7 @@ export interface SimpleRequest__Output { /** * Optional input payload sent along with the request. */ - 'payload'?: (_grpc_testing_Payload__Output); + 'payload': (_grpc_testing_Payload__Output | null); /** * Whether SimpleResponse should include username. */ @@ -86,15 +86,15 @@ export interface SimpleRequest__Output { * implement the full compression tests by introspecting the call to verify * the response's compression status. */ - 'response_compressed'?: (_grpc_testing_BoolValue__Output); + 'response_compressed': (_grpc_testing_BoolValue__Output | null); /** * Whether server should return a given status */ - 'response_status'?: (_grpc_testing_EchoStatus__Output); + 'response_status': (_grpc_testing_EchoStatus__Output | null); /** * Whether the server should expect this request to be compressed. */ - 'expect_compressed'?: (_grpc_testing_BoolValue__Output); + 'expect_compressed': (_grpc_testing_BoolValue__Output | null); /** * Whether SimpleResponse should include server_id. */ diff --git a/packages/grpc-js-xds/interop/generated/grpc/testing/SimpleResponse.ts b/packages/grpc-js-xds/interop/generated/grpc/testing/SimpleResponse.ts index 661f336ce..b737c31fa 100644 --- a/packages/grpc-js-xds/interop/generated/grpc/testing/SimpleResponse.ts +++ b/packages/grpc-js-xds/interop/generated/grpc/testing/SimpleResponse.ts @@ -1,7 +1,7 @@ // Original file: proto/grpc/testing/messages.proto import type { Payload as _grpc_testing_Payload, Payload__Output as _grpc_testing_Payload__Output } from '../../grpc/testing/Payload'; -import type { GrpclbRouteType as _grpc_testing_GrpclbRouteType } from '../../grpc/testing/GrpclbRouteType'; +import type { GrpclbRouteType as _grpc_testing_GrpclbRouteType, GrpclbRouteType__Output as _grpc_testing_GrpclbRouteType__Output } from '../../grpc/testing/GrpclbRouteType'; /** * Unary response, as configured by the request. @@ -10,7 +10,7 @@ export interface SimpleResponse { /** * Payload to increase message size. */ - 'payload'?: (_grpc_testing_Payload); + 'payload'?: (_grpc_testing_Payload | null); /** * The user the request came from, for verifying authentication was * successful when the client expected it. @@ -28,7 +28,7 @@ export interface SimpleResponse { /** * gRPCLB Path. */ - 'grpclb_route_type'?: (_grpc_testing_GrpclbRouteType | keyof typeof _grpc_testing_GrpclbRouteType); + 'grpclb_route_type'?: (_grpc_testing_GrpclbRouteType); /** * Server hostname. */ @@ -42,7 +42,7 @@ export interface SimpleResponse__Output { /** * Payload to increase message size. */ - 'payload'?: (_grpc_testing_Payload__Output); + 'payload': (_grpc_testing_Payload__Output | null); /** * The user the request came from, for verifying authentication was * successful when the client expected it. @@ -60,7 +60,7 @@ export interface SimpleResponse__Output { /** * gRPCLB Path. */ - 'grpclb_route_type': (keyof typeof _grpc_testing_GrpclbRouteType); + 'grpclb_route_type': (_grpc_testing_GrpclbRouteType__Output); /** * Server hostname. */ diff --git a/packages/grpc-js-xds/interop/generated/grpc/testing/StreamingInputCallRequest.ts b/packages/grpc-js-xds/interop/generated/grpc/testing/StreamingInputCallRequest.ts index 56ad2b217..f45568849 100644 --- a/packages/grpc-js-xds/interop/generated/grpc/testing/StreamingInputCallRequest.ts +++ b/packages/grpc-js-xds/interop/generated/grpc/testing/StreamingInputCallRequest.ts @@ -10,14 +10,14 @@ export interface StreamingInputCallRequest { /** * Optional input payload sent along with the request. */ - 'payload'?: (_grpc_testing_Payload); + 'payload'?: (_grpc_testing_Payload | null); /** * Whether the server should expect this request to be compressed. This field * is "nullable" in order to interoperate seamlessly with servers not able to * implement the full compression tests by introspecting the call to verify * the request's compression status. */ - 'expect_compressed'?: (_grpc_testing_BoolValue); + 'expect_compressed'?: (_grpc_testing_BoolValue | null); } /** @@ -27,12 +27,12 @@ export interface StreamingInputCallRequest__Output { /** * Optional input payload sent along with the request. */ - 'payload'?: (_grpc_testing_Payload__Output); + 'payload': (_grpc_testing_Payload__Output | null); /** * Whether the server should expect this request to be compressed. This field * is "nullable" in order to interoperate seamlessly with servers not able to * implement the full compression tests by introspecting the call to verify * the request's compression status. */ - 'expect_compressed'?: (_grpc_testing_BoolValue__Output); + 'expect_compressed': (_grpc_testing_BoolValue__Output | null); } diff --git a/packages/grpc-js-xds/interop/generated/grpc/testing/StreamingOutputCallRequest.ts b/packages/grpc-js-xds/interop/generated/grpc/testing/StreamingOutputCallRequest.ts index 52922062d..0d812b74f 100644 --- a/packages/grpc-js-xds/interop/generated/grpc/testing/StreamingOutputCallRequest.ts +++ b/packages/grpc-js-xds/interop/generated/grpc/testing/StreamingOutputCallRequest.ts @@ -1,6 +1,6 @@ // Original file: proto/grpc/testing/messages.proto -import type { PayloadType as _grpc_testing_PayloadType } from '../../grpc/testing/PayloadType'; +import type { PayloadType as _grpc_testing_PayloadType, PayloadType__Output as _grpc_testing_PayloadType__Output } from '../../grpc/testing/PayloadType'; import type { ResponseParameters as _grpc_testing_ResponseParameters, ResponseParameters__Output as _grpc_testing_ResponseParameters__Output } from '../../grpc/testing/ResponseParameters'; import type { Payload as _grpc_testing_Payload, Payload__Output as _grpc_testing_Payload__Output } from '../../grpc/testing/Payload'; import type { EchoStatus as _grpc_testing_EchoStatus, EchoStatus__Output as _grpc_testing_EchoStatus__Output } from '../../grpc/testing/EchoStatus'; @@ -15,7 +15,7 @@ export interface StreamingOutputCallRequest { * might be of different types. This is to simulate a mixed type of payload * stream. */ - 'response_type'?: (_grpc_testing_PayloadType | keyof typeof _grpc_testing_PayloadType); + 'response_type'?: (_grpc_testing_PayloadType); /** * Configuration for each expected response message. */ @@ -23,11 +23,11 @@ export interface StreamingOutputCallRequest { /** * Optional input payload sent along with the request. */ - 'payload'?: (_grpc_testing_Payload); + 'payload'?: (_grpc_testing_Payload | null); /** * Whether server should return a given status */ - 'response_status'?: (_grpc_testing_EchoStatus); + 'response_status'?: (_grpc_testing_EchoStatus | null); } /** @@ -40,7 +40,7 @@ export interface StreamingOutputCallRequest__Output { * might be of different types. This is to simulate a mixed type of payload * stream. */ - 'response_type': (keyof typeof _grpc_testing_PayloadType); + 'response_type': (_grpc_testing_PayloadType__Output); /** * Configuration for each expected response message. */ @@ -48,9 +48,9 @@ export interface StreamingOutputCallRequest__Output { /** * Optional input payload sent along with the request. */ - 'payload'?: (_grpc_testing_Payload__Output); + 'payload': (_grpc_testing_Payload__Output | null); /** * Whether server should return a given status */ - 'response_status'?: (_grpc_testing_EchoStatus__Output); + 'response_status': (_grpc_testing_EchoStatus__Output | null); } diff --git a/packages/grpc-js-xds/interop/generated/grpc/testing/StreamingOutputCallResponse.ts b/packages/grpc-js-xds/interop/generated/grpc/testing/StreamingOutputCallResponse.ts index 19ab306dd..e2eb435cd 100644 --- a/packages/grpc-js-xds/interop/generated/grpc/testing/StreamingOutputCallResponse.ts +++ b/packages/grpc-js-xds/interop/generated/grpc/testing/StreamingOutputCallResponse.ts @@ -9,7 +9,7 @@ export interface StreamingOutputCallResponse { /** * Payload to increase response size. */ - 'payload'?: (_grpc_testing_Payload); + 'payload'?: (_grpc_testing_Payload | null); } /** @@ -19,5 +19,5 @@ export interface StreamingOutputCallResponse__Output { /** * Payload to increase response size. */ - 'payload'?: (_grpc_testing_Payload__Output); + 'payload': (_grpc_testing_Payload__Output | null); } diff --git a/packages/grpc-js-xds/interop/generated/grpc/testing/TestService.ts b/packages/grpc-js-xds/interop/generated/grpc/testing/TestService.ts index dbb606c83..139d3c0ef 100644 --- a/packages/grpc-js-xds/interop/generated/grpc/testing/TestService.ts +++ b/packages/grpc-js-xds/interop/generated/grpc/testing/TestService.ts @@ -1,6 +1,7 @@ // Original file: proto/grpc/testing/test.proto import type * as grpc from '@grpc/grpc-js' +import type { MethodDefinition } from '@grpc/proto-loader' import type { Empty as _grpc_testing_Empty, Empty__Output as _grpc_testing_Empty__Output } from '../../grpc/testing/Empty'; import type { SimpleRequest as _grpc_testing_SimpleRequest, SimpleRequest__Output as _grpc_testing_SimpleRequest__Output } from '../../grpc/testing/SimpleRequest'; import type { SimpleResponse as _grpc_testing_SimpleResponse, SimpleResponse__Output as _grpc_testing_SimpleResponse__Output } from '../../grpc/testing/SimpleResponse'; @@ -19,34 +20,34 @@ export interface TestServiceClient extends grpc.Client { * headers set such that a caching HTTP proxy (such as GFE) can * satisfy subsequent requests. */ - CacheableUnaryCall(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_SimpleResponse__Output) => void): grpc.ClientUnaryCall; - CacheableUnaryCall(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_testing_SimpleResponse__Output) => void): grpc.ClientUnaryCall; - CacheableUnaryCall(argument: _grpc_testing_SimpleRequest, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_SimpleResponse__Output) => void): grpc.ClientUnaryCall; - CacheableUnaryCall(argument: _grpc_testing_SimpleRequest, callback: (error?: grpc.ServiceError, result?: _grpc_testing_SimpleResponse__Output) => void): grpc.ClientUnaryCall; + CacheableUnaryCall(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + CacheableUnaryCall(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + CacheableUnaryCall(argument: _grpc_testing_SimpleRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + CacheableUnaryCall(argument: _grpc_testing_SimpleRequest, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; /** * One request followed by one response. Response has cache control * headers set such that a caching HTTP proxy (such as GFE) can * satisfy subsequent requests. */ - cacheableUnaryCall(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_SimpleResponse__Output) => void): grpc.ClientUnaryCall; - cacheableUnaryCall(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_testing_SimpleResponse__Output) => void): grpc.ClientUnaryCall; - cacheableUnaryCall(argument: _grpc_testing_SimpleRequest, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_SimpleResponse__Output) => void): grpc.ClientUnaryCall; - cacheableUnaryCall(argument: _grpc_testing_SimpleRequest, callback: (error?: grpc.ServiceError, result?: _grpc_testing_SimpleResponse__Output) => void): grpc.ClientUnaryCall; + cacheableUnaryCall(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + cacheableUnaryCall(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + cacheableUnaryCall(argument: _grpc_testing_SimpleRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + cacheableUnaryCall(argument: _grpc_testing_SimpleRequest, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; /** * One empty request followed by one empty response. */ - EmptyCall(argument: _grpc_testing_Empty, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - EmptyCall(argument: _grpc_testing_Empty, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - EmptyCall(argument: _grpc_testing_Empty, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - EmptyCall(argument: _grpc_testing_Empty, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; + EmptyCall(argument: _grpc_testing_Empty, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + EmptyCall(argument: _grpc_testing_Empty, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + EmptyCall(argument: _grpc_testing_Empty, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + EmptyCall(argument: _grpc_testing_Empty, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; /** * One empty request followed by one empty response. */ - emptyCall(argument: _grpc_testing_Empty, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - emptyCall(argument: _grpc_testing_Empty, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - emptyCall(argument: _grpc_testing_Empty, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - emptyCall(argument: _grpc_testing_Empty, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; + emptyCall(argument: _grpc_testing_Empty, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + emptyCall(argument: _grpc_testing_Empty, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + emptyCall(argument: _grpc_testing_Empty, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + emptyCall(argument: _grpc_testing_Empty, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; /** * A sequence of requests with each request served by the server immediately. @@ -84,18 +85,18 @@ export interface TestServiceClient extends grpc.Client { * A sequence of requests followed by one response (streamed upload). * The server returns the aggregated size of client payload as the result. */ - StreamingInputCall(metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_StreamingInputCallResponse__Output) => void): grpc.ClientWritableStream<_grpc_testing_StreamingInputCallRequest>; - StreamingInputCall(metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_testing_StreamingInputCallResponse__Output) => void): grpc.ClientWritableStream<_grpc_testing_StreamingInputCallRequest>; - StreamingInputCall(options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_StreamingInputCallResponse__Output) => void): grpc.ClientWritableStream<_grpc_testing_StreamingInputCallRequest>; - StreamingInputCall(callback: (error?: grpc.ServiceError, result?: _grpc_testing_StreamingInputCallResponse__Output) => void): grpc.ClientWritableStream<_grpc_testing_StreamingInputCallRequest>; + StreamingInputCall(metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_StreamingInputCallResponse__Output>): grpc.ClientWritableStream<_grpc_testing_StreamingInputCallRequest>; + StreamingInputCall(metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_StreamingInputCallResponse__Output>): grpc.ClientWritableStream<_grpc_testing_StreamingInputCallRequest>; + StreamingInputCall(options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_StreamingInputCallResponse__Output>): grpc.ClientWritableStream<_grpc_testing_StreamingInputCallRequest>; + StreamingInputCall(callback: grpc.requestCallback<_grpc_testing_StreamingInputCallResponse__Output>): grpc.ClientWritableStream<_grpc_testing_StreamingInputCallRequest>; /** * A sequence of requests followed by one response (streamed upload). * The server returns the aggregated size of client payload as the result. */ - streamingInputCall(metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_StreamingInputCallResponse__Output) => void): grpc.ClientWritableStream<_grpc_testing_StreamingInputCallRequest>; - streamingInputCall(metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_testing_StreamingInputCallResponse__Output) => void): grpc.ClientWritableStream<_grpc_testing_StreamingInputCallRequest>; - streamingInputCall(options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_StreamingInputCallResponse__Output) => void): grpc.ClientWritableStream<_grpc_testing_StreamingInputCallRequest>; - streamingInputCall(callback: (error?: grpc.ServiceError, result?: _grpc_testing_StreamingInputCallResponse__Output) => void): grpc.ClientWritableStream<_grpc_testing_StreamingInputCallRequest>; + streamingInputCall(metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_StreamingInputCallResponse__Output>): grpc.ClientWritableStream<_grpc_testing_StreamingInputCallRequest>; + streamingInputCall(metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_StreamingInputCallResponse__Output>): grpc.ClientWritableStream<_grpc_testing_StreamingInputCallRequest>; + streamingInputCall(options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_StreamingInputCallResponse__Output>): grpc.ClientWritableStream<_grpc_testing_StreamingInputCallRequest>; + streamingInputCall(callback: grpc.requestCallback<_grpc_testing_StreamingInputCallResponse__Output>): grpc.ClientWritableStream<_grpc_testing_StreamingInputCallRequest>; /** * One request followed by a sequence of responses (streamed download). @@ -113,34 +114,34 @@ export interface TestServiceClient extends grpc.Client { /** * One request followed by one response. */ - UnaryCall(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_SimpleResponse__Output) => void): grpc.ClientUnaryCall; - UnaryCall(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_testing_SimpleResponse__Output) => void): grpc.ClientUnaryCall; - UnaryCall(argument: _grpc_testing_SimpleRequest, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_SimpleResponse__Output) => void): grpc.ClientUnaryCall; - UnaryCall(argument: _grpc_testing_SimpleRequest, callback: (error?: grpc.ServiceError, result?: _grpc_testing_SimpleResponse__Output) => void): grpc.ClientUnaryCall; + UnaryCall(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + UnaryCall(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + UnaryCall(argument: _grpc_testing_SimpleRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + UnaryCall(argument: _grpc_testing_SimpleRequest, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; /** * One request followed by one response. */ - unaryCall(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_SimpleResponse__Output) => void): grpc.ClientUnaryCall; - unaryCall(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_testing_SimpleResponse__Output) => void): grpc.ClientUnaryCall; - unaryCall(argument: _grpc_testing_SimpleRequest, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_SimpleResponse__Output) => void): grpc.ClientUnaryCall; - unaryCall(argument: _grpc_testing_SimpleRequest, callback: (error?: grpc.ServiceError, result?: _grpc_testing_SimpleResponse__Output) => void): grpc.ClientUnaryCall; + unaryCall(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + unaryCall(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + unaryCall(argument: _grpc_testing_SimpleRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + unaryCall(argument: _grpc_testing_SimpleRequest, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; /** * The test server will not implement this method. It will be used * to test the behavior when clients call unimplemented methods. */ - UnimplementedCall(argument: _grpc_testing_Empty, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - UnimplementedCall(argument: _grpc_testing_Empty, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - UnimplementedCall(argument: _grpc_testing_Empty, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - UnimplementedCall(argument: _grpc_testing_Empty, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; + UnimplementedCall(argument: _grpc_testing_Empty, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + UnimplementedCall(argument: _grpc_testing_Empty, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + UnimplementedCall(argument: _grpc_testing_Empty, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + UnimplementedCall(argument: _grpc_testing_Empty, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; /** * The test server will not implement this method. It will be used * to test the behavior when clients call unimplemented methods. */ - unimplementedCall(argument: _grpc_testing_Empty, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - unimplementedCall(argument: _grpc_testing_Empty, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - unimplementedCall(argument: _grpc_testing_Empty, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - unimplementedCall(argument: _grpc_testing_Empty, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; + unimplementedCall(argument: _grpc_testing_Empty, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + unimplementedCall(argument: _grpc_testing_Empty, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + unimplementedCall(argument: _grpc_testing_Empty, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + unimplementedCall(argument: _grpc_testing_Empty, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; } @@ -200,3 +201,14 @@ export interface TestServiceHandlers extends grpc.UntypedServiceImplementation { UnimplementedCall: grpc.handleUnaryCall<_grpc_testing_Empty__Output, _grpc_testing_Empty>; } + +export interface TestServiceDefinition extends grpc.ServiceDefinition { + CacheableUnaryCall: MethodDefinition<_grpc_testing_SimpleRequest, _grpc_testing_SimpleResponse, _grpc_testing_SimpleRequest__Output, _grpc_testing_SimpleResponse__Output> + EmptyCall: MethodDefinition<_grpc_testing_Empty, _grpc_testing_Empty, _grpc_testing_Empty__Output, _grpc_testing_Empty__Output> + FullDuplexCall: MethodDefinition<_grpc_testing_StreamingOutputCallRequest, _grpc_testing_StreamingOutputCallResponse, _grpc_testing_StreamingOutputCallRequest__Output, _grpc_testing_StreamingOutputCallResponse__Output> + HalfDuplexCall: MethodDefinition<_grpc_testing_StreamingOutputCallRequest, _grpc_testing_StreamingOutputCallResponse, _grpc_testing_StreamingOutputCallRequest__Output, _grpc_testing_StreamingOutputCallResponse__Output> + StreamingInputCall: MethodDefinition<_grpc_testing_StreamingInputCallRequest, _grpc_testing_StreamingInputCallResponse, _grpc_testing_StreamingInputCallRequest__Output, _grpc_testing_StreamingInputCallResponse__Output> + StreamingOutputCall: MethodDefinition<_grpc_testing_StreamingOutputCallRequest, _grpc_testing_StreamingOutputCallResponse, _grpc_testing_StreamingOutputCallRequest__Output, _grpc_testing_StreamingOutputCallResponse__Output> + UnaryCall: MethodDefinition<_grpc_testing_SimpleRequest, _grpc_testing_SimpleResponse, _grpc_testing_SimpleRequest__Output, _grpc_testing_SimpleResponse__Output> + UnimplementedCall: MethodDefinition<_grpc_testing_Empty, _grpc_testing_Empty, _grpc_testing_Empty__Output, _grpc_testing_Empty__Output> +} diff --git a/packages/grpc-js-xds/interop/generated/grpc/testing/UnimplementedService.ts b/packages/grpc-js-xds/interop/generated/grpc/testing/UnimplementedService.ts index d21dfcd0f..aea5d8b4a 100644 --- a/packages/grpc-js-xds/interop/generated/grpc/testing/UnimplementedService.ts +++ b/packages/grpc-js-xds/interop/generated/grpc/testing/UnimplementedService.ts @@ -1,6 +1,7 @@ // Original file: proto/grpc/testing/test.proto import type * as grpc from '@grpc/grpc-js' +import type { MethodDefinition } from '@grpc/proto-loader' import type { Empty as _grpc_testing_Empty, Empty__Output as _grpc_testing_Empty__Output } from '../../grpc/testing/Empty'; /** @@ -11,17 +12,17 @@ export interface UnimplementedServiceClient extends grpc.Client { /** * A call that no server should implement */ - UnimplementedCall(argument: _grpc_testing_Empty, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - UnimplementedCall(argument: _grpc_testing_Empty, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - UnimplementedCall(argument: _grpc_testing_Empty, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - UnimplementedCall(argument: _grpc_testing_Empty, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; + UnimplementedCall(argument: _grpc_testing_Empty, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + UnimplementedCall(argument: _grpc_testing_Empty, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + UnimplementedCall(argument: _grpc_testing_Empty, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + UnimplementedCall(argument: _grpc_testing_Empty, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; /** * A call that no server should implement */ - unimplementedCall(argument: _grpc_testing_Empty, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - unimplementedCall(argument: _grpc_testing_Empty, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - unimplementedCall(argument: _grpc_testing_Empty, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - unimplementedCall(argument: _grpc_testing_Empty, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; + unimplementedCall(argument: _grpc_testing_Empty, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + unimplementedCall(argument: _grpc_testing_Empty, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + unimplementedCall(argument: _grpc_testing_Empty, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + unimplementedCall(argument: _grpc_testing_Empty, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; } @@ -36,3 +37,7 @@ export interface UnimplementedServiceHandlers extends grpc.UntypedServiceImpleme UnimplementedCall: grpc.handleUnaryCall<_grpc_testing_Empty__Output, _grpc_testing_Empty>; } + +export interface UnimplementedServiceDefinition extends grpc.ServiceDefinition { + UnimplementedCall: MethodDefinition<_grpc_testing_Empty, _grpc_testing_Empty, _grpc_testing_Empty__Output, _grpc_testing_Empty__Output> +} diff --git a/packages/grpc-js-xds/interop/generated/grpc/testing/XdsUpdateClientConfigureService.ts b/packages/grpc-js-xds/interop/generated/grpc/testing/XdsUpdateClientConfigureService.ts index 22947619c..76826b812 100644 --- a/packages/grpc-js-xds/interop/generated/grpc/testing/XdsUpdateClientConfigureService.ts +++ b/packages/grpc-js-xds/interop/generated/grpc/testing/XdsUpdateClientConfigureService.ts @@ -1,6 +1,7 @@ // Original file: proto/grpc/testing/test.proto import type * as grpc from '@grpc/grpc-js' +import type { MethodDefinition } from '@grpc/proto-loader' import type { ClientConfigureRequest as _grpc_testing_ClientConfigureRequest, ClientConfigureRequest__Output as _grpc_testing_ClientConfigureRequest__Output } from '../../grpc/testing/ClientConfigureRequest'; import type { ClientConfigureResponse as _grpc_testing_ClientConfigureResponse, ClientConfigureResponse__Output as _grpc_testing_ClientConfigureResponse__Output } from '../../grpc/testing/ClientConfigureResponse'; @@ -11,17 +12,17 @@ export interface XdsUpdateClientConfigureServiceClient extends grpc.Client { /** * Update the tes client's configuration. */ - Configure(argument: _grpc_testing_ClientConfigureRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_ClientConfigureResponse__Output) => void): grpc.ClientUnaryCall; - Configure(argument: _grpc_testing_ClientConfigureRequest, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_testing_ClientConfigureResponse__Output) => void): grpc.ClientUnaryCall; - Configure(argument: _grpc_testing_ClientConfigureRequest, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_ClientConfigureResponse__Output) => void): grpc.ClientUnaryCall; - Configure(argument: _grpc_testing_ClientConfigureRequest, callback: (error?: grpc.ServiceError, result?: _grpc_testing_ClientConfigureResponse__Output) => void): grpc.ClientUnaryCall; + Configure(argument: _grpc_testing_ClientConfigureRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_ClientConfigureResponse__Output>): grpc.ClientUnaryCall; + Configure(argument: _grpc_testing_ClientConfigureRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_ClientConfigureResponse__Output>): grpc.ClientUnaryCall; + Configure(argument: _grpc_testing_ClientConfigureRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_ClientConfigureResponse__Output>): grpc.ClientUnaryCall; + Configure(argument: _grpc_testing_ClientConfigureRequest, callback: grpc.requestCallback<_grpc_testing_ClientConfigureResponse__Output>): grpc.ClientUnaryCall; /** * Update the tes client's configuration. */ - configure(argument: _grpc_testing_ClientConfigureRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_ClientConfigureResponse__Output) => void): grpc.ClientUnaryCall; - configure(argument: _grpc_testing_ClientConfigureRequest, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_testing_ClientConfigureResponse__Output) => void): grpc.ClientUnaryCall; - configure(argument: _grpc_testing_ClientConfigureRequest, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_ClientConfigureResponse__Output) => void): grpc.ClientUnaryCall; - configure(argument: _grpc_testing_ClientConfigureRequest, callback: (error?: grpc.ServiceError, result?: _grpc_testing_ClientConfigureResponse__Output) => void): grpc.ClientUnaryCall; + configure(argument: _grpc_testing_ClientConfigureRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_ClientConfigureResponse__Output>): grpc.ClientUnaryCall; + configure(argument: _grpc_testing_ClientConfigureRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_ClientConfigureResponse__Output>): grpc.ClientUnaryCall; + configure(argument: _grpc_testing_ClientConfigureRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_ClientConfigureResponse__Output>): grpc.ClientUnaryCall; + configure(argument: _grpc_testing_ClientConfigureRequest, callback: grpc.requestCallback<_grpc_testing_ClientConfigureResponse__Output>): grpc.ClientUnaryCall; } @@ -35,3 +36,7 @@ export interface XdsUpdateClientConfigureServiceHandlers extends grpc.UntypedSer Configure: grpc.handleUnaryCall<_grpc_testing_ClientConfigureRequest__Output, _grpc_testing_ClientConfigureResponse>; } + +export interface XdsUpdateClientConfigureServiceDefinition extends grpc.ServiceDefinition { + Configure: MethodDefinition<_grpc_testing_ClientConfigureRequest, _grpc_testing_ClientConfigureResponse, _grpc_testing_ClientConfigureRequest__Output, _grpc_testing_ClientConfigureResponse__Output> +} diff --git a/packages/grpc-js-xds/interop/generated/grpc/testing/XdsUpdateHealthService.ts b/packages/grpc-js-xds/interop/generated/grpc/testing/XdsUpdateHealthService.ts index aa1e35dca..aa3d6e9c6 100644 --- a/packages/grpc-js-xds/interop/generated/grpc/testing/XdsUpdateHealthService.ts +++ b/packages/grpc-js-xds/interop/generated/grpc/testing/XdsUpdateHealthService.ts @@ -1,29 +1,30 @@ // Original file: proto/grpc/testing/test.proto import type * as grpc from '@grpc/grpc-js' +import type { MethodDefinition } from '@grpc/proto-loader' import type { Empty as _grpc_testing_Empty, Empty__Output as _grpc_testing_Empty__Output } from '../../grpc/testing/Empty'; /** * A service to remotely control health status of an xDS test server. */ export interface XdsUpdateHealthServiceClient extends grpc.Client { - SetNotServing(argument: _grpc_testing_Empty, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - SetNotServing(argument: _grpc_testing_Empty, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - SetNotServing(argument: _grpc_testing_Empty, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - SetNotServing(argument: _grpc_testing_Empty, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - setNotServing(argument: _grpc_testing_Empty, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - setNotServing(argument: _grpc_testing_Empty, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - setNotServing(argument: _grpc_testing_Empty, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - setNotServing(argument: _grpc_testing_Empty, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; + SetNotServing(argument: _grpc_testing_Empty, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + SetNotServing(argument: _grpc_testing_Empty, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + SetNotServing(argument: _grpc_testing_Empty, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + SetNotServing(argument: _grpc_testing_Empty, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + setNotServing(argument: _grpc_testing_Empty, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + setNotServing(argument: _grpc_testing_Empty, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + setNotServing(argument: _grpc_testing_Empty, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + setNotServing(argument: _grpc_testing_Empty, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; - SetServing(argument: _grpc_testing_Empty, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - SetServing(argument: _grpc_testing_Empty, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - SetServing(argument: _grpc_testing_Empty, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - SetServing(argument: _grpc_testing_Empty, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - setServing(argument: _grpc_testing_Empty, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - setServing(argument: _grpc_testing_Empty, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - setServing(argument: _grpc_testing_Empty, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; - setServing(argument: _grpc_testing_Empty, callback: (error?: grpc.ServiceError, result?: _grpc_testing_Empty__Output) => void): grpc.ClientUnaryCall; + SetServing(argument: _grpc_testing_Empty, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + SetServing(argument: _grpc_testing_Empty, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + SetServing(argument: _grpc_testing_Empty, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + SetServing(argument: _grpc_testing_Empty, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + setServing(argument: _grpc_testing_Empty, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + setServing(argument: _grpc_testing_Empty, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + setServing(argument: _grpc_testing_Empty, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; + setServing(argument: _grpc_testing_Empty, callback: grpc.requestCallback<_grpc_testing_Empty__Output>): grpc.ClientUnaryCall; } @@ -36,3 +37,8 @@ export interface XdsUpdateHealthServiceHandlers extends grpc.UntypedServiceImple SetServing: grpc.handleUnaryCall<_grpc_testing_Empty__Output, _grpc_testing_Empty>; } + +export interface XdsUpdateHealthServiceDefinition extends grpc.ServiceDefinition { + SetNotServing: MethodDefinition<_grpc_testing_Empty, _grpc_testing_Empty, _grpc_testing_Empty__Output, _grpc_testing_Empty__Output> + SetServing: MethodDefinition<_grpc_testing_Empty, _grpc_testing_Empty, _grpc_testing_Empty__Output, _grpc_testing_Empty__Output> +} diff --git a/packages/grpc-js-xds/interop/generated/test.ts b/packages/grpc-js-xds/interop/generated/test.ts index f91f0c970..722f8fe28 100644 --- a/packages/grpc-js-xds/interop/generated/test.ts +++ b/packages/grpc-js-xds/interop/generated/test.ts @@ -1,12 +1,12 @@ import type * as grpc from '@grpc/grpc-js'; -import type { ServiceDefinition, EnumTypeDefinition, MessageTypeDefinition } from '@grpc/proto-loader'; +import type { EnumTypeDefinition, MessageTypeDefinition } from '@grpc/proto-loader'; -import type { LoadBalancerStatsServiceClient as _grpc_testing_LoadBalancerStatsServiceClient } from './grpc/testing/LoadBalancerStatsService'; -import type { ReconnectServiceClient as _grpc_testing_ReconnectServiceClient } from './grpc/testing/ReconnectService'; -import type { TestServiceClient as _grpc_testing_TestServiceClient } from './grpc/testing/TestService'; -import type { UnimplementedServiceClient as _grpc_testing_UnimplementedServiceClient } from './grpc/testing/UnimplementedService'; -import type { XdsUpdateClientConfigureServiceClient as _grpc_testing_XdsUpdateClientConfigureServiceClient } from './grpc/testing/XdsUpdateClientConfigureService'; -import type { XdsUpdateHealthServiceClient as _grpc_testing_XdsUpdateHealthServiceClient } from './grpc/testing/XdsUpdateHealthService'; +import type { LoadBalancerStatsServiceClient as _grpc_testing_LoadBalancerStatsServiceClient, LoadBalancerStatsServiceDefinition as _grpc_testing_LoadBalancerStatsServiceDefinition } from './grpc/testing/LoadBalancerStatsService'; +import type { ReconnectServiceClient as _grpc_testing_ReconnectServiceClient, ReconnectServiceDefinition as _grpc_testing_ReconnectServiceDefinition } from './grpc/testing/ReconnectService'; +import type { TestServiceClient as _grpc_testing_TestServiceClient, TestServiceDefinition as _grpc_testing_TestServiceDefinition } from './grpc/testing/TestService'; +import type { UnimplementedServiceClient as _grpc_testing_UnimplementedServiceClient, UnimplementedServiceDefinition as _grpc_testing_UnimplementedServiceDefinition } from './grpc/testing/UnimplementedService'; +import type { XdsUpdateClientConfigureServiceClient as _grpc_testing_XdsUpdateClientConfigureServiceClient, XdsUpdateClientConfigureServiceDefinition as _grpc_testing_XdsUpdateClientConfigureServiceDefinition } from './grpc/testing/XdsUpdateClientConfigureService'; +import type { XdsUpdateHealthServiceClient as _grpc_testing_XdsUpdateHealthServiceClient, XdsUpdateHealthServiceDefinition as _grpc_testing_XdsUpdateHealthServiceDefinition } from './grpc/testing/XdsUpdateHealthService'; type SubtypeConstructor any, Subtype> = { new(...args: ConstructorParameters): Subtype; @@ -28,7 +28,7 @@ export interface ProtoGrpcType { /** * A service used to obtain stats for verifying LB behavior. */ - LoadBalancerStatsService: SubtypeConstructor & { service: ServiceDefinition } + LoadBalancerStatsService: SubtypeConstructor & { service: _grpc_testing_LoadBalancerStatsServiceDefinition } Payload: MessageTypeDefinition PayloadType: EnumTypeDefinition ReconnectInfo: MessageTypeDefinition @@ -36,7 +36,7 @@ export interface ProtoGrpcType { /** * A service used to control reconnect server. */ - ReconnectService: SubtypeConstructor & { service: ServiceDefinition } + ReconnectService: SubtypeConstructor & { service: _grpc_testing_ReconnectServiceDefinition } ResponseParameters: MessageTypeDefinition SimpleRequest: MessageTypeDefinition SimpleResponse: MessageTypeDefinition @@ -48,20 +48,20 @@ export interface ProtoGrpcType { * A simple service to test the various types of RPCs and experiment with * performance with various types of payload. */ - TestService: SubtypeConstructor & { service: ServiceDefinition } + TestService: SubtypeConstructor & { service: _grpc_testing_TestServiceDefinition } /** * A simple service NOT implemented at servers so clients can test for * that case. */ - UnimplementedService: SubtypeConstructor & { service: ServiceDefinition } + UnimplementedService: SubtypeConstructor & { service: _grpc_testing_UnimplementedServiceDefinition } /** * A service to dynamically update the configuration of an xDS test client. */ - XdsUpdateClientConfigureService: SubtypeConstructor & { service: ServiceDefinition } + XdsUpdateClientConfigureService: SubtypeConstructor & { service: _grpc_testing_XdsUpdateClientConfigureServiceDefinition } /** * A service to remotely control health status of an xDS test server. */ - XdsUpdateHealthService: SubtypeConstructor & { service: ServiceDefinition } + XdsUpdateHealthService: SubtypeConstructor & { service: _grpc_testing_XdsUpdateHealthServiceDefinition } } } } diff --git a/packages/grpc-js-xds/interop/xds-interop-client.ts b/packages/grpc-js-xds/interop/xds-interop-client.ts index 1fdaa3a69..a245ad09f 100644 --- a/packages/grpc-js-xds/interop/xds-interop-client.ts +++ b/packages/grpc-js-xds/interop/xds-interop-client.ts @@ -398,7 +398,7 @@ function makeSingleRequest(client: TestServiceClient, type: CallType, failOnFail const startTime = process.hrtime.bigint(); const deadline = new Date(); deadline.setSeconds(deadline.getSeconds() + currentConfig.timeoutSec); - const callback = (error: grpc.ServiceError | undefined, value: Empty__Output | undefined) => { + const callback = (error: grpc.ServiceError | null, value: Empty__Output | undefined) => { const statusCode = error?.code ?? grpc.status.OK; const duration = process.hrtime.bigint() - startTime; const durationSeconds = Number(duration / TIMESTAMP_ONE_SECOND) | 0; diff --git a/packages/grpc-js-xds/package.json b/packages/grpc-js-xds/package.json index 1286a16a8..9e7d2f18a 100644 --- a/packages/grpc-js-xds/package.json +++ b/packages/grpc-js-xds/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js-xds", - "version": "1.10.0", + "version": "1.10.1", "description": "Plugin for @grpc/grpc-js. Adds the xds:// URL scheme and associated features.", "main": "build/src/index.js", "scripts": { @@ -12,7 +12,7 @@ "prepare": "npm run generate-types && npm run compile", "pretest": "npm run compile", "posttest": "npm run check", - "generate-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --includeDirs deps/envoy-api/ deps/xds/ deps/googleapis/ deps/protoc-gen-validate/ -O src/generated/ --grpcLib @grpc/grpc-js envoy/service/discovery/v3/ads.proto envoy/service/load_stats/v3/lrs.proto envoy/config/listener/v3/listener.proto envoy/config/route/v3/route.proto envoy/config/cluster/v3/cluster.proto envoy/config/endpoint/v3/endpoint.proto envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto udpa/type/v1/typed_struct.proto xds/type/v3/typed_struct.proto envoy/extensions/filters/http/fault/v3/fault.proto envoy/service/status/v3/csds.proto envoy/extensions/load_balancing_policies/wrr_locality/v3/wrr_locality.proto envoy/extensions/load_balancing_policies/ring_hash/v3/ring_hash.proto envoy/extensions/load_balancing_policies/pick_first/v3/pick_first.proto", + "generate-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --includeDirs deps/envoy-api/ deps/xds/ deps/googleapis/ deps/protoc-gen-validate/ -O src/generated/ --grpcLib @grpc/grpc-js envoy/service/discovery/v3/ads.proto envoy/service/load_stats/v3/lrs.proto envoy/config/listener/v3/listener.proto envoy/config/route/v3/route.proto envoy/config/cluster/v3/cluster.proto envoy/config/endpoint/v3/endpoint.proto envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto udpa/type/v1/typed_struct.proto xds/type/v3/typed_struct.proto envoy/extensions/filters/http/fault/v3/fault.proto envoy/service/status/v3/csds.proto envoy/extensions/load_balancing_policies/wrr_locality/v3/wrr_locality.proto envoy/extensions/load_balancing_policies/ring_hash/v3/ring_hash.proto envoy/extensions/load_balancing_policies/pick_first/v3/pick_first.proto envoy/extensions/clusters/aggregate/v3/cluster.proto", "generate-interop-types": "proto-loader-gen-types --keep-case --longs String --enums String --defaults --oneofs --json --includeComments --includeDirs proto/ -O interop/generated --grpcLib @grpc/grpc-js grpc/testing/test.proto", "generate-test-types": "proto-loader-gen-types --keep-case --longs String --enums String --defaults --oneofs --json --includeComments --includeDirs proto/ -O test/generated --grpcLib @grpc/grpc-js grpc/testing/echo.proto" }, @@ -43,7 +43,7 @@ "yargs": "^15.4.1" }, "dependencies": { - "@grpc/proto-loader": "^0.6.0", + "@grpc/proto-loader": "^0.7.13", "google-auth-library": "^7.0.2", "re2-wasm": "^1.0.1", "vscode-uri": "^3.0.7", diff --git a/packages/grpc-js-xds/src/generated/cluster.ts b/packages/grpc-js-xds/src/generated/cluster.ts index 1aa37589b..6e8c5f985 100644 --- a/packages/grpc-js-xds/src/generated/cluster.ts +++ b/packages/grpc-js-xds/src/generated/cluster.ts @@ -101,6 +101,15 @@ export interface ProtoGrpcType { } } } + extensions: { + clusters: { + aggregate: { + v3: { + ClusterConfig: MessageTypeDefinition + } + } + } + } type: { matcher: { v3: { diff --git a/packages/grpc-js-xds/src/generated/envoy/admin/v3/BootstrapConfigDump.ts b/packages/grpc-js-xds/src/generated/envoy/admin/v3/BootstrapConfigDump.ts deleted file mode 100644 index d47f00ef3..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/admin/v3/BootstrapConfigDump.ts +++ /dev/null @@ -1,32 +0,0 @@ -// Original file: deps/envoy-api/envoy/admin/v3/config_dump.proto - -import type { Bootstrap as _envoy_config_bootstrap_v3_Bootstrap, Bootstrap__Output as _envoy_config_bootstrap_v3_Bootstrap__Output } from '../../../envoy/config/bootstrap/v3/Bootstrap'; -import type { Timestamp as _google_protobuf_Timestamp, Timestamp__Output as _google_protobuf_Timestamp__Output } from '../../../google/protobuf/Timestamp'; - -/** - * This message describes the bootstrap configuration that Envoy was started with. This includes - * any CLI overrides that were merged. Bootstrap configuration information can be used to recreate - * the static portions of an Envoy configuration by reusing the output as the bootstrap - * configuration for another Envoy. - */ -export interface BootstrapConfigDump { - 'bootstrap'?: (_envoy_config_bootstrap_v3_Bootstrap | null); - /** - * The timestamp when the BootstrapConfig was last updated. - */ - 'last_updated'?: (_google_protobuf_Timestamp | null); -} - -/** - * This message describes the bootstrap configuration that Envoy was started with. This includes - * any CLI overrides that were merged. Bootstrap configuration information can be used to recreate - * the static portions of an Envoy configuration by reusing the output as the bootstrap - * configuration for another Envoy. - */ -export interface BootstrapConfigDump__Output { - 'bootstrap': (_envoy_config_bootstrap_v3_Bootstrap__Output | null); - /** - * The timestamp when the BootstrapConfig was last updated. - */ - 'last_updated': (_google_protobuf_Timestamp__Output | null); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/admin/v3/ClientResourceStatus.ts b/packages/grpc-js-xds/src/generated/envoy/admin/v3/ClientResourceStatus.ts index 8488bbdd7..b7a78a338 100644 --- a/packages/grpc-js-xds/src/generated/envoy/admin/v3/ClientResourceStatus.ts +++ b/packages/grpc-js-xds/src/generated/envoy/admin/v3/ClientResourceStatus.ts @@ -4,17 +4,17 @@ * Resource status from the view of a xDS client, which tells the synchronization * status between the xDS client and the xDS server. */ -export enum ClientResourceStatus { +export const ClientResourceStatus = { /** * Resource status is not available/unknown. */ - UNKNOWN = 0, + UNKNOWN: 'UNKNOWN', /** * Client requested this resource but hasn't received any update from management * server. The client will not fail requests, but will queue them until update * arrives or the client times out waiting for the resource. */ - REQUESTED = 1, + REQUESTED: 'REQUESTED', /** * This resource has been requested by the client but has either not been * delivered by the server or was previously delivered by the server and then @@ -22,13 +22,56 @@ export enum ClientResourceStatus { * information, please refer to the :ref:`"Knowing When a Requested Resource * Does Not Exist" ` section. */ - DOES_NOT_EXIST = 2, + DOES_NOT_EXIST: 'DOES_NOT_EXIST', /** * Client received this resource and replied with ACK. */ - ACKED = 3, + ACKED: 'ACKED', /** * Client received this resource and replied with NACK. */ - NACKED = 4, -} + NACKED: 'NACKED', +} as const; + +/** + * Resource status from the view of a xDS client, which tells the synchronization + * status between the xDS client and the xDS server. + */ +export type ClientResourceStatus = + /** + * Resource status is not available/unknown. + */ + | 'UNKNOWN' + | 0 + /** + * Client requested this resource but hasn't received any update from management + * server. The client will not fail requests, but will queue them until update + * arrives or the client times out waiting for the resource. + */ + | 'REQUESTED' + | 1 + /** + * This resource has been requested by the client but has either not been + * delivered by the server or was previously delivered by the server and then + * subsequently removed from resources provided by the server. For more + * information, please refer to the :ref:`"Knowing When a Requested Resource + * Does Not Exist" ` section. + */ + | 'DOES_NOT_EXIST' + | 2 + /** + * Client received this resource and replied with ACK. + */ + | 'ACKED' + | 3 + /** + * Client received this resource and replied with NACK. + */ + | 'NACKED' + | 4 + +/** + * Resource status from the view of a xDS client, which tells the synchronization + * status between the xDS client and the xDS server. + */ +export type ClientResourceStatus__Output = typeof ClientResourceStatus[keyof typeof ClientResourceStatus] diff --git a/packages/grpc-js-xds/src/generated/envoy/admin/v3/ClustersConfigDump.ts b/packages/grpc-js-xds/src/generated/envoy/admin/v3/ClustersConfigDump.ts index aabcd212a..2c3b4f8a5 100644 --- a/packages/grpc-js-xds/src/generated/envoy/admin/v3/ClustersConfigDump.ts +++ b/packages/grpc-js-xds/src/generated/envoy/admin/v3/ClustersConfigDump.ts @@ -3,7 +3,7 @@ import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../google/protobuf/Any'; import type { Timestamp as _google_protobuf_Timestamp, Timestamp__Output as _google_protobuf_Timestamp__Output } from '../../../google/protobuf/Timestamp'; import type { UpdateFailureState as _envoy_admin_v3_UpdateFailureState, UpdateFailureState__Output as _envoy_admin_v3_UpdateFailureState__Output } from '../../../envoy/admin/v3/UpdateFailureState'; -import type { ClientResourceStatus as _envoy_admin_v3_ClientResourceStatus } from '../../../envoy/admin/v3/ClientResourceStatus'; +import type { ClientResourceStatus as _envoy_admin_v3_ClientResourceStatus, ClientResourceStatus__Output as _envoy_admin_v3_ClientResourceStatus__Output } from '../../../envoy/admin/v3/ClientResourceStatus'; /** * Describes a dynamically loaded cluster via the CDS API. @@ -37,7 +37,7 @@ export interface _envoy_admin_v3_ClustersConfigDump_DynamicCluster { * The client status of this resource. * [#not-implemented-hide:] */ - 'client_status'?: (_envoy_admin_v3_ClientResourceStatus | keyof typeof _envoy_admin_v3_ClientResourceStatus); + 'client_status'?: (_envoy_admin_v3_ClientResourceStatus); } /** @@ -72,7 +72,7 @@ export interface _envoy_admin_v3_ClustersConfigDump_DynamicCluster__Output { * The client status of this resource. * [#not-implemented-hide:] */ - 'client_status': (keyof typeof _envoy_admin_v3_ClientResourceStatus); + 'client_status': (_envoy_admin_v3_ClientResourceStatus__Output); } /** diff --git a/packages/grpc-js-xds/src/generated/envoy/admin/v3/ConfigDump.ts b/packages/grpc-js-xds/src/generated/envoy/admin/v3/ConfigDump.ts deleted file mode 100644 index 8a0ab65c2..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/admin/v3/ConfigDump.ts +++ /dev/null @@ -1,65 +0,0 @@ -// Original file: deps/envoy-api/envoy/admin/v3/config_dump.proto - -import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../google/protobuf/Any'; - -/** - * The :ref:`/config_dump ` admin endpoint uses this wrapper - * message to maintain and serve arbitrary configuration information from any component in Envoy. - */ -export interface ConfigDump { - /** - * This list is serialized and dumped in its entirety at the - * :ref:`/config_dump ` endpoint. - * - * The following configurations are currently supported and will be dumped in the order given - * below: - * - * * *bootstrap*: :ref:`BootstrapConfigDump ` - * * *clusters*: :ref:`ClustersConfigDump ` - * * *endpoints*: :ref:`EndpointsConfigDump ` - * * *listeners*: :ref:`ListenersConfigDump ` - * * *scoped_routes*: :ref:`ScopedRoutesConfigDump ` - * * *routes*: :ref:`RoutesConfigDump ` - * * *secrets*: :ref:`SecretsConfigDump ` - * - * EDS Configuration will only be dumped by using parameter `?include_eds` - * - * You can filter output with the resource and mask query parameters. - * See :ref:`/config_dump?resource={} `, - * :ref:`/config_dump?mask={} `, - * or :ref:`/config_dump?resource={},mask={} - * ` for more information. - */ - 'configs'?: (_google_protobuf_Any)[]; -} - -/** - * The :ref:`/config_dump ` admin endpoint uses this wrapper - * message to maintain and serve arbitrary configuration information from any component in Envoy. - */ -export interface ConfigDump__Output { - /** - * This list is serialized and dumped in its entirety at the - * :ref:`/config_dump ` endpoint. - * - * The following configurations are currently supported and will be dumped in the order given - * below: - * - * * *bootstrap*: :ref:`BootstrapConfigDump ` - * * *clusters*: :ref:`ClustersConfigDump ` - * * *endpoints*: :ref:`EndpointsConfigDump ` - * * *listeners*: :ref:`ListenersConfigDump ` - * * *scoped_routes*: :ref:`ScopedRoutesConfigDump ` - * * *routes*: :ref:`RoutesConfigDump ` - * * *secrets*: :ref:`SecretsConfigDump ` - * - * EDS Configuration will only be dumped by using parameter `?include_eds` - * - * You can filter output with the resource and mask query parameters. - * See :ref:`/config_dump?resource={} `, - * :ref:`/config_dump?mask={} `, - * or :ref:`/config_dump?resource={},mask={} - * ` for more information. - */ - 'configs': (_google_protobuf_Any__Output)[]; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/admin/v3/EcdsConfigDump.ts b/packages/grpc-js-xds/src/generated/envoy/admin/v3/EcdsConfigDump.ts index e63307cb5..70562962b 100644 --- a/packages/grpc-js-xds/src/generated/envoy/admin/v3/EcdsConfigDump.ts +++ b/packages/grpc-js-xds/src/generated/envoy/admin/v3/EcdsConfigDump.ts @@ -3,7 +3,7 @@ import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../google/protobuf/Any'; import type { Timestamp as _google_protobuf_Timestamp, Timestamp__Output as _google_protobuf_Timestamp__Output } from '../../../google/protobuf/Timestamp'; import type { UpdateFailureState as _envoy_admin_v3_UpdateFailureState, UpdateFailureState__Output as _envoy_admin_v3_UpdateFailureState__Output } from '../../../envoy/admin/v3/UpdateFailureState'; -import type { ClientResourceStatus as _envoy_admin_v3_ClientResourceStatus } from '../../../envoy/admin/v3/ClientResourceStatus'; +import type { ClientResourceStatus as _envoy_admin_v3_ClientResourceStatus, ClientResourceStatus__Output as _envoy_admin_v3_ClientResourceStatus__Output } from '../../../envoy/admin/v3/ClientResourceStatus'; /** * [#next-free-field: 6] @@ -36,7 +36,7 @@ export interface _envoy_admin_v3_EcdsConfigDump_EcdsFilterConfig { * The client status of this resource. * [#not-implemented-hide:] */ - 'client_status'?: (_envoy_admin_v3_ClientResourceStatus | keyof typeof _envoy_admin_v3_ClientResourceStatus); + 'client_status'?: (_envoy_admin_v3_ClientResourceStatus); } /** @@ -70,7 +70,7 @@ export interface _envoy_admin_v3_EcdsConfigDump_EcdsFilterConfig__Output { * The client status of this resource. * [#not-implemented-hide:] */ - 'client_status': (keyof typeof _envoy_admin_v3_ClientResourceStatus); + 'client_status': (_envoy_admin_v3_ClientResourceStatus__Output); } /** diff --git a/packages/grpc-js-xds/src/generated/envoy/admin/v3/EndpointsConfigDump.ts b/packages/grpc-js-xds/src/generated/envoy/admin/v3/EndpointsConfigDump.ts index ab5485dbe..3f362c5c3 100644 --- a/packages/grpc-js-xds/src/generated/envoy/admin/v3/EndpointsConfigDump.ts +++ b/packages/grpc-js-xds/src/generated/envoy/admin/v3/EndpointsConfigDump.ts @@ -3,7 +3,7 @@ import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../google/protobuf/Any'; import type { Timestamp as _google_protobuf_Timestamp, Timestamp__Output as _google_protobuf_Timestamp__Output } from '../../../google/protobuf/Timestamp'; import type { UpdateFailureState as _envoy_admin_v3_UpdateFailureState, UpdateFailureState__Output as _envoy_admin_v3_UpdateFailureState__Output } from '../../../envoy/admin/v3/UpdateFailureState'; -import type { ClientResourceStatus as _envoy_admin_v3_ClientResourceStatus } from '../../../envoy/admin/v3/ClientResourceStatus'; +import type { ClientResourceStatus as _envoy_admin_v3_ClientResourceStatus, ClientResourceStatus__Output as _envoy_admin_v3_ClientResourceStatus__Output } from '../../../envoy/admin/v3/ClientResourceStatus'; /** * [#next-free-field: 6] @@ -35,7 +35,7 @@ export interface _envoy_admin_v3_EndpointsConfigDump_DynamicEndpointConfig { * The client status of this resource. * [#not-implemented-hide:] */ - 'client_status'?: (_envoy_admin_v3_ClientResourceStatus | keyof typeof _envoy_admin_v3_ClientResourceStatus); + 'client_status'?: (_envoy_admin_v3_ClientResourceStatus); } /** @@ -68,7 +68,7 @@ export interface _envoy_admin_v3_EndpointsConfigDump_DynamicEndpointConfig__Outp * The client status of this resource. * [#not-implemented-hide:] */ - 'client_status': (keyof typeof _envoy_admin_v3_ClientResourceStatus); + 'client_status': (_envoy_admin_v3_ClientResourceStatus__Output); } export interface _envoy_admin_v3_EndpointsConfigDump_StaticEndpointConfig { diff --git a/packages/grpc-js-xds/src/generated/envoy/admin/v3/ListenersConfigDump.ts b/packages/grpc-js-xds/src/generated/envoy/admin/v3/ListenersConfigDump.ts index 946e37953..a90338fdf 100644 --- a/packages/grpc-js-xds/src/generated/envoy/admin/v3/ListenersConfigDump.ts +++ b/packages/grpc-js-xds/src/generated/envoy/admin/v3/ListenersConfigDump.ts @@ -3,7 +3,7 @@ import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../google/protobuf/Any'; import type { Timestamp as _google_protobuf_Timestamp, Timestamp__Output as _google_protobuf_Timestamp__Output } from '../../../google/protobuf/Timestamp'; import type { UpdateFailureState as _envoy_admin_v3_UpdateFailureState, UpdateFailureState__Output as _envoy_admin_v3_UpdateFailureState__Output } from '../../../envoy/admin/v3/UpdateFailureState'; -import type { ClientResourceStatus as _envoy_admin_v3_ClientResourceStatus } from '../../../envoy/admin/v3/ClientResourceStatus'; +import type { ClientResourceStatus as _envoy_admin_v3_ClientResourceStatus, ClientResourceStatus__Output as _envoy_admin_v3_ClientResourceStatus__Output } from '../../../envoy/admin/v3/ClientResourceStatus'; /** * Describes a dynamically loaded listener via the LDS API. @@ -44,7 +44,7 @@ export interface _envoy_admin_v3_ListenersConfigDump_DynamicListener { * The client status of this resource. * [#not-implemented-hide:] */ - 'client_status'?: (_envoy_admin_v3_ClientResourceStatus | keyof typeof _envoy_admin_v3_ClientResourceStatus); + 'client_status'?: (_envoy_admin_v3_ClientResourceStatus); } /** @@ -86,7 +86,7 @@ export interface _envoy_admin_v3_ListenersConfigDump_DynamicListener__Output { * The client status of this resource. * [#not-implemented-hide:] */ - 'client_status': (keyof typeof _envoy_admin_v3_ClientResourceStatus); + 'client_status': (_envoy_admin_v3_ClientResourceStatus__Output); } export interface _envoy_admin_v3_ListenersConfigDump_DynamicListenerState { diff --git a/packages/grpc-js-xds/src/generated/envoy/admin/v3/RoutesConfigDump.ts b/packages/grpc-js-xds/src/generated/envoy/admin/v3/RoutesConfigDump.ts index 7b9bb29d0..6de43f0eb 100644 --- a/packages/grpc-js-xds/src/generated/envoy/admin/v3/RoutesConfigDump.ts +++ b/packages/grpc-js-xds/src/generated/envoy/admin/v3/RoutesConfigDump.ts @@ -3,7 +3,7 @@ import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../google/protobuf/Any'; import type { Timestamp as _google_protobuf_Timestamp, Timestamp__Output as _google_protobuf_Timestamp__Output } from '../../../google/protobuf/Timestamp'; import type { UpdateFailureState as _envoy_admin_v3_UpdateFailureState, UpdateFailureState__Output as _envoy_admin_v3_UpdateFailureState__Output } from '../../../envoy/admin/v3/UpdateFailureState'; -import type { ClientResourceStatus as _envoy_admin_v3_ClientResourceStatus } from '../../../envoy/admin/v3/ClientResourceStatus'; +import type { ClientResourceStatus as _envoy_admin_v3_ClientResourceStatus, ClientResourceStatus__Output as _envoy_admin_v3_ClientResourceStatus__Output } from '../../../envoy/admin/v3/ClientResourceStatus'; /** * [#next-free-field: 6] @@ -35,7 +35,7 @@ export interface _envoy_admin_v3_RoutesConfigDump_DynamicRouteConfig { * The client status of this resource. * [#not-implemented-hide:] */ - 'client_status'?: (_envoy_admin_v3_ClientResourceStatus | keyof typeof _envoy_admin_v3_ClientResourceStatus); + 'client_status'?: (_envoy_admin_v3_ClientResourceStatus); } /** @@ -68,7 +68,7 @@ export interface _envoy_admin_v3_RoutesConfigDump_DynamicRouteConfig__Output { * The client status of this resource. * [#not-implemented-hide:] */ - 'client_status': (keyof typeof _envoy_admin_v3_ClientResourceStatus); + 'client_status': (_envoy_admin_v3_ClientResourceStatus__Output); } export interface _envoy_admin_v3_RoutesConfigDump_StaticRouteConfig { diff --git a/packages/grpc-js-xds/src/generated/envoy/admin/v3/ScopedRoutesConfigDump.ts b/packages/grpc-js-xds/src/generated/envoy/admin/v3/ScopedRoutesConfigDump.ts index c0723ce69..1ce3934cc 100644 --- a/packages/grpc-js-xds/src/generated/envoy/admin/v3/ScopedRoutesConfigDump.ts +++ b/packages/grpc-js-xds/src/generated/envoy/admin/v3/ScopedRoutesConfigDump.ts @@ -3,7 +3,7 @@ import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../google/protobuf/Any'; import type { Timestamp as _google_protobuf_Timestamp, Timestamp__Output as _google_protobuf_Timestamp__Output } from '../../../google/protobuf/Timestamp'; import type { UpdateFailureState as _envoy_admin_v3_UpdateFailureState, UpdateFailureState__Output as _envoy_admin_v3_UpdateFailureState__Output } from '../../../envoy/admin/v3/UpdateFailureState'; -import type { ClientResourceStatus as _envoy_admin_v3_ClientResourceStatus } from '../../../envoy/admin/v3/ClientResourceStatus'; +import type { ClientResourceStatus as _envoy_admin_v3_ClientResourceStatus, ClientResourceStatus__Output as _envoy_admin_v3_ClientResourceStatus__Output } from '../../../envoy/admin/v3/ClientResourceStatus'; /** * [#next-free-field: 7] @@ -39,7 +39,7 @@ export interface _envoy_admin_v3_ScopedRoutesConfigDump_DynamicScopedRouteConfig * The client status of this resource. * [#not-implemented-hide:] */ - 'client_status'?: (_envoy_admin_v3_ClientResourceStatus | keyof typeof _envoy_admin_v3_ClientResourceStatus); + 'client_status'?: (_envoy_admin_v3_ClientResourceStatus); } /** @@ -76,7 +76,7 @@ export interface _envoy_admin_v3_ScopedRoutesConfigDump_DynamicScopedRouteConfig * The client status of this resource. * [#not-implemented-hide:] */ - 'client_status': (keyof typeof _envoy_admin_v3_ClientResourceStatus); + 'client_status': (_envoy_admin_v3_ClientResourceStatus__Output); } export interface _envoy_admin_v3_ScopedRoutesConfigDump_InlineScopedRouteConfigs { diff --git a/packages/grpc-js-xds/src/generated/envoy/admin/v3/SecretsConfigDump.ts b/packages/grpc-js-xds/src/generated/envoy/admin/v3/SecretsConfigDump.ts deleted file mode 100644 index 21921edec..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/admin/v3/SecretsConfigDump.ts +++ /dev/null @@ -1,162 +0,0 @@ -// Original file: deps/envoy-api/envoy/admin/v3/config_dump.proto - -import type { Timestamp as _google_protobuf_Timestamp, Timestamp__Output as _google_protobuf_Timestamp__Output } from '../../../google/protobuf/Timestamp'; -import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../google/protobuf/Any'; -import type { UpdateFailureState as _envoy_admin_v3_UpdateFailureState, UpdateFailureState__Output as _envoy_admin_v3_UpdateFailureState__Output } from '../../../envoy/admin/v3/UpdateFailureState'; -import type { ClientResourceStatus as _envoy_admin_v3_ClientResourceStatus } from '../../../envoy/admin/v3/ClientResourceStatus'; - -/** - * DynamicSecret contains secret information fetched via SDS. - * [#next-free-field: 7] - */ -export interface _envoy_admin_v3_SecretsConfigDump_DynamicSecret { - /** - * The name assigned to the secret. - */ - 'name'?: (string); - /** - * This is the per-resource version information. - */ - 'version_info'?: (string); - /** - * The timestamp when the secret was last updated. - */ - 'last_updated'?: (_google_protobuf_Timestamp | null); - /** - * The actual secret information. - * Security sensitive information is redacted (replaced with "[redacted]") for - * private keys and passwords in TLS certificates. - */ - 'secret'?: (_google_protobuf_Any | null); - /** - * Set if the last update failed, cleared after the next successful update. - * The *error_state* field contains the rejected version of this particular - * resource along with the reason and timestamp. For successfully updated or - * acknowledged resource, this field should be empty. - * [#not-implemented-hide:] - */ - 'error_state'?: (_envoy_admin_v3_UpdateFailureState | null); - /** - * The client status of this resource. - * [#not-implemented-hide:] - */ - 'client_status'?: (_envoy_admin_v3_ClientResourceStatus | keyof typeof _envoy_admin_v3_ClientResourceStatus); -} - -/** - * DynamicSecret contains secret information fetched via SDS. - * [#next-free-field: 7] - */ -export interface _envoy_admin_v3_SecretsConfigDump_DynamicSecret__Output { - /** - * The name assigned to the secret. - */ - 'name': (string); - /** - * This is the per-resource version information. - */ - 'version_info': (string); - /** - * The timestamp when the secret was last updated. - */ - 'last_updated': (_google_protobuf_Timestamp__Output | null); - /** - * The actual secret information. - * Security sensitive information is redacted (replaced with "[redacted]") for - * private keys and passwords in TLS certificates. - */ - 'secret': (_google_protobuf_Any__Output | null); - /** - * Set if the last update failed, cleared after the next successful update. - * The *error_state* field contains the rejected version of this particular - * resource along with the reason and timestamp. For successfully updated or - * acknowledged resource, this field should be empty. - * [#not-implemented-hide:] - */ - 'error_state': (_envoy_admin_v3_UpdateFailureState__Output | null); - /** - * The client status of this resource. - * [#not-implemented-hide:] - */ - 'client_status': (keyof typeof _envoy_admin_v3_ClientResourceStatus); -} - -/** - * StaticSecret specifies statically loaded secret in bootstrap. - */ -export interface _envoy_admin_v3_SecretsConfigDump_StaticSecret { - /** - * The name assigned to the secret. - */ - 'name'?: (string); - /** - * The timestamp when the secret was last updated. - */ - 'last_updated'?: (_google_protobuf_Timestamp | null); - /** - * The actual secret information. - * Security sensitive information is redacted (replaced with "[redacted]") for - * private keys and passwords in TLS certificates. - */ - 'secret'?: (_google_protobuf_Any | null); -} - -/** - * StaticSecret specifies statically loaded secret in bootstrap. - */ -export interface _envoy_admin_v3_SecretsConfigDump_StaticSecret__Output { - /** - * The name assigned to the secret. - */ - 'name': (string); - /** - * The timestamp when the secret was last updated. - */ - 'last_updated': (_google_protobuf_Timestamp__Output | null); - /** - * The actual secret information. - * Security sensitive information is redacted (replaced with "[redacted]") for - * private keys and passwords in TLS certificates. - */ - 'secret': (_google_protobuf_Any__Output | null); -} - -/** - * Envoys SDS implementation fills this message with all secrets fetched dynamically via SDS. - */ -export interface SecretsConfigDump { - /** - * The statically loaded secrets. - */ - 'static_secrets'?: (_envoy_admin_v3_SecretsConfigDump_StaticSecret)[]; - /** - * The dynamically loaded active secrets. These are secrets that are available to service - * clusters or listeners. - */ - 'dynamic_active_secrets'?: (_envoy_admin_v3_SecretsConfigDump_DynamicSecret)[]; - /** - * The dynamically loaded warming secrets. These are secrets that are currently undergoing - * warming in preparation to service clusters or listeners. - */ - 'dynamic_warming_secrets'?: (_envoy_admin_v3_SecretsConfigDump_DynamicSecret)[]; -} - -/** - * Envoys SDS implementation fills this message with all secrets fetched dynamically via SDS. - */ -export interface SecretsConfigDump__Output { - /** - * The statically loaded secrets. - */ - 'static_secrets': (_envoy_admin_v3_SecretsConfigDump_StaticSecret__Output)[]; - /** - * The dynamically loaded active secrets. These are secrets that are available to service - * clusters or listeners. - */ - 'dynamic_active_secrets': (_envoy_admin_v3_SecretsConfigDump_DynamicSecret__Output)[]; - /** - * The dynamically loaded warming secrets. These are secrets that are currently undergoing - * warming in preparation to service clusters or listeners. - */ - 'dynamic_warming_secrets': (_envoy_admin_v3_SecretsConfigDump_DynamicSecret__Output)[]; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/ComparisonFilter.ts b/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/ComparisonFilter.ts index 07893f2dd..68c8f1b65 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/ComparisonFilter.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/ComparisonFilter.ts @@ -4,20 +4,39 @@ import type { RuntimeUInt32 as _envoy_config_core_v3_RuntimeUInt32, RuntimeUInt3 // Original file: deps/envoy-api/envoy/config/accesslog/v3/accesslog.proto -export enum _envoy_config_accesslog_v3_ComparisonFilter_Op { +export const _envoy_config_accesslog_v3_ComparisonFilter_Op = { /** * = */ - EQ = 0, + EQ: 'EQ', /** * >= */ - GE = 1, + GE: 'GE', /** * <= */ - LE = 2, -} + LE: 'LE', +} as const; + +export type _envoy_config_accesslog_v3_ComparisonFilter_Op = + /** + * = + */ + | 'EQ' + | 0 + /** + * >= + */ + | 'GE' + | 1 + /** + * <= + */ + | 'LE' + | 2 + +export type _envoy_config_accesslog_v3_ComparisonFilter_Op__Output = typeof _envoy_config_accesslog_v3_ComparisonFilter_Op[keyof typeof _envoy_config_accesslog_v3_ComparisonFilter_Op] /** * Filter on an integer comparison. @@ -26,7 +45,7 @@ export interface ComparisonFilter { /** * Comparison operator. */ - 'op'?: (_envoy_config_accesslog_v3_ComparisonFilter_Op | keyof typeof _envoy_config_accesslog_v3_ComparisonFilter_Op); + 'op'?: (_envoy_config_accesslog_v3_ComparisonFilter_Op); /** * Value to compare against. */ @@ -40,7 +59,7 @@ export interface ComparisonFilter__Output { /** * Comparison operator. */ - 'op': (keyof typeof _envoy_config_accesslog_v3_ComparisonFilter_Op); + 'op': (_envoy_config_accesslog_v3_ComparisonFilter_Op__Output); /** * Value to compare against. */ diff --git a/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/GrpcStatusFilter.ts b/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/GrpcStatusFilter.ts index f7ccc8052..ec18bde8a 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/GrpcStatusFilter.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/GrpcStatusFilter.ts @@ -3,25 +3,63 @@ // Original file: deps/envoy-api/envoy/config/accesslog/v3/accesslog.proto -export enum _envoy_config_accesslog_v3_GrpcStatusFilter_Status { - OK = 0, - CANCELED = 1, - UNKNOWN = 2, - INVALID_ARGUMENT = 3, - DEADLINE_EXCEEDED = 4, - NOT_FOUND = 5, - ALREADY_EXISTS = 6, - PERMISSION_DENIED = 7, - RESOURCE_EXHAUSTED = 8, - FAILED_PRECONDITION = 9, - ABORTED = 10, - OUT_OF_RANGE = 11, - UNIMPLEMENTED = 12, - INTERNAL = 13, - UNAVAILABLE = 14, - DATA_LOSS = 15, - UNAUTHENTICATED = 16, -} +export const _envoy_config_accesslog_v3_GrpcStatusFilter_Status = { + OK: 'OK', + CANCELED: 'CANCELED', + UNKNOWN: 'UNKNOWN', + INVALID_ARGUMENT: 'INVALID_ARGUMENT', + DEADLINE_EXCEEDED: 'DEADLINE_EXCEEDED', + NOT_FOUND: 'NOT_FOUND', + ALREADY_EXISTS: 'ALREADY_EXISTS', + PERMISSION_DENIED: 'PERMISSION_DENIED', + RESOURCE_EXHAUSTED: 'RESOURCE_EXHAUSTED', + FAILED_PRECONDITION: 'FAILED_PRECONDITION', + ABORTED: 'ABORTED', + OUT_OF_RANGE: 'OUT_OF_RANGE', + UNIMPLEMENTED: 'UNIMPLEMENTED', + INTERNAL: 'INTERNAL', + UNAVAILABLE: 'UNAVAILABLE', + DATA_LOSS: 'DATA_LOSS', + UNAUTHENTICATED: 'UNAUTHENTICATED', +} as const; + +export type _envoy_config_accesslog_v3_GrpcStatusFilter_Status = + | 'OK' + | 0 + | 'CANCELED' + | 1 + | 'UNKNOWN' + | 2 + | 'INVALID_ARGUMENT' + | 3 + | 'DEADLINE_EXCEEDED' + | 4 + | 'NOT_FOUND' + | 5 + | 'ALREADY_EXISTS' + | 6 + | 'PERMISSION_DENIED' + | 7 + | 'RESOURCE_EXHAUSTED' + | 8 + | 'FAILED_PRECONDITION' + | 9 + | 'ABORTED' + | 10 + | 'OUT_OF_RANGE' + | 11 + | 'UNIMPLEMENTED' + | 12 + | 'INTERNAL' + | 13 + | 'UNAVAILABLE' + | 14 + | 'DATA_LOSS' + | 15 + | 'UNAUTHENTICATED' + | 16 + +export type _envoy_config_accesslog_v3_GrpcStatusFilter_Status__Output = typeof _envoy_config_accesslog_v3_GrpcStatusFilter_Status[keyof typeof _envoy_config_accesslog_v3_GrpcStatusFilter_Status] /** * Filters gRPC requests based on their response status. If a gRPC status is not @@ -31,7 +69,7 @@ export interface GrpcStatusFilter { /** * Logs only responses that have any one of the gRPC statuses in this field. */ - 'statuses'?: (_envoy_config_accesslog_v3_GrpcStatusFilter_Status | keyof typeof _envoy_config_accesslog_v3_GrpcStatusFilter_Status)[]; + 'statuses'?: (_envoy_config_accesslog_v3_GrpcStatusFilter_Status)[]; /** * If included and set to true, the filter will instead block all responses * with a gRPC status or inferred gRPC status enumerated in statuses, and @@ -48,7 +86,7 @@ export interface GrpcStatusFilter__Output { /** * Logs only responses that have any one of the gRPC statuses in this field. */ - 'statuses': (keyof typeof _envoy_config_accesslog_v3_GrpcStatusFilter_Status)[]; + 'statuses': (_envoy_config_accesslog_v3_GrpcStatusFilter_Status__Output)[]; /** * If included and set to true, the filter will instead block all responses * with a gRPC status or inferred gRPC status enumerated in statuses, and diff --git a/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/LogTypeFilter.ts b/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/LogTypeFilter.ts index 8d51cd33f..59ad8e308 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/LogTypeFilter.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/accesslog/v3/LogTypeFilter.ts @@ -1,6 +1,6 @@ // Original file: deps/envoy-api/envoy/config/accesslog/v3/accesslog.proto -import type { AccessLogType as _envoy_data_accesslog_v3_AccessLogType } from '../../../../envoy/data/accesslog/v3/AccessLogType'; +import type { AccessLogType as _envoy_data_accesslog_v3_AccessLogType, AccessLogType__Output as _envoy_data_accesslog_v3_AccessLogType__Output } from '../../../../envoy/data/accesslog/v3/AccessLogType'; /** * Filters based on access log type. @@ -9,7 +9,7 @@ export interface LogTypeFilter { /** * Logs only records which their type is one of the types defined in this field. */ - 'types'?: (_envoy_data_accesslog_v3_AccessLogType | keyof typeof _envoy_data_accesslog_v3_AccessLogType)[]; + 'types'?: (_envoy_data_accesslog_v3_AccessLogType)[]; /** * If this field is set to true, the filter will instead block all records * with a access log type in types field, and allow all other records. @@ -24,7 +24,7 @@ export interface LogTypeFilter__Output { /** * Logs only records which their type is one of the types defined in this field. */ - 'types': (keyof typeof _envoy_data_accesslog_v3_AccessLogType)[]; + 'types': (_envoy_data_accesslog_v3_AccessLogType__Output)[]; /** * If this field is set to true, the filter will instead block all records * with a access log type in types field, and allow all other records. diff --git a/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/Admin.ts b/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/Admin.ts deleted file mode 100644 index a7f3826a1..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/Admin.ts +++ /dev/null @@ -1,75 +0,0 @@ -// Original file: deps/envoy-api/envoy/config/bootstrap/v3/bootstrap.proto - -import type { Address as _envoy_config_core_v3_Address, Address__Output as _envoy_config_core_v3_Address__Output } from '../../../../envoy/config/core/v3/Address'; -import type { SocketOption as _envoy_config_core_v3_SocketOption, SocketOption__Output as _envoy_config_core_v3_SocketOption__Output } from '../../../../envoy/config/core/v3/SocketOption'; -import type { AccessLog as _envoy_config_accesslog_v3_AccessLog, AccessLog__Output as _envoy_config_accesslog_v3_AccessLog__Output } from '../../../../envoy/config/accesslog/v3/AccessLog'; - -/** - * Administration interface :ref:`operations documentation - * `. - * [#next-free-field: 6] - */ -export interface Admin { - /** - * The path to write the access log for the administration server. If no - * access log is desired specify ‘/dev/null’. This is only required if - * :ref:`address ` is set. - * Deprecated in favor of *access_log* which offers more options. - */ - 'access_log_path'?: (string); - /** - * The cpu profiler output path for the administration server. If no profile - * path is specified, the default is ‘/var/log/envoy/envoy.prof’. - */ - 'profile_path'?: (string); - /** - * The TCP address that the administration server will listen on. - * If not specified, Envoy will not start an administration server. - */ - 'address'?: (_envoy_config_core_v3_Address | null); - /** - * Additional socket options that may not be present in Envoy source code or - * precompiled binaries. - */ - 'socket_options'?: (_envoy_config_core_v3_SocketOption)[]; - /** - * Configuration for :ref:`access logs ` - * emitted by the administration server. - */ - 'access_log'?: (_envoy_config_accesslog_v3_AccessLog)[]; -} - -/** - * Administration interface :ref:`operations documentation - * `. - * [#next-free-field: 6] - */ -export interface Admin__Output { - /** - * The path to write the access log for the administration server. If no - * access log is desired specify ‘/dev/null’. This is only required if - * :ref:`address ` is set. - * Deprecated in favor of *access_log* which offers more options. - */ - 'access_log_path': (string); - /** - * The cpu profiler output path for the administration server. If no profile - * path is specified, the default is ‘/var/log/envoy/envoy.prof’. - */ - 'profile_path': (string); - /** - * The TCP address that the administration server will listen on. - * If not specified, Envoy will not start an administration server. - */ - 'address': (_envoy_config_core_v3_Address__Output | null); - /** - * Additional socket options that may not be present in Envoy source code or - * precompiled binaries. - */ - 'socket_options': (_envoy_config_core_v3_SocketOption__Output)[]; - /** - * Configuration for :ref:`access logs ` - * emitted by the administration server. - */ - 'access_log': (_envoy_config_accesslog_v3_AccessLog__Output)[]; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/Bootstrap.ts b/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/Bootstrap.ts deleted file mode 100644 index 797148675..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/Bootstrap.ts +++ /dev/null @@ -1,642 +0,0 @@ -// Original file: deps/envoy-api/envoy/config/bootstrap/v3/bootstrap.proto - -import type { Node as _envoy_config_core_v3_Node, Node__Output as _envoy_config_core_v3_Node__Output } from '../../../../envoy/config/core/v3/Node'; -import type { ClusterManager as _envoy_config_bootstrap_v3_ClusterManager, ClusterManager__Output as _envoy_config_bootstrap_v3_ClusterManager__Output } from '../../../../envoy/config/bootstrap/v3/ClusterManager'; -import type { StatsSink as _envoy_config_metrics_v3_StatsSink, StatsSink__Output as _envoy_config_metrics_v3_StatsSink__Output } from '../../../../envoy/config/metrics/v3/StatsSink'; -import type { Duration as _google_protobuf_Duration, Duration__Output as _google_protobuf_Duration__Output } from '../../../../google/protobuf/Duration'; -import type { Watchdog as _envoy_config_bootstrap_v3_Watchdog, Watchdog__Output as _envoy_config_bootstrap_v3_Watchdog__Output } from '../../../../envoy/config/bootstrap/v3/Watchdog'; -import type { Tracing as _envoy_config_trace_v3_Tracing, Tracing__Output as _envoy_config_trace_v3_Tracing__Output } from '../../../../envoy/config/trace/v3/Tracing'; -import type { Admin as _envoy_config_bootstrap_v3_Admin, Admin__Output as _envoy_config_bootstrap_v3_Admin__Output } from '../../../../envoy/config/bootstrap/v3/Admin'; -import type { StatsConfig as _envoy_config_metrics_v3_StatsConfig, StatsConfig__Output as _envoy_config_metrics_v3_StatsConfig__Output } from '../../../../envoy/config/metrics/v3/StatsConfig'; -import type { ApiConfigSource as _envoy_config_core_v3_ApiConfigSource, ApiConfigSource__Output as _envoy_config_core_v3_ApiConfigSource__Output } from '../../../../envoy/config/core/v3/ApiConfigSource'; -import type { OverloadManager as _envoy_config_overload_v3_OverloadManager, OverloadManager__Output as _envoy_config_overload_v3_OverloadManager__Output } from '../../../../envoy/config/overload/v3/OverloadManager'; -import type { LayeredRuntime as _envoy_config_bootstrap_v3_LayeredRuntime, LayeredRuntime__Output as _envoy_config_bootstrap_v3_LayeredRuntime__Output } from '../../../../envoy/config/bootstrap/v3/LayeredRuntime'; -import type { UInt64Value as _google_protobuf_UInt64Value, UInt64Value__Output as _google_protobuf_UInt64Value__Output } from '../../../../google/protobuf/UInt64Value'; -import type { TypedExtensionConfig as _envoy_config_core_v3_TypedExtensionConfig, TypedExtensionConfig__Output as _envoy_config_core_v3_TypedExtensionConfig__Output } from '../../../../envoy/config/core/v3/TypedExtensionConfig'; -import type { ConfigSource as _envoy_config_core_v3_ConfigSource, ConfigSource__Output as _envoy_config_core_v3_ConfigSource__Output } from '../../../../envoy/config/core/v3/ConfigSource'; -import type { Watchdogs as _envoy_config_bootstrap_v3_Watchdogs, Watchdogs__Output as _envoy_config_bootstrap_v3_Watchdogs__Output } from '../../../../envoy/config/bootstrap/v3/Watchdogs'; -import type { FatalAction as _envoy_config_bootstrap_v3_FatalAction, FatalAction__Output as _envoy_config_bootstrap_v3_FatalAction__Output } from '../../../../envoy/config/bootstrap/v3/FatalAction'; -import type { DnsResolutionConfig as _envoy_config_core_v3_DnsResolutionConfig, DnsResolutionConfig__Output as _envoy_config_core_v3_DnsResolutionConfig__Output } from '../../../../envoy/config/core/v3/DnsResolutionConfig'; -import type { CustomInlineHeader as _envoy_config_bootstrap_v3_CustomInlineHeader, CustomInlineHeader__Output as _envoy_config_bootstrap_v3_CustomInlineHeader__Output } from '../../../../envoy/config/bootstrap/v3/CustomInlineHeader'; -import type { Listener as _envoy_config_listener_v3_Listener, Listener__Output as _envoy_config_listener_v3_Listener__Output } from '../../../../envoy/config/listener/v3/Listener'; -import type { Cluster as _envoy_config_cluster_v3_Cluster, Cluster__Output as _envoy_config_cluster_v3_Cluster__Output } from '../../../../envoy/config/cluster/v3/Cluster'; -import type { Secret as _envoy_extensions_transport_sockets_tls_v3_Secret, Secret__Output as _envoy_extensions_transport_sockets_tls_v3_Secret__Output } from '../../../../envoy/extensions/transport_sockets/tls/v3/Secret'; -import type { Long } from '@grpc/proto-loader'; - -/** - * [#next-free-field: 7] - */ -export interface _envoy_config_bootstrap_v3_Bootstrap_DynamicResources { - /** - * All :ref:`Listeners ` are provided by a single - * :ref:`LDS ` configuration source. - */ - 'lds_config'?: (_envoy_config_core_v3_ConfigSource | null); - /** - * xdstp:// resource locator for listener collection. - * [#not-implemented-hide:] - */ - 'lds_resources_locator'?: (string); - /** - * All post-bootstrap :ref:`Cluster ` definitions are - * provided by a single :ref:`CDS ` - * configuration source. - */ - 'cds_config'?: (_envoy_config_core_v3_ConfigSource | null); - /** - * xdstp:// resource locator for cluster collection. - * [#not-implemented-hide:] - */ - 'cds_resources_locator'?: (string); - /** - * A single :ref:`ADS ` source may be optionally - * specified. This must have :ref:`api_type - * ` :ref:`GRPC - * `. Only - * :ref:`ConfigSources ` that have - * the :ref:`ads ` field set will be - * streamed on the ADS channel. - */ - 'ads_config'?: (_envoy_config_core_v3_ApiConfigSource | null); -} - -/** - * [#next-free-field: 7] - */ -export interface _envoy_config_bootstrap_v3_Bootstrap_DynamicResources__Output { - /** - * All :ref:`Listeners ` are provided by a single - * :ref:`LDS ` configuration source. - */ - 'lds_config': (_envoy_config_core_v3_ConfigSource__Output | null); - /** - * xdstp:// resource locator for listener collection. - * [#not-implemented-hide:] - */ - 'lds_resources_locator': (string); - /** - * All post-bootstrap :ref:`Cluster ` definitions are - * provided by a single :ref:`CDS ` - * configuration source. - */ - 'cds_config': (_envoy_config_core_v3_ConfigSource__Output | null); - /** - * xdstp:// resource locator for cluster collection. - * [#not-implemented-hide:] - */ - 'cds_resources_locator': (string); - /** - * A single :ref:`ADS ` source may be optionally - * specified. This must have :ref:`api_type - * ` :ref:`GRPC - * `. Only - * :ref:`ConfigSources ` that have - * the :ref:`ads ` field set will be - * streamed on the ADS channel. - */ - 'ads_config': (_envoy_config_core_v3_ApiConfigSource__Output | null); -} - -export interface _envoy_config_bootstrap_v3_Bootstrap_StaticResources { - /** - * Static :ref:`Listeners `. These listeners are - * available regardless of LDS configuration. - */ - 'listeners'?: (_envoy_config_listener_v3_Listener)[]; - /** - * If a network based configuration source is specified for :ref:`cds_config - * `, it's necessary - * to have some initial cluster definitions available to allow Envoy to know - * how to speak to the management server. These cluster definitions may not - * use :ref:`EDS ` (i.e. they should be static - * IP or DNS-based). - */ - 'clusters'?: (_envoy_config_cluster_v3_Cluster)[]; - /** - * These static secrets can be used by :ref:`SdsSecretConfig - * ` - */ - 'secrets'?: (_envoy_extensions_transport_sockets_tls_v3_Secret)[]; -} - -export interface _envoy_config_bootstrap_v3_Bootstrap_StaticResources__Output { - /** - * Static :ref:`Listeners `. These listeners are - * available regardless of LDS configuration. - */ - 'listeners': (_envoy_config_listener_v3_Listener__Output)[]; - /** - * If a network based configuration source is specified for :ref:`cds_config - * `, it's necessary - * to have some initial cluster definitions available to allow Envoy to know - * how to speak to the management server. These cluster definitions may not - * use :ref:`EDS ` (i.e. they should be static - * IP or DNS-based). - */ - 'clusters': (_envoy_config_cluster_v3_Cluster__Output)[]; - /** - * These static secrets can be used by :ref:`SdsSecretConfig - * ` - */ - 'secrets': (_envoy_extensions_transport_sockets_tls_v3_Secret__Output)[]; -} - -/** - * Bootstrap :ref:`configuration overview `. - * [#next-free-field: 33] - */ -export interface Bootstrap { - /** - * Node identity to present to the management server and for instance - * identification purposes (e.g. in generated headers). - */ - 'node'?: (_envoy_config_core_v3_Node | null); - /** - * Statically specified resources. - */ - 'static_resources'?: (_envoy_config_bootstrap_v3_Bootstrap_StaticResources | null); - /** - * xDS configuration sources. - */ - 'dynamic_resources'?: (_envoy_config_bootstrap_v3_Bootstrap_DynamicResources | null); - /** - * Configuration for the cluster manager which owns all upstream clusters - * within the server. - */ - 'cluster_manager'?: (_envoy_config_bootstrap_v3_ClusterManager | null); - /** - * Optional file system path to search for startup flag files. - */ - 'flags_path'?: (string); - /** - * Optional set of stats sinks. - */ - 'stats_sinks'?: (_envoy_config_metrics_v3_StatsSink)[]; - /** - * Optional duration between flushes to configured stats sinks. For - * performance reasons Envoy latches counters and only flushes counters and - * gauges at a periodic interval. If not specified the default is 5000ms (5 - * seconds). Only one of `stats_flush_interval` or `stats_flush_on_admin` - * can be set. - * Duration must be at least 1ms and at most 5 min. - */ - 'stats_flush_interval'?: (_google_protobuf_Duration | null); - /** - * Optional watchdog configuration. - * This is for a single watchdog configuration for the entire system. - * Deprecated in favor of *watchdogs* which has finer granularity. - */ - 'watchdog'?: (_envoy_config_bootstrap_v3_Watchdog | null); - /** - * Configuration for an external tracing provider. - * - * .. attention:: - * This field has been deprecated in favor of :ref:`HttpConnectionManager.Tracing.provider - * `. - */ - 'tracing'?: (_envoy_config_trace_v3_Tracing | null); - /** - * Configuration for the local administration HTTP server. - */ - 'admin'?: (_envoy_config_bootstrap_v3_Admin | null); - /** - * Configuration for internal processing of stats. - */ - 'stats_config'?: (_envoy_config_metrics_v3_StatsConfig | null); - /** - * Health discovery service config option. - * (:ref:`core.ApiConfigSource `) - */ - 'hds_config'?: (_envoy_config_core_v3_ApiConfigSource | null); - /** - * Optional overload manager configuration. - */ - 'overload_manager'?: (_envoy_config_overload_v3_OverloadManager | null); - /** - * Enable :ref:`stats for event dispatcher `, defaults to false. - * Note that this records a value for each iteration of the event loop on every thread. This - * should normally be minimal overhead, but when using - * :ref:`statsd `, it will send each observed value - * over the wire individually because the statsd protocol doesn't have any way to represent a - * histogram summary. Be aware that this can be a very large volume of data. - */ - 'enable_dispatcher_stats'?: (boolean); - /** - * Configuration for the runtime configuration provider. If not - * specified, a “null” provider will be used which will result in all defaults - * being used. - */ - 'layered_runtime'?: (_envoy_config_bootstrap_v3_LayeredRuntime | null); - /** - * Optional string which will be used in lieu of x-envoy in prefixing headers. - * - * For example, if this string is present and set to X-Foo, then x-envoy-retry-on will be - * transformed into x-foo-retry-on etc. - * - * Note this applies to the headers Envoy will generate, the headers Envoy will sanitize, and the - * headers Envoy will trust for core code and core extensions only. Be VERY careful making - * changes to this string, especially in multi-layer Envoy deployments or deployments using - * extensions which are not upstream. - */ - 'header_prefix'?: (string); - /** - * Optional proxy version which will be used to set the value of :ref:`server.version statistic - * ` if specified. Envoy will not process this value, it will be sent as is to - * :ref:`stats sinks `. - */ - 'stats_server_version_override'?: (_google_protobuf_UInt64Value | null); - /** - * Always use TCP queries instead of UDP queries for DNS lookups. - * This may be overridden on a per-cluster basis in cds_config, - * when :ref:`dns_resolvers ` and - * :ref:`use_tcp_for_dns_lookups ` are - * specified. - * Setting this value causes failure if the - * ``envoy.restart_features.use_apple_api_for_dns_lookups`` runtime value is true during - * server startup. Apple' API only uses UDP for DNS resolution. - * This field is deprecated in favor of *dns_resolution_config* - * which aggregates all of the DNS resolver configuration in a single message. - */ - 'use_tcp_for_dns_lookups'?: (boolean); - /** - * Specifies optional bootstrap extensions to be instantiated at startup time. - * Each item contains extension specific configuration. - * [#extension-category: envoy.bootstrap] - */ - 'bootstrap_extensions'?: (_envoy_config_core_v3_TypedExtensionConfig)[]; - /** - * Configuration sources that will participate in - * xdstp:// URL authority resolution. The algorithm is as - * follows: - * 1. The authority field is taken from the xdstp:// URL, call - * this *resource_authority*. - * 2. *resource_authority* is compared against the authorities in any peer - * *ConfigSource*. The peer *ConfigSource* is the configuration source - * message which would have been used unconditionally for resolution - * with opaque resource names. If there is a match with an authority, the - * peer *ConfigSource* message is used. - * 3. *resource_authority* is compared sequentially with the authorities in - * each configuration source in *config_sources*. The first *ConfigSource* - * to match wins. - * 4. As a fallback, if no configuration source matches, then - * *default_config_source* is used. - * 5. If *default_config_source* is not specified, resolution fails. - * [#not-implemented-hide:] - */ - 'config_sources'?: (_envoy_config_core_v3_ConfigSource)[]; - /** - * Default configuration source for xdstp:// URLs if all - * other resolution fails. - * [#not-implemented-hide:] - */ - 'default_config_source'?: (_envoy_config_core_v3_ConfigSource | null); - /** - * Optional overriding of default socket interface. The value must be the name of one of the - * socket interface factories initialized through a bootstrap extension - */ - 'default_socket_interface'?: (string); - /** - * Global map of CertificateProvider instances. These instances are referred to by name in the - * :ref:`CommonTlsContext.CertificateProviderInstance.instance_name - * ` - * field. - * [#not-implemented-hide:] - */ - 'certificate_provider_instances'?: ({[key: string]: _envoy_config_core_v3_TypedExtensionConfig}); - /** - * A list of :ref:`Node ` field names - * that will be included in the context parameters of the effective - * xdstp:// URL that is sent in a discovery request when resource - * locators are used for LDS/CDS. Any non-string field will have its JSON - * encoding set as the context parameter value, with the exception of - * metadata, which will be flattened (see example below). The supported field - * names are: - * - "cluster" - * - "id" - * - "locality.region" - * - "locality.sub_zone" - * - "locality.zone" - * - "metadata" - * - "user_agent_build_version.metadata" - * - "user_agent_build_version.version" - * - "user_agent_name" - * - "user_agent_version" - * - * The node context parameters act as a base layer dictionary for the context - * parameters (i.e. more specific resource specific context parameters will - * override). Field names will be prefixed with “udpa.node.” when included in - * context parameters. - * - * For example, if node_context_params is ``["user_agent_name", "metadata"]``, - * the implied context parameters might be:: - * - * node.user_agent_name: "envoy" - * node.metadata.foo: "{\"bar\": \"baz\"}" - * node.metadata.some: "42" - * node.metadata.thing: "\"thing\"" - * - * [#not-implemented-hide:] - */ - 'node_context_params'?: (string)[]; - /** - * Optional watchdogs configuration. - * This is used for specifying different watchdogs for the different subsystems. - * [#extension-category: envoy.guarddog_actions] - */ - 'watchdogs'?: (_envoy_config_bootstrap_v3_Watchdogs | null); - /** - * Specifies optional extensions instantiated at startup time and - * invoked during crash time on the request that caused the crash. - */ - 'fatal_actions'?: (_envoy_config_bootstrap_v3_FatalAction)[]; - /** - * Flush stats to sinks only when queried for on the admin interface. If set, - * a flush timer is not created. Only one of `stats_flush_on_admin` or - * `stats_flush_interval` can be set. - */ - 'stats_flush_on_admin'?: (boolean); - /** - * DNS resolution configuration which includes the underlying dns resolver addresses and options. - * This may be overridden on a per-cluster basis in cds_config, when - * :ref:`dns_resolution_config ` - * is specified. - * *dns_resolution_config* will be deprecated once - * :ref:'typed_dns_resolver_config ' - * is fully supported. - */ - 'dns_resolution_config'?: (_envoy_config_core_v3_DnsResolutionConfig | null); - /** - * DNS resolver type configuration extension. This extension can be used to configure c-ares, apple, - * or any other DNS resolver types and the related parameters. - * For example, an object of :ref:`DnsResolutionConfig ` - * can be packed into this *typed_dns_resolver_config*. This configuration will replace the - * :ref:'dns_resolution_config ' - * configuration eventually. - * TODO(yanjunxiang): Investigate the deprecation plan for *dns_resolution_config*. - * During the transition period when both *dns_resolution_config* and *typed_dns_resolver_config* exists, - * this configuration is optional. - * When *typed_dns_resolver_config* is in place, Envoy will use it and ignore *dns_resolution_config*. - * When *typed_dns_resolver_config* is missing, the default behavior is in place. - * [#not-implemented-hide:] - */ - 'typed_dns_resolver_config'?: (_envoy_config_core_v3_TypedExtensionConfig | null); - /** - * Specifies a set of headers that need to be registered as inline header. This configuration - * allows users to customize the inline headers on-demand at Envoy startup without modifying - * Envoy's source code. - * - * Note that the 'set-cookie' header cannot be registered as inline header. - */ - 'inline_headers'?: (_envoy_config_bootstrap_v3_CustomInlineHeader)[]; - 'stats_flush'?: "stats_flush_on_admin"; -} - -/** - * Bootstrap :ref:`configuration overview `. - * [#next-free-field: 33] - */ -export interface Bootstrap__Output { - /** - * Node identity to present to the management server and for instance - * identification purposes (e.g. in generated headers). - */ - 'node': (_envoy_config_core_v3_Node__Output | null); - /** - * Statically specified resources. - */ - 'static_resources': (_envoy_config_bootstrap_v3_Bootstrap_StaticResources__Output | null); - /** - * xDS configuration sources. - */ - 'dynamic_resources': (_envoy_config_bootstrap_v3_Bootstrap_DynamicResources__Output | null); - /** - * Configuration for the cluster manager which owns all upstream clusters - * within the server. - */ - 'cluster_manager': (_envoy_config_bootstrap_v3_ClusterManager__Output | null); - /** - * Optional file system path to search for startup flag files. - */ - 'flags_path': (string); - /** - * Optional set of stats sinks. - */ - 'stats_sinks': (_envoy_config_metrics_v3_StatsSink__Output)[]; - /** - * Optional duration between flushes to configured stats sinks. For - * performance reasons Envoy latches counters and only flushes counters and - * gauges at a periodic interval. If not specified the default is 5000ms (5 - * seconds). Only one of `stats_flush_interval` or `stats_flush_on_admin` - * can be set. - * Duration must be at least 1ms and at most 5 min. - */ - 'stats_flush_interval': (_google_protobuf_Duration__Output | null); - /** - * Optional watchdog configuration. - * This is for a single watchdog configuration for the entire system. - * Deprecated in favor of *watchdogs* which has finer granularity. - */ - 'watchdog': (_envoy_config_bootstrap_v3_Watchdog__Output | null); - /** - * Configuration for an external tracing provider. - * - * .. attention:: - * This field has been deprecated in favor of :ref:`HttpConnectionManager.Tracing.provider - * `. - */ - 'tracing': (_envoy_config_trace_v3_Tracing__Output | null); - /** - * Configuration for the local administration HTTP server. - */ - 'admin': (_envoy_config_bootstrap_v3_Admin__Output | null); - /** - * Configuration for internal processing of stats. - */ - 'stats_config': (_envoy_config_metrics_v3_StatsConfig__Output | null); - /** - * Health discovery service config option. - * (:ref:`core.ApiConfigSource `) - */ - 'hds_config': (_envoy_config_core_v3_ApiConfigSource__Output | null); - /** - * Optional overload manager configuration. - */ - 'overload_manager': (_envoy_config_overload_v3_OverloadManager__Output | null); - /** - * Enable :ref:`stats for event dispatcher `, defaults to false. - * Note that this records a value for each iteration of the event loop on every thread. This - * should normally be minimal overhead, but when using - * :ref:`statsd `, it will send each observed value - * over the wire individually because the statsd protocol doesn't have any way to represent a - * histogram summary. Be aware that this can be a very large volume of data. - */ - 'enable_dispatcher_stats': (boolean); - /** - * Configuration for the runtime configuration provider. If not - * specified, a “null” provider will be used which will result in all defaults - * being used. - */ - 'layered_runtime': (_envoy_config_bootstrap_v3_LayeredRuntime__Output | null); - /** - * Optional string which will be used in lieu of x-envoy in prefixing headers. - * - * For example, if this string is present and set to X-Foo, then x-envoy-retry-on will be - * transformed into x-foo-retry-on etc. - * - * Note this applies to the headers Envoy will generate, the headers Envoy will sanitize, and the - * headers Envoy will trust for core code and core extensions only. Be VERY careful making - * changes to this string, especially in multi-layer Envoy deployments or deployments using - * extensions which are not upstream. - */ - 'header_prefix': (string); - /** - * Optional proxy version which will be used to set the value of :ref:`server.version statistic - * ` if specified. Envoy will not process this value, it will be sent as is to - * :ref:`stats sinks `. - */ - 'stats_server_version_override': (_google_protobuf_UInt64Value__Output | null); - /** - * Always use TCP queries instead of UDP queries for DNS lookups. - * This may be overridden on a per-cluster basis in cds_config, - * when :ref:`dns_resolvers ` and - * :ref:`use_tcp_for_dns_lookups ` are - * specified. - * Setting this value causes failure if the - * ``envoy.restart_features.use_apple_api_for_dns_lookups`` runtime value is true during - * server startup. Apple' API only uses UDP for DNS resolution. - * This field is deprecated in favor of *dns_resolution_config* - * which aggregates all of the DNS resolver configuration in a single message. - */ - 'use_tcp_for_dns_lookups': (boolean); - /** - * Specifies optional bootstrap extensions to be instantiated at startup time. - * Each item contains extension specific configuration. - * [#extension-category: envoy.bootstrap] - */ - 'bootstrap_extensions': (_envoy_config_core_v3_TypedExtensionConfig__Output)[]; - /** - * Configuration sources that will participate in - * xdstp:// URL authority resolution. The algorithm is as - * follows: - * 1. The authority field is taken from the xdstp:// URL, call - * this *resource_authority*. - * 2. *resource_authority* is compared against the authorities in any peer - * *ConfigSource*. The peer *ConfigSource* is the configuration source - * message which would have been used unconditionally for resolution - * with opaque resource names. If there is a match with an authority, the - * peer *ConfigSource* message is used. - * 3. *resource_authority* is compared sequentially with the authorities in - * each configuration source in *config_sources*. The first *ConfigSource* - * to match wins. - * 4. As a fallback, if no configuration source matches, then - * *default_config_source* is used. - * 5. If *default_config_source* is not specified, resolution fails. - * [#not-implemented-hide:] - */ - 'config_sources': (_envoy_config_core_v3_ConfigSource__Output)[]; - /** - * Default configuration source for xdstp:// URLs if all - * other resolution fails. - * [#not-implemented-hide:] - */ - 'default_config_source': (_envoy_config_core_v3_ConfigSource__Output | null); - /** - * Optional overriding of default socket interface. The value must be the name of one of the - * socket interface factories initialized through a bootstrap extension - */ - 'default_socket_interface': (string); - /** - * Global map of CertificateProvider instances. These instances are referred to by name in the - * :ref:`CommonTlsContext.CertificateProviderInstance.instance_name - * ` - * field. - * [#not-implemented-hide:] - */ - 'certificate_provider_instances': ({[key: string]: _envoy_config_core_v3_TypedExtensionConfig__Output}); - /** - * A list of :ref:`Node ` field names - * that will be included in the context parameters of the effective - * xdstp:// URL that is sent in a discovery request when resource - * locators are used for LDS/CDS. Any non-string field will have its JSON - * encoding set as the context parameter value, with the exception of - * metadata, which will be flattened (see example below). The supported field - * names are: - * - "cluster" - * - "id" - * - "locality.region" - * - "locality.sub_zone" - * - "locality.zone" - * - "metadata" - * - "user_agent_build_version.metadata" - * - "user_agent_build_version.version" - * - "user_agent_name" - * - "user_agent_version" - * - * The node context parameters act as a base layer dictionary for the context - * parameters (i.e. more specific resource specific context parameters will - * override). Field names will be prefixed with “udpa.node.” when included in - * context parameters. - * - * For example, if node_context_params is ``["user_agent_name", "metadata"]``, - * the implied context parameters might be:: - * - * node.user_agent_name: "envoy" - * node.metadata.foo: "{\"bar\": \"baz\"}" - * node.metadata.some: "42" - * node.metadata.thing: "\"thing\"" - * - * [#not-implemented-hide:] - */ - 'node_context_params': (string)[]; - /** - * Optional watchdogs configuration. - * This is used for specifying different watchdogs for the different subsystems. - * [#extension-category: envoy.guarddog_actions] - */ - 'watchdogs': (_envoy_config_bootstrap_v3_Watchdogs__Output | null); - /** - * Specifies optional extensions instantiated at startup time and - * invoked during crash time on the request that caused the crash. - */ - 'fatal_actions': (_envoy_config_bootstrap_v3_FatalAction__Output)[]; - /** - * Flush stats to sinks only when queried for on the admin interface. If set, - * a flush timer is not created. Only one of `stats_flush_on_admin` or - * `stats_flush_interval` can be set. - */ - 'stats_flush_on_admin'?: (boolean); - /** - * DNS resolution configuration which includes the underlying dns resolver addresses and options. - * This may be overridden on a per-cluster basis in cds_config, when - * :ref:`dns_resolution_config ` - * is specified. - * *dns_resolution_config* will be deprecated once - * :ref:'typed_dns_resolver_config ' - * is fully supported. - */ - 'dns_resolution_config': (_envoy_config_core_v3_DnsResolutionConfig__Output | null); - /** - * DNS resolver type configuration extension. This extension can be used to configure c-ares, apple, - * or any other DNS resolver types and the related parameters. - * For example, an object of :ref:`DnsResolutionConfig ` - * can be packed into this *typed_dns_resolver_config*. This configuration will replace the - * :ref:'dns_resolution_config ' - * configuration eventually. - * TODO(yanjunxiang): Investigate the deprecation plan for *dns_resolution_config*. - * During the transition period when both *dns_resolution_config* and *typed_dns_resolver_config* exists, - * this configuration is optional. - * When *typed_dns_resolver_config* is in place, Envoy will use it and ignore *dns_resolution_config*. - * When *typed_dns_resolver_config* is missing, the default behavior is in place. - * [#not-implemented-hide:] - */ - 'typed_dns_resolver_config': (_envoy_config_core_v3_TypedExtensionConfig__Output | null); - /** - * Specifies a set of headers that need to be registered as inline header. This configuration - * allows users to customize the inline headers on-demand at Envoy startup without modifying - * Envoy's source code. - * - * Note that the 'set-cookie' header cannot be registered as inline header. - */ - 'inline_headers': (_envoy_config_bootstrap_v3_CustomInlineHeader__Output)[]; - 'stats_flush': "stats_flush_on_admin"; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/ClusterManager.ts b/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/ClusterManager.ts deleted file mode 100644 index 571b96fb7..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/ClusterManager.ts +++ /dev/null @@ -1,99 +0,0 @@ -// Original file: deps/envoy-api/envoy/config/bootstrap/v3/bootstrap.proto - -import type { BindConfig as _envoy_config_core_v3_BindConfig, BindConfig__Output as _envoy_config_core_v3_BindConfig__Output } from '../../../../envoy/config/core/v3/BindConfig'; -import type { ApiConfigSource as _envoy_config_core_v3_ApiConfigSource, ApiConfigSource__Output as _envoy_config_core_v3_ApiConfigSource__Output } from '../../../../envoy/config/core/v3/ApiConfigSource'; -import type { EventServiceConfig as _envoy_config_core_v3_EventServiceConfig, EventServiceConfig__Output as _envoy_config_core_v3_EventServiceConfig__Output } from '../../../../envoy/config/core/v3/EventServiceConfig'; - -export interface _envoy_config_bootstrap_v3_ClusterManager_OutlierDetection { - /** - * Specifies the path to the outlier event log. - */ - 'event_log_path'?: (string); - /** - * [#not-implemented-hide:] - * The gRPC service for the outlier detection event service. - * If empty, outlier detection events won't be sent to a remote endpoint. - */ - 'event_service'?: (_envoy_config_core_v3_EventServiceConfig | null); -} - -export interface _envoy_config_bootstrap_v3_ClusterManager_OutlierDetection__Output { - /** - * Specifies the path to the outlier event log. - */ - 'event_log_path': (string); - /** - * [#not-implemented-hide:] - * The gRPC service for the outlier detection event service. - * If empty, outlier detection events won't be sent to a remote endpoint. - */ - 'event_service': (_envoy_config_core_v3_EventServiceConfig__Output | null); -} - -/** - * Cluster manager :ref:`architecture overview `. - */ -export interface ClusterManager { - /** - * Name of the local cluster (i.e., the cluster that owns the Envoy running - * this configuration). In order to enable :ref:`zone aware routing - * ` this option must be set. - * If *local_cluster_name* is defined then :ref:`clusters - * ` must be defined in the :ref:`Bootstrap - * static cluster resources - * `. This is unrelated to - * the :option:`--service-cluster` option which does not `affect zone aware - * routing `_. - */ - 'local_cluster_name'?: (string); - /** - * Optional global configuration for outlier detection. - */ - 'outlier_detection'?: (_envoy_config_bootstrap_v3_ClusterManager_OutlierDetection | null); - /** - * Optional configuration used to bind newly established upstream connections. - * This may be overridden on a per-cluster basis by upstream_bind_config in the cds_config. - */ - 'upstream_bind_config'?: (_envoy_config_core_v3_BindConfig | null); - /** - * A management server endpoint to stream load stats to via - * *StreamLoadStats*. This must have :ref:`api_type - * ` :ref:`GRPC - * `. - */ - 'load_stats_config'?: (_envoy_config_core_v3_ApiConfigSource | null); -} - -/** - * Cluster manager :ref:`architecture overview `. - */ -export interface ClusterManager__Output { - /** - * Name of the local cluster (i.e., the cluster that owns the Envoy running - * this configuration). In order to enable :ref:`zone aware routing - * ` this option must be set. - * If *local_cluster_name* is defined then :ref:`clusters - * ` must be defined in the :ref:`Bootstrap - * static cluster resources - * `. This is unrelated to - * the :option:`--service-cluster` option which does not `affect zone aware - * routing `_. - */ - 'local_cluster_name': (string); - /** - * Optional global configuration for outlier detection. - */ - 'outlier_detection': (_envoy_config_bootstrap_v3_ClusterManager_OutlierDetection__Output | null); - /** - * Optional configuration used to bind newly established upstream connections. - * This may be overridden on a per-cluster basis by upstream_bind_config in the cds_config. - */ - 'upstream_bind_config': (_envoy_config_core_v3_BindConfig__Output | null); - /** - * A management server endpoint to stream load stats to via - * *StreamLoadStats*. This must have :ref:`api_type - * ` :ref:`GRPC - * `. - */ - 'load_stats_config': (_envoy_config_core_v3_ApiConfigSource__Output | null); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/CustomInlineHeader.ts b/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/CustomInlineHeader.ts deleted file mode 100644 index f0e2d29aa..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/CustomInlineHeader.ts +++ /dev/null @@ -1,85 +0,0 @@ -// Original file: deps/envoy-api/envoy/config/bootstrap/v3/bootstrap.proto - - -// Original file: deps/envoy-api/envoy/config/bootstrap/v3/bootstrap.proto - -export enum _envoy_config_bootstrap_v3_CustomInlineHeader_InlineHeaderType { - REQUEST_HEADER = 0, - REQUEST_TRAILER = 1, - RESPONSE_HEADER = 2, - RESPONSE_TRAILER = 3, -} - -/** - * Used to specify the header that needs to be registered as an inline header. - * - * If request or response contain multiple headers with the same name and the header - * name is registered as an inline header. Then multiple headers will be folded - * into one, and multiple header values will be concatenated by a suitable delimiter. - * The delimiter is generally a comma. - * - * For example, if 'foo' is registered as an inline header, and the headers contains - * the following two headers: - * - * .. code-block:: text - * - * foo: bar - * foo: eep - * - * Then they will eventually be folded into: - * - * .. code-block:: text - * - * foo: bar, eep - * - * Inline headers provide O(1) search performance, but each inline header imposes - * an additional memory overhead on all instances of the corresponding type of - * HeaderMap or TrailerMap. - */ -export interface CustomInlineHeader { - /** - * The name of the header that is expected to be set as the inline header. - */ - 'inline_header_name'?: (string); - /** - * The type of the header that is expected to be set as the inline header. - */ - 'inline_header_type'?: (_envoy_config_bootstrap_v3_CustomInlineHeader_InlineHeaderType | keyof typeof _envoy_config_bootstrap_v3_CustomInlineHeader_InlineHeaderType); -} - -/** - * Used to specify the header that needs to be registered as an inline header. - * - * If request or response contain multiple headers with the same name and the header - * name is registered as an inline header. Then multiple headers will be folded - * into one, and multiple header values will be concatenated by a suitable delimiter. - * The delimiter is generally a comma. - * - * For example, if 'foo' is registered as an inline header, and the headers contains - * the following two headers: - * - * .. code-block:: text - * - * foo: bar - * foo: eep - * - * Then they will eventually be folded into: - * - * .. code-block:: text - * - * foo: bar, eep - * - * Inline headers provide O(1) search performance, but each inline header imposes - * an additional memory overhead on all instances of the corresponding type of - * HeaderMap or TrailerMap. - */ -export interface CustomInlineHeader__Output { - /** - * The name of the header that is expected to be set as the inline header. - */ - 'inline_header_name': (string); - /** - * The type of the header that is expected to be set as the inline header. - */ - 'inline_header_type': (keyof typeof _envoy_config_bootstrap_v3_CustomInlineHeader_InlineHeaderType); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/FatalAction.ts b/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/FatalAction.ts deleted file mode 100644 index 236afded5..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/FatalAction.ts +++ /dev/null @@ -1,39 +0,0 @@ -// Original file: deps/envoy-api/envoy/config/bootstrap/v3/bootstrap.proto - -import type { TypedExtensionConfig as _envoy_config_core_v3_TypedExtensionConfig, TypedExtensionConfig__Output as _envoy_config_core_v3_TypedExtensionConfig__Output } from '../../../../envoy/config/core/v3/TypedExtensionConfig'; - -/** - * Fatal actions to run while crashing. Actions can be safe (meaning they are - * async-signal safe) or unsafe. We run all safe actions before we run unsafe actions. - * If using an unsafe action that could get stuck or deadlock, it important to - * have an out of band system to terminate the process. - * - * The interface for the extension is ``Envoy::Server::Configuration::FatalAction``. - * *FatalAction* extensions live in the ``envoy.extensions.fatal_actions`` API - * namespace. - */ -export interface FatalAction { - /** - * Extension specific configuration for the action. It's expected to conform - * to the ``Envoy::Server::Configuration::FatalAction`` interface. - */ - 'config'?: (_envoy_config_core_v3_TypedExtensionConfig | null); -} - -/** - * Fatal actions to run while crashing. Actions can be safe (meaning they are - * async-signal safe) or unsafe. We run all safe actions before we run unsafe actions. - * If using an unsafe action that could get stuck or deadlock, it important to - * have an out of band system to terminate the process. - * - * The interface for the extension is ``Envoy::Server::Configuration::FatalAction``. - * *FatalAction* extensions live in the ``envoy.extensions.fatal_actions`` API - * namespace. - */ -export interface FatalAction__Output { - /** - * Extension specific configuration for the action. It's expected to conform - * to the ``Envoy::Server::Configuration::FatalAction`` interface. - */ - 'config': (_envoy_config_core_v3_TypedExtensionConfig__Output | null); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/LayeredRuntime.ts b/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/LayeredRuntime.ts deleted file mode 100644 index 3514d3140..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/LayeredRuntime.ts +++ /dev/null @@ -1,25 +0,0 @@ -// Original file: deps/envoy-api/envoy/config/bootstrap/v3/bootstrap.proto - -import type { RuntimeLayer as _envoy_config_bootstrap_v3_RuntimeLayer, RuntimeLayer__Output as _envoy_config_bootstrap_v3_RuntimeLayer__Output } from '../../../../envoy/config/bootstrap/v3/RuntimeLayer'; - -/** - * Runtime :ref:`configuration overview `. - */ -export interface LayeredRuntime { - /** - * The :ref:`layers ` of the runtime. This is ordered - * such that later layers in the list overlay earlier entries. - */ - 'layers'?: (_envoy_config_bootstrap_v3_RuntimeLayer)[]; -} - -/** - * Runtime :ref:`configuration overview `. - */ -export interface LayeredRuntime__Output { - /** - * The :ref:`layers ` of the runtime. This is ordered - * such that later layers in the list overlay earlier entries. - */ - 'layers': (_envoy_config_bootstrap_v3_RuntimeLayer__Output)[]; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/Runtime.ts b/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/Runtime.ts deleted file mode 100644 index 4f7713bcf..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/Runtime.ts +++ /dev/null @@ -1,77 +0,0 @@ -// Original file: deps/envoy-api/envoy/config/bootstrap/v3/bootstrap.proto - -import type { Struct as _google_protobuf_Struct, Struct__Output as _google_protobuf_Struct__Output } from '../../../../google/protobuf/Struct'; - -/** - * Runtime :ref:`configuration overview ` (deprecated). - */ -export interface Runtime { - /** - * The implementation assumes that the file system tree is accessed via a - * symbolic link. An atomic link swap is used when a new tree should be - * switched to. This parameter specifies the path to the symbolic link. Envoy - * will watch the location for changes and reload the file system tree when - * they happen. If this parameter is not set, there will be no disk based - * runtime. - */ - 'symlink_root'?: (string); - /** - * Specifies the subdirectory to load within the root directory. This is - * useful if multiple systems share the same delivery mechanism. Envoy - * configuration elements can be contained in a dedicated subdirectory. - */ - 'subdirectory'?: (string); - /** - * Specifies an optional subdirectory to load within the root directory. If - * specified and the directory exists, configuration values within this - * directory will override those found in the primary subdirectory. This is - * useful when Envoy is deployed across many different types of servers. - * Sometimes it is useful to have a per service cluster directory for runtime - * configuration. See below for exactly how the override directory is used. - */ - 'override_subdirectory'?: (string); - /** - * Static base runtime. This will be :ref:`overridden - * ` by other runtime layers, e.g. - * disk or admin. This follows the :ref:`runtime protobuf JSON representation - * encoding `. - */ - 'base'?: (_google_protobuf_Struct | null); -} - -/** - * Runtime :ref:`configuration overview ` (deprecated). - */ -export interface Runtime__Output { - /** - * The implementation assumes that the file system tree is accessed via a - * symbolic link. An atomic link swap is used when a new tree should be - * switched to. This parameter specifies the path to the symbolic link. Envoy - * will watch the location for changes and reload the file system tree when - * they happen. If this parameter is not set, there will be no disk based - * runtime. - */ - 'symlink_root': (string); - /** - * Specifies the subdirectory to load within the root directory. This is - * useful if multiple systems share the same delivery mechanism. Envoy - * configuration elements can be contained in a dedicated subdirectory. - */ - 'subdirectory': (string); - /** - * Specifies an optional subdirectory to load within the root directory. If - * specified and the directory exists, configuration values within this - * directory will override those found in the primary subdirectory. This is - * useful when Envoy is deployed across many different types of servers. - * Sometimes it is useful to have a per service cluster directory for runtime - * configuration. See below for exactly how the override directory is used. - */ - 'override_subdirectory': (string); - /** - * Static base runtime. This will be :ref:`overridden - * ` by other runtime layers, e.g. - * disk or admin. This follows the :ref:`runtime protobuf JSON representation - * encoding `. - */ - 'base': (_google_protobuf_Struct__Output | null); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/RuntimeLayer.ts b/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/RuntimeLayer.ts deleted file mode 100644 index b072bfa7d..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/RuntimeLayer.ts +++ /dev/null @@ -1,142 +0,0 @@ -// Original file: deps/envoy-api/envoy/config/bootstrap/v3/bootstrap.proto - -import type { Struct as _google_protobuf_Struct, Struct__Output as _google_protobuf_Struct__Output } from '../../../../google/protobuf/Struct'; -import type { ConfigSource as _envoy_config_core_v3_ConfigSource, ConfigSource__Output as _envoy_config_core_v3_ConfigSource__Output } from '../../../../envoy/config/core/v3/ConfigSource'; - -/** - * :ref:`Admin console runtime ` layer. - */ -export interface _envoy_config_bootstrap_v3_RuntimeLayer_AdminLayer { -} - -/** - * :ref:`Admin console runtime ` layer. - */ -export interface _envoy_config_bootstrap_v3_RuntimeLayer_AdminLayer__Output { -} - -/** - * :ref:`Disk runtime ` layer. - */ -export interface _envoy_config_bootstrap_v3_RuntimeLayer_DiskLayer { - /** - * The implementation assumes that the file system tree is accessed via a - * symbolic link. An atomic link swap is used when a new tree should be - * switched to. This parameter specifies the path to the symbolic link. - * Envoy will watch the location for changes and reload the file system tree - * when they happen. See documentation on runtime :ref:`atomicity - * ` for further details on how reloads are - * treated. - */ - 'symlink_root'?: (string); - /** - * Specifies the subdirectory to load within the root directory. This is - * useful if multiple systems share the same delivery mechanism. Envoy - * configuration elements can be contained in a dedicated subdirectory. - */ - 'subdirectory'?: (string); - /** - * :ref:`Append ` the - * service cluster to the path under symlink root. - */ - 'append_service_cluster'?: (boolean); -} - -/** - * :ref:`Disk runtime ` layer. - */ -export interface _envoy_config_bootstrap_v3_RuntimeLayer_DiskLayer__Output { - /** - * The implementation assumes that the file system tree is accessed via a - * symbolic link. An atomic link swap is used when a new tree should be - * switched to. This parameter specifies the path to the symbolic link. - * Envoy will watch the location for changes and reload the file system tree - * when they happen. See documentation on runtime :ref:`atomicity - * ` for further details on how reloads are - * treated. - */ - 'symlink_root': (string); - /** - * Specifies the subdirectory to load within the root directory. This is - * useful if multiple systems share the same delivery mechanism. Envoy - * configuration elements can be contained in a dedicated subdirectory. - */ - 'subdirectory': (string); - /** - * :ref:`Append ` the - * service cluster to the path under symlink root. - */ - 'append_service_cluster': (boolean); -} - -/** - * :ref:`Runtime Discovery Service (RTDS) ` layer. - */ -export interface _envoy_config_bootstrap_v3_RuntimeLayer_RtdsLayer { - /** - * Resource to subscribe to at *rtds_config* for the RTDS layer. - */ - 'name'?: (string); - /** - * RTDS configuration source. - */ - 'rtds_config'?: (_envoy_config_core_v3_ConfigSource | null); -} - -/** - * :ref:`Runtime Discovery Service (RTDS) ` layer. - */ -export interface _envoy_config_bootstrap_v3_RuntimeLayer_RtdsLayer__Output { - /** - * Resource to subscribe to at *rtds_config* for the RTDS layer. - */ - 'name': (string); - /** - * RTDS configuration source. - */ - 'rtds_config': (_envoy_config_core_v3_ConfigSource__Output | null); -} - -/** - * [#next-free-field: 6] - */ -export interface RuntimeLayer { - /** - * Descriptive name for the runtime layer. This is only used for the runtime - * :http:get:`/runtime` output. - */ - 'name'?: (string); - /** - * :ref:`Static runtime ` layer. - * This follows the :ref:`runtime protobuf JSON representation encoding - * `. Unlike static xDS resources, this static - * layer is overridable by later layers in the runtime virtual filesystem. - */ - 'static_layer'?: (_google_protobuf_Struct | null); - 'disk_layer'?: (_envoy_config_bootstrap_v3_RuntimeLayer_DiskLayer | null); - 'admin_layer'?: (_envoy_config_bootstrap_v3_RuntimeLayer_AdminLayer | null); - 'rtds_layer'?: (_envoy_config_bootstrap_v3_RuntimeLayer_RtdsLayer | null); - 'layer_specifier'?: "static_layer"|"disk_layer"|"admin_layer"|"rtds_layer"; -} - -/** - * [#next-free-field: 6] - */ -export interface RuntimeLayer__Output { - /** - * Descriptive name for the runtime layer. This is only used for the runtime - * :http:get:`/runtime` output. - */ - 'name': (string); - /** - * :ref:`Static runtime ` layer. - * This follows the :ref:`runtime protobuf JSON representation encoding - * `. Unlike static xDS resources, this static - * layer is overridable by later layers in the runtime virtual filesystem. - */ - 'static_layer'?: (_google_protobuf_Struct__Output | null); - 'disk_layer'?: (_envoy_config_bootstrap_v3_RuntimeLayer_DiskLayer__Output | null); - 'admin_layer'?: (_envoy_config_bootstrap_v3_RuntimeLayer_AdminLayer__Output | null); - 'rtds_layer'?: (_envoy_config_bootstrap_v3_RuntimeLayer_RtdsLayer__Output | null); - 'layer_specifier': "static_layer"|"disk_layer"|"admin_layer"|"rtds_layer"; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/Watchdog.ts b/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/Watchdog.ts deleted file mode 100644 index 8cd743b56..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/Watchdog.ts +++ /dev/null @@ -1,141 +0,0 @@ -// Original file: deps/envoy-api/envoy/config/bootstrap/v3/bootstrap.proto - -import type { Duration as _google_protobuf_Duration, Duration__Output as _google_protobuf_Duration__Output } from '../../../../google/protobuf/Duration'; -import type { Percent as _envoy_type_v3_Percent, Percent__Output as _envoy_type_v3_Percent__Output } from '../../../../envoy/type/v3/Percent'; -import type { TypedExtensionConfig as _envoy_config_core_v3_TypedExtensionConfig, TypedExtensionConfig__Output as _envoy_config_core_v3_TypedExtensionConfig__Output } from '../../../../envoy/config/core/v3/TypedExtensionConfig'; - -export interface _envoy_config_bootstrap_v3_Watchdog_WatchdogAction { - /** - * Extension specific configuration for the action. - */ - 'config'?: (_envoy_config_core_v3_TypedExtensionConfig | null); - 'event'?: (_envoy_config_bootstrap_v3_Watchdog_WatchdogAction_WatchdogEvent | keyof typeof _envoy_config_bootstrap_v3_Watchdog_WatchdogAction_WatchdogEvent); -} - -export interface _envoy_config_bootstrap_v3_Watchdog_WatchdogAction__Output { - /** - * Extension specific configuration for the action. - */ - 'config': (_envoy_config_core_v3_TypedExtensionConfig__Output | null); - 'event': (keyof typeof _envoy_config_bootstrap_v3_Watchdog_WatchdogAction_WatchdogEvent); -} - -// Original file: deps/envoy-api/envoy/config/bootstrap/v3/bootstrap.proto - -/** - * The events are fired in this order: KILL, MULTIKILL, MEGAMISS, MISS. - * Within an event type, actions execute in the order they are configured. - * For KILL/MULTIKILL there is a default PANIC that will run after the - * registered actions and kills the process if it wasn't already killed. - * It might be useful to specify several debug actions, and possibly an - * alternate FATAL action. - */ -export enum _envoy_config_bootstrap_v3_Watchdog_WatchdogAction_WatchdogEvent { - UNKNOWN = 0, - KILL = 1, - MULTIKILL = 2, - MEGAMISS = 3, - MISS = 4, -} - -/** - * Envoy process watchdog configuration. When configured, this monitors for - * nonresponsive threads and kills the process after the configured thresholds. - * See the :ref:`watchdog documentation ` for more information. - * [#next-free-field: 8] - */ -export interface Watchdog { - /** - * The duration after which Envoy counts a nonresponsive thread in the - * *watchdog_miss* statistic. If not specified the default is 200ms. - */ - 'miss_timeout'?: (_google_protobuf_Duration | null); - /** - * The duration after which Envoy counts a nonresponsive thread in the - * *watchdog_mega_miss* statistic. If not specified the default is - * 1000ms. - */ - 'megamiss_timeout'?: (_google_protobuf_Duration | null); - /** - * If a watched thread has been nonresponsive for this duration, assume a - * programming error and kill the entire Envoy process. Set to 0 to disable - * kill behavior. If not specified the default is 0 (disabled). - */ - 'kill_timeout'?: (_google_protobuf_Duration | null); - /** - * If max(2, ceil(registered_threads * Fraction(*multikill_threshold*))) - * threads have been nonresponsive for at least this duration kill the entire - * Envoy process. Set to 0 to disable this behavior. If not specified the - * default is 0 (disabled). - */ - 'multikill_timeout'?: (_google_protobuf_Duration | null); - /** - * Sets the threshold for *multikill_timeout* in terms of the percentage of - * nonresponsive threads required for the *multikill_timeout*. - * If not specified the default is 0. - */ - 'multikill_threshold'?: (_envoy_type_v3_Percent | null); - /** - * Defines the maximum jitter used to adjust the *kill_timeout* if *kill_timeout* is - * enabled. Enabling this feature would help to reduce risk of synchronized - * watchdog kill events across proxies due to external triggers. Set to 0 to - * disable. If not specified the default is 0 (disabled). - */ - 'max_kill_timeout_jitter'?: (_google_protobuf_Duration | null); - /** - * Register actions that will fire on given WatchDog events. - * See *WatchDogAction* for priority of events. - */ - 'actions'?: (_envoy_config_bootstrap_v3_Watchdog_WatchdogAction)[]; -} - -/** - * Envoy process watchdog configuration. When configured, this monitors for - * nonresponsive threads and kills the process after the configured thresholds. - * See the :ref:`watchdog documentation ` for more information. - * [#next-free-field: 8] - */ -export interface Watchdog__Output { - /** - * The duration after which Envoy counts a nonresponsive thread in the - * *watchdog_miss* statistic. If not specified the default is 200ms. - */ - 'miss_timeout': (_google_protobuf_Duration__Output | null); - /** - * The duration after which Envoy counts a nonresponsive thread in the - * *watchdog_mega_miss* statistic. If not specified the default is - * 1000ms. - */ - 'megamiss_timeout': (_google_protobuf_Duration__Output | null); - /** - * If a watched thread has been nonresponsive for this duration, assume a - * programming error and kill the entire Envoy process. Set to 0 to disable - * kill behavior. If not specified the default is 0 (disabled). - */ - 'kill_timeout': (_google_protobuf_Duration__Output | null); - /** - * If max(2, ceil(registered_threads * Fraction(*multikill_threshold*))) - * threads have been nonresponsive for at least this duration kill the entire - * Envoy process. Set to 0 to disable this behavior. If not specified the - * default is 0 (disabled). - */ - 'multikill_timeout': (_google_protobuf_Duration__Output | null); - /** - * Sets the threshold for *multikill_timeout* in terms of the percentage of - * nonresponsive threads required for the *multikill_timeout*. - * If not specified the default is 0. - */ - 'multikill_threshold': (_envoy_type_v3_Percent__Output | null); - /** - * Defines the maximum jitter used to adjust the *kill_timeout* if *kill_timeout* is - * enabled. Enabling this feature would help to reduce risk of synchronized - * watchdog kill events across proxies due to external triggers. Set to 0 to - * disable. If not specified the default is 0 (disabled). - */ - 'max_kill_timeout_jitter': (_google_protobuf_Duration__Output | null); - /** - * Register actions that will fire on given WatchDog events. - * See *WatchDogAction* for priority of events. - */ - 'actions': (_envoy_config_bootstrap_v3_Watchdog_WatchdogAction__Output)[]; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/Watchdogs.ts b/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/Watchdogs.ts deleted file mode 100644 index b478615ea..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/config/bootstrap/v3/Watchdogs.ts +++ /dev/null @@ -1,35 +0,0 @@ -// Original file: deps/envoy-api/envoy/config/bootstrap/v3/bootstrap.proto - -import type { Watchdog as _envoy_config_bootstrap_v3_Watchdog, Watchdog__Output as _envoy_config_bootstrap_v3_Watchdog__Output } from '../../../../envoy/config/bootstrap/v3/Watchdog'; - -/** - * Allows you to specify different watchdog configs for different subsystems. - * This allows finer tuned policies for the watchdog. If a subsystem is omitted - * the default values for that system will be used. - */ -export interface Watchdogs { - /** - * Watchdog for the main thread. - */ - 'main_thread_watchdog'?: (_envoy_config_bootstrap_v3_Watchdog | null); - /** - * Watchdog for the worker threads. - */ - 'worker_watchdog'?: (_envoy_config_bootstrap_v3_Watchdog | null); -} - -/** - * Allows you to specify different watchdog configs for different subsystems. - * This allows finer tuned policies for the watchdog. If a subsystem is omitted - * the default values for that system will be used. - */ -export interface Watchdogs__Output { - /** - * Watchdog for the main thread. - */ - 'main_thread_watchdog': (_envoy_config_bootstrap_v3_Watchdog__Output | null); - /** - * Watchdog for the worker threads. - */ - 'worker_watchdog': (_envoy_config_bootstrap_v3_Watchdog__Output | null); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/CircuitBreakers.ts b/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/CircuitBreakers.ts index 4a8a4be36..61f473134 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/CircuitBreakers.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/CircuitBreakers.ts @@ -1,6 +1,6 @@ // Original file: deps/envoy-api/envoy/config/cluster/v3/circuit_breaker.proto -import type { RoutingPriority as _envoy_config_core_v3_RoutingPriority } from '../../../../envoy/config/core/v3/RoutingPriority'; +import type { RoutingPriority as _envoy_config_core_v3_RoutingPriority, RoutingPriority__Output as _envoy_config_core_v3_RoutingPriority__Output } from '../../../../envoy/config/core/v3/RoutingPriority'; import type { UInt32Value as _google_protobuf_UInt32Value, UInt32Value__Output as _google_protobuf_UInt32Value__Output } from '../../../../google/protobuf/UInt32Value'; import type { Percent as _envoy_type_v3_Percent, Percent__Output as _envoy_type_v3_Percent__Output } from '../../../../envoy/type/v3/Percent'; @@ -50,7 +50,7 @@ export interface _envoy_config_cluster_v3_CircuitBreakers_Thresholds { * The :ref:`RoutingPriority` * the specified CircuitBreaker settings apply to. */ - 'priority'?: (_envoy_config_core_v3_RoutingPriority | keyof typeof _envoy_config_core_v3_RoutingPriority); + 'priority'?: (_envoy_config_core_v3_RoutingPriority); /** * The maximum number of connections that Envoy will make to the upstream * cluster. If not specified, the default is 1024. @@ -114,7 +114,7 @@ export interface _envoy_config_cluster_v3_CircuitBreakers_Thresholds__Output { * The :ref:`RoutingPriority` * the specified CircuitBreaker settings apply to. */ - 'priority': (keyof typeof _envoy_config_core_v3_RoutingPriority); + 'priority': (_envoy_config_core_v3_RoutingPriority__Output); /** * The maximum number of connections that Envoy will make to the upstream * cluster. If not specified, the default is 1024. diff --git a/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/Cluster.ts b/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/Cluster.ts index be30d8212..8cab36301 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/Cluster.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/Cluster.ts @@ -29,22 +29,37 @@ import type { Percent as _envoy_type_v3_Percent, Percent__Output as _envoy_type_ import type { UInt64Value as _google_protobuf_UInt64Value, UInt64Value__Output as _google_protobuf_UInt64Value__Output } from '../../../../google/protobuf/UInt64Value'; import type { HealthStatusSet as _envoy_config_core_v3_HealthStatusSet, HealthStatusSet__Output as _envoy_config_core_v3_HealthStatusSet__Output } from '../../../../envoy/config/core/v3/HealthStatusSet'; import type { DoubleValue as _google_protobuf_DoubleValue, DoubleValue__Output as _google_protobuf_DoubleValue__Output } from '../../../../google/protobuf/DoubleValue'; -import type { Long } from '@grpc/proto-loader'; // Original file: deps/envoy-api/envoy/config/cluster/v3/cluster.proto -export enum _envoy_config_cluster_v3_Cluster_ClusterProtocolSelection { +export const _envoy_config_cluster_v3_Cluster_ClusterProtocolSelection = { /** * Cluster can only operate on one of the possible upstream protocols (HTTP1.1, HTTP2). * If :ref:`http2_protocol_options ` are * present, HTTP2 will be used, otherwise HTTP1.1 will be used. */ - USE_CONFIGURED_PROTOCOL = 0, + USE_CONFIGURED_PROTOCOL: 'USE_CONFIGURED_PROTOCOL', /** * Use HTTP1.1 or HTTP2, depending on which one is used on the downstream connection. */ - USE_DOWNSTREAM_PROTOCOL = 1, -} + USE_DOWNSTREAM_PROTOCOL: 'USE_DOWNSTREAM_PROTOCOL', +} as const; + +export type _envoy_config_cluster_v3_Cluster_ClusterProtocolSelection = + /** + * Cluster can only operate on one of the possible upstream protocols (HTTP1.1, HTTP2). + * If :ref:`http2_protocol_options ` are + * present, HTTP2 will be used, otherwise HTTP1.1 will be used. + */ + | 'USE_CONFIGURED_PROTOCOL' + | 0 + /** + * Use HTTP1.1 or HTTP2, depending on which one is used on the downstream connection. + */ + | 'USE_DOWNSTREAM_PROTOCOL' + | 1 + +export type _envoy_config_cluster_v3_Cluster_ClusterProtocolSelection__Output = typeof _envoy_config_cluster_v3_Cluster_ClusterProtocolSelection[keyof typeof _envoy_config_cluster_v3_Cluster_ClusterProtocolSelection] /** * Common configuration for all load balancer implementations. @@ -268,36 +283,81 @@ export interface _envoy_config_cluster_v3_Cluster_CustomClusterType__Output { * Refer to :ref:`service discovery type ` * for an explanation on each type. */ -export enum _envoy_config_cluster_v3_Cluster_DiscoveryType { +export const _envoy_config_cluster_v3_Cluster_DiscoveryType = { /** * Refer to the :ref:`static discovery type` * for an explanation. */ - STATIC = 0, + STATIC: 'STATIC', /** * Refer to the :ref:`strict DNS discovery * type` * for an explanation. */ - STRICT_DNS = 1, + STRICT_DNS: 'STRICT_DNS', /** * Refer to the :ref:`logical DNS discovery * type` * for an explanation. */ - LOGICAL_DNS = 2, + LOGICAL_DNS: 'LOGICAL_DNS', /** * Refer to the :ref:`service discovery type` * for an explanation. */ - EDS = 3, + EDS: 'EDS', /** * Refer to the :ref:`original destination discovery * type` * for an explanation. */ - ORIGINAL_DST = 4, -} + ORIGINAL_DST: 'ORIGINAL_DST', +} as const; + +/** + * Refer to :ref:`service discovery type ` + * for an explanation on each type. + */ +export type _envoy_config_cluster_v3_Cluster_DiscoveryType = + /** + * Refer to the :ref:`static discovery type` + * for an explanation. + */ + | 'STATIC' + | 0 + /** + * Refer to the :ref:`strict DNS discovery + * type` + * for an explanation. + */ + | 'STRICT_DNS' + | 1 + /** + * Refer to the :ref:`logical DNS discovery + * type` + * for an explanation. + */ + | 'LOGICAL_DNS' + | 2 + /** + * Refer to the :ref:`service discovery type` + * for an explanation. + */ + | 'EDS' + | 3 + /** + * Refer to the :ref:`original destination discovery + * type` + * for an explanation. + */ + | 'ORIGINAL_DST' + | 4 + +/** + * Refer to :ref:`service discovery type ` + * for an explanation on each type. + */ +export type _envoy_config_cluster_v3_Cluster_DiscoveryType__Output = typeof _envoy_config_cluster_v3_Cluster_DiscoveryType[keyof typeof _envoy_config_cluster_v3_Cluster_DiscoveryType] // Original file: deps/envoy-api/envoy/config/cluster/v3/cluster.proto @@ -324,13 +384,73 @@ export enum _envoy_config_cluster_v3_Cluster_DiscoveryType { * ignored. * [#next-major-version: deprecate AUTO in favor of a V6_PREFERRED option.] */ -export enum _envoy_config_cluster_v3_Cluster_DnsLookupFamily { - AUTO = 0, - V4_ONLY = 1, - V6_ONLY = 2, - V4_PREFERRED = 3, - ALL = 4, -} +export const _envoy_config_cluster_v3_Cluster_DnsLookupFamily = { + AUTO: 'AUTO', + V4_ONLY: 'V4_ONLY', + V6_ONLY: 'V6_ONLY', + V4_PREFERRED: 'V4_PREFERRED', + ALL: 'ALL', +} as const; + +/** + * When V4_ONLY is selected, the DNS resolver will only perform a lookup for + * addresses in the IPv4 family. If V6_ONLY is selected, the DNS resolver will + * only perform a lookup for addresses in the IPv6 family. If AUTO is + * specified, the DNS resolver will first perform a lookup for addresses in + * the IPv6 family and fallback to a lookup for addresses in the IPv4 family. + * This is semantically equivalent to a non-existent V6_PREFERRED option. + * AUTO is a legacy name that is more opaque than + * necessary and will be deprecated in favor of V6_PREFERRED in a future major version of the API. + * If V4_PREFERRED is specified, the DNS resolver will first perform a lookup for addresses in the + * IPv4 family and fallback to a lookup for addresses in the IPv6 family. i.e., the callback + * target will only get v6 addresses if there were NO v4 addresses to return. + * If ALL is specified, the DNS resolver will perform a lookup for both IPv4 and IPv6 families, + * and return all resolved addresses. When this is used, Happy Eyeballs will be enabled for + * upstream connections. Refer to :ref:`Happy Eyeballs Support ` + * for more information. + * For cluster types other than + * :ref:`STRICT_DNS` and + * :ref:`LOGICAL_DNS`, + * this setting is + * ignored. + * [#next-major-version: deprecate AUTO in favor of a V6_PREFERRED option.] + */ +export type _envoy_config_cluster_v3_Cluster_DnsLookupFamily = + | 'AUTO' + | 0 + | 'V4_ONLY' + | 1 + | 'V6_ONLY' + | 2 + | 'V4_PREFERRED' + | 3 + | 'ALL' + | 4 + +/** + * When V4_ONLY is selected, the DNS resolver will only perform a lookup for + * addresses in the IPv4 family. If V6_ONLY is selected, the DNS resolver will + * only perform a lookup for addresses in the IPv6 family. If AUTO is + * specified, the DNS resolver will first perform a lookup for addresses in + * the IPv6 family and fallback to a lookup for addresses in the IPv4 family. + * This is semantically equivalent to a non-existent V6_PREFERRED option. + * AUTO is a legacy name that is more opaque than + * necessary and will be deprecated in favor of V6_PREFERRED in a future major version of the API. + * If V4_PREFERRED is specified, the DNS resolver will first perform a lookup for addresses in the + * IPv4 family and fallback to a lookup for addresses in the IPv6 family. i.e., the callback + * target will only get v6 addresses if there were NO v4 addresses to return. + * If ALL is specified, the DNS resolver will perform a lookup for both IPv4 and IPv6 families, + * and return all resolved addresses. When this is used, Happy Eyeballs will be enabled for + * upstream connections. Refer to :ref:`Happy Eyeballs Support ` + * for more information. + * For cluster types other than + * :ref:`STRICT_DNS` and + * :ref:`LOGICAL_DNS`, + * this setting is + * ignored. + * [#next-major-version: deprecate AUTO in favor of a V6_PREFERRED option.] + */ +export type _envoy_config_cluster_v3_Cluster_DnsLookupFamily__Output = typeof _envoy_config_cluster_v3_Cluster_DnsLookupFamily[keyof typeof _envoy_config_cluster_v3_Cluster_DnsLookupFamily] /** * Only valid when discovery type is EDS. @@ -369,18 +489,40 @@ export interface _envoy_config_cluster_v3_Cluster_EdsClusterConfig__Output { /** * The hash function used to hash hosts onto the ketama ring. */ -export enum _envoy_config_cluster_v3_Cluster_RingHashLbConfig_HashFunction { +export const _envoy_config_cluster_v3_Cluster_RingHashLbConfig_HashFunction = { /** * Use `xxHash `_, this is the default hash function. */ - XX_HASH = 0, + XX_HASH: 'XX_HASH', /** * Use `MurmurHash2 `_, this is compatible with * std:hash in GNU libstdc++ 3.4.20 or above. This is typically the case when compiled * on Linux and not macOS. */ - MURMUR_HASH_2 = 1, -} + MURMUR_HASH_2: 'MURMUR_HASH_2', +} as const; + +/** + * The hash function used to hash hosts onto the ketama ring. + */ +export type _envoy_config_cluster_v3_Cluster_RingHashLbConfig_HashFunction = + /** + * Use `xxHash `_, this is the default hash function. + */ + | 'XX_HASH' + | 0 + /** + * Use `MurmurHash2 `_, this is compatible with + * std:hash in GNU libstdc++ 3.4.20 or above. This is typically the case when compiled + * on Linux and not macOS. + */ + | 'MURMUR_HASH_2' + | 1 + +/** + * The hash function used to hash hosts onto the ketama ring. + */ +export type _envoy_config_cluster_v3_Cluster_RingHashLbConfig_HashFunction__Output = typeof _envoy_config_cluster_v3_Cluster_RingHashLbConfig_HashFunction[keyof typeof _envoy_config_cluster_v3_Cluster_RingHashLbConfig_HashFunction] // Original file: deps/envoy-api/envoy/config/cluster/v3/cluster.proto @@ -388,42 +530,42 @@ export enum _envoy_config_cluster_v3_Cluster_RingHashLbConfig_HashFunction { * Refer to :ref:`load balancer type ` architecture * overview section for information on each type. */ -export enum _envoy_config_cluster_v3_Cluster_LbPolicy { +export const _envoy_config_cluster_v3_Cluster_LbPolicy = { /** * Refer to the :ref:`round robin load balancing * policy` * for an explanation. */ - ROUND_ROBIN = 0, + ROUND_ROBIN: 'ROUND_ROBIN', /** * Refer to the :ref:`least request load balancing * policy` * for an explanation. */ - LEAST_REQUEST = 1, + LEAST_REQUEST: 'LEAST_REQUEST', /** * Refer to the :ref:`ring hash load balancing * policy` * for an explanation. */ - RING_HASH = 2, + RING_HASH: 'RING_HASH', /** * Refer to the :ref:`random load balancing * policy` * for an explanation. */ - RANDOM = 3, + RANDOM: 'RANDOM', /** * Refer to the :ref:`Maglev load balancing policy` * for an explanation. */ - MAGLEV = 5, + MAGLEV: 'MAGLEV', /** * This load balancer type must be specified if the configured cluster provides a cluster * specific load balancer. Consult the configured cluster's documentation for whether to set * this option or not. */ - CLUSTER_PROVIDED = 6, + CLUSTER_PROVIDED: 'CLUSTER_PROVIDED', /** * Use the new :ref:`load_balancing_policy * ` field to determine the LB policy. @@ -431,8 +573,70 @@ export enum _envoy_config_cluster_v3_Cluster_LbPolicy { * ` field without * setting any value in :ref:`lb_policy`. */ - LOAD_BALANCING_POLICY_CONFIG = 7, -} + LOAD_BALANCING_POLICY_CONFIG: 'LOAD_BALANCING_POLICY_CONFIG', +} as const; + +/** + * Refer to :ref:`load balancer type ` architecture + * overview section for information on each type. + */ +export type _envoy_config_cluster_v3_Cluster_LbPolicy = + /** + * Refer to the :ref:`round robin load balancing + * policy` + * for an explanation. + */ + | 'ROUND_ROBIN' + | 0 + /** + * Refer to the :ref:`least request load balancing + * policy` + * for an explanation. + */ + | 'LEAST_REQUEST' + | 1 + /** + * Refer to the :ref:`ring hash load balancing + * policy` + * for an explanation. + */ + | 'RING_HASH' + | 2 + /** + * Refer to the :ref:`random load balancing + * policy` + * for an explanation. + */ + | 'RANDOM' + | 3 + /** + * Refer to the :ref:`Maglev load balancing policy` + * for an explanation. + */ + | 'MAGLEV' + | 5 + /** + * This load balancer type must be specified if the configured cluster provides a cluster + * specific load balancer. Consult the configured cluster's documentation for whether to set + * this option or not. + */ + | 'CLUSTER_PROVIDED' + | 6 + /** + * Use the new :ref:`load_balancing_policy + * ` field to determine the LB policy. + * This has been deprecated in favor of using the :ref:`load_balancing_policy + * ` field without + * setting any value in :ref:`lb_policy`. + */ + | 'LOAD_BALANCING_POLICY_CONFIG' + | 7 + +/** + * Refer to :ref:`load balancer type ` architecture + * overview section for information on each type. + */ +export type _envoy_config_cluster_v3_Cluster_LbPolicy__Output = typeof _envoy_config_cluster_v3_Cluster_LbPolicy[keyof typeof _envoy_config_cluster_v3_Cluster_LbPolicy] /** * Optionally divide the endpoints in this cluster into subsets defined by @@ -445,7 +649,7 @@ export interface _envoy_config_cluster_v3_Cluster_LbSubsetConfig { * metadata. The value defaults to * :ref:`NO_FALLBACK`. */ - 'fallback_policy'?: (_envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetFallbackPolicy | keyof typeof _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetFallbackPolicy); + 'fallback_policy'?: (_envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetFallbackPolicy); /** * Specifies the default subset of endpoints used during fallback if * fallback_policy is @@ -518,7 +722,7 @@ export interface _envoy_config_cluster_v3_Cluster_LbSubsetConfig { * The value defaults to * :ref:`METADATA_NO_FALLBACK`. */ - 'metadata_fallback_policy'?: (_envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetMetadataFallbackPolicy | keyof typeof _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetMetadataFallbackPolicy); + 'metadata_fallback_policy'?: (_envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetMetadataFallbackPolicy); } /** @@ -532,7 +736,7 @@ export interface _envoy_config_cluster_v3_Cluster_LbSubsetConfig__Output { * metadata. The value defaults to * :ref:`NO_FALLBACK`. */ - 'fallback_policy': (keyof typeof _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetFallbackPolicy); + 'fallback_policy': (_envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetFallbackPolicy__Output); /** * Specifies the default subset of endpoints used during fallback if * fallback_policy is @@ -605,7 +809,7 @@ export interface _envoy_config_cluster_v3_Cluster_LbSubsetConfig__Output { * The value defaults to * :ref:`METADATA_NO_FALLBACK`. */ - 'metadata_fallback_policy': (keyof typeof _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetMetadataFallbackPolicy); + 'metadata_fallback_policy': (_envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetMetadataFallbackPolicy__Output); } // Original file: deps/envoy-api/envoy/config/cluster/v3/cluster.proto @@ -617,19 +821,43 @@ export interface _envoy_config_cluster_v3_Cluster_LbSubsetConfig__Output { * etc). If DEFAULT_SUBSET is selected, load balancing is performed over the * endpoints matching the values from the default_subset field. */ -export enum _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetFallbackPolicy { - NO_FALLBACK = 0, - ANY_ENDPOINT = 1, - DEFAULT_SUBSET = 2, -} +export const _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetFallbackPolicy = { + NO_FALLBACK: 'NO_FALLBACK', + ANY_ENDPOINT: 'ANY_ENDPOINT', + DEFAULT_SUBSET: 'DEFAULT_SUBSET', +} as const; + +/** + * If NO_FALLBACK is selected, a result + * equivalent to no healthy hosts is reported. If ANY_ENDPOINT is selected, + * any cluster endpoint may be returned (subject to policy, health checks, + * etc). If DEFAULT_SUBSET is selected, load balancing is performed over the + * endpoints matching the values from the default_subset field. + */ +export type _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetFallbackPolicy = + | 'NO_FALLBACK' + | 0 + | 'ANY_ENDPOINT' + | 1 + | 'DEFAULT_SUBSET' + | 2 + +/** + * If NO_FALLBACK is selected, a result + * equivalent to no healthy hosts is reported. If ANY_ENDPOINT is selected, + * any cluster endpoint may be returned (subject to policy, health checks, + * etc). If DEFAULT_SUBSET is selected, load balancing is performed over the + * endpoints matching the values from the default_subset field. + */ +export type _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetFallbackPolicy__Output = typeof _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetFallbackPolicy[keyof typeof _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetFallbackPolicy] // Original file: deps/envoy-api/envoy/config/cluster/v3/cluster.proto -export enum _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetMetadataFallbackPolicy { +export const _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetMetadataFallbackPolicy = { /** * No fallback. Route metadata will be used as-is. */ - METADATA_NO_FALLBACK = 0, + METADATA_NO_FALLBACK: 'METADATA_NO_FALLBACK', /** * A special metadata key ``fallback_list`` will be used to provide variants of metadata to try. * Value of ``fallback_list`` key has to be a list. Every list element has to be a struct - it will @@ -671,8 +899,60 @@ export enum _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetMetadataFall * * is used. */ - FALLBACK_LIST = 1, -} + FALLBACK_LIST: 'FALLBACK_LIST', +} as const; + +export type _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetMetadataFallbackPolicy = + /** + * No fallback. Route metadata will be used as-is. + */ + | 'METADATA_NO_FALLBACK' + | 0 + /** + * A special metadata key ``fallback_list`` will be used to provide variants of metadata to try. + * Value of ``fallback_list`` key has to be a list. Every list element has to be a struct - it will + * be merged with route metadata, overriding keys that appear in both places. + * ``fallback_list`` entries will be used in order until a host is found. + * + * ``fallback_list`` key itself is removed from metadata before subset load balancing is performed. + * + * Example: + * + * for metadata: + * + * .. code-block:: yaml + * + * version: 1.0 + * fallback_list: + * - version: 2.0 + * hardware: c64 + * - hardware: c32 + * - version: 3.0 + * + * at first, metadata: + * + * .. code-block:: json + * + * {"version": "2.0", "hardware": "c64"} + * + * will be used for load balancing. If no host is found, metadata: + * + * .. code-block:: json + * + * {"version": "1.0", "hardware": "c32"} + * + * is next to try. If it still results in no host, finally metadata: + * + * .. code-block:: json + * + * {"version": "3.0"} + * + * is used. + */ + | 'FALLBACK_LIST' + | 1 + +export type _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetMetadataFallbackPolicy__Output = typeof _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetMetadataFallbackPolicy[keyof typeof _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetMetadataFallbackPolicy] /** * Specifications for subsets. @@ -698,7 +978,7 @@ export interface _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetSelecto * The behavior used when no endpoint subset matches the selected route's * metadata. */ - 'fallback_policy'?: (_envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetSelector_LbSubsetSelectorFallbackPolicy | keyof typeof _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetSelector_LbSubsetSelectorFallbackPolicy); + 'fallback_policy'?: (_envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetSelector_LbSubsetSelectorFallbackPolicy); /** * Subset of * :ref:`keys` used by @@ -737,7 +1017,7 @@ export interface _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetSelecto * The behavior used when no endpoint subset matches the selected route's * metadata. */ - 'fallback_policy': (keyof typeof _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetSelector_LbSubsetSelectorFallbackPolicy); + 'fallback_policy': (_envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetSelector_LbSubsetSelectorFallbackPolicy__Output); /** * Subset of * :ref:`keys` used by @@ -757,25 +1037,25 @@ export interface _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetSelecto /** * Allows to override top level fallback policy per selector. */ -export enum _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetSelector_LbSubsetSelectorFallbackPolicy { +export const _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetSelector_LbSubsetSelectorFallbackPolicy = { /** * If NOT_DEFINED top level config fallback policy is used instead. */ - NOT_DEFINED = 0, + NOT_DEFINED: 'NOT_DEFINED', /** * If NO_FALLBACK is selected, a result equivalent to no healthy hosts is reported. */ - NO_FALLBACK = 1, + NO_FALLBACK: 'NO_FALLBACK', /** * If ANY_ENDPOINT is selected, any cluster endpoint may be returned * (subject to policy, health checks, etc). */ - ANY_ENDPOINT = 2, + ANY_ENDPOINT: 'ANY_ENDPOINT', /** * If DEFAULT_SUBSET is selected, load balancing is performed over the * endpoints matching the values from the default_subset field. */ - DEFAULT_SUBSET = 3, + DEFAULT_SUBSET: 'DEFAULT_SUBSET', /** * If KEYS_SUBSET is selected, subset selector matching is performed again with metadata * keys reduced to @@ -783,8 +1063,49 @@ export enum _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetSelector_LbS * It allows for a fallback to a different, less specific selector if some of the keys of * the selector are considered optional. */ - KEYS_SUBSET = 4, -} + KEYS_SUBSET: 'KEYS_SUBSET', +} as const; + +/** + * Allows to override top level fallback policy per selector. + */ +export type _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetSelector_LbSubsetSelectorFallbackPolicy = + /** + * If NOT_DEFINED top level config fallback policy is used instead. + */ + | 'NOT_DEFINED' + | 0 + /** + * If NO_FALLBACK is selected, a result equivalent to no healthy hosts is reported. + */ + | 'NO_FALLBACK' + | 1 + /** + * If ANY_ENDPOINT is selected, any cluster endpoint may be returned + * (subject to policy, health checks, etc). + */ + | 'ANY_ENDPOINT' + | 2 + /** + * If DEFAULT_SUBSET is selected, load balancing is performed over the + * endpoints matching the values from the default_subset field. + */ + | 'DEFAULT_SUBSET' + | 3 + /** + * If KEYS_SUBSET is selected, subset selector matching is performed again with metadata + * keys reduced to + * :ref:`fallback_keys_subset`. + * It allows for a fallback to a different, less specific selector if some of the keys of + * the selector are considered optional. + */ + | 'KEYS_SUBSET' + | 4 + +/** + * Allows to override top level fallback policy per selector. + */ +export type _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetSelector_LbSubsetSelectorFallbackPolicy__Output = typeof _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetSelector_LbSubsetSelectorFallbackPolicy[keyof typeof _envoy_config_cluster_v3_Cluster_LbSubsetConfig_LbSubsetSelector_LbSubsetSelectorFallbackPolicy] /** * Specific configuration for the LeastRequest load balancing policy. @@ -1138,7 +1459,7 @@ export interface _envoy_config_cluster_v3_Cluster_RingHashLbConfig { * The hash function used to hash hosts onto the ketama ring. The value defaults to * :ref:`XX_HASH`. */ - 'hash_function'?: (_envoy_config_cluster_v3_Cluster_RingHashLbConfig_HashFunction | keyof typeof _envoy_config_cluster_v3_Cluster_RingHashLbConfig_HashFunction); + 'hash_function'?: (_envoy_config_cluster_v3_Cluster_RingHashLbConfig_HashFunction); /** * Maximum hash ring size. Defaults to 8M entries, and limited to 8M entries, but can be lowered * to further constrain resource use. See also @@ -1163,7 +1484,7 @@ export interface _envoy_config_cluster_v3_Cluster_RingHashLbConfig__Output { * The hash function used to hash hosts onto the ketama ring. The value defaults to * :ref:`XX_HASH`. */ - 'hash_function': (keyof typeof _envoy_config_cluster_v3_Cluster_RingHashLbConfig_HashFunction); + 'hash_function': (_envoy_config_cluster_v3_Cluster_RingHashLbConfig_HashFunction__Output); /** * Maximum hash ring size. Defaults to 8M entries, and limited to 8M entries, but can be lowered * to further constrain resource use. See also @@ -1383,7 +1704,7 @@ export interface Cluster { * The :ref:`service discovery type ` * to use for resolving the cluster. */ - 'type'?: (_envoy_config_cluster_v3_Cluster_DiscoveryType | keyof typeof _envoy_config_cluster_v3_Cluster_DiscoveryType); + 'type'?: (_envoy_config_cluster_v3_Cluster_DiscoveryType); /** * Configuration to use for EDS updates for the Cluster. */ @@ -1402,7 +1723,7 @@ export interface Cluster { * The :ref:`load balancer type ` to use * when picking a host in the cluster. */ - 'lb_policy'?: (_envoy_config_cluster_v3_Cluster_LbPolicy | keyof typeof _envoy_config_cluster_v3_Cluster_LbPolicy); + 'lb_policy'?: (_envoy_config_cluster_v3_Cluster_LbPolicy); /** * Optional :ref:`active health checking ` * configuration for the cluster. If no @@ -1418,6 +1739,7 @@ export interface Cluster { * * .. attention:: * This field has been deprecated in favor of the :ref:`max_requests_per_connection ` field. + * @deprecated */ 'max_requests_per_connection'?: (_google_protobuf_UInt32Value | null); /** @@ -1433,6 +1755,7 @@ export interface Cluster { * See :ref:`upstream_http_protocol_options * ` * for example usage. + * @deprecated */ 'http_protocol_options'?: (_envoy_config_core_v3_Http1ProtocolOptions | null); /** @@ -1449,6 +1772,7 @@ export interface Cluster { * See :ref:`upstream_http_protocol_options * ` * for example usage. + * @deprecated */ 'http2_protocol_options'?: (_envoy_config_core_v3_Http2ProtocolOptions | null); /** @@ -1468,7 +1792,7 @@ export interface Cluster { * value defaults to * :ref:`AUTO`. */ - 'dns_lookup_family'?: (_envoy_config_cluster_v3_Cluster_DnsLookupFamily | keyof typeof _envoy_config_cluster_v3_Cluster_DnsLookupFamily); + 'dns_lookup_family'?: (_envoy_config_cluster_v3_Cluster_DnsLookupFamily); /** * If DNS resolvers are specified and the cluster type is either * :ref:`STRICT_DNS`, @@ -1482,6 +1806,7 @@ export interface Cluster { * this setting is ignored. * This field is deprecated in favor of ``dns_resolution_config`` * which aggregates all of the DNS resolver configuration in a single message. + * @deprecated */ 'dns_resolvers'?: (_envoy_config_core_v3_Address)[]; /** @@ -1543,8 +1868,9 @@ export interface Cluster { * ` message. * http_protocol_options can be set via the cluster's * :ref:`extension_protocol_options`. + * @deprecated */ - 'protocol_selection'?: (_envoy_config_cluster_v3_Cluster_ClusterProtocolSelection | keyof typeof _envoy_config_cluster_v3_Cluster_ClusterProtocolSelection); + 'protocol_selection'?: (_envoy_config_cluster_v3_Cluster_ClusterProtocolSelection); /** * Common configuration for all load balancer implementations. */ @@ -1570,6 +1896,7 @@ export interface Cluster { * See :ref:`upstream_http_protocol_options * ` * for example usage. + * @deprecated */ 'common_http_protocol_options'?: (_envoy_config_core_v3_HttpProtocolOptions | null); /** @@ -1732,6 +2059,7 @@ export interface Cluster { * Always use TCP queries instead of UDP queries for DNS lookups. * This field is deprecated in favor of ``dns_resolution_config`` * which aggregates all of the DNS resolver configuration in a single message. + * @deprecated */ 'use_tcp_for_dns_lookups'?: (boolean); /** @@ -1745,6 +2073,7 @@ export interface Cluster { * See :ref:`upstream_http_protocol_options * ` * for example usage. + * @deprecated */ 'upstream_http_protocol_options'?: (_envoy_config_core_v3_UpstreamHttpProtocolOptions | null); /** @@ -1758,6 +2087,7 @@ export interface Cluster { * * This field has been deprecated in favor of ``timeout_budgets``, part of * :ref:`track_cluster_stats `. + * @deprecated */ 'track_timeout_budgets'?: (boolean); /** @@ -1802,6 +2132,7 @@ export interface Cluster { * DNS resolution configuration which includes the underlying dns resolver addresses and options. * This field is deprecated in favor of * :ref:`typed_dns_resolver_config `. + * @deprecated */ 'dns_resolution_config'?: (_envoy_config_core_v3_DnsResolutionConfig | null); /** @@ -1862,7 +2193,7 @@ export interface Cluster__Output { * The :ref:`service discovery type ` * to use for resolving the cluster. */ - 'type'?: (keyof typeof _envoy_config_cluster_v3_Cluster_DiscoveryType); + 'type'?: (_envoy_config_cluster_v3_Cluster_DiscoveryType__Output); /** * Configuration to use for EDS updates for the Cluster. */ @@ -1881,7 +2212,7 @@ export interface Cluster__Output { * The :ref:`load balancer type ` to use * when picking a host in the cluster. */ - 'lb_policy': (keyof typeof _envoy_config_cluster_v3_Cluster_LbPolicy); + 'lb_policy': (_envoy_config_cluster_v3_Cluster_LbPolicy__Output); /** * Optional :ref:`active health checking ` * configuration for the cluster. If no @@ -1897,6 +2228,7 @@ export interface Cluster__Output { * * .. attention:: * This field has been deprecated in favor of the :ref:`max_requests_per_connection ` field. + * @deprecated */ 'max_requests_per_connection': (_google_protobuf_UInt32Value__Output | null); /** @@ -1912,6 +2244,7 @@ export interface Cluster__Output { * See :ref:`upstream_http_protocol_options * ` * for example usage. + * @deprecated */ 'http_protocol_options': (_envoy_config_core_v3_Http1ProtocolOptions__Output | null); /** @@ -1928,6 +2261,7 @@ export interface Cluster__Output { * See :ref:`upstream_http_protocol_options * ` * for example usage. + * @deprecated */ 'http2_protocol_options': (_envoy_config_core_v3_Http2ProtocolOptions__Output | null); /** @@ -1947,7 +2281,7 @@ export interface Cluster__Output { * value defaults to * :ref:`AUTO`. */ - 'dns_lookup_family': (keyof typeof _envoy_config_cluster_v3_Cluster_DnsLookupFamily); + 'dns_lookup_family': (_envoy_config_cluster_v3_Cluster_DnsLookupFamily__Output); /** * If DNS resolvers are specified and the cluster type is either * :ref:`STRICT_DNS`, @@ -1961,6 +2295,7 @@ export interface Cluster__Output { * this setting is ignored. * This field is deprecated in favor of ``dns_resolution_config`` * which aggregates all of the DNS resolver configuration in a single message. + * @deprecated */ 'dns_resolvers': (_envoy_config_core_v3_Address__Output)[]; /** @@ -2022,8 +2357,9 @@ export interface Cluster__Output { * ` message. * http_protocol_options can be set via the cluster's * :ref:`extension_protocol_options`. + * @deprecated */ - 'protocol_selection': (keyof typeof _envoy_config_cluster_v3_Cluster_ClusterProtocolSelection); + 'protocol_selection': (_envoy_config_cluster_v3_Cluster_ClusterProtocolSelection__Output); /** * Common configuration for all load balancer implementations. */ @@ -2049,6 +2385,7 @@ export interface Cluster__Output { * See :ref:`upstream_http_protocol_options * ` * for example usage. + * @deprecated */ 'common_http_protocol_options': (_envoy_config_core_v3_HttpProtocolOptions__Output | null); /** @@ -2211,6 +2548,7 @@ export interface Cluster__Output { * Always use TCP queries instead of UDP queries for DNS lookups. * This field is deprecated in favor of ``dns_resolution_config`` * which aggregates all of the DNS resolver configuration in a single message. + * @deprecated */ 'use_tcp_for_dns_lookups': (boolean); /** @@ -2224,6 +2562,7 @@ export interface Cluster__Output { * See :ref:`upstream_http_protocol_options * ` * for example usage. + * @deprecated */ 'upstream_http_protocol_options': (_envoy_config_core_v3_UpstreamHttpProtocolOptions__Output | null); /** @@ -2237,6 +2576,7 @@ export interface Cluster__Output { * * This field has been deprecated in favor of ``timeout_budgets``, part of * :ref:`track_cluster_stats `. + * @deprecated */ 'track_timeout_budgets': (boolean); /** @@ -2281,6 +2621,7 @@ export interface Cluster__Output { * DNS resolution configuration which includes the underlying dns resolver addresses and options. * This field is deprecated in favor of * :ref:`typed_dns_resolver_config `. + * @deprecated */ 'dns_resolution_config': (_envoy_config_core_v3_DnsResolutionConfig__Output | null); /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/UpstreamBindConfig.ts b/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/UpstreamBindConfig.ts deleted file mode 100644 index f2dbd0608..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/config/cluster/v3/UpstreamBindConfig.ts +++ /dev/null @@ -1,25 +0,0 @@ -// Original file: deps/envoy-api/envoy/config/cluster/v3/cluster.proto - -import type { Address as _envoy_config_core_v3_Address, Address__Output as _envoy_config_core_v3_Address__Output } from '../../../../envoy/config/core/v3/Address'; - -/** - * An extensible structure containing the address Envoy should bind to when - * establishing upstream connections. - */ -export interface UpstreamBindConfig { - /** - * The address Envoy should bind to when establishing upstream connections. - */ - 'source_address'?: (_envoy_config_core_v3_Address | null); -} - -/** - * An extensible structure containing the address Envoy should bind to when - * establishing upstream connections. - */ -export interface UpstreamBindConfig__Output { - /** - * The address Envoy should bind to when establishing upstream connections. - */ - 'source_address': (_envoy_config_core_v3_Address__Output | null); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ApiConfigSource.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ApiConfigSource.ts index 691ab93ba..0a2f11bdf 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ApiConfigSource.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ApiConfigSource.ts @@ -3,7 +3,7 @@ import type { Duration as _google_protobuf_Duration, Duration__Output as _google_protobuf_Duration__Output } from '../../../../google/protobuf/Duration'; import type { GrpcService as _envoy_config_core_v3_GrpcService, GrpcService__Output as _envoy_config_core_v3_GrpcService__Output } from '../../../../envoy/config/core/v3/GrpcService'; import type { RateLimitSettings as _envoy_config_core_v3_RateLimitSettings, RateLimitSettings__Output as _envoy_config_core_v3_RateLimitSettings__Output } from '../../../../envoy/config/core/v3/RateLimitSettings'; -import type { ApiVersion as _envoy_config_core_v3_ApiVersion } from '../../../../envoy/config/core/v3/ApiVersion'; +import type { ApiVersion as _envoy_config_core_v3_ApiVersion, ApiVersion__Output as _envoy_config_core_v3_ApiVersion__Output } from '../../../../envoy/config/core/v3/ApiVersion'; import type { TypedExtensionConfig as _envoy_config_core_v3_TypedExtensionConfig, TypedExtensionConfig__Output as _envoy_config_core_v3_TypedExtensionConfig__Output } from '../../../../envoy/config/core/v3/TypedExtensionConfig'; // Original file: deps/envoy-api/envoy/config/core/v3/config_source.proto @@ -11,41 +11,91 @@ import type { TypedExtensionConfig as _envoy_config_core_v3_TypedExtensionConfig /** * APIs may be fetched via either REST or gRPC. */ -export enum _envoy_config_core_v3_ApiConfigSource_ApiType { +export const _envoy_config_core_v3_ApiConfigSource_ApiType = { /** * Ideally this would be 'reserved 0' but one can't reserve the default * value. Instead we throw an exception if this is ever used. + * @deprecated */ - DEPRECATED_AND_UNAVAILABLE_DO_NOT_USE = 0, + DEPRECATED_AND_UNAVAILABLE_DO_NOT_USE: 'DEPRECATED_AND_UNAVAILABLE_DO_NOT_USE', /** * REST-JSON v2 API. The `canonical JSON encoding * `_ for * the v2 protos is used. */ - REST = 1, + REST: 'REST', /** * SotW gRPC service. */ - GRPC = 2, + GRPC: 'GRPC', /** * Using the delta xDS gRPC service, i.e. DeltaDiscovery{Request,Response} * rather than Discovery{Request,Response}. Rather than sending Envoy the entire state * with every update, the xDS server only sends what has changed since the last update. */ - DELTA_GRPC = 3, + DELTA_GRPC: 'DELTA_GRPC', /** * SotW xDS gRPC with ADS. All resources which resolve to this configuration source will be * multiplexed on a single connection to an ADS endpoint. * [#not-implemented-hide:] */ - AGGREGATED_GRPC = 5, + AGGREGATED_GRPC: 'AGGREGATED_GRPC', /** * Delta xDS gRPC with ADS. All resources which resolve to this configuration source will be * multiplexed on a single connection to an ADS endpoint. * [#not-implemented-hide:] */ - AGGREGATED_DELTA_GRPC = 6, -} + AGGREGATED_DELTA_GRPC: 'AGGREGATED_DELTA_GRPC', +} as const; + +/** + * APIs may be fetched via either REST or gRPC. + */ +export type _envoy_config_core_v3_ApiConfigSource_ApiType = + /** + * Ideally this would be 'reserved 0' but one can't reserve the default + * value. Instead we throw an exception if this is ever used. + */ + | 'DEPRECATED_AND_UNAVAILABLE_DO_NOT_USE' + | 0 + /** + * REST-JSON v2 API. The `canonical JSON encoding + * `_ for + * the v2 protos is used. + */ + | 'REST' + | 1 + /** + * SotW gRPC service. + */ + | 'GRPC' + | 2 + /** + * Using the delta xDS gRPC service, i.e. DeltaDiscovery{Request,Response} + * rather than Discovery{Request,Response}. Rather than sending Envoy the entire state + * with every update, the xDS server only sends what has changed since the last update. + */ + | 'DELTA_GRPC' + | 3 + /** + * SotW xDS gRPC with ADS. All resources which resolve to this configuration source will be + * multiplexed on a single connection to an ADS endpoint. + * [#not-implemented-hide:] + */ + | 'AGGREGATED_GRPC' + | 5 + /** + * Delta xDS gRPC with ADS. All resources which resolve to this configuration source will be + * multiplexed on a single connection to an ADS endpoint. + * [#not-implemented-hide:] + */ + | 'AGGREGATED_DELTA_GRPC' + | 6 + +/** + * APIs may be fetched via either REST or gRPC. + */ +export type _envoy_config_core_v3_ApiConfigSource_ApiType__Output = typeof _envoy_config_core_v3_ApiConfigSource_ApiType[keyof typeof _envoy_config_core_v3_ApiConfigSource_ApiType] /** * API configuration source. This identifies the API type and cluster that Envoy @@ -56,7 +106,7 @@ export interface ApiConfigSource { /** * API type (gRPC, REST, delta gRPC) */ - 'api_type'?: (_envoy_config_core_v3_ApiConfigSource_ApiType | keyof typeof _envoy_config_core_v3_ApiConfigSource_ApiType); + 'api_type'?: (_envoy_config_core_v3_ApiConfigSource_ApiType); /** * Cluster names should be used only with REST. If > 1 * cluster is defined, clusters will be cycled through if any kind of failure @@ -94,7 +144,7 @@ export interface ApiConfigSource { * API version for xDS transport protocol. This describes the xDS gRPC/REST * endpoint and version of [Delta]DiscoveryRequest/Response used on the wire. */ - 'transport_api_version'?: (_envoy_config_core_v3_ApiVersion | keyof typeof _envoy_config_core_v3_ApiVersion); + 'transport_api_version'?: (_envoy_config_core_v3_ApiVersion); /** * A list of config validators that will be executed when a new update is * received from the ApiConfigSource. Note that each validator handles a @@ -117,7 +167,7 @@ export interface ApiConfigSource__Output { /** * API type (gRPC, REST, delta gRPC) */ - 'api_type': (keyof typeof _envoy_config_core_v3_ApiConfigSource_ApiType); + 'api_type': (_envoy_config_core_v3_ApiConfigSource_ApiType__Output); /** * Cluster names should be used only with REST. If > 1 * cluster is defined, clusters will be cycled through if any kind of failure @@ -155,7 +205,7 @@ export interface ApiConfigSource__Output { * API version for xDS transport protocol. This describes the xDS gRPC/REST * endpoint and version of [Delta]DiscoveryRequest/Response used on the wire. */ - 'transport_api_version': (keyof typeof _envoy_config_core_v3_ApiVersion); + 'transport_api_version': (_envoy_config_core_v3_ApiVersion__Output); /** * A list of config validators that will be executed when a new update is * received from the ApiConfigSource. Note that each validator handles a diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ApiVersion.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ApiVersion.ts index b46f6ec43..d3bad5d4e 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ApiVersion.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ApiVersion.ts @@ -4,19 +4,50 @@ * xDS API and non-xDS services version. This is used to describe both resource and transport * protocol versions (in distinct configuration fields). */ -export enum ApiVersion { +export const ApiVersion = { /** * When not specified, we assume v2, to ease migration to Envoy's stable API * versioning. If a client does not support v2 (e.g. due to deprecation), this * is an invalid value. + * @deprecated */ - AUTO = 0, + AUTO: 'AUTO', /** * Use xDS v2 API. + * @deprecated */ - V2 = 1, + V2: 'V2', /** * Use xDS v3 API. */ - V3 = 2, -} + V3: 'V3', +} as const; + +/** + * xDS API and non-xDS services version. This is used to describe both resource and transport + * protocol versions (in distinct configuration fields). + */ +export type ApiVersion = + /** + * When not specified, we assume v2, to ease migration to Envoy's stable API + * versioning. If a client does not support v2 (e.g. due to deprecation), this + * is an invalid value. + */ + | 'AUTO' + | 0 + /** + * Use xDS v2 API. + */ + | 'V2' + | 1 + /** + * Use xDS v3 API. + */ + | 'V3' + | 2 + +/** + * xDS API and non-xDS services version. This is used to describe both resource and transport + * protocol versions (in distinct configuration fields). + */ +export type ApiVersion__Output = typeof ApiVersion[keyof typeof ApiVersion] diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/BindConfig.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/BindConfig.ts index 733c609b1..54543facc 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/BindConfig.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/BindConfig.ts @@ -31,6 +31,7 @@ export interface BindConfig { /** * Deprecated by * :ref:`extra_source_addresses ` + * @deprecated */ 'additional_source_addresses'?: (_envoy_config_core_v3_SocketAddress)[]; /** @@ -72,6 +73,7 @@ export interface BindConfig__Output { /** * Deprecated by * :ref:`extra_source_addresses ` + * @deprecated */ 'additional_source_addresses': (_envoy_config_core_v3_SocketAddress__Output)[]; /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ConfigSource.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ConfigSource.ts index 5438b6e7d..1b98848ef 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ConfigSource.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ConfigSource.ts @@ -4,7 +4,7 @@ import type { ApiConfigSource as _envoy_config_core_v3_ApiConfigSource, ApiConfi import type { AggregatedConfigSource as _envoy_config_core_v3_AggregatedConfigSource, AggregatedConfigSource__Output as _envoy_config_core_v3_AggregatedConfigSource__Output } from '../../../../envoy/config/core/v3/AggregatedConfigSource'; import type { Duration as _google_protobuf_Duration, Duration__Output as _google_protobuf_Duration__Output } from '../../../../google/protobuf/Duration'; import type { SelfConfigSource as _envoy_config_core_v3_SelfConfigSource, SelfConfigSource__Output as _envoy_config_core_v3_SelfConfigSource__Output } from '../../../../envoy/config/core/v3/SelfConfigSource'; -import type { ApiVersion as _envoy_config_core_v3_ApiVersion } from '../../../../envoy/config/core/v3/ApiVersion'; +import type { ApiVersion as _envoy_config_core_v3_ApiVersion, ApiVersion__Output as _envoy_config_core_v3_ApiVersion__Output } from '../../../../envoy/config/core/v3/ApiVersion'; import type { Authority as _xds_core_v3_Authority, Authority__Output as _xds_core_v3_Authority__Output } from '../../../../xds/core/v3/Authority'; import type { PathConfigSource as _envoy_config_core_v3_PathConfigSource, PathConfigSource__Output as _envoy_config_core_v3_PathConfigSource__Output } from '../../../../envoy/config/core/v3/PathConfigSource'; @@ -20,6 +20,7 @@ import type { PathConfigSource as _envoy_config_core_v3_PathConfigSource, PathCo export interface ConfigSource { /** * Deprecated in favor of ``path_config_source``. Use that field instead. + * @deprecated */ 'path'?: (string); /** @@ -60,7 +61,7 @@ export interface ConfigSource { * will request for resources and the resource type that the client will in * turn expect to be delivered. */ - 'resource_api_version'?: (_envoy_config_core_v3_ApiVersion | keyof typeof _envoy_config_core_v3_ApiVersion); + 'resource_api_version'?: (_envoy_config_core_v3_ApiVersion); /** * Authorities that this config source may be used for. An authority specified in a xdstp:// URL * is resolved to a ``ConfigSource`` prior to configuration fetch. This field provides the @@ -87,6 +88,7 @@ export interface ConfigSource { export interface ConfigSource__Output { /** * Deprecated in favor of ``path_config_source``. Use that field instead. + * @deprecated */ 'path'?: (string); /** @@ -127,7 +129,7 @@ export interface ConfigSource__Output { * will request for resources and the resource type that the client will in * turn expect to be delivered. */ - 'resource_api_version': (keyof typeof _envoy_config_core_v3_ApiVersion); + 'resource_api_version': (_envoy_config_core_v3_ApiVersion__Output); /** * Authorities that this config source may be used for. An authority specified in a xdstp:// URL * is resolved to a ``ConfigSource`` prior to configuration fetch. This field provides the diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Extension.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Extension.ts index b25c15cce..dbbdb0cee 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Extension.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Extension.ts @@ -24,6 +24,7 @@ export interface Extension { * [#not-implemented-hide:] Type descriptor of extension configuration proto. * [#comment:TODO(yanavlasov): Link to the doc with existing configuration protos.] * [#comment:TODO(yanavlasov): Add tests when PR #9391 lands.] + * @deprecated */ 'type_descriptor'?: (string); /** @@ -64,6 +65,7 @@ export interface Extension__Output { * [#not-implemented-hide:] Type descriptor of extension configuration proto. * [#comment:TODO(yanavlasov): Link to the doc with existing configuration protos.] * [#comment:TODO(yanavlasov): Add tests when PR #9391 lands.] + * @deprecated */ 'type_descriptor': (string); /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HeaderValueOption.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HeaderValueOption.ts index efb656303..e7a0a8d87 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HeaderValueOption.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HeaderValueOption.ts @@ -8,25 +8,55 @@ import type { BoolValue as _google_protobuf_BoolValue, BoolValue__Output as _goo /** * Describes the supported actions types for header append action. */ -export enum _envoy_config_core_v3_HeaderValueOption_HeaderAppendAction { +export const _envoy_config_core_v3_HeaderValueOption_HeaderAppendAction = { /** * This action will append the specified value to the existing values if the header * already exists. If the header doesn't exist then this will add the header with * specified key and value. */ - APPEND_IF_EXISTS_OR_ADD = 0, + APPEND_IF_EXISTS_OR_ADD: 'APPEND_IF_EXISTS_OR_ADD', /** * This action will add the header if it doesn't already exist. If the header * already exists then this will be a no-op. */ - ADD_IF_ABSENT = 1, + ADD_IF_ABSENT: 'ADD_IF_ABSENT', /** * This action will overwrite the specified value by discarding any existing values if * the header already exists. If the header doesn't exist then this will add the header * with specified key and value. */ - OVERWRITE_IF_EXISTS_OR_ADD = 2, -} + OVERWRITE_IF_EXISTS_OR_ADD: 'OVERWRITE_IF_EXISTS_OR_ADD', +} as const; + +/** + * Describes the supported actions types for header append action. + */ +export type _envoy_config_core_v3_HeaderValueOption_HeaderAppendAction = + /** + * This action will append the specified value to the existing values if the header + * already exists. If the header doesn't exist then this will add the header with + * specified key and value. + */ + | 'APPEND_IF_EXISTS_OR_ADD' + | 0 + /** + * This action will add the header if it doesn't already exist. If the header + * already exists then this will be a no-op. + */ + | 'ADD_IF_ABSENT' + | 1 + /** + * This action will overwrite the specified value by discarding any existing values if + * the header already exists. If the header doesn't exist then this will add the header + * with specified key and value. + */ + | 'OVERWRITE_IF_EXISTS_OR_ADD' + | 2 + +/** + * Describes the supported actions types for header append action. + */ +export type _envoy_config_core_v3_HeaderValueOption_HeaderAppendAction__Output = typeof _envoy_config_core_v3_HeaderValueOption_HeaderAppendAction[keyof typeof _envoy_config_core_v3_HeaderValueOption_HeaderAppendAction] /** * Header name/value pair plus option to control append behavior. @@ -46,6 +76,7 @@ export interface HeaderValueOption { * The :ref:`external authorization service ` and * :ref:`external processor service ` have * default value (``false``) for this field. + * @deprecated */ 'append'?: (_google_protobuf_BoolValue | null); /** @@ -54,7 +85,7 @@ export interface HeaderValueOption { * Value defaults to :ref:`APPEND_IF_EXISTS_OR_ADD * `. */ - 'append_action'?: (_envoy_config_core_v3_HeaderValueOption_HeaderAppendAction | keyof typeof _envoy_config_core_v3_HeaderValueOption_HeaderAppendAction); + 'append_action'?: (_envoy_config_core_v3_HeaderValueOption_HeaderAppendAction); /** * Is the header value allowed to be empty? If false (default), custom headers with empty values are dropped, * otherwise they are added. @@ -80,6 +111,7 @@ export interface HeaderValueOption__Output { * The :ref:`external authorization service ` and * :ref:`external processor service ` have * default value (``false``) for this field. + * @deprecated */ 'append': (_google_protobuf_BoolValue__Output | null); /** @@ -88,7 +120,7 @@ export interface HeaderValueOption__Output { * Value defaults to :ref:`APPEND_IF_EXISTS_OR_ADD * `. */ - 'append_action': (keyof typeof _envoy_config_core_v3_HeaderValueOption_HeaderAppendAction); + 'append_action': (_envoy_config_core_v3_HeaderValueOption_HeaderAppendAction__Output); /** * Is the header value allowed to be empty? If false (default), custom headers with empty values are dropped, * otherwise they are added. diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HealthCheck.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HealthCheck.ts index e0638df7d..f6605412e 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HealthCheck.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HealthCheck.ts @@ -9,11 +9,10 @@ import type { TypedExtensionConfig as _envoy_config_core_v3_TypedExtensionConfig import type { UInt64Value as _google_protobuf_UInt64Value, UInt64Value__Output as _google_protobuf_UInt64Value__Output } from '../../../../google/protobuf/UInt64Value'; import type { HeaderValueOption as _envoy_config_core_v3_HeaderValueOption, HeaderValueOption__Output as _envoy_config_core_v3_HeaderValueOption__Output } from '../../../../envoy/config/core/v3/HeaderValueOption'; import type { Int64Range as _envoy_type_v3_Int64Range, Int64Range__Output as _envoy_type_v3_Int64Range__Output } from '../../../../envoy/type/v3/Int64Range'; -import type { CodecClientType as _envoy_type_v3_CodecClientType } from '../../../../envoy/type/v3/CodecClientType'; +import type { CodecClientType as _envoy_type_v3_CodecClientType, CodecClientType__Output as _envoy_type_v3_CodecClientType__Output } from '../../../../envoy/type/v3/CodecClientType'; import type { StringMatcher as _envoy_type_matcher_v3_StringMatcher, StringMatcher__Output as _envoy_type_matcher_v3_StringMatcher__Output } from '../../../../envoy/type/matcher/v3/StringMatcher'; -import type { RequestMethod as _envoy_config_core_v3_RequestMethod } from '../../../../envoy/config/core/v3/RequestMethod'; +import type { RequestMethod as _envoy_config_core_v3_RequestMethod, RequestMethod__Output as _envoy_config_core_v3_RequestMethod__Output } from '../../../../envoy/config/core/v3/RequestMethod'; import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../../google/protobuf/Any'; -import type { Long } from '@grpc/proto-loader'; /** * Custom health check. @@ -183,7 +182,7 @@ export interface _envoy_config_core_v3_HealthCheck_HttpHealthCheck { /** * Use specified application protocol for health checks. */ - 'codec_client_type'?: (_envoy_type_v3_CodecClientType | keyof typeof _envoy_type_v3_CodecClientType); + 'codec_client_type'?: (_envoy_type_v3_CodecClientType); /** * An optional service name parameter which is used to validate the identity of * the health checked cluster using a :ref:`StringMatcher @@ -197,7 +196,7 @@ export interface _envoy_config_core_v3_HealthCheck_HttpHealthCheck { * CONNECT method is disallowed because it is not appropriate for health check request. * If a non-200 response is expected by the method, it needs to be set in :ref:`expected_statuses `. */ - 'method'?: (_envoy_config_core_v3_RequestMethod | keyof typeof _envoy_config_core_v3_RequestMethod); + 'method'?: (_envoy_config_core_v3_RequestMethod); } /** @@ -272,7 +271,7 @@ export interface _envoy_config_core_v3_HealthCheck_HttpHealthCheck__Output { /** * Use specified application protocol for health checks. */ - 'codec_client_type': (keyof typeof _envoy_type_v3_CodecClientType); + 'codec_client_type': (_envoy_type_v3_CodecClientType__Output); /** * An optional service name parameter which is used to validate the identity of * the health checked cluster using a :ref:`StringMatcher @@ -286,7 +285,7 @@ export interface _envoy_config_core_v3_HealthCheck_HttpHealthCheck__Output { * CONNECT method is disallowed because it is not appropriate for health check request. * If a non-200 response is expected by the method, it needs to be set in :ref:`expected_statuses `. */ - 'method': (keyof typeof _envoy_config_core_v3_RequestMethod); + 'method': (_envoy_config_core_v3_RequestMethod__Output); } /** @@ -497,6 +496,7 @@ export interface HealthCheck { * in the file sink extension. * * Specifies the path to the :ref:`health check event log `. + * @deprecated */ 'event_log_path'?: (string); /** @@ -687,6 +687,7 @@ export interface HealthCheck__Output { * in the file sink extension. * * Specifies the path to the :ref:`health check event log `. + * @deprecated */ 'event_log_path': (string); /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HealthStatus.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HealthStatus.ts index 7d3d76569..54298f59b 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HealthStatus.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HealthStatus.ts @@ -3,19 +3,19 @@ /** * Endpoint health status. */ -export enum HealthStatus { +export const HealthStatus = { /** * The health status is not known. This is interpreted by Envoy as ``HEALTHY``. */ - UNKNOWN = 0, + UNKNOWN: 'UNKNOWN', /** * Healthy. */ - HEALTHY = 1, + HEALTHY: 'HEALTHY', /** * Unhealthy. */ - UNHEALTHY = 2, + UNHEALTHY: 'UNHEALTHY', /** * Connection draining in progress. E.g., * ``_ @@ -23,14 +23,59 @@ export enum HealthStatus { * ``_. * This is interpreted by Envoy as ``UNHEALTHY``. */ - DRAINING = 3, + DRAINING: 'DRAINING', /** * Health check timed out. This is part of HDS and is interpreted by Envoy as * ``UNHEALTHY``. */ - TIMEOUT = 4, + TIMEOUT: 'TIMEOUT', /** * Degraded. */ - DEGRADED = 5, -} + DEGRADED: 'DEGRADED', +} as const; + +/** + * Endpoint health status. + */ +export type HealthStatus = + /** + * The health status is not known. This is interpreted by Envoy as ``HEALTHY``. + */ + | 'UNKNOWN' + | 0 + /** + * Healthy. + */ + | 'HEALTHY' + | 1 + /** + * Unhealthy. + */ + | 'UNHEALTHY' + | 2 + /** + * Connection draining in progress. E.g., + * ``_ + * or + * ``_. + * This is interpreted by Envoy as ``UNHEALTHY``. + */ + | 'DRAINING' + | 3 + /** + * Health check timed out. This is part of HDS and is interpreted by Envoy as + * ``UNHEALTHY``. + */ + | 'TIMEOUT' + | 4 + /** + * Degraded. + */ + | 'DEGRADED' + | 5 + +/** + * Endpoint health status. + */ +export type HealthStatus__Output = typeof HealthStatus[keyof typeof HealthStatus] diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HealthStatusSet.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HealthStatusSet.ts index c518192d7..c94bf049c 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HealthStatusSet.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HealthStatusSet.ts @@ -1,17 +1,17 @@ // Original file: deps/envoy-api/envoy/config/core/v3/health_check.proto -import type { HealthStatus as _envoy_config_core_v3_HealthStatus } from '../../../../envoy/config/core/v3/HealthStatus'; +import type { HealthStatus as _envoy_config_core_v3_HealthStatus, HealthStatus__Output as _envoy_config_core_v3_HealthStatus__Output } from '../../../../envoy/config/core/v3/HealthStatus'; export interface HealthStatusSet { /** * An order-independent set of health status. */ - 'statuses'?: (_envoy_config_core_v3_HealthStatus | keyof typeof _envoy_config_core_v3_HealthStatus)[]; + 'statuses'?: (_envoy_config_core_v3_HealthStatus)[]; } export interface HealthStatusSet__Output { /** * An order-independent set of health status. */ - 'statuses': (keyof typeof _envoy_config_core_v3_HealthStatus)[]; + 'statuses': (_envoy_config_core_v3_HealthStatus__Output)[]; } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Http2ProtocolOptions.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Http2ProtocolOptions.ts index cc22ce44c..9e0ae3d6e 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Http2ProtocolOptions.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Http2ProtocolOptions.ts @@ -159,6 +159,7 @@ export interface Http2ProtocolOptions { * ` * * See `RFC7540, sec. 8.1 `_ for details. + * @deprecated */ 'stream_error_on_invalid_http_messaging'?: (boolean); /** @@ -339,6 +340,7 @@ export interface Http2ProtocolOptions__Output { * ` * * See `RFC7540, sec. 8.1 `_ for details. + * @deprecated */ 'stream_error_on_invalid_http_messaging': (boolean); /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HttpProtocolOptions.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HttpProtocolOptions.ts index a3064110f..dfa800c3b 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HttpProtocolOptions.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/HttpProtocolOptions.ts @@ -12,24 +12,61 @@ import type { UInt32Value as _google_protobuf_UInt32Value, UInt32Value__Output a * as a security measure due to systems that treat '_' and '-' as interchangeable. Envoy by default allows client request headers with underscore * characters. */ -export enum _envoy_config_core_v3_HttpProtocolOptions_HeadersWithUnderscoresAction { +export const _envoy_config_core_v3_HttpProtocolOptions_HeadersWithUnderscoresAction = { /** * Allow headers with underscores. This is the default behavior. */ - ALLOW = 0, + ALLOW: 'ALLOW', /** * Reject client request. HTTP/1 requests are rejected with the 400 status. HTTP/2 requests * end with the stream reset. The "httpN.requests_rejected_with_underscores_in_headers" counter * is incremented for each rejected request. */ - REJECT_REQUEST = 1, + REJECT_REQUEST: 'REJECT_REQUEST', /** * Drop the client header with name containing underscores. The header is dropped before the filter chain is * invoked and as such filters will not see dropped headers. The * "httpN.dropped_headers_with_underscores" is incremented for each dropped header. */ - DROP_HEADER = 2, -} + DROP_HEADER: 'DROP_HEADER', +} as const; + +/** + * Action to take when Envoy receives client request with header names containing underscore + * characters. + * Underscore character is allowed in header names by the RFC-7230 and this behavior is implemented + * as a security measure due to systems that treat '_' and '-' as interchangeable. Envoy by default allows client request headers with underscore + * characters. + */ +export type _envoy_config_core_v3_HttpProtocolOptions_HeadersWithUnderscoresAction = + /** + * Allow headers with underscores. This is the default behavior. + */ + | 'ALLOW' + | 0 + /** + * Reject client request. HTTP/1 requests are rejected with the 400 status. HTTP/2 requests + * end with the stream reset. The "httpN.requests_rejected_with_underscores_in_headers" counter + * is incremented for each rejected request. + */ + | 'REJECT_REQUEST' + | 1 + /** + * Drop the client header with name containing underscores. The header is dropped before the filter chain is + * invoked and as such filters will not see dropped headers. The + * "httpN.dropped_headers_with_underscores" is incremented for each dropped header. + */ + | 'DROP_HEADER' + | 2 + +/** + * Action to take when Envoy receives client request with header names containing underscore + * characters. + * Underscore character is allowed in header names by the RFC-7230 and this behavior is implemented + * as a security measure due to systems that treat '_' and '-' as interchangeable. Envoy by default allows client request headers with underscore + * characters. + */ +export type _envoy_config_core_v3_HttpProtocolOptions_HeadersWithUnderscoresAction__Output = typeof _envoy_config_core_v3_HttpProtocolOptions_HeadersWithUnderscoresAction[keyof typeof _envoy_config_core_v3_HttpProtocolOptions_HeadersWithUnderscoresAction] /** * [#next-free-field: 7] @@ -81,7 +118,7 @@ export interface HttpProtocolOptions { * Note: this only affects client headers. It does not affect headers added * by Envoy filters and does not have any impact if added to cluster config. */ - 'headers_with_underscores_action'?: (_envoy_config_core_v3_HttpProtocolOptions_HeadersWithUnderscoresAction | keyof typeof _envoy_config_core_v3_HttpProtocolOptions_HeadersWithUnderscoresAction); + 'headers_with_underscores_action'?: (_envoy_config_core_v3_HttpProtocolOptions_HeadersWithUnderscoresAction); /** * Optional maximum requests for both upstream and downstream connections. * If not specified, there is no limit. @@ -141,7 +178,7 @@ export interface HttpProtocolOptions__Output { * Note: this only affects client headers. It does not affect headers added * by Envoy filters and does not have any impact if added to cluster config. */ - 'headers_with_underscores_action': (keyof typeof _envoy_config_core_v3_HttpProtocolOptions_HeadersWithUnderscoresAction); + 'headers_with_underscores_action': (_envoy_config_core_v3_HttpProtocolOptions_HeadersWithUnderscoresAction__Output); /** * Optional maximum requests for both upstream and downstream connections. * If not specified, there is no limit. diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Node.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Node.ts index 6aef94d8e..b29b68502 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Node.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/Node.ts @@ -78,6 +78,7 @@ export interface Node { * for filtering :ref:`listeners ` to be returned. For example, * if there is a listener bound to port 80, the list can optionally contain the * SocketAddress ``(0.0.0.0,80)``. The field is optional and just a hint. + * @deprecated */ 'listening_addresses'?: (_envoy_config_core_v3_Address)[]; /** @@ -162,6 +163,7 @@ export interface Node__Output { * for filtering :ref:`listeners ` to be returned. For example, * if there is a listener bound to port 80, the list can optionally contain the * SocketAddress ``(0.0.0.0,80)``. The field is optional and just a hint. + * @deprecated */ 'listening_addresses': (_envoy_config_core_v3_Address__Output)[]; /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ProxyProtocolConfig.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ProxyProtocolConfig.ts index 7da9d569e..34cf7475f 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ProxyProtocolConfig.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ProxyProtocolConfig.ts @@ -4,22 +4,36 @@ import type { ProxyProtocolPassThroughTLVs as _envoy_config_core_v3_ProxyProtoco // Original file: deps/envoy-api/envoy/config/core/v3/proxy_protocol.proto -export enum _envoy_config_core_v3_ProxyProtocolConfig_Version { +export const _envoy_config_core_v3_ProxyProtocolConfig_Version = { /** * PROXY protocol version 1. Human readable format. */ - V1 = 0, + V1: 'V1', /** * PROXY protocol version 2. Binary format. */ - V2 = 1, -} + V2: 'V2', +} as const; + +export type _envoy_config_core_v3_ProxyProtocolConfig_Version = + /** + * PROXY protocol version 1. Human readable format. + */ + | 'V1' + | 0 + /** + * PROXY protocol version 2. Binary format. + */ + | 'V2' + | 1 + +export type _envoy_config_core_v3_ProxyProtocolConfig_Version__Output = typeof _envoy_config_core_v3_ProxyProtocolConfig_Version[keyof typeof _envoy_config_core_v3_ProxyProtocolConfig_Version] export interface ProxyProtocolConfig { /** * The PROXY protocol version to use. See https://www.haproxy.org/download/2.1/doc/proxy-protocol.txt for details */ - 'version'?: (_envoy_config_core_v3_ProxyProtocolConfig_Version | keyof typeof _envoy_config_core_v3_ProxyProtocolConfig_Version); + 'version'?: (_envoy_config_core_v3_ProxyProtocolConfig_Version); /** * This config controls which TLVs can be passed to upstream if it is Proxy Protocol * V2 header. If there is no setting for this field, no TLVs will be passed through. @@ -31,7 +45,7 @@ export interface ProxyProtocolConfig__Output { /** * The PROXY protocol version to use. See https://www.haproxy.org/download/2.1/doc/proxy-protocol.txt for details */ - 'version': (keyof typeof _envoy_config_core_v3_ProxyProtocolConfig_Version); + 'version': (_envoy_config_core_v3_ProxyProtocolConfig_Version__Output); /** * This config controls which TLVs can be passed to upstream if it is Proxy Protocol * V2 header. If there is no setting for this field, no TLVs will be passed through. diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ProxyProtocolPassThroughTLVs.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ProxyProtocolPassThroughTLVs.ts index 0dddbf79f..9f253ceff 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ProxyProtocolPassThroughTLVs.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/ProxyProtocolPassThroughTLVs.ts @@ -3,23 +3,37 @@ // Original file: deps/envoy-api/envoy/config/core/v3/proxy_protocol.proto -export enum _envoy_config_core_v3_ProxyProtocolPassThroughTLVs_PassTLVsMatchType { +export const _envoy_config_core_v3_ProxyProtocolPassThroughTLVs_PassTLVsMatchType = { /** * Pass all TLVs. */ - INCLUDE_ALL = 0, + INCLUDE_ALL: 'INCLUDE_ALL', /** * Pass specific TLVs defined in tlv_type. */ - INCLUDE = 1, -} + INCLUDE: 'INCLUDE', +} as const; + +export type _envoy_config_core_v3_ProxyProtocolPassThroughTLVs_PassTLVsMatchType = + /** + * Pass all TLVs. + */ + | 'INCLUDE_ALL' + | 0 + /** + * Pass specific TLVs defined in tlv_type. + */ + | 'INCLUDE' + | 1 + +export type _envoy_config_core_v3_ProxyProtocolPassThroughTLVs_PassTLVsMatchType__Output = typeof _envoy_config_core_v3_ProxyProtocolPassThroughTLVs_PassTLVsMatchType[keyof typeof _envoy_config_core_v3_ProxyProtocolPassThroughTLVs_PassTLVsMatchType] export interface ProxyProtocolPassThroughTLVs { /** * The strategy to pass through TLVs. Default is INCLUDE_ALL. * If INCLUDE_ALL is set, all TLVs will be passed through no matter the tlv_type field. */ - 'match_type'?: (_envoy_config_core_v3_ProxyProtocolPassThroughTLVs_PassTLVsMatchType | keyof typeof _envoy_config_core_v3_ProxyProtocolPassThroughTLVs_PassTLVsMatchType); + 'match_type'?: (_envoy_config_core_v3_ProxyProtocolPassThroughTLVs_PassTLVsMatchType); /** * The TLV types that are applied based on match_type. * TLV type is defined as uint8_t in proxy protocol. See `the spec @@ -33,7 +47,7 @@ export interface ProxyProtocolPassThroughTLVs__Output { * The strategy to pass through TLVs. Default is INCLUDE_ALL. * If INCLUDE_ALL is set, all TLVs will be passed through no matter the tlv_type field. */ - 'match_type': (keyof typeof _envoy_config_core_v3_ProxyProtocolPassThroughTLVs_PassTLVsMatchType); + 'match_type': (_envoy_config_core_v3_ProxyProtocolPassThroughTLVs_PassTLVsMatchType__Output); /** * The TLV types that are applied based on match_type. * TLV type is defined as uint8_t in proxy protocol. See `the spec diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/RequestMethod.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/RequestMethod.ts index 9be1aa6d1..67d40fda6 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/RequestMethod.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/RequestMethod.ts @@ -3,15 +3,45 @@ /** * HTTP request method. */ -export enum RequestMethod { - METHOD_UNSPECIFIED = 0, - GET = 1, - HEAD = 2, - POST = 3, - PUT = 4, - DELETE = 5, - CONNECT = 6, - OPTIONS = 7, - TRACE = 8, - PATCH = 9, -} +export const RequestMethod = { + METHOD_UNSPECIFIED: 'METHOD_UNSPECIFIED', + GET: 'GET', + HEAD: 'HEAD', + POST: 'POST', + PUT: 'PUT', + DELETE: 'DELETE', + CONNECT: 'CONNECT', + OPTIONS: 'OPTIONS', + TRACE: 'TRACE', + PATCH: 'PATCH', +} as const; + +/** + * HTTP request method. + */ +export type RequestMethod = + | 'METHOD_UNSPECIFIED' + | 0 + | 'GET' + | 1 + | 'HEAD' + | 2 + | 'POST' + | 3 + | 'PUT' + | 4 + | 'DELETE' + | 5 + | 'CONNECT' + | 6 + | 'OPTIONS' + | 7 + | 'TRACE' + | 8 + | 'PATCH' + | 9 + +/** + * HTTP request method. + */ +export type RequestMethod__Output = typeof RequestMethod[keyof typeof RequestMethod] diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/RoutingPriority.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/RoutingPriority.ts index 917d8a3df..41172d92f 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/RoutingPriority.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/RoutingPriority.ts @@ -9,7 +9,33 @@ * upstream host. In the future Envoy will likely support true HTTP/2 priority * over a single upstream connection. */ -export enum RoutingPriority { - DEFAULT = 0, - HIGH = 1, -} +export const RoutingPriority = { + DEFAULT: 'DEFAULT', + HIGH: 'HIGH', +} as const; + +/** + * Envoy supports :ref:`upstream priority routing + * ` both at the route and the virtual + * cluster level. The current priority implementation uses different connection + * pool and circuit breaking settings for each priority level. This means that + * even for HTTP/2 requests, two physical connections will be used to an + * upstream host. In the future Envoy will likely support true HTTP/2 priority + * over a single upstream connection. + */ +export type RoutingPriority = + | 'DEFAULT' + | 0 + | 'HIGH' + | 1 + +/** + * Envoy supports :ref:`upstream priority routing + * ` both at the route and the virtual + * cluster level. The current priority implementation uses different connection + * pool and circuit breaking settings for each priority level. This means that + * even for HTTP/2 requests, two physical connections will be used to an + * upstream host. In the future Envoy will likely support true HTTP/2 priority + * over a single upstream connection. + */ +export type RoutingPriority__Output = typeof RoutingPriority[keyof typeof RoutingPriority] diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SelfConfigSource.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SelfConfigSource.ts index 3912fd1cf..939c4fd3d 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SelfConfigSource.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SelfConfigSource.ts @@ -1,6 +1,6 @@ // Original file: deps/envoy-api/envoy/config/core/v3/config_source.proto -import type { ApiVersion as _envoy_config_core_v3_ApiVersion } from '../../../../envoy/config/core/v3/ApiVersion'; +import type { ApiVersion as _envoy_config_core_v3_ApiVersion, ApiVersion__Output as _envoy_config_core_v3_ApiVersion__Output } from '../../../../envoy/config/core/v3/ApiVersion'; /** * [#not-implemented-hide:] @@ -13,7 +13,7 @@ export interface SelfConfigSource { * API version for xDS transport protocol. This describes the xDS gRPC/REST * endpoint and version of [Delta]DiscoveryRequest/Response used on the wire. */ - 'transport_api_version'?: (_envoy_config_core_v3_ApiVersion | keyof typeof _envoy_config_core_v3_ApiVersion); + 'transport_api_version'?: (_envoy_config_core_v3_ApiVersion); } /** @@ -27,5 +27,5 @@ export interface SelfConfigSource__Output { * API version for xDS transport protocol. This describes the xDS gRPC/REST * endpoint and version of [Delta]DiscoveryRequest/Response used on the wire. */ - 'transport_api_version': (keyof typeof _envoy_config_core_v3_ApiVersion); + 'transport_api_version': (_envoy_config_core_v3_ApiVersion__Output); } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SocketAddress.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SocketAddress.ts index ae87b9edd..f939393fb 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SocketAddress.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SocketAddress.ts @@ -3,16 +3,24 @@ // Original file: deps/envoy-api/envoy/config/core/v3/address.proto -export enum _envoy_config_core_v3_SocketAddress_Protocol { - TCP = 0, - UDP = 1, -} +export const _envoy_config_core_v3_SocketAddress_Protocol = { + TCP: 'TCP', + UDP: 'UDP', +} as const; + +export type _envoy_config_core_v3_SocketAddress_Protocol = + | 'TCP' + | 0 + | 'UDP' + | 1 + +export type _envoy_config_core_v3_SocketAddress_Protocol__Output = typeof _envoy_config_core_v3_SocketAddress_Protocol[keyof typeof _envoy_config_core_v3_SocketAddress_Protocol] /** * [#next-free-field: 7] */ export interface SocketAddress { - 'protocol'?: (_envoy_config_core_v3_SocketAddress_Protocol | keyof typeof _envoy_config_core_v3_SocketAddress_Protocol); + 'protocol'?: (_envoy_config_core_v3_SocketAddress_Protocol); /** * The address for this socket. :ref:`Listeners ` will bind * to the address. An empty address is not allowed. Specify ``0.0.0.0`` or ``::`` @@ -56,7 +64,7 @@ export interface SocketAddress { * [#next-free-field: 7] */ export interface SocketAddress__Output { - 'protocol': (keyof typeof _envoy_config_core_v3_SocketAddress_Protocol); + 'protocol': (_envoy_config_core_v3_SocketAddress_Protocol__Output); /** * The address for this socket. :ref:`Listeners ` will bind * to the address. An empty address is not allowed. Specify ``0.0.0.0`` or ``::`` diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SocketOption.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SocketOption.ts index edff50a63..9b3bc0019 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SocketOption.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SocketOption.ts @@ -4,20 +4,39 @@ import type { Long } from '@grpc/proto-loader'; // Original file: deps/envoy-api/envoy/config/core/v3/socket_option.proto -export enum _envoy_config_core_v3_SocketOption_SocketState { +export const _envoy_config_core_v3_SocketOption_SocketState = { /** * Socket options are applied after socket creation but before binding the socket to a port */ - STATE_PREBIND = 0, + STATE_PREBIND: 'STATE_PREBIND', /** * Socket options are applied after binding the socket to a port but before calling listen() */ - STATE_BOUND = 1, + STATE_BOUND: 'STATE_BOUND', /** * Socket options are applied after calling listen() */ - STATE_LISTENING = 2, -} + STATE_LISTENING: 'STATE_LISTENING', +} as const; + +export type _envoy_config_core_v3_SocketOption_SocketState = + /** + * Socket options are applied after socket creation but before binding the socket to a port + */ + | 'STATE_PREBIND' + | 0 + /** + * Socket options are applied after binding the socket to a port but before calling listen() + */ + | 'STATE_BOUND' + | 1 + /** + * Socket options are applied after calling listen() + */ + | 'STATE_LISTENING' + | 2 + +export type _envoy_config_core_v3_SocketOption_SocketState__Output = typeof _envoy_config_core_v3_SocketOption_SocketState[keyof typeof _envoy_config_core_v3_SocketOption_SocketState] /** * Generic socket option message. This would be used to set socket options that @@ -70,7 +89,7 @@ export interface SocketOption { * The state in which the option will be applied. When used in BindConfig * STATE_PREBIND is currently the only valid value. */ - 'state'?: (_envoy_config_core_v3_SocketOption_SocketState | keyof typeof _envoy_config_core_v3_SocketOption_SocketState); + 'state'?: (_envoy_config_core_v3_SocketOption_SocketState); 'value'?: "int_value"|"buf_value"; } @@ -125,6 +144,6 @@ export interface SocketOption__Output { * The state in which the option will be applied. When used in BindConfig * STATE_PREBIND is currently the only valid value. */ - 'state': (keyof typeof _envoy_config_core_v3_SocketOption_SocketState); + 'state': (_envoy_config_core_v3_SocketOption_SocketState__Output); 'value': "int_value"|"buf_value"; } diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SubstitutionFormatString.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SubstitutionFormatString.ts index be0237e38..01a97441c 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SubstitutionFormatString.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/SubstitutionFormatString.ts @@ -28,6 +28,7 @@ export interface SubstitutionFormatString { * upstream connect error:503:path=/foo * * Deprecated in favor of :ref:`text_format_source `. To migrate text format strings, use the :ref:`inline_string ` field. + * @deprecated */ 'text_format'?: (string); /** @@ -125,6 +126,7 @@ export interface SubstitutionFormatString__Output { * upstream connect error:503:path=/foo * * Deprecated in favor of :ref:`text_format_source `. To migrate text format strings, use the :ref:`inline_string ` field. + * @deprecated */ 'text_format'?: (string); /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/TrafficDirection.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/TrafficDirection.ts index b68323b09..e450d43cb 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/TrafficDirection.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/TrafficDirection.ts @@ -3,17 +3,42 @@ /** * Identifies the direction of the traffic relative to the local Envoy. */ -export enum TrafficDirection { +export const TrafficDirection = { /** * Default option is unspecified. */ - UNSPECIFIED = 0, + UNSPECIFIED: 'UNSPECIFIED', /** * The transport is used for incoming traffic. */ - INBOUND = 1, + INBOUND: 'INBOUND', /** * The transport is used for outgoing traffic. */ - OUTBOUND = 2, -} + OUTBOUND: 'OUTBOUND', +} as const; + +/** + * Identifies the direction of the traffic relative to the local Envoy. + */ +export type TrafficDirection = + /** + * Default option is unspecified. + */ + | 'UNSPECIFIED' + | 0 + /** + * The transport is used for incoming traffic. + */ + | 'INBOUND' + | 1 + /** + * The transport is used for outgoing traffic. + */ + | 'OUTBOUND' + | 2 + +/** + * Identifies the direction of the traffic relative to the local Envoy. + */ +export type TrafficDirection__Output = typeof TrafficDirection[keyof typeof TrafficDirection] diff --git a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/UdpSocketConfig.ts b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/UdpSocketConfig.ts index fe1b038db..f5e38b2b4 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/core/v3/UdpSocketConfig.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/core/v3/UdpSocketConfig.ts @@ -2,7 +2,6 @@ import type { UInt64Value as _google_protobuf_UInt64Value, UInt64Value__Output as _google_protobuf_UInt64Value__Output } from '../../../../google/protobuf/UInt64Value'; import type { BoolValue as _google_protobuf_BoolValue, BoolValue__Output as _google_protobuf_BoolValue__Output } from '../../../../google/protobuf/BoolValue'; -import type { Long } from '@grpc/proto-loader'; /** * Generic UDP socket configuration. diff --git a/packages/grpc-js-xds/src/generated/envoy/config/endpoint/v3/LbEndpoint.ts b/packages/grpc-js-xds/src/generated/envoy/config/endpoint/v3/LbEndpoint.ts index 8d184b8a0..4130eb838 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/endpoint/v3/LbEndpoint.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/endpoint/v3/LbEndpoint.ts @@ -1,7 +1,7 @@ // Original file: deps/envoy-api/envoy/config/endpoint/v3/endpoint_components.proto import type { Endpoint as _envoy_config_endpoint_v3_Endpoint, Endpoint__Output as _envoy_config_endpoint_v3_Endpoint__Output } from '../../../../envoy/config/endpoint/v3/Endpoint'; -import type { HealthStatus as _envoy_config_core_v3_HealthStatus } from '../../../../envoy/config/core/v3/HealthStatus'; +import type { HealthStatus as _envoy_config_core_v3_HealthStatus, HealthStatus__Output as _envoy_config_core_v3_HealthStatus__Output } from '../../../../envoy/config/core/v3/HealthStatus'; import type { Metadata as _envoy_config_core_v3_Metadata, Metadata__Output as _envoy_config_core_v3_Metadata__Output } from '../../../../envoy/config/core/v3/Metadata'; import type { UInt32Value as _google_protobuf_UInt32Value, UInt32Value__Output as _google_protobuf_UInt32Value__Output } from '../../../../google/protobuf/UInt32Value'; @@ -14,7 +14,7 @@ export interface LbEndpoint { /** * Optional health status when known and supplied by EDS server. */ - 'health_status'?: (_envoy_config_core_v3_HealthStatus | keyof typeof _envoy_config_core_v3_HealthStatus); + 'health_status'?: (_envoy_config_core_v3_HealthStatus); /** * The endpoint metadata specifies values that may be used by the load * balancer to select endpoints in a cluster for a given request. The filter @@ -56,7 +56,7 @@ export interface LbEndpoint__Output { /** * Optional health status when known and supplied by EDS server. */ - 'health_status': (keyof typeof _envoy_config_core_v3_HealthStatus); + 'health_status': (_envoy_config_core_v3_HealthStatus__Output); /** * The endpoint metadata specifies values that may be used by the load * balancer to select endpoints in a cluster for a given request. The filter diff --git a/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/FilterChain.ts b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/FilterChain.ts index f27000d71..77b08f48e 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/FilterChain.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/FilterChain.ts @@ -79,6 +79,7 @@ export interface FilterChain { * This field is deprecated. Add a * :ref:`PROXY protocol listener filter ` * explicitly instead. + * @deprecated */ 'use_proxy_proto'?: (_google_protobuf_BoolValue | null); /** @@ -149,6 +150,7 @@ export interface FilterChain__Output { * This field is deprecated. Add a * :ref:`PROXY protocol listener filter ` * explicitly instead. + * @deprecated */ 'use_proxy_proto': (_google_protobuf_BoolValue__Output | null); /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/FilterChainMatch.ts b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/FilterChainMatch.ts index 87b1503cb..fcbfc1b3e 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/FilterChainMatch.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/FilterChainMatch.ts @@ -5,20 +5,39 @@ import type { UInt32Value as _google_protobuf_UInt32Value, UInt32Value__Output a // Original file: deps/envoy-api/envoy/config/listener/v3/listener_components.proto -export enum _envoy_config_listener_v3_FilterChainMatch_ConnectionSourceType { +export const _envoy_config_listener_v3_FilterChainMatch_ConnectionSourceType = { /** * Any connection source matches. */ - ANY = 0, + ANY: 'ANY', /** * Match a connection originating from the same host. */ - SAME_IP_OR_LOOPBACK = 1, + SAME_IP_OR_LOOPBACK: 'SAME_IP_OR_LOOPBACK', /** * Match a connection originating from a different host. */ - EXTERNAL = 2, -} + EXTERNAL: 'EXTERNAL', +} as const; + +export type _envoy_config_listener_v3_FilterChainMatch_ConnectionSourceType = + /** + * Any connection source matches. + */ + | 'ANY' + | 0 + /** + * Match a connection originating from the same host. + */ + | 'SAME_IP_OR_LOOPBACK' + | 1 + /** + * Match a connection originating from a different host. + */ + | 'EXTERNAL' + | 2 + +export type _envoy_config_listener_v3_FilterChainMatch_ConnectionSourceType__Output = typeof _envoy_config_listener_v3_FilterChainMatch_ConnectionSourceType[keyof typeof _envoy_config_listener_v3_FilterChainMatch_ConnectionSourceType] /** * Specifies the match criteria for selecting a specific filter chain for a @@ -154,7 +173,7 @@ export interface FilterChainMatch { /** * Specifies the connection source IP match type. Can be any, local or external network. */ - 'source_type'?: (_envoy_config_listener_v3_FilterChainMatch_ConnectionSourceType | keyof typeof _envoy_config_listener_v3_FilterChainMatch_ConnectionSourceType); + 'source_type'?: (_envoy_config_listener_v3_FilterChainMatch_ConnectionSourceType); /** * The criteria is satisfied if the directly connected source IP address of the downstream * connection is contained in at least one of the specified subnets. If the parameter is not @@ -297,7 +316,7 @@ export interface FilterChainMatch__Output { /** * Specifies the connection source IP match type. Can be any, local or external network. */ - 'source_type': (keyof typeof _envoy_config_listener_v3_FilterChainMatch_ConnectionSourceType); + 'source_type': (_envoy_config_listener_v3_FilterChainMatch_ConnectionSourceType__Output); /** * The criteria is satisfied if the directly connected source IP address of the downstream * connection is contained in at least one of the specified subnets. If the parameter is not diff --git a/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/Listener.ts b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/Listener.ts index ca12fc6de..8897eacf5 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/Listener.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/listener/v3/Listener.ts @@ -8,7 +8,7 @@ import type { Metadata as _envoy_config_core_v3_Metadata, Metadata__Output as _e import type { ListenerFilter as _envoy_config_listener_v3_ListenerFilter, ListenerFilter__Output as _envoy_config_listener_v3_ListenerFilter__Output } from '../../../../envoy/config/listener/v3/ListenerFilter'; import type { SocketOption as _envoy_config_core_v3_SocketOption, SocketOption__Output as _envoy_config_core_v3_SocketOption__Output } from '../../../../envoy/config/core/v3/SocketOption'; import type { Duration as _google_protobuf_Duration, Duration__Output as _google_protobuf_Duration__Output } from '../../../../google/protobuf/Duration'; -import type { TrafficDirection as _envoy_config_core_v3_TrafficDirection } from '../../../../envoy/config/core/v3/TrafficDirection'; +import type { TrafficDirection as _envoy_config_core_v3_TrafficDirection, TrafficDirection__Output as _envoy_config_core_v3_TrafficDirection__Output } from '../../../../envoy/config/core/v3/TrafficDirection'; import type { UdpListenerConfig as _envoy_config_listener_v3_UdpListenerConfig, UdpListenerConfig__Output as _envoy_config_listener_v3_UdpListenerConfig__Output } from '../../../../envoy/config/listener/v3/UdpListenerConfig'; import type { ApiListener as _envoy_config_listener_v3_ApiListener, ApiListener__Output as _envoy_config_listener_v3_ApiListener__Output } from '../../../../envoy/config/listener/v3/ApiListener'; import type { AccessLog as _envoy_config_accesslog_v3_AccessLog, AccessLog__Output as _envoy_config_accesslog_v3_AccessLog__Output } from '../../../../envoy/config/accesslog/v3/AccessLog'; @@ -82,19 +82,36 @@ export interface _envoy_config_listener_v3_Listener_DeprecatedV1__Output { // Original file: deps/envoy-api/envoy/config/listener/v3/listener.proto -export enum _envoy_config_listener_v3_Listener_DrainType { +export const _envoy_config_listener_v3_Listener_DrainType = { /** * Drain in response to calling /healthcheck/fail admin endpoint (along with the health check * filter), listener removal/modification, and hot restart. */ - DEFAULT = 0, + DEFAULT: 'DEFAULT', /** * Drain in response to listener removal/modification and hot restart. This setting does not * include /healthcheck/fail. This setting may be desirable if Envoy is hosting both ingress * and egress listeners. */ - MODIFY_ONLY = 1, -} + MODIFY_ONLY: 'MODIFY_ONLY', +} as const; + +export type _envoy_config_listener_v3_Listener_DrainType = + /** + * Drain in response to calling /healthcheck/fail admin endpoint (along with the health check + * filter), listener removal/modification, and hot restart. + */ + | 'DEFAULT' + | 0 + /** + * Drain in response to listener removal/modification and hot restart. This setting does not + * include /healthcheck/fail. This setting may be desirable if Envoy is hosting both ingress + * and egress listeners. + */ + | 'MODIFY_ONLY' + | 1 + +export type _envoy_config_listener_v3_Listener_DrainType__Output = typeof _envoy_config_listener_v3_Listener_DrainType[keyof typeof _envoy_config_listener_v3_Listener_DrainType] /** * A connection balancer implementation that does exact balancing. This means that a lock is @@ -176,12 +193,13 @@ export interface Listener { 'metadata'?: (_envoy_config_core_v3_Metadata | null); /** * [#not-implemented-hide:] + * @deprecated */ 'deprecated_v1'?: (_envoy_config_listener_v3_Listener_DeprecatedV1 | null); /** * The type of draining to perform at a listener-wide level. */ - 'drain_type'?: (_envoy_config_listener_v3_Listener_DrainType | keyof typeof _envoy_config_listener_v3_Listener_DrainType); + 'drain_type'?: (_envoy_config_listener_v3_Listener_DrainType); /** * Listener filters have the opportunity to manipulate and augment the connection metadata that * is used in connection filter chain matching, for example. These filters are run before any in @@ -256,7 +274,7 @@ export interface Listener { * This property is required on Windows for listeners using the original destination filter, * see :ref:`Original Destination `. */ - 'traffic_direction'?: (_envoy_config_core_v3_TrafficDirection | keyof typeof _envoy_config_core_v3_TrafficDirection); + 'traffic_direction'?: (_envoy_config_core_v3_TrafficDirection); /** * Whether a connection should be created when listener filters timeout. Default is false. * @@ -307,6 +325,7 @@ export interface Listener { 'connection_balance_config'?: (_envoy_config_listener_v3_Listener_ConnectionBalanceConfig | null); /** * Deprecated. Use ``enable_reuse_port`` instead. + * @deprecated */ 'reuse_port'?: (boolean); /** @@ -466,12 +485,13 @@ export interface Listener__Output { 'metadata': (_envoy_config_core_v3_Metadata__Output | null); /** * [#not-implemented-hide:] + * @deprecated */ 'deprecated_v1': (_envoy_config_listener_v3_Listener_DeprecatedV1__Output | null); /** * The type of draining to perform at a listener-wide level. */ - 'drain_type': (keyof typeof _envoy_config_listener_v3_Listener_DrainType); + 'drain_type': (_envoy_config_listener_v3_Listener_DrainType__Output); /** * Listener filters have the opportunity to manipulate and augment the connection metadata that * is used in connection filter chain matching, for example. These filters are run before any in @@ -546,7 +566,7 @@ export interface Listener__Output { * This property is required on Windows for listeners using the original destination filter, * see :ref:`Original Destination `. */ - 'traffic_direction': (keyof typeof _envoy_config_core_v3_TrafficDirection); + 'traffic_direction': (_envoy_config_core_v3_TrafficDirection__Output); /** * Whether a connection should be created when listener filters timeout. Default is false. * @@ -597,6 +617,7 @@ export interface Listener__Output { 'connection_balance_config': (_envoy_config_listener_v3_Listener_ConnectionBalanceConfig__Output | null); /** * Deprecated. Use ``enable_reuse_port`` instead. + * @deprecated */ 'reuse_port': (boolean); /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/DogStatsdSink.ts b/packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/DogStatsdSink.ts deleted file mode 100644 index 4cf705f10..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/DogStatsdSink.ts +++ /dev/null @@ -1,65 +0,0 @@ -// Original file: deps/envoy-api/envoy/config/metrics/v3/stats.proto - -import type { Address as _envoy_config_core_v3_Address, Address__Output as _envoy_config_core_v3_Address__Output } from '../../../../envoy/config/core/v3/Address'; -import type { UInt64Value as _google_protobuf_UInt64Value, UInt64Value__Output as _google_protobuf_UInt64Value__Output } from '../../../../google/protobuf/UInt64Value'; -import type { Long } from '@grpc/proto-loader'; - -/** - * Stats configuration proto schema for built-in *envoy.stat_sinks.dog_statsd* sink. - * The sink emits stats with `DogStatsD `_ - * compatible tags. Tags are configurable via :ref:`StatsConfig - * `. - * [#extension: envoy.stat_sinks.dog_statsd] - */ -export interface DogStatsdSink { - /** - * The UDP address of a running DogStatsD compliant listener. If specified, - * statistics will be flushed to this address. - */ - 'address'?: (_envoy_config_core_v3_Address | null); - /** - * Optional custom metric name prefix. See :ref:`StatsdSink's prefix field - * ` for more details. - */ - 'prefix'?: (string); - /** - * Optional max datagram size to use when sending UDP messages. By default Envoy - * will emit one metric per datagram. By specifying a max-size larger than a single - * metric, Envoy will emit multiple, new-line separated metrics. The max datagram - * size should not exceed your network's MTU. - * - * Note that this value may not be respected if smaller than a single metric. - */ - 'max_bytes_per_datagram'?: (_google_protobuf_UInt64Value | null); - 'dog_statsd_specifier'?: "address"; -} - -/** - * Stats configuration proto schema for built-in *envoy.stat_sinks.dog_statsd* sink. - * The sink emits stats with `DogStatsD `_ - * compatible tags. Tags are configurable via :ref:`StatsConfig - * `. - * [#extension: envoy.stat_sinks.dog_statsd] - */ -export interface DogStatsdSink__Output { - /** - * The UDP address of a running DogStatsD compliant listener. If specified, - * statistics will be flushed to this address. - */ - 'address'?: (_envoy_config_core_v3_Address__Output | null); - /** - * Optional custom metric name prefix. See :ref:`StatsdSink's prefix field - * ` for more details. - */ - 'prefix': (string); - /** - * Optional max datagram size to use when sending UDP messages. By default Envoy - * will emit one metric per datagram. By specifying a max-size larger than a single - * metric, Envoy will emit multiple, new-line separated metrics. The max datagram - * size should not exceed your network's MTU. - * - * Note that this value may not be respected if smaller than a single metric. - */ - 'max_bytes_per_datagram': (_google_protobuf_UInt64Value__Output | null); - 'dog_statsd_specifier': "address"; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/HistogramBucketSettings.ts b/packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/HistogramBucketSettings.ts deleted file mode 100644 index 036958a49..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/HistogramBucketSettings.ts +++ /dev/null @@ -1,35 +0,0 @@ -// Original file: deps/envoy-api/envoy/config/metrics/v3/stats.proto - -import type { StringMatcher as _envoy_type_matcher_v3_StringMatcher, StringMatcher__Output as _envoy_type_matcher_v3_StringMatcher__Output } from '../../../../envoy/type/matcher/v3/StringMatcher'; - -/** - * Specifies a matcher for stats and the buckets that matching stats should use. - */ -export interface HistogramBucketSettings { - /** - * The stats that this rule applies to. The match is applied to the original stat name - * before tag-extraction, for example `cluster.exampleclustername.upstream_cx_length_ms`. - */ - 'match'?: (_envoy_type_matcher_v3_StringMatcher | null); - /** - * Each value is the upper bound of a bucket. Each bucket must be greater than 0 and unique. - * The order of the buckets does not matter. - */ - 'buckets'?: (number | string)[]; -} - -/** - * Specifies a matcher for stats and the buckets that matching stats should use. - */ -export interface HistogramBucketSettings__Output { - /** - * The stats that this rule applies to. The match is applied to the original stat name - * before tag-extraction, for example `cluster.exampleclustername.upstream_cx_length_ms`. - */ - 'match': (_envoy_type_matcher_v3_StringMatcher__Output | null); - /** - * Each value is the upper bound of a bucket. Each bucket must be greater than 0 and unique. - * The order of the buckets does not matter. - */ - 'buckets': (number)[]; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/HystrixSink.ts b/packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/HystrixSink.ts deleted file mode 100644 index b8fb2ed8e..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/HystrixSink.ts +++ /dev/null @@ -1,61 +0,0 @@ -// Original file: deps/envoy-api/envoy/config/metrics/v3/stats.proto - -import type { Long } from '@grpc/proto-loader'; - -/** - * Stats configuration proto schema for built-in *envoy.stat_sinks.hystrix* sink. - * The sink emits stats in `text/event-stream - * `_ - * formatted stream for use by `Hystrix dashboard - * `_. - * - * Note that only a single HystrixSink should be configured. - * - * Streaming is started through an admin endpoint :http:get:`/hystrix_event_stream`. - * [#extension: envoy.stat_sinks.hystrix] - */ -export interface HystrixSink { - /** - * The number of buckets the rolling statistical window is divided into. - * - * Each time the sink is flushed, all relevant Envoy statistics are sampled and - * added to the rolling window (removing the oldest samples in the window - * in the process). The sink then outputs the aggregate statistics across the - * current rolling window to the event stream(s). - * - * rolling_window(ms) = stats_flush_interval(ms) * num_of_buckets - * - * More detailed explanation can be found in `Hystrix wiki - * `_. - */ - 'num_buckets'?: (number | string | Long); -} - -/** - * Stats configuration proto schema for built-in *envoy.stat_sinks.hystrix* sink. - * The sink emits stats in `text/event-stream - * `_ - * formatted stream for use by `Hystrix dashboard - * `_. - * - * Note that only a single HystrixSink should be configured. - * - * Streaming is started through an admin endpoint :http:get:`/hystrix_event_stream`. - * [#extension: envoy.stat_sinks.hystrix] - */ -export interface HystrixSink__Output { - /** - * The number of buckets the rolling statistical window is divided into. - * - * Each time the sink is flushed, all relevant Envoy statistics are sampled and - * added to the rolling window (removing the oldest samples in the window - * in the process). The sink then outputs the aggregate statistics across the - * current rolling window to the event stream(s). - * - * rolling_window(ms) = stats_flush_interval(ms) * num_of_buckets - * - * More detailed explanation can be found in `Hystrix wiki - * `_. - */ - 'num_buckets': (string); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/StatsConfig.ts b/packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/StatsConfig.ts deleted file mode 100644 index df5d7c7e4..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/StatsConfig.ts +++ /dev/null @@ -1,148 +0,0 @@ -// Original file: deps/envoy-api/envoy/config/metrics/v3/stats.proto - -import type { TagSpecifier as _envoy_config_metrics_v3_TagSpecifier, TagSpecifier__Output as _envoy_config_metrics_v3_TagSpecifier__Output } from '../../../../envoy/config/metrics/v3/TagSpecifier'; -import type { BoolValue as _google_protobuf_BoolValue, BoolValue__Output as _google_protobuf_BoolValue__Output } from '../../../../google/protobuf/BoolValue'; -import type { StatsMatcher as _envoy_config_metrics_v3_StatsMatcher, StatsMatcher__Output as _envoy_config_metrics_v3_StatsMatcher__Output } from '../../../../envoy/config/metrics/v3/StatsMatcher'; -import type { HistogramBucketSettings as _envoy_config_metrics_v3_HistogramBucketSettings, HistogramBucketSettings__Output as _envoy_config_metrics_v3_HistogramBucketSettings__Output } from '../../../../envoy/config/metrics/v3/HistogramBucketSettings'; - -/** - * Statistics configuration such as tagging. - */ -export interface StatsConfig { - /** - * Each stat name is iteratively processed through these tag specifiers. - * When a tag is matched, the first capture group is removed from the name so - * later :ref:`TagSpecifiers ` cannot match that - * same portion of the match. - */ - 'stats_tags'?: (_envoy_config_metrics_v3_TagSpecifier)[]; - /** - * Use all default tag regexes specified in Envoy. These can be combined with - * custom tags specified in :ref:`stats_tags - * `. They will be processed before - * the custom tags. - * - * .. note:: - * - * If any default tags are specified twice, the config will be considered - * invalid. - * - * See :repo:`well_known_names.h ` for a list of the - * default tags in Envoy. - * - * If not provided, the value is assumed to be true. - */ - 'use_all_default_tags'?: (_google_protobuf_BoolValue | null); - /** - * Inclusion/exclusion matcher for stat name creation. If not provided, all stats are instantiated - * as normal. Preventing the instantiation of certain families of stats can improve memory - * performance for Envoys running especially large configs. - * - * .. warning:: - * Excluding stats may affect Envoy's behavior in undocumented ways. See - * `issue #8771 `_ for more information. - * If any unexpected behavior changes are observed, please open a new issue immediately. - */ - 'stats_matcher'?: (_envoy_config_metrics_v3_StatsMatcher | null); - /** - * Defines rules for setting the histogram buckets. Rules are evaluated in order, and the first - * match is applied. If no match is found (or if no rules are set), the following default buckets - * are used: - * - * .. code-block:: json - * - * [ - * 0.5, - * 1, - * 5, - * 10, - * 25, - * 50, - * 100, - * 250, - * 500, - * 1000, - * 2500, - * 5000, - * 10000, - * 30000, - * 60000, - * 300000, - * 600000, - * 1800000, - * 3600000 - * ] - */ - 'histogram_bucket_settings'?: (_envoy_config_metrics_v3_HistogramBucketSettings)[]; -} - -/** - * Statistics configuration such as tagging. - */ -export interface StatsConfig__Output { - /** - * Each stat name is iteratively processed through these tag specifiers. - * When a tag is matched, the first capture group is removed from the name so - * later :ref:`TagSpecifiers ` cannot match that - * same portion of the match. - */ - 'stats_tags': (_envoy_config_metrics_v3_TagSpecifier__Output)[]; - /** - * Use all default tag regexes specified in Envoy. These can be combined with - * custom tags specified in :ref:`stats_tags - * `. They will be processed before - * the custom tags. - * - * .. note:: - * - * If any default tags are specified twice, the config will be considered - * invalid. - * - * See :repo:`well_known_names.h ` for a list of the - * default tags in Envoy. - * - * If not provided, the value is assumed to be true. - */ - 'use_all_default_tags': (_google_protobuf_BoolValue__Output | null); - /** - * Inclusion/exclusion matcher for stat name creation. If not provided, all stats are instantiated - * as normal. Preventing the instantiation of certain families of stats can improve memory - * performance for Envoys running especially large configs. - * - * .. warning:: - * Excluding stats may affect Envoy's behavior in undocumented ways. See - * `issue #8771 `_ for more information. - * If any unexpected behavior changes are observed, please open a new issue immediately. - */ - 'stats_matcher': (_envoy_config_metrics_v3_StatsMatcher__Output | null); - /** - * Defines rules for setting the histogram buckets. Rules are evaluated in order, and the first - * match is applied. If no match is found (or if no rules are set), the following default buckets - * are used: - * - * .. code-block:: json - * - * [ - * 0.5, - * 1, - * 5, - * 10, - * 25, - * 50, - * 100, - * 250, - * 500, - * 1000, - * 2500, - * 5000, - * 10000, - * 30000, - * 60000, - * 300000, - * 600000, - * 1800000, - * 3600000 - * ] - */ - 'histogram_bucket_settings': (_envoy_config_metrics_v3_HistogramBucketSettings__Output)[]; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/StatsMatcher.ts b/packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/StatsMatcher.ts deleted file mode 100644 index 9df9e9529..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/StatsMatcher.ts +++ /dev/null @@ -1,47 +0,0 @@ -// Original file: deps/envoy-api/envoy/config/metrics/v3/stats.proto - -import type { ListStringMatcher as _envoy_type_matcher_v3_ListStringMatcher, ListStringMatcher__Output as _envoy_type_matcher_v3_ListStringMatcher__Output } from '../../../../envoy/type/matcher/v3/ListStringMatcher'; - -/** - * Configuration for disabling stat instantiation. - */ -export interface StatsMatcher { - /** - * If `reject_all` is true, then all stats are disabled. If `reject_all` is false, then all - * stats are enabled. - */ - 'reject_all'?: (boolean); - /** - * Exclusive match. All stats are enabled except for those matching one of the supplied - * StringMatcher protos. - */ - 'exclusion_list'?: (_envoy_type_matcher_v3_ListStringMatcher | null); - /** - * Inclusive match. No stats are enabled except for those matching one of the supplied - * StringMatcher protos. - */ - 'inclusion_list'?: (_envoy_type_matcher_v3_ListStringMatcher | null); - 'stats_matcher'?: "reject_all"|"exclusion_list"|"inclusion_list"; -} - -/** - * Configuration for disabling stat instantiation. - */ -export interface StatsMatcher__Output { - /** - * If `reject_all` is true, then all stats are disabled. If `reject_all` is false, then all - * stats are enabled. - */ - 'reject_all'?: (boolean); - /** - * Exclusive match. All stats are enabled except for those matching one of the supplied - * StringMatcher protos. - */ - 'exclusion_list'?: (_envoy_type_matcher_v3_ListStringMatcher__Output | null); - /** - * Inclusive match. No stats are enabled except for those matching one of the supplied - * StringMatcher protos. - */ - 'inclusion_list'?: (_envoy_type_matcher_v3_ListStringMatcher__Output | null); - 'stats_matcher': "reject_all"|"exclusion_list"|"inclusion_list"; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/StatsSink.ts b/packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/StatsSink.ts deleted file mode 100644 index 3eb8926fa..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/StatsSink.ts +++ /dev/null @@ -1,43 +0,0 @@ -// Original file: deps/envoy-api/envoy/config/metrics/v3/stats.proto - -import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../../google/protobuf/Any'; - -/** - * Configuration for pluggable stats sinks. - */ -export interface StatsSink { - /** - * The name of the stats sink to instantiate. The name must match a supported - * stats sink. - * See the :ref:`extensions listed in typed_config below ` for the default list of available stats sink. - * Sinks optionally support tagged/multiple dimensional metrics. - */ - 'name'?: (string); - 'typed_config'?: (_google_protobuf_Any | null); - /** - * Stats sink specific configuration which depends on the sink being instantiated. See - * :ref:`StatsdSink ` for an example. - * [#extension-category: envoy.stats_sinks] - */ - 'config_type'?: "typed_config"; -} - -/** - * Configuration for pluggable stats sinks. - */ -export interface StatsSink__Output { - /** - * The name of the stats sink to instantiate. The name must match a supported - * stats sink. - * See the :ref:`extensions listed in typed_config below ` for the default list of available stats sink. - * Sinks optionally support tagged/multiple dimensional metrics. - */ - 'name': (string); - 'typed_config'?: (_google_protobuf_Any__Output | null); - /** - * Stats sink specific configuration which depends on the sink being instantiated. See - * :ref:`StatsdSink ` for an example. - * [#extension-category: envoy.stats_sinks] - */ - 'config_type': "typed_config"; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/StatsdSink.ts b/packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/StatsdSink.ts deleted file mode 100644 index 69d978920..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/StatsdSink.ts +++ /dev/null @@ -1,103 +0,0 @@ -// Original file: deps/envoy-api/envoy/config/metrics/v3/stats.proto - -import type { Address as _envoy_config_core_v3_Address, Address__Output as _envoy_config_core_v3_Address__Output } from '../../../../envoy/config/core/v3/Address'; - -/** - * Stats configuration proto schema for built-in *envoy.stat_sinks.statsd* sink. This sink does not support - * tagged metrics. - * [#extension: envoy.stat_sinks.statsd] - */ -export interface StatsdSink { - /** - * The UDP address of a running `statsd `_ - * compliant listener. If specified, statistics will be flushed to this - * address. - */ - 'address'?: (_envoy_config_core_v3_Address | null); - /** - * The name of a cluster that is running a TCP `statsd - * `_ compliant listener. If specified, - * Envoy will connect to this cluster to flush statistics. - */ - 'tcp_cluster_name'?: (string); - /** - * Optional custom prefix for StatsdSink. If - * specified, this will override the default prefix. - * For example: - * - * .. code-block:: json - * - * { - * "prefix" : "envoy-prod" - * } - * - * will change emitted stats to - * - * .. code-block:: cpp - * - * envoy-prod.test_counter:1|c - * envoy-prod.test_timer:5|ms - * - * Note that the default prefix, "envoy", will be used if a prefix is not - * specified. - * - * Stats with default prefix: - * - * .. code-block:: cpp - * - * envoy.test_counter:1|c - * envoy.test_timer:5|ms - */ - 'prefix'?: (string); - 'statsd_specifier'?: "address"|"tcp_cluster_name"; -} - -/** - * Stats configuration proto schema for built-in *envoy.stat_sinks.statsd* sink. This sink does not support - * tagged metrics. - * [#extension: envoy.stat_sinks.statsd] - */ -export interface StatsdSink__Output { - /** - * The UDP address of a running `statsd `_ - * compliant listener. If specified, statistics will be flushed to this - * address. - */ - 'address'?: (_envoy_config_core_v3_Address__Output | null); - /** - * The name of a cluster that is running a TCP `statsd - * `_ compliant listener. If specified, - * Envoy will connect to this cluster to flush statistics. - */ - 'tcp_cluster_name'?: (string); - /** - * Optional custom prefix for StatsdSink. If - * specified, this will override the default prefix. - * For example: - * - * .. code-block:: json - * - * { - * "prefix" : "envoy-prod" - * } - * - * will change emitted stats to - * - * .. code-block:: cpp - * - * envoy-prod.test_counter:1|c - * envoy-prod.test_timer:5|ms - * - * Note that the default prefix, "envoy", will be used if a prefix is not - * specified. - * - * Stats with default prefix: - * - * .. code-block:: cpp - * - * envoy.test_counter:1|c - * envoy.test_timer:5|ms - */ - 'prefix': (string); - 'statsd_specifier': "address"|"tcp_cluster_name"; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/TagSpecifier.ts b/packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/TagSpecifier.ts deleted file mode 100644 index 9b0bb2467..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/config/metrics/v3/TagSpecifier.ts +++ /dev/null @@ -1,174 +0,0 @@ -// Original file: deps/envoy-api/envoy/config/metrics/v3/stats.proto - - -/** - * Designates a tag name and value pair. The value may be either a fixed value - * or a regex providing the value via capture groups. The specified tag will be - * unconditionally set if a fixed value, otherwise it will only be set if one - * or more capture groups in the regex match. - */ -export interface TagSpecifier { - /** - * Attaches an identifier to the tag values to identify the tag being in the - * sink. Envoy has a set of default names and regexes to extract dynamic - * portions of existing stats, which can be found in :repo:`well_known_names.h - * ` in the Envoy repository. If a :ref:`tag_name - * ` is provided in the config and - * neither :ref:`regex ` or - * :ref:`fixed_value ` were specified, - * Envoy will attempt to find that name in its set of defaults and use the accompanying regex. - * - * .. note:: - * - * It is invalid to specify the same tag name twice in a config. - */ - 'tag_name'?: (string); - /** - * Designates a tag to strip from the tag extracted name and provide as a named - * tag value for all statistics. This will only occur if any part of the name - * matches the regex provided with one or more capture groups. - * - * The first capture group identifies the portion of the name to remove. The - * second capture group (which will normally be nested inside the first) will - * designate the value of the tag for the statistic. If no second capture - * group is provided, the first will also be used to set the value of the tag. - * All other capture groups will be ignored. - * - * Example 1. a stat name ``cluster.foo_cluster.upstream_rq_timeout`` and - * one tag specifier: - * - * .. code-block:: json - * - * { - * "tag_name": "envoy.cluster_name", - * "regex": "^cluster\\.((.+?)\\.)" - * } - * - * Note that the regex will remove ``foo_cluster.`` making the tag extracted - * name ``cluster.upstream_rq_timeout`` and the tag value for - * ``envoy.cluster_name`` will be ``foo_cluster`` (note: there will be no - * ``.`` character because of the second capture group). - * - * Example 2. a stat name - * ``http.connection_manager_1.user_agent.ios.downstream_cx_total`` and two - * tag specifiers: - * - * .. code-block:: json - * - * [ - * { - * "tag_name": "envoy.http_user_agent", - * "regex": "^http(?=\\.).*?\\.user_agent\\.((.+?)\\.)\\w+?$" - * }, - * { - * "tag_name": "envoy.http_conn_manager_prefix", - * "regex": "^http\\.((.*?)\\.)" - * } - * ] - * - * The two regexes of the specifiers will be processed in the definition order. - * - * The first regex will remove ``ios.``, leaving the tag extracted name - * ``http.connection_manager_1.user_agent.downstream_cx_total``. The tag - * ``envoy.http_user_agent`` will be added with tag value ``ios``. - * - * The second regex will remove ``connection_manager_1.`` from the tag - * extracted name produced by the first regex - * ``http.connection_manager_1.user_agent.downstream_cx_total``, leaving - * ``http.user_agent.downstream_cx_total`` as the tag extracted name. The tag - * ``envoy.http_conn_manager_prefix`` will be added with the tag value - * ``connection_manager_1``. - */ - 'regex'?: (string); - /** - * Specifies a fixed tag value for the ``tag_name``. - */ - 'fixed_value'?: (string); - 'tag_value'?: "regex"|"fixed_value"; -} - -/** - * Designates a tag name and value pair. The value may be either a fixed value - * or a regex providing the value via capture groups. The specified tag will be - * unconditionally set if a fixed value, otherwise it will only be set if one - * or more capture groups in the regex match. - */ -export interface TagSpecifier__Output { - /** - * Attaches an identifier to the tag values to identify the tag being in the - * sink. Envoy has a set of default names and regexes to extract dynamic - * portions of existing stats, which can be found in :repo:`well_known_names.h - * ` in the Envoy repository. If a :ref:`tag_name - * ` is provided in the config and - * neither :ref:`regex ` or - * :ref:`fixed_value ` were specified, - * Envoy will attempt to find that name in its set of defaults and use the accompanying regex. - * - * .. note:: - * - * It is invalid to specify the same tag name twice in a config. - */ - 'tag_name': (string); - /** - * Designates a tag to strip from the tag extracted name and provide as a named - * tag value for all statistics. This will only occur if any part of the name - * matches the regex provided with one or more capture groups. - * - * The first capture group identifies the portion of the name to remove. The - * second capture group (which will normally be nested inside the first) will - * designate the value of the tag for the statistic. If no second capture - * group is provided, the first will also be used to set the value of the tag. - * All other capture groups will be ignored. - * - * Example 1. a stat name ``cluster.foo_cluster.upstream_rq_timeout`` and - * one tag specifier: - * - * .. code-block:: json - * - * { - * "tag_name": "envoy.cluster_name", - * "regex": "^cluster\\.((.+?)\\.)" - * } - * - * Note that the regex will remove ``foo_cluster.`` making the tag extracted - * name ``cluster.upstream_rq_timeout`` and the tag value for - * ``envoy.cluster_name`` will be ``foo_cluster`` (note: there will be no - * ``.`` character because of the second capture group). - * - * Example 2. a stat name - * ``http.connection_manager_1.user_agent.ios.downstream_cx_total`` and two - * tag specifiers: - * - * .. code-block:: json - * - * [ - * { - * "tag_name": "envoy.http_user_agent", - * "regex": "^http(?=\\.).*?\\.user_agent\\.((.+?)\\.)\\w+?$" - * }, - * { - * "tag_name": "envoy.http_conn_manager_prefix", - * "regex": "^http\\.((.*?)\\.)" - * } - * ] - * - * The two regexes of the specifiers will be processed in the definition order. - * - * The first regex will remove ``ios.``, leaving the tag extracted name - * ``http.connection_manager_1.user_agent.downstream_cx_total``. The tag - * ``envoy.http_user_agent`` will be added with tag value ``ios``. - * - * The second regex will remove ``connection_manager_1.`` from the tag - * extracted name produced by the first regex - * ``http.connection_manager_1.user_agent.downstream_cx_total``, leaving - * ``http.user_agent.downstream_cx_total`` as the tag extracted name. The tag - * ``envoy.http_conn_manager_prefix`` will be added with the tag value - * ``connection_manager_1``. - */ - 'regex'?: (string); - /** - * Specifies a fixed tag value for the ``tag_name``. - */ - 'fixed_value'?: (string); - 'tag_value': "regex"|"fixed_value"; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/overload/v3/BufferFactoryConfig.ts b/packages/grpc-js-xds/src/generated/envoy/config/overload/v3/BufferFactoryConfig.ts deleted file mode 100644 index b3fbe1459..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/config/overload/v3/BufferFactoryConfig.ts +++ /dev/null @@ -1,50 +0,0 @@ -// Original file: deps/envoy-api/envoy/config/overload/v3/overload.proto - - -/** - * Configuration for which accounts the WatermarkBuffer Factories should - * track. - */ -export interface BufferFactoryConfig { - /** - * The minimum power of two at which Envoy starts tracking an account. - * - * Envoy has 8 power of two buckets starting with the provided exponent below. - * Concretely the 1st bucket contains accounts for streams that use - * [2^minimum_account_to_track_power_of_two, - * 2^(minimum_account_to_track_power_of_two + 1)) bytes. - * With the 8th bucket tracking accounts - * >= 128 * 2^minimum_account_to_track_power_of_two. - * - * The maximum value is 56, since we're using uint64_t for bytes counting, - * and that's the last value that would use the 8 buckets. In practice, - * we don't expect the proxy to be holding 2^56 bytes. - * - * If omitted, Envoy should not do any tracking. - */ - 'minimum_account_to_track_power_of_two'?: (number); -} - -/** - * Configuration for which accounts the WatermarkBuffer Factories should - * track. - */ -export interface BufferFactoryConfig__Output { - /** - * The minimum power of two at which Envoy starts tracking an account. - * - * Envoy has 8 power of two buckets starting with the provided exponent below. - * Concretely the 1st bucket contains accounts for streams that use - * [2^minimum_account_to_track_power_of_two, - * 2^(minimum_account_to_track_power_of_two + 1)) bytes. - * With the 8th bucket tracking accounts - * >= 128 * 2^minimum_account_to_track_power_of_two. - * - * The maximum value is 56, since we're using uint64_t for bytes counting, - * and that's the last value that would use the 8 buckets. In practice, - * we don't expect the proxy to be holding 2^56 bytes. - * - * If omitted, Envoy should not do any tracking. - */ - 'minimum_account_to_track_power_of_two': (number); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/overload/v3/OverloadAction.ts b/packages/grpc-js-xds/src/generated/envoy/config/overload/v3/OverloadAction.ts deleted file mode 100644 index 84f4db34b..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/config/overload/v3/OverloadAction.ts +++ /dev/null @@ -1,42 +0,0 @@ -// Original file: deps/envoy-api/envoy/config/overload/v3/overload.proto - -import type { Trigger as _envoy_config_overload_v3_Trigger, Trigger__Output as _envoy_config_overload_v3_Trigger__Output } from '../../../../envoy/config/overload/v3/Trigger'; -import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../../google/protobuf/Any'; - -export interface OverloadAction { - /** - * The name of the overload action. This is just a well-known string that listeners can - * use for registering callbacks. Custom overload actions should be named using reverse - * DNS to ensure uniqueness. - */ - 'name'?: (string); - /** - * A set of triggers for this action. The state of the action is the maximum - * state of all triggers, which can be scaling between 0 and 1 or saturated. Listeners - * are notified when the overload action changes state. - */ - 'triggers'?: (_envoy_config_overload_v3_Trigger)[]; - /** - * Configuration for the action being instantiated. - */ - 'typed_config'?: (_google_protobuf_Any | null); -} - -export interface OverloadAction__Output { - /** - * The name of the overload action. This is just a well-known string that listeners can - * use for registering callbacks. Custom overload actions should be named using reverse - * DNS to ensure uniqueness. - */ - 'name': (string); - /** - * A set of triggers for this action. The state of the action is the maximum - * state of all triggers, which can be scaling between 0 and 1 or saturated. Listeners - * are notified when the overload action changes state. - */ - 'triggers': (_envoy_config_overload_v3_Trigger__Output)[]; - /** - * Configuration for the action being instantiated. - */ - 'typed_config': (_google_protobuf_Any__Output | null); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/overload/v3/OverloadManager.ts b/packages/grpc-js-xds/src/generated/envoy/config/overload/v3/OverloadManager.ts deleted file mode 100644 index e7f75b8e9..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/config/overload/v3/OverloadManager.ts +++ /dev/null @@ -1,44 +0,0 @@ -// Original file: deps/envoy-api/envoy/config/overload/v3/overload.proto - -import type { Duration as _google_protobuf_Duration, Duration__Output as _google_protobuf_Duration__Output } from '../../../../google/protobuf/Duration'; -import type { ResourceMonitor as _envoy_config_overload_v3_ResourceMonitor, ResourceMonitor__Output as _envoy_config_overload_v3_ResourceMonitor__Output } from '../../../../envoy/config/overload/v3/ResourceMonitor'; -import type { OverloadAction as _envoy_config_overload_v3_OverloadAction, OverloadAction__Output as _envoy_config_overload_v3_OverloadAction__Output } from '../../../../envoy/config/overload/v3/OverloadAction'; -import type { BufferFactoryConfig as _envoy_config_overload_v3_BufferFactoryConfig, BufferFactoryConfig__Output as _envoy_config_overload_v3_BufferFactoryConfig__Output } from '../../../../envoy/config/overload/v3/BufferFactoryConfig'; - -export interface OverloadManager { - /** - * The interval for refreshing resource usage. - */ - 'refresh_interval'?: (_google_protobuf_Duration | null); - /** - * The set of resources to monitor. - */ - 'resource_monitors'?: (_envoy_config_overload_v3_ResourceMonitor)[]; - /** - * The set of overload actions. - */ - 'actions'?: (_envoy_config_overload_v3_OverloadAction)[]; - /** - * Configuration for buffer factory. - */ - 'buffer_factory_config'?: (_envoy_config_overload_v3_BufferFactoryConfig | null); -} - -export interface OverloadManager__Output { - /** - * The interval for refreshing resource usage. - */ - 'refresh_interval': (_google_protobuf_Duration__Output | null); - /** - * The set of resources to monitor. - */ - 'resource_monitors': (_envoy_config_overload_v3_ResourceMonitor__Output)[]; - /** - * The set of overload actions. - */ - 'actions': (_envoy_config_overload_v3_OverloadAction__Output)[]; - /** - * Configuration for buffer factory. - */ - 'buffer_factory_config': (_envoy_config_overload_v3_BufferFactoryConfig__Output | null); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/overload/v3/ResourceMonitor.ts b/packages/grpc-js-xds/src/generated/envoy/config/overload/v3/ResourceMonitor.ts deleted file mode 100644 index 02fde2411..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/config/overload/v3/ResourceMonitor.ts +++ /dev/null @@ -1,33 +0,0 @@ -// Original file: deps/envoy-api/envoy/config/overload/v3/overload.proto - -import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../../google/protobuf/Any'; - -export interface ResourceMonitor { - /** - * The name of the resource monitor to instantiate. Must match a registered - * resource monitor type. - * See the :ref:`extensions listed in typed_config below ` for the default list of available resource monitor. - */ - 'name'?: (string); - 'typed_config'?: (_google_protobuf_Any | null); - /** - * Configuration for the resource monitor being instantiated. - * [#extension-category: envoy.resource_monitors] - */ - 'config_type'?: "typed_config"; -} - -export interface ResourceMonitor__Output { - /** - * The name of the resource monitor to instantiate. Must match a registered - * resource monitor type. - * See the :ref:`extensions listed in typed_config below ` for the default list of available resource monitor. - */ - 'name': (string); - 'typed_config'?: (_google_protobuf_Any__Output | null); - /** - * Configuration for the resource monitor being instantiated. - * [#extension-category: envoy.resource_monitors] - */ - 'config_type': "typed_config"; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/overload/v3/ScaleTimersOverloadActionConfig.ts b/packages/grpc-js-xds/src/generated/envoy/config/overload/v3/ScaleTimersOverloadActionConfig.ts deleted file mode 100644 index bb48fe3f6..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/config/overload/v3/ScaleTimersOverloadActionConfig.ts +++ /dev/null @@ -1,89 +0,0 @@ -// Original file: deps/envoy-api/envoy/config/overload/v3/overload.proto - -import type { Duration as _google_protobuf_Duration, Duration__Output as _google_protobuf_Duration__Output } from '../../../../google/protobuf/Duration'; -import type { Percent as _envoy_type_v3_Percent, Percent__Output as _envoy_type_v3_Percent__Output } from '../../../../envoy/type/v3/Percent'; - -export interface _envoy_config_overload_v3_ScaleTimersOverloadActionConfig_ScaleTimer { - /** - * The type of timer this minimum applies to. - */ - 'timer'?: (_envoy_config_overload_v3_ScaleTimersOverloadActionConfig_TimerType | keyof typeof _envoy_config_overload_v3_ScaleTimersOverloadActionConfig_TimerType); - /** - * Sets the minimum duration as an absolute value. - */ - 'min_timeout'?: (_google_protobuf_Duration | null); - /** - * Sets the minimum duration as a percentage of the maximum value. - */ - 'min_scale'?: (_envoy_type_v3_Percent | null); - 'overload_adjust'?: "min_timeout"|"min_scale"; -} - -export interface _envoy_config_overload_v3_ScaleTimersOverloadActionConfig_ScaleTimer__Output { - /** - * The type of timer this minimum applies to. - */ - 'timer': (keyof typeof _envoy_config_overload_v3_ScaleTimersOverloadActionConfig_TimerType); - /** - * Sets the minimum duration as an absolute value. - */ - 'min_timeout'?: (_google_protobuf_Duration__Output | null); - /** - * Sets the minimum duration as a percentage of the maximum value. - */ - 'min_scale'?: (_envoy_type_v3_Percent__Output | null); - 'overload_adjust': "min_timeout"|"min_scale"; -} - -// Original file: deps/envoy-api/envoy/config/overload/v3/overload.proto - -export enum _envoy_config_overload_v3_ScaleTimersOverloadActionConfig_TimerType { - /** - * Unsupported value; users must explicitly specify the timer they want scaled. - */ - UNSPECIFIED = 0, - /** - * Adjusts the idle timer for downstream HTTP connections that takes effect when there are no active streams. - * This affects the value of :ref:`HttpConnectionManager.common_http_protocol_options.idle_timeout - * ` - */ - HTTP_DOWNSTREAM_CONNECTION_IDLE = 1, - /** - * Adjusts the idle timer for HTTP streams initiated by downstream clients. - * This affects the value of :ref:`RouteAction.idle_timeout ` and - * :ref:`HttpConnectionManager.stream_idle_timeout - * ` - */ - HTTP_DOWNSTREAM_STREAM_IDLE = 2, - /** - * Adjusts the timer for how long downstream clients have to finish transport-level negotiations - * before the connection is closed. - * This affects the value of - * :ref:`FilterChain.transport_socket_connect_timeout `. - */ - TRANSPORT_SOCKET_CONNECT = 3, -} - -/** - * Typed configuration for the "envoy.overload_actions.reduce_timeouts" action. See - * :ref:`the docs ` for an example of how to configure - * the action with different timeouts and minimum values. - */ -export interface ScaleTimersOverloadActionConfig { - /** - * A set of timer scaling rules to be applied. - */ - 'timer_scale_factors'?: (_envoy_config_overload_v3_ScaleTimersOverloadActionConfig_ScaleTimer)[]; -} - -/** - * Typed configuration for the "envoy.overload_actions.reduce_timeouts" action. See - * :ref:`the docs ` for an example of how to configure - * the action with different timeouts and minimum values. - */ -export interface ScaleTimersOverloadActionConfig__Output { - /** - * A set of timer scaling rules to be applied. - */ - 'timer_scale_factors': (_envoy_config_overload_v3_ScaleTimersOverloadActionConfig_ScaleTimer__Output)[]; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/overload/v3/ScaledTrigger.ts b/packages/grpc-js-xds/src/generated/envoy/config/overload/v3/ScaledTrigger.ts deleted file mode 100644 index 8c6574f56..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/config/overload/v3/ScaledTrigger.ts +++ /dev/null @@ -1,28 +0,0 @@ -// Original file: deps/envoy-api/envoy/config/overload/v3/overload.proto - - -export interface ScaledTrigger { - /** - * If the resource pressure is greater than this value, the trigger will be in the - * :ref:`scaling ` state with value - * `(pressure - scaling_threshold) / (saturation_threshold - scaling_threshold)`. - */ - 'scaling_threshold'?: (number | string); - /** - * If the resource pressure is greater than this value, the trigger will enter saturation. - */ - 'saturation_threshold'?: (number | string); -} - -export interface ScaledTrigger__Output { - /** - * If the resource pressure is greater than this value, the trigger will be in the - * :ref:`scaling ` state with value - * `(pressure - scaling_threshold) / (saturation_threshold - scaling_threshold)`. - */ - 'scaling_threshold': (number); - /** - * If the resource pressure is greater than this value, the trigger will enter saturation. - */ - 'saturation_threshold': (number); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/overload/v3/ThresholdTrigger.ts b/packages/grpc-js-xds/src/generated/envoy/config/overload/v3/ThresholdTrigger.ts deleted file mode 100644 index b02ddd47d..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/config/overload/v3/ThresholdTrigger.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Original file: deps/envoy-api/envoy/config/overload/v3/overload.proto - - -export interface ThresholdTrigger { - /** - * If the resource pressure is greater than or equal to this value, the trigger - * will enter saturation. - */ - 'value'?: (number | string); -} - -export interface ThresholdTrigger__Output { - /** - * If the resource pressure is greater than or equal to this value, the trigger - * will enter saturation. - */ - 'value': (number); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/overload/v3/Trigger.ts b/packages/grpc-js-xds/src/generated/envoy/config/overload/v3/Trigger.ts deleted file mode 100644 index 38f360ee7..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/config/overload/v3/Trigger.ts +++ /dev/null @@ -1,24 +0,0 @@ -// Original file: deps/envoy-api/envoy/config/overload/v3/overload.proto - -import type { ThresholdTrigger as _envoy_config_overload_v3_ThresholdTrigger, ThresholdTrigger__Output as _envoy_config_overload_v3_ThresholdTrigger__Output } from '../../../../envoy/config/overload/v3/ThresholdTrigger'; -import type { ScaledTrigger as _envoy_config_overload_v3_ScaledTrigger, ScaledTrigger__Output as _envoy_config_overload_v3_ScaledTrigger__Output } from '../../../../envoy/config/overload/v3/ScaledTrigger'; - -export interface Trigger { - /** - * The name of the resource this is a trigger for. - */ - 'name'?: (string); - 'threshold'?: (_envoy_config_overload_v3_ThresholdTrigger | null); - 'scaled'?: (_envoy_config_overload_v3_ScaledTrigger | null); - 'trigger_oneof'?: "threshold"|"scaled"; -} - -export interface Trigger__Output { - /** - * The name of the resource this is a trigger for. - */ - 'name': (string); - 'threshold'?: (_envoy_config_overload_v3_ThresholdTrigger__Output | null); - 'scaled'?: (_envoy_config_overload_v3_ScaledTrigger__Output | null); - 'trigger_oneof': "threshold"|"scaled"; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/HeaderMatcher.ts b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/HeaderMatcher.ts index e073a8f13..b5b085ae7 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/HeaderMatcher.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/HeaderMatcher.ts @@ -3,7 +3,6 @@ import type { Int64Range as _envoy_type_v3_Int64Range, Int64Range__Output as _envoy_type_v3_Int64Range__Output } from '../../../../envoy/type/v3/Int64Range'; import type { RegexMatcher as _envoy_type_matcher_v3_RegexMatcher, RegexMatcher__Output as _envoy_type_matcher_v3_RegexMatcher__Output } from '../../../../envoy/type/matcher/v3/RegexMatcher'; import type { StringMatcher as _envoy_type_matcher_v3_StringMatcher, StringMatcher__Output as _envoy_type_matcher_v3_StringMatcher__Output } from '../../../../envoy/type/matcher/v3/StringMatcher'; -import type { Long } from '@grpc/proto-loader'; /** * .. attention:: @@ -42,6 +41,7 @@ export interface HeaderMatcher { /** * If specified, header match will be performed based on the value of the header. * This field is deprecated. Please use :ref:`string_match `. + * @deprecated */ 'exact_match'?: (string); /** @@ -80,6 +80,7 @@ export interface HeaderMatcher { * Examples: * * * The prefix ``abcd`` matches the value ``abcdxyz``, but not for ``abcxyz``. + * @deprecated */ 'prefix_match'?: (string); /** @@ -90,6 +91,7 @@ export interface HeaderMatcher { * Examples: * * * The suffix ``abcd`` matches the value ``xyzabcd``, but not for ``xyzbcd``. + * @deprecated */ 'suffix_match'?: (string); /** @@ -97,6 +99,7 @@ export interface HeaderMatcher { * header value must match the regex. The rule will not match if only a subsequence of the * request header value matches the regex. * This field is deprecated. Please use :ref:`string_match `. + * @deprecated */ 'safe_regex_match'?: (_envoy_type_matcher_v3_RegexMatcher | null); /** @@ -108,6 +111,7 @@ export interface HeaderMatcher { * Examples: * * * The value ``abcd`` matches the value ``xyzabcdpqr``, but not for ``xyzbcdpqr``. + * @deprecated */ 'contains_match'?: (string); /** @@ -186,6 +190,7 @@ export interface HeaderMatcher__Output { /** * If specified, header match will be performed based on the value of the header. * This field is deprecated. Please use :ref:`string_match `. + * @deprecated */ 'exact_match'?: (string); /** @@ -224,6 +229,7 @@ export interface HeaderMatcher__Output { * Examples: * * * The prefix ``abcd`` matches the value ``abcdxyz``, but not for ``abcxyz``. + * @deprecated */ 'prefix_match'?: (string); /** @@ -234,6 +240,7 @@ export interface HeaderMatcher__Output { * Examples: * * * The suffix ``abcd`` matches the value ``xyzabcd``, but not for ``xyzbcd``. + * @deprecated */ 'suffix_match'?: (string); /** @@ -241,6 +248,7 @@ export interface HeaderMatcher__Output { * header value must match the regex. The rule will not match if only a subsequence of the * request header value matches the regex. * This field is deprecated. Please use :ref:`string_match `. + * @deprecated */ 'safe_regex_match'?: (_envoy_type_matcher_v3_RegexMatcher__Output | null); /** @@ -252,6 +260,7 @@ export interface HeaderMatcher__Output { * Examples: * * * The value ``abcd`` matches the value ``xyzabcdpqr``, but not for ``xyzbcdpqr``. + * @deprecated */ 'contains_match'?: (string); /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RateLimit.ts b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RateLimit.ts index cd47e471a..28d17667f 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RateLimit.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RateLimit.ts @@ -40,6 +40,7 @@ export interface _envoy_config_route_v3_RateLimit_Action { * * .. attention:: * This field has been deprecated in favor of the :ref:`metadata ` field + * @deprecated */ 'dynamic_metadata'?: (_envoy_config_route_v3_RateLimit_Action_DynamicMetaData | null); /** @@ -101,6 +102,7 @@ export interface _envoy_config_route_v3_RateLimit_Action__Output { * * .. attention:: * This field has been deprecated in favor of the :ref:`metadata ` field + * @deprecated */ 'dynamic_metadata'?: (_envoy_config_route_v3_RateLimit_Action_DynamicMetaData__Output | null); /** @@ -438,7 +440,7 @@ export interface _envoy_config_route_v3_RateLimit_Action_MetaData { /** * Source of metadata */ - 'source'?: (_envoy_config_route_v3_RateLimit_Action_MetaData_Source | keyof typeof _envoy_config_route_v3_RateLimit_Action_MetaData_Source); + 'source'?: (_envoy_config_route_v3_RateLimit_Action_MetaData_Source); /** * If set to true, Envoy skips the descriptor while calling rate limiting service * when ``metadata_key`` is empty and ``default_value`` is not set. By default it skips calling the @@ -474,7 +476,7 @@ export interface _envoy_config_route_v3_RateLimit_Action_MetaData__Output { /** * Source of metadata */ - 'source': (keyof typeof _envoy_config_route_v3_RateLimit_Action_MetaData_Source); + 'source': (_envoy_config_route_v3_RateLimit_Action_MetaData_Source__Output); /** * If set to true, Envoy skips the descriptor while calling rate limiting service * when ``metadata_key`` is empty and ``default_value`` is not set. By default it skips calling the @@ -643,16 +645,30 @@ export interface _envoy_config_route_v3_RateLimit_Action_RequestHeaders__Output // Original file: deps/envoy-api/envoy/config/route/v3/route_components.proto -export enum _envoy_config_route_v3_RateLimit_Action_MetaData_Source { +export const _envoy_config_route_v3_RateLimit_Action_MetaData_Source = { /** * Query :ref:`dynamic metadata ` */ - DYNAMIC = 0, + DYNAMIC: 'DYNAMIC', /** * Query :ref:`route entry metadata ` */ - ROUTE_ENTRY = 1, -} + ROUTE_ENTRY: 'ROUTE_ENTRY', +} as const; + +export type _envoy_config_route_v3_RateLimit_Action_MetaData_Source = + /** + * Query :ref:`dynamic metadata ` + */ + | 'DYNAMIC' + | 0 + /** + * Query :ref:`route entry metadata ` + */ + | 'ROUTE_ENTRY' + | 1 + +export type _envoy_config_route_v3_RateLimit_Action_MetaData_Source__Output = typeof _envoy_config_route_v3_RateLimit_Action_MetaData_Source[keyof typeof _envoy_config_route_v3_RateLimit_Action_MetaData_Source] /** * The following descriptor entry is appended to the descriptor: diff --git a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RedirectAction.ts b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RedirectAction.ts index fd11a681b..070470af3 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RedirectAction.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RedirectAction.ts @@ -4,28 +4,57 @@ import type { RegexMatchAndSubstitute as _envoy_type_matcher_v3_RegexMatchAndSub // Original file: deps/envoy-api/envoy/config/route/v3/route_components.proto -export enum _envoy_config_route_v3_RedirectAction_RedirectResponseCode { +export const _envoy_config_route_v3_RedirectAction_RedirectResponseCode = { /** * Moved Permanently HTTP Status Code - 301. */ - MOVED_PERMANENTLY = 0, + MOVED_PERMANENTLY: 'MOVED_PERMANENTLY', /** * Found HTTP Status Code - 302. */ - FOUND = 1, + FOUND: 'FOUND', /** * See Other HTTP Status Code - 303. */ - SEE_OTHER = 2, + SEE_OTHER: 'SEE_OTHER', /** * Temporary Redirect HTTP Status Code - 307. */ - TEMPORARY_REDIRECT = 3, + TEMPORARY_REDIRECT: 'TEMPORARY_REDIRECT', /** * Permanent Redirect HTTP Status Code - 308. */ - PERMANENT_REDIRECT = 4, -} + PERMANENT_REDIRECT: 'PERMANENT_REDIRECT', +} as const; + +export type _envoy_config_route_v3_RedirectAction_RedirectResponseCode = + /** + * Moved Permanently HTTP Status Code - 301. + */ + | 'MOVED_PERMANENTLY' + | 0 + /** + * Found HTTP Status Code - 302. + */ + | 'FOUND' + | 1 + /** + * See Other HTTP Status Code - 303. + */ + | 'SEE_OTHER' + | 2 + /** + * Temporary Redirect HTTP Status Code - 307. + */ + | 'TEMPORARY_REDIRECT' + | 3 + /** + * Permanent Redirect HTTP Status Code - 308. + */ + | 'PERMANENT_REDIRECT' + | 4 + +export type _envoy_config_route_v3_RedirectAction_RedirectResponseCode__Output = typeof _envoy_config_route_v3_RedirectAction_RedirectResponseCode[keyof typeof _envoy_config_route_v3_RedirectAction_RedirectResponseCode] /** * [#next-free-field: 10] @@ -58,7 +87,7 @@ export interface RedirectAction { * The HTTP status code to use in the redirect response. The default response * code is MOVED_PERMANENTLY (301). */ - 'response_code'?: (_envoy_config_route_v3_RedirectAction_RedirectResponseCode | keyof typeof _envoy_config_route_v3_RedirectAction_RedirectResponseCode); + 'response_code'?: (_envoy_config_route_v3_RedirectAction_RedirectResponseCode); /** * The scheme portion of the URL will be swapped with "https". */ @@ -155,7 +184,7 @@ export interface RedirectAction__Output { * The HTTP status code to use in the redirect response. The default response * code is MOVED_PERMANENTLY (301). */ - 'response_code': (keyof typeof _envoy_config_route_v3_RedirectAction_RedirectResponseCode); + 'response_code': (_envoy_config_route_v3_RedirectAction_RedirectResponseCode__Output); /** * The scheme portion of the URL will be swapped with "https". */ diff --git a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RetryPolicy.ts b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RetryPolicy.ts index d60458728..773943b7f 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RetryPolicy.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RetryPolicy.ts @@ -141,7 +141,7 @@ export interface _envoy_config_route_v3_RetryPolicy_ResetHeader { /** * The format of the reset header. */ - 'format'?: (_envoy_config_route_v3_RetryPolicy_ResetHeaderFormat | keyof typeof _envoy_config_route_v3_RetryPolicy_ResetHeaderFormat); + 'format'?: (_envoy_config_route_v3_RetryPolicy_ResetHeaderFormat); } export interface _envoy_config_route_v3_RetryPolicy_ResetHeader__Output { @@ -156,15 +156,23 @@ export interface _envoy_config_route_v3_RetryPolicy_ResetHeader__Output { /** * The format of the reset header. */ - 'format': (keyof typeof _envoy_config_route_v3_RetryPolicy_ResetHeaderFormat); + 'format': (_envoy_config_route_v3_RetryPolicy_ResetHeaderFormat__Output); } // Original file: deps/envoy-api/envoy/config/route/v3/route_components.proto -export enum _envoy_config_route_v3_RetryPolicy_ResetHeaderFormat { - SECONDS = 0, - UNIX_TIMESTAMP = 1, -} +export const _envoy_config_route_v3_RetryPolicy_ResetHeaderFormat = { + SECONDS: 'SECONDS', + UNIX_TIMESTAMP: 'UNIX_TIMESTAMP', +} as const; + +export type _envoy_config_route_v3_RetryPolicy_ResetHeaderFormat = + | 'SECONDS' + | 0 + | 'UNIX_TIMESTAMP' + | 1 + +export type _envoy_config_route_v3_RetryPolicy_ResetHeaderFormat__Output = typeof _envoy_config_route_v3_RetryPolicy_ResetHeaderFormat[keyof typeof _envoy_config_route_v3_RetryPolicy_ResetHeaderFormat] export interface _envoy_config_route_v3_RetryPolicy_RetryBackOff { /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RouteAction.ts b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RouteAction.ts index 9dd8b7c2c..fd38da92e 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RouteAction.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/RouteAction.ts @@ -5,7 +5,7 @@ import type { Metadata as _envoy_config_core_v3_Metadata, Metadata__Output as _e import type { BoolValue as _google_protobuf_BoolValue, BoolValue__Output as _google_protobuf_BoolValue__Output } from '../../../../google/protobuf/BoolValue'; import type { Duration as _google_protobuf_Duration, Duration__Output as _google_protobuf_Duration__Output } from '../../../../google/protobuf/Duration'; import type { RetryPolicy as _envoy_config_route_v3_RetryPolicy, RetryPolicy__Output as _envoy_config_route_v3_RetryPolicy__Output } from '../../../../envoy/config/route/v3/RetryPolicy'; -import type { RoutingPriority as _envoy_config_core_v3_RoutingPriority } from '../../../../envoy/config/core/v3/RoutingPriority'; +import type { RoutingPriority as _envoy_config_core_v3_RoutingPriority, RoutingPriority__Output as _envoy_config_core_v3_RoutingPriority__Output } from '../../../../envoy/config/core/v3/RoutingPriority'; import type { RateLimit as _envoy_config_route_v3_RateLimit, RateLimit__Output as _envoy_config_route_v3_RateLimit__Output } from '../../../../envoy/config/route/v3/RateLimit'; import type { CorsPolicy as _envoy_config_route_v3_CorsPolicy, CorsPolicy__Output as _envoy_config_route_v3_CorsPolicy__Output } from '../../../../envoy/config/route/v3/CorsPolicy'; import type { HedgePolicy as _envoy_config_route_v3_HedgePolicy, HedgePolicy__Output as _envoy_config_route_v3_HedgePolicy__Output } from '../../../../envoy/config/route/v3/HedgePolicy'; @@ -20,20 +20,39 @@ import type { ProxyProtocolConfig as _envoy_config_core_v3_ProxyProtocolConfig, // Original file: deps/envoy-api/envoy/config/route/v3/route_components.proto -export enum _envoy_config_route_v3_RouteAction_ClusterNotFoundResponseCode { +export const _envoy_config_route_v3_RouteAction_ClusterNotFoundResponseCode = { /** * HTTP status code - 503 Service Unavailable. */ - SERVICE_UNAVAILABLE = 0, + SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE', /** * HTTP status code - 404 Not Found. */ - NOT_FOUND = 1, + NOT_FOUND: 'NOT_FOUND', /** * HTTP status code - 500 Internal Server Error. */ - INTERNAL_SERVER_ERROR = 2, -} + INTERNAL_SERVER_ERROR: 'INTERNAL_SERVER_ERROR', +} as const; + +export type _envoy_config_route_v3_RouteAction_ClusterNotFoundResponseCode = + /** + * HTTP status code - 503 Service Unavailable. + */ + | 'SERVICE_UNAVAILABLE' + | 0 + /** + * HTTP status code - 404 Not Found. + */ + | 'NOT_FOUND' + | 1 + /** + * HTTP status code - 500 Internal Server Error. + */ + | 'INTERNAL_SERVER_ERROR' + | 2 + +export type _envoy_config_route_v3_RouteAction_ClusterNotFoundResponseCode__Output = typeof _envoy_config_route_v3_RouteAction_ClusterNotFoundResponseCode[keyof typeof _envoy_config_route_v3_RouteAction_ClusterNotFoundResponseCode] /** * Configuration for sending data upstream as a raw data payload. This is used for @@ -302,11 +321,30 @@ export interface _envoy_config_route_v3_RouteAction_HashPolicy_Header__Output { /** * Configures :ref:`internal redirect ` behavior. * [#next-major-version: remove this definition - it's defined in the InternalRedirectPolicy message.] + * @deprecated */ -export enum _envoy_config_route_v3_RouteAction_InternalRedirectAction { - PASS_THROUGH_INTERNAL_REDIRECT = 0, - HANDLE_INTERNAL_REDIRECT = 1, -} +export const _envoy_config_route_v3_RouteAction_InternalRedirectAction = { + PASS_THROUGH_INTERNAL_REDIRECT: 'PASS_THROUGH_INTERNAL_REDIRECT', + HANDLE_INTERNAL_REDIRECT: 'HANDLE_INTERNAL_REDIRECT', +} as const; + +/** + * Configures :ref:`internal redirect ` behavior. + * [#next-major-version: remove this definition - it's defined in the InternalRedirectPolicy message.] + * @deprecated + */ +export type _envoy_config_route_v3_RouteAction_InternalRedirectAction = + | 'PASS_THROUGH_INTERNAL_REDIRECT' + | 0 + | 'HANDLE_INTERNAL_REDIRECT' + | 1 + +/** + * Configures :ref:`internal redirect ` behavior. + * [#next-major-version: remove this definition - it's defined in the InternalRedirectPolicy message.] + * @deprecated + */ +export type _envoy_config_route_v3_RouteAction_InternalRedirectAction__Output = typeof _envoy_config_route_v3_RouteAction_InternalRedirectAction[keyof typeof _envoy_config_route_v3_RouteAction_InternalRedirectAction] export interface _envoy_config_route_v3_RouteAction_MaxStreamDuration { /** @@ -679,7 +717,7 @@ export interface RouteAction { /** * Optionally specifies the :ref:`routing priority `. */ - 'priority'?: (_envoy_config_core_v3_RoutingPriority | keyof typeof _envoy_config_core_v3_RoutingPriority); + 'priority'?: (_envoy_config_core_v3_RoutingPriority); /** * Specifies a set of rate limit configurations that could be applied to the * route. @@ -692,6 +730,7 @@ export interface RouteAction { * request. * * This field is deprecated. Please use :ref:`vh_rate_limits ` + * @deprecated */ 'include_vh_rate_limits'?: (_google_protobuf_BoolValue | null); /** @@ -720,13 +759,14 @@ export interface RouteAction { * :ref:`Route.typed_per_filter_config` or * :ref:`WeightedCluster.ClusterWeight.typed_per_filter_config` * to configure the CORS HTTP filter. + * @deprecated */ 'cors'?: (_envoy_config_route_v3_CorsPolicy | null); /** * The HTTP status code to use when configured cluster is not found. * The default response code is 503 Service Unavailable. */ - 'cluster_not_found_response_code'?: (_envoy_config_route_v3_RouteAction_ClusterNotFoundResponseCode | keyof typeof _envoy_config_route_v3_RouteAction_ClusterNotFoundResponseCode); + 'cluster_not_found_response_code'?: (_envoy_config_route_v3_RouteAction_ClusterNotFoundResponseCode); /** * Deprecated by :ref:`grpc_timeout_header_max ` * If present, and the request is a gRPC request, use the @@ -748,6 +788,7 @@ export interface RouteAction { * :ref:`config_http_filters_router_x-envoy-upstream-rq-timeout-ms`, * :ref:`config_http_filters_router_x-envoy-upstream-rq-per-try-timeout-ms`, and the * :ref:`retry overview `. + * @deprecated */ 'max_grpc_timeout'?: (_google_protobuf_Duration | null); /** @@ -776,7 +817,10 @@ export interface RouteAction { */ 'idle_timeout'?: (_google_protobuf_Duration | null); 'upgrade_configs'?: (_envoy_config_route_v3_RouteAction_UpgradeConfig)[]; - 'internal_redirect_action'?: (_envoy_config_route_v3_RouteAction_InternalRedirectAction | keyof typeof _envoy_config_route_v3_RouteAction_InternalRedirectAction); + /** + * @deprecated + */ + 'internal_redirect_action'?: (_envoy_config_route_v3_RouteAction_InternalRedirectAction); /** * Indicates that the route has a hedge policy. Note that if this is set, * it'll take precedence over the virtual host level hedge policy entirely @@ -792,6 +836,7 @@ export interface RouteAction { * The offset will only be applied if the provided grpc_timeout is greater than the offset. This * ensures that the offset will only ever decrease the timeout and never set it to 0 (meaning * infinity). + * @deprecated */ 'grpc_timeout_offset'?: (_google_protobuf_Duration | null); /** @@ -833,6 +878,7 @@ export interface RouteAction { * will pass the redirect back to downstream. * * If not specified, at most one redirect will be followed. + * @deprecated */ 'max_internal_redirects'?: (_google_protobuf_UInt32Value | null); /** @@ -1063,7 +1109,7 @@ export interface RouteAction__Output { /** * Optionally specifies the :ref:`routing priority `. */ - 'priority': (keyof typeof _envoy_config_core_v3_RoutingPriority); + 'priority': (_envoy_config_core_v3_RoutingPriority__Output); /** * Specifies a set of rate limit configurations that could be applied to the * route. @@ -1076,6 +1122,7 @@ export interface RouteAction__Output { * request. * * This field is deprecated. Please use :ref:`vh_rate_limits ` + * @deprecated */ 'include_vh_rate_limits': (_google_protobuf_BoolValue__Output | null); /** @@ -1104,13 +1151,14 @@ export interface RouteAction__Output { * :ref:`Route.typed_per_filter_config` or * :ref:`WeightedCluster.ClusterWeight.typed_per_filter_config` * to configure the CORS HTTP filter. + * @deprecated */ 'cors': (_envoy_config_route_v3_CorsPolicy__Output | null); /** * The HTTP status code to use when configured cluster is not found. * The default response code is 503 Service Unavailable. */ - 'cluster_not_found_response_code': (keyof typeof _envoy_config_route_v3_RouteAction_ClusterNotFoundResponseCode); + 'cluster_not_found_response_code': (_envoy_config_route_v3_RouteAction_ClusterNotFoundResponseCode__Output); /** * Deprecated by :ref:`grpc_timeout_header_max ` * If present, and the request is a gRPC request, use the @@ -1132,6 +1180,7 @@ export interface RouteAction__Output { * :ref:`config_http_filters_router_x-envoy-upstream-rq-timeout-ms`, * :ref:`config_http_filters_router_x-envoy-upstream-rq-per-try-timeout-ms`, and the * :ref:`retry overview `. + * @deprecated */ 'max_grpc_timeout': (_google_protobuf_Duration__Output | null); /** @@ -1160,7 +1209,10 @@ export interface RouteAction__Output { */ 'idle_timeout': (_google_protobuf_Duration__Output | null); 'upgrade_configs': (_envoy_config_route_v3_RouteAction_UpgradeConfig__Output)[]; - 'internal_redirect_action': (keyof typeof _envoy_config_route_v3_RouteAction_InternalRedirectAction); + /** + * @deprecated + */ + 'internal_redirect_action': (_envoy_config_route_v3_RouteAction_InternalRedirectAction__Output); /** * Indicates that the route has a hedge policy. Note that if this is set, * it'll take precedence over the virtual host level hedge policy entirely @@ -1176,6 +1228,7 @@ export interface RouteAction__Output { * The offset will only be applied if the provided grpc_timeout is greater than the offset. This * ensures that the offset will only ever decrease the timeout and never set it to 0 (meaning * infinity). + * @deprecated */ 'grpc_timeout_offset': (_google_protobuf_Duration__Output | null); /** @@ -1217,6 +1270,7 @@ export interface RouteAction__Output { * will pass the redirect back to downstream. * * If not specified, at most one redirect will be followed. + * @deprecated */ 'max_internal_redirects': (_google_protobuf_UInt32Value__Output | null); /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/VirtualHost.ts b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/VirtualHost.ts index b2c344fff..5109be872 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/VirtualHost.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/VirtualHost.ts @@ -14,22 +14,43 @@ import type { _envoy_config_route_v3_RouteAction_RequestMirrorPolicy, _envoy_con // Original file: deps/envoy-api/envoy/config/route/v3/route_components.proto -export enum _envoy_config_route_v3_VirtualHost_TlsRequirementType { +export const _envoy_config_route_v3_VirtualHost_TlsRequirementType = { /** * No TLS requirement for the virtual host. */ - NONE = 0, + NONE: 'NONE', /** * External requests must use TLS. If a request is external and it is not * using TLS, a 301 redirect will be sent telling the client to use HTTPS. */ - EXTERNAL_ONLY = 1, + EXTERNAL_ONLY: 'EXTERNAL_ONLY', /** * All requests must use TLS. If a request is not using TLS, a 301 redirect * will be sent telling the client to use HTTPS. */ - ALL = 2, -} + ALL: 'ALL', +} as const; + +export type _envoy_config_route_v3_VirtualHost_TlsRequirementType = + /** + * No TLS requirement for the virtual host. + */ + | 'NONE' + | 0 + /** + * External requests must use TLS. If a request is external and it is not + * using TLS, a 301 redirect will be sent telling the client to use HTTPS. + */ + | 'EXTERNAL_ONLY' + | 1 + /** + * All requests must use TLS. If a request is not using TLS, a 301 redirect + * will be sent telling the client to use HTTPS. + */ + | 'ALL' + | 2 + +export type _envoy_config_route_v3_VirtualHost_TlsRequirementType__Output = typeof _envoy_config_route_v3_VirtualHost_TlsRequirementType[keyof typeof _envoy_config_route_v3_VirtualHost_TlsRequirementType] /** * The top level element in the routing configuration is a virtual host. Each virtual host has @@ -76,7 +97,7 @@ export interface VirtualHost { * Specifies the type of TLS enforcement the virtual host expects. If this option is not * specified, there is no TLS requirement for the virtual host. */ - 'require_tls'?: (_envoy_config_route_v3_VirtualHost_TlsRequirementType | keyof typeof _envoy_config_route_v3_VirtualHost_TlsRequirementType); + 'require_tls'?: (_envoy_config_route_v3_VirtualHost_TlsRequirementType); /** * A list of virtual clusters defined for this virtual host. Virtual clusters * are used for additional statistics gathering. @@ -106,6 +127,7 @@ export interface VirtualHost { * This option has been deprecated. Please use * :ref:`VirtualHost.typed_per_filter_config` * to configure the CORS HTTP filter. + * @deprecated */ 'cors'?: (_envoy_config_route_v3_CorsPolicy | null); /** @@ -256,7 +278,7 @@ export interface VirtualHost__Output { * Specifies the type of TLS enforcement the virtual host expects. If this option is not * specified, there is no TLS requirement for the virtual host. */ - 'require_tls': (keyof typeof _envoy_config_route_v3_VirtualHost_TlsRequirementType); + 'require_tls': (_envoy_config_route_v3_VirtualHost_TlsRequirementType__Output); /** * A list of virtual clusters defined for this virtual host. Virtual clusters * are used for additional statistics gathering. @@ -286,6 +308,7 @@ export interface VirtualHost__Output { * This option has been deprecated. Please use * :ref:`VirtualHost.typed_per_filter_config` * to configure the CORS HTTP filter. + * @deprecated */ 'cors': (_envoy_config_route_v3_CorsPolicy__Output | null); /** diff --git a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/WeightedCluster.ts b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/WeightedCluster.ts index cc820654d..91f4c6aeb 100644 --- a/packages/grpc-js-xds/src/generated/envoy/config/route/v3/WeightedCluster.ts +++ b/packages/grpc-js-xds/src/generated/envoy/config/route/v3/WeightedCluster.ts @@ -232,6 +232,7 @@ export interface WeightedCluster { * value, if this is greater than 0. * This field is now deprecated, and the client will use the sum of all * cluster weights. It is up to the management server to supply the correct weights. + * @deprecated */ 'total_weight'?: (_google_protobuf_UInt32Value | null); /** @@ -274,6 +275,7 @@ export interface WeightedCluster__Output { * value, if this is greater than 0. * This field is now deprecated, and the client will use the sum of all * cluster weights. It is up to the management server to supply the correct weights. + * @deprecated */ 'total_weight': (_google_protobuf_UInt32Value__Output | null); /** diff --git a/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/AccessLogCommon.ts b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/AccessLogCommon.ts index 7679dfac1..7ca3c5b19 100644 --- a/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/AccessLogCommon.ts +++ b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/AccessLogCommon.ts @@ -7,7 +7,7 @@ import type { Duration as _google_protobuf_Duration, Duration__Output as _google import type { ResponseFlags as _envoy_data_accesslog_v3_ResponseFlags, ResponseFlags__Output as _envoy_data_accesslog_v3_ResponseFlags__Output } from '../../../../envoy/data/accesslog/v3/ResponseFlags'; import type { Metadata as _envoy_config_core_v3_Metadata, Metadata__Output as _envoy_config_core_v3_Metadata__Output } from '../../../../envoy/config/core/v3/Metadata'; import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../../google/protobuf/Any'; -import type { AccessLogType as _envoy_data_accesslog_v3_AccessLogType } from '../../../../envoy/data/accesslog/v3/AccessLogType'; +import type { AccessLogType as _envoy_data_accesslog_v3_AccessLogType, AccessLogType__Output as _envoy_data_accesslog_v3_AccessLogType__Output } from '../../../../envoy/data/accesslog/v3/AccessLogType'; import type { Long } from '@grpc/proto-loader'; /** @@ -176,6 +176,7 @@ export interface AccessLogCommon { * * This field is deprecated in favor of ``access_log_type`` for better indication of the * type of the access log record. + * @deprecated */ 'intermediate_log_entry'?: (boolean); /** @@ -212,7 +213,7 @@ export interface AccessLogCommon { * For more information about how access log behaves and when it is being recorded, * please refer to :ref:`access logging `. */ - 'access_log_type'?: (_envoy_data_accesslog_v3_AccessLogType | keyof typeof _envoy_data_accesslog_v3_AccessLogType); + 'access_log_type'?: (_envoy_data_accesslog_v3_AccessLogType); } /** @@ -381,6 +382,7 @@ export interface AccessLogCommon__Output { * * This field is deprecated in favor of ``access_log_type`` for better indication of the * type of the access log record. + * @deprecated */ 'intermediate_log_entry': (boolean); /** @@ -417,5 +419,5 @@ export interface AccessLogCommon__Output { * For more information about how access log behaves and when it is being recorded, * please refer to :ref:`access logging `. */ - 'access_log_type': (keyof typeof _envoy_data_accesslog_v3_AccessLogType); + 'access_log_type': (_envoy_data_accesslog_v3_AccessLogType__Output); } diff --git a/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/AccessLogType.ts b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/AccessLogType.ts index a50bb42c1..29ee32f5a 100644 --- a/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/AccessLogType.ts +++ b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/AccessLogType.ts @@ -1,15 +1,41 @@ // Original file: deps/envoy-api/envoy/data/accesslog/v3/accesslog.proto -export enum AccessLogType { - NotSet = 0, - TcpUpstreamConnected = 1, - TcpPeriodic = 2, - TcpConnectionEnd = 3, - DownstreamStart = 4, - DownstreamPeriodic = 5, - DownstreamEnd = 6, - UpstreamPoolReady = 7, - UpstreamPeriodic = 8, - UpstreamEnd = 9, - DownstreamTunnelSuccessfullyEstablished = 10, -} +export const AccessLogType = { + NotSet: 'NotSet', + TcpUpstreamConnected: 'TcpUpstreamConnected', + TcpPeriodic: 'TcpPeriodic', + TcpConnectionEnd: 'TcpConnectionEnd', + DownstreamStart: 'DownstreamStart', + DownstreamPeriodic: 'DownstreamPeriodic', + DownstreamEnd: 'DownstreamEnd', + UpstreamPoolReady: 'UpstreamPoolReady', + UpstreamPeriodic: 'UpstreamPeriodic', + UpstreamEnd: 'UpstreamEnd', + DownstreamTunnelSuccessfullyEstablished: 'DownstreamTunnelSuccessfullyEstablished', +} as const; + +export type AccessLogType = + | 'NotSet' + | 0 + | 'TcpUpstreamConnected' + | 1 + | 'TcpPeriodic' + | 2 + | 'TcpConnectionEnd' + | 3 + | 'DownstreamStart' + | 4 + | 'DownstreamPeriodic' + | 5 + | 'DownstreamEnd' + | 6 + | 'UpstreamPoolReady' + | 7 + | 'UpstreamPeriodic' + | 8 + | 'UpstreamEnd' + | 9 + | 'DownstreamTunnelSuccessfullyEstablished' + | 10 + +export type AccessLogType__Output = typeof AccessLogType[keyof typeof AccessLogType] diff --git a/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/HTTPAccessLogEntry.ts b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/HTTPAccessLogEntry.ts index 760954bb1..31daac364 100644 --- a/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/HTTPAccessLogEntry.ts +++ b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/HTTPAccessLogEntry.ts @@ -9,20 +9,40 @@ import type { HTTPResponseProperties as _envoy_data_accesslog_v3_HTTPResponsePro /** * HTTP version */ -export enum _envoy_data_accesslog_v3_HTTPAccessLogEntry_HTTPVersion { - PROTOCOL_UNSPECIFIED = 0, - HTTP10 = 1, - HTTP11 = 2, - HTTP2 = 3, - HTTP3 = 4, -} +export const _envoy_data_accesslog_v3_HTTPAccessLogEntry_HTTPVersion = { + PROTOCOL_UNSPECIFIED: 'PROTOCOL_UNSPECIFIED', + HTTP10: 'HTTP10', + HTTP11: 'HTTP11', + HTTP2: 'HTTP2', + HTTP3: 'HTTP3', +} as const; + +/** + * HTTP version + */ +export type _envoy_data_accesslog_v3_HTTPAccessLogEntry_HTTPVersion = + | 'PROTOCOL_UNSPECIFIED' + | 0 + | 'HTTP10' + | 1 + | 'HTTP11' + | 2 + | 'HTTP2' + | 3 + | 'HTTP3' + | 4 + +/** + * HTTP version + */ +export type _envoy_data_accesslog_v3_HTTPAccessLogEntry_HTTPVersion__Output = typeof _envoy_data_accesslog_v3_HTTPAccessLogEntry_HTTPVersion[keyof typeof _envoy_data_accesslog_v3_HTTPAccessLogEntry_HTTPVersion] export interface HTTPAccessLogEntry { /** * Common properties shared by all Envoy access logs. */ 'common_properties'?: (_envoy_data_accesslog_v3_AccessLogCommon | null); - 'protocol_version'?: (_envoy_data_accesslog_v3_HTTPAccessLogEntry_HTTPVersion | keyof typeof _envoy_data_accesslog_v3_HTTPAccessLogEntry_HTTPVersion); + 'protocol_version'?: (_envoy_data_accesslog_v3_HTTPAccessLogEntry_HTTPVersion); /** * Description of the incoming HTTP request. */ @@ -38,7 +58,7 @@ export interface HTTPAccessLogEntry__Output { * Common properties shared by all Envoy access logs. */ 'common_properties': (_envoy_data_accesslog_v3_AccessLogCommon__Output | null); - 'protocol_version': (keyof typeof _envoy_data_accesslog_v3_HTTPAccessLogEntry_HTTPVersion); + 'protocol_version': (_envoy_data_accesslog_v3_HTTPAccessLogEntry_HTTPVersion__Output); /** * Description of the incoming HTTP request. */ diff --git a/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/HTTPRequestProperties.ts b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/HTTPRequestProperties.ts index 9e145503c..f1271ba16 100644 --- a/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/HTTPRequestProperties.ts +++ b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/HTTPRequestProperties.ts @@ -1,6 +1,6 @@ // Original file: deps/envoy-api/envoy/data/accesslog/v3/accesslog.proto -import type { RequestMethod as _envoy_config_core_v3_RequestMethod } from '../../../../envoy/config/core/v3/RequestMethod'; +import type { RequestMethod as _envoy_config_core_v3_RequestMethod, RequestMethod__Output as _envoy_config_core_v3_RequestMethod__Output } from '../../../../envoy/config/core/v3/RequestMethod'; import type { UInt32Value as _google_protobuf_UInt32Value, UInt32Value__Output as _google_protobuf_UInt32Value__Output } from '../../../../google/protobuf/UInt32Value'; import type { Long } from '@grpc/proto-loader'; @@ -11,7 +11,7 @@ export interface HTTPRequestProperties { /** * The request method (RFC 7231/2616). */ - 'request_method'?: (_envoy_config_core_v3_RequestMethod | keyof typeof _envoy_config_core_v3_RequestMethod); + 'request_method'?: (_envoy_config_core_v3_RequestMethod); /** * The scheme portion of the incoming request URI. */ @@ -90,7 +90,7 @@ export interface HTTPRequestProperties__Output { /** * The request method (RFC 7231/2616). */ - 'request_method': (keyof typeof _envoy_config_core_v3_RequestMethod); + 'request_method': (_envoy_config_core_v3_RequestMethod__Output); /** * The scheme portion of the incoming request URI. */ diff --git a/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/ResponseFlags.ts b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/ResponseFlags.ts index ec45824b3..f42e11ee3 100644 --- a/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/ResponseFlags.ts +++ b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/ResponseFlags.ts @@ -6,20 +6,37 @@ /** * Reasons why the request was unauthorized */ -export enum _envoy_data_accesslog_v3_ResponseFlags_Unauthorized_Reason { - REASON_UNSPECIFIED = 0, +export const _envoy_data_accesslog_v3_ResponseFlags_Unauthorized_Reason = { + REASON_UNSPECIFIED: 'REASON_UNSPECIFIED', /** * The request was denied by the external authorization service. */ - EXTERNAL_SERVICE = 1, -} + EXTERNAL_SERVICE: 'EXTERNAL_SERVICE', +} as const; + +/** + * Reasons why the request was unauthorized + */ +export type _envoy_data_accesslog_v3_ResponseFlags_Unauthorized_Reason = + | 'REASON_UNSPECIFIED' + | 0 + /** + * The request was denied by the external authorization service. + */ + | 'EXTERNAL_SERVICE' + | 1 + +/** + * Reasons why the request was unauthorized + */ +export type _envoy_data_accesslog_v3_ResponseFlags_Unauthorized_Reason__Output = typeof _envoy_data_accesslog_v3_ResponseFlags_Unauthorized_Reason[keyof typeof _envoy_data_accesslog_v3_ResponseFlags_Unauthorized_Reason] export interface _envoy_data_accesslog_v3_ResponseFlags_Unauthorized { - 'reason'?: (_envoy_data_accesslog_v3_ResponseFlags_Unauthorized_Reason | keyof typeof _envoy_data_accesslog_v3_ResponseFlags_Unauthorized_Reason); + 'reason'?: (_envoy_data_accesslog_v3_ResponseFlags_Unauthorized_Reason); } export interface _envoy_data_accesslog_v3_ResponseFlags_Unauthorized__Output { - 'reason': (keyof typeof _envoy_data_accesslog_v3_ResponseFlags_Unauthorized_Reason); + 'reason': (_envoy_data_accesslog_v3_ResponseFlags_Unauthorized_Reason__Output); } /** diff --git a/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/TLSProperties.ts b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/TLSProperties.ts index 106d24d09..ddeb9a1ae 100644 --- a/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/TLSProperties.ts +++ b/packages/grpc-js-xds/src/generated/envoy/data/accesslog/v3/TLSProperties.ts @@ -44,13 +44,27 @@ export interface _envoy_data_accesslog_v3_TLSProperties_CertificateProperties_Su // Original file: deps/envoy-api/envoy/data/accesslog/v3/accesslog.proto -export enum _envoy_data_accesslog_v3_TLSProperties_TLSVersion { - VERSION_UNSPECIFIED = 0, - TLSv1 = 1, - TLSv1_1 = 2, - TLSv1_2 = 3, - TLSv1_3 = 4, -} +export const _envoy_data_accesslog_v3_TLSProperties_TLSVersion = { + VERSION_UNSPECIFIED: 'VERSION_UNSPECIFIED', + TLSv1: 'TLSv1', + TLSv1_1: 'TLSv1_1', + TLSv1_2: 'TLSv1_2', + TLSv1_3: 'TLSv1_3', +} as const; + +export type _envoy_data_accesslog_v3_TLSProperties_TLSVersion = + | 'VERSION_UNSPECIFIED' + | 0 + | 'TLSv1' + | 1 + | 'TLSv1_1' + | 2 + | 'TLSv1_2' + | 3 + | 'TLSv1_3' + | 4 + +export type _envoy_data_accesslog_v3_TLSProperties_TLSVersion__Output = typeof _envoy_data_accesslog_v3_TLSProperties_TLSVersion[keyof typeof _envoy_data_accesslog_v3_TLSProperties_TLSVersion] /** * Properties of a negotiated TLS connection. @@ -60,7 +74,7 @@ export interface TLSProperties { /** * Version of TLS that was negotiated. */ - 'tls_version'?: (_envoy_data_accesslog_v3_TLSProperties_TLSVersion | keyof typeof _envoy_data_accesslog_v3_TLSProperties_TLSVersion); + 'tls_version'?: (_envoy_data_accesslog_v3_TLSProperties_TLSVersion); /** * TLS cipher suite negotiated during handshake. The value is a * four-digit hex code defined by the IANA TLS Cipher Suite Registry @@ -99,7 +113,7 @@ export interface TLSProperties__Output { /** * Version of TLS that was negotiated. */ - 'tls_version': (keyof typeof _envoy_data_accesslog_v3_TLSProperties_TLSVersion); + 'tls_version': (_envoy_data_accesslog_v3_TLSProperties_TLSVersion__Output); /** * TLS cipher suite negotiated during handshake. The value is a * four-digit hex code defined by the IANA TLS Cipher Suite Registry diff --git a/packages/grpc-js-xds/src/generated/envoy/extensions/filters/common/fault/v3/FaultDelay.ts b/packages/grpc-js-xds/src/generated/envoy/extensions/filters/common/fault/v3/FaultDelay.ts index bec0403e4..e070ae913 100644 --- a/packages/grpc-js-xds/src/generated/envoy/extensions/filters/common/fault/v3/FaultDelay.ts +++ b/packages/grpc-js-xds/src/generated/envoy/extensions/filters/common/fault/v3/FaultDelay.ts @@ -5,12 +5,21 @@ import type { FractionalPercent as _envoy_type_v3_FractionalPercent, FractionalP // Original file: deps/envoy-api/envoy/extensions/filters/common/fault/v3/fault.proto -export enum _envoy_extensions_filters_common_fault_v3_FaultDelay_FaultDelayType { +export const _envoy_extensions_filters_common_fault_v3_FaultDelay_FaultDelayType = { /** * Unused and deprecated. */ - FIXED = 0, -} + FIXED: 'FIXED', +} as const; + +export type _envoy_extensions_filters_common_fault_v3_FaultDelay_FaultDelayType = + /** + * Unused and deprecated. + */ + | 'FIXED' + | 0 + +export type _envoy_extensions_filters_common_fault_v3_FaultDelay_FaultDelayType__Output = typeof _envoy_extensions_filters_common_fault_v3_FaultDelay_FaultDelayType[keyof typeof _envoy_extensions_filters_common_fault_v3_FaultDelay_FaultDelayType] /** * Fault delays are controlled via an HTTP header (if applicable). See the diff --git a/packages/grpc-js-xds/src/generated/envoy/extensions/filters/network/http_connection_manager/v3/HttpConnectionManager.ts b/packages/grpc-js-xds/src/generated/envoy/extensions/filters/network/http_connection_manager/v3/HttpConnectionManager.ts index 1b4f36fd8..1a452635c 100644 --- a/packages/grpc-js-xds/src/generated/envoy/extensions/filters/network/http_connection_manager/v3/HttpConnectionManager.ts +++ b/packages/grpc-js-xds/src/generated/envoy/extensions/filters/network/http_connection_manager/v3/HttpConnectionManager.ts @@ -24,7 +24,7 @@ import type { PathTransformation as _envoy_type_http_v3_PathTransformation, Path // Original file: deps/envoy-api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto -export enum _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_CodecType { +export const _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_CodecType = { /** * For every new connection, the connection manager will determine which * codec to use. This mode supports both ALPN for TLS listeners as well as @@ -32,24 +32,56 @@ export enum _envoy_extensions_filters_network_http_connection_manager_v3_HttpCon * is preferred, otherwise protocol inference is used. In almost all cases, * this is the right option to choose for this setting. */ - AUTO = 0, + AUTO: 'AUTO', /** * The connection manager will assume that the client is speaking HTTP/1.1. */ - HTTP1 = 1, + HTTP1: 'HTTP1', /** * The connection manager will assume that the client is speaking HTTP/2 * (Envoy does not require HTTP/2 to take place over TLS or to use ALPN. * Prior knowledge is allowed). */ - HTTP2 = 2, + HTTP2: 'HTTP2', /** * [#not-implemented-hide:] QUIC implementation is not production ready yet. Use this enum with * caution to prevent accidental execution of QUIC code. I.e. `!= HTTP2` is no longer sufficient * to distinguish HTTP1 and HTTP2 traffic. */ - HTTP3 = 3, -} + HTTP3: 'HTTP3', +} as const; + +export type _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_CodecType = + /** + * For every new connection, the connection manager will determine which + * codec to use. This mode supports both ALPN for TLS listeners as well as + * protocol inference for plaintext listeners. If ALPN data is available, it + * is preferred, otherwise protocol inference is used. In almost all cases, + * this is the right option to choose for this setting. + */ + | 'AUTO' + | 0 + /** + * The connection manager will assume that the client is speaking HTTP/1.1. + */ + | 'HTTP1' + | 1 + /** + * The connection manager will assume that the client is speaking HTTP/2 + * (Envoy does not require HTTP/2 to take place over TLS or to use ALPN. + * Prior knowledge is allowed). + */ + | 'HTTP2' + | 2 + /** + * [#not-implemented-hide:] QUIC implementation is not production ready yet. Use this enum with + * caution to prevent accidental execution of QUIC code. I.e. `!= HTTP2` is no longer sufficient + * to distinguish HTTP1 and HTTP2 traffic. + */ + | 'HTTP3' + | 3 + +export type _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_CodecType__Output = typeof _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_CodecType[keyof typeof _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_CodecType] // Original file: deps/envoy-api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto @@ -57,32 +89,73 @@ export enum _envoy_extensions_filters_network_http_connection_manager_v3_HttpCon * How to handle the :ref:`config_http_conn_man_headers_x-forwarded-client-cert` (XFCC) HTTP * header. */ -export enum _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_ForwardClientCertDetails { +export const _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_ForwardClientCertDetails = { /** * Do not send the XFCC header to the next hop. This is the default value. */ - SANITIZE = 0, + SANITIZE: 'SANITIZE', /** * When the client connection is mTLS (Mutual TLS), forward the XFCC header * in the request. */ - FORWARD_ONLY = 1, + FORWARD_ONLY: 'FORWARD_ONLY', /** * When the client connection is mTLS, append the client certificate * information to the request’s XFCC header and forward it. */ - APPEND_FORWARD = 2, + APPEND_FORWARD: 'APPEND_FORWARD', /** * When the client connection is mTLS, reset the XFCC header with the client * certificate information and send it to the next hop. */ - SANITIZE_SET = 3, + SANITIZE_SET: 'SANITIZE_SET', /** * Always forward the XFCC header in the request, regardless of whether the * client connection is mTLS. */ - ALWAYS_FORWARD_ONLY = 4, -} + ALWAYS_FORWARD_ONLY: 'ALWAYS_FORWARD_ONLY', +} as const; + +/** + * How to handle the :ref:`config_http_conn_man_headers_x-forwarded-client-cert` (XFCC) HTTP + * header. + */ +export type _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_ForwardClientCertDetails = + /** + * Do not send the XFCC header to the next hop. This is the default value. + */ + | 'SANITIZE' + | 0 + /** + * When the client connection is mTLS (Mutual TLS), forward the XFCC header + * in the request. + */ + | 'FORWARD_ONLY' + | 1 + /** + * When the client connection is mTLS, append the client certificate + * information to the request’s XFCC header and forward it. + */ + | 'APPEND_FORWARD' + | 2 + /** + * When the client connection is mTLS, reset the XFCC header with the client + * certificate information and send it to the next hop. + */ + | 'SANITIZE_SET' + | 3 + /** + * Always forward the XFCC header in the request, regardless of whether the + * client connection is mTLS. + */ + | 'ALWAYS_FORWARD_ONLY' + | 4 + +/** + * How to handle the :ref:`config_http_conn_man_headers_x-forwarded-client-cert` (XFCC) HTTP + * header. + */ +export type _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_ForwardClientCertDetails__Output = typeof _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_ForwardClientCertDetails[keyof typeof _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_ForwardClientCertDetails] export interface _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_HcmAccessLogOptions { /** @@ -162,16 +235,30 @@ export interface _envoy_extensions_filters_network_http_connection_manager_v3_Ht // Original file: deps/envoy-api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto -export enum _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_Tracing_OperationName { +export const _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_Tracing_OperationName = { /** * The HTTP listener is used for ingress/incoming requests. */ - INGRESS = 0, + INGRESS: 'INGRESS', /** * The HTTP listener is used for egress/outgoing requests. */ - EGRESS = 1, -} + EGRESS: 'EGRESS', +} as const; + +export type _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_Tracing_OperationName = + /** + * The HTTP listener is used for ingress/incoming requests. + */ + | 'INGRESS' + | 0 + /** + * The HTTP listener is used for egress/outgoing requests. + */ + | 'EGRESS' + | 1 + +export type _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_Tracing_OperationName__Output = typeof _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_Tracing_OperationName[keyof typeof _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_Tracing_OperationName] /** * [#not-implemented-hide:] Transformations that apply to path headers. Transformations are applied @@ -253,22 +340,22 @@ export interface _envoy_extensions_filters_network_http_connection_manager_v3_Ht * Determines the action for request that contain %2F, %2f, %5C or %5c sequences in the URI path. * This operation occurs before URL normalization and the merge slashes transformations if they were enabled. */ -export enum _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_PathWithEscapedSlashesAction { +export const _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_PathWithEscapedSlashesAction = { /** * Default behavior specific to implementation (i.e. Envoy) of this configuration option. * Envoy, by default, takes the KEEP_UNCHANGED action. * NOTE: the implementation may change the default behavior at-will. */ - IMPLEMENTATION_SPECIFIC_DEFAULT = 0, + IMPLEMENTATION_SPECIFIC_DEFAULT: 'IMPLEMENTATION_SPECIFIC_DEFAULT', /** * Keep escaped slashes. */ - KEEP_UNCHANGED = 1, + KEEP_UNCHANGED: 'KEEP_UNCHANGED', /** * Reject client request with the 400 status. gRPC requests will be rejected with the INTERNAL (13) error code. * The "httpN.downstream_rq_failed_path_normalization" counter is incremented for each rejected request. */ - REJECT_REQUEST = 2, + REJECT_REQUEST: 'REJECT_REQUEST', /** * Unescape %2F and %5C sequences and redirect request to the new path if these sequences were present. * Redirect occurs after path normalization and merge slashes transformations if they were configured. @@ -278,14 +365,62 @@ export enum _envoy_extensions_filters_network_http_connection_manager_v3_HttpCon * The "httpN.downstream_rq_redirected_with_normalized_path" counter is incremented for each * redirected request. */ - UNESCAPE_AND_REDIRECT = 3, + UNESCAPE_AND_REDIRECT: 'UNESCAPE_AND_REDIRECT', /** * Unescape %2F and %5C sequences. * Note: this option should not be enabled if intermediaries perform path based access control as * it may lead to path confusion vulnerabilities. */ - UNESCAPE_AND_FORWARD = 4, -} + UNESCAPE_AND_FORWARD: 'UNESCAPE_AND_FORWARD', +} as const; + +/** + * Determines the action for request that contain %2F, %2f, %5C or %5c sequences in the URI path. + * This operation occurs before URL normalization and the merge slashes transformations if they were enabled. + */ +export type _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_PathWithEscapedSlashesAction = + /** + * Default behavior specific to implementation (i.e. Envoy) of this configuration option. + * Envoy, by default, takes the KEEP_UNCHANGED action. + * NOTE: the implementation may change the default behavior at-will. + */ + | 'IMPLEMENTATION_SPECIFIC_DEFAULT' + | 0 + /** + * Keep escaped slashes. + */ + | 'KEEP_UNCHANGED' + | 1 + /** + * Reject client request with the 400 status. gRPC requests will be rejected with the INTERNAL (13) error code. + * The "httpN.downstream_rq_failed_path_normalization" counter is incremented for each rejected request. + */ + | 'REJECT_REQUEST' + | 2 + /** + * Unescape %2F and %5C sequences and redirect request to the new path if these sequences were present. + * Redirect occurs after path normalization and merge slashes transformations if they were configured. + * NOTE: gRPC requests will be rejected with the INTERNAL (13) error code. + * This option minimizes possibility of path confusion exploits by forcing request with unescaped slashes to + * traverse all parties: downstream client, intermediate proxies, Envoy and upstream server. + * The "httpN.downstream_rq_redirected_with_normalized_path" counter is incremented for each + * redirected request. + */ + | 'UNESCAPE_AND_REDIRECT' + | 3 + /** + * Unescape %2F and %5C sequences. + * Note: this option should not be enabled if intermediaries perform path based access control as + * it may lead to path confusion vulnerabilities. + */ + | 'UNESCAPE_AND_FORWARD' + | 4 + +/** + * Determines the action for request that contain %2F, %2f, %5C or %5c sequences in the URI path. + * This operation occurs before URL normalization and the merge slashes transformations if they were enabled. + */ +export type _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_PathWithEscapedSlashesAction__Output = typeof _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_PathWithEscapedSlashesAction[keyof typeof _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_PathWithEscapedSlashesAction] /** * Configures the manner in which the Proxy-Status HTTP response header is @@ -405,22 +540,43 @@ export interface _envoy_extensions_filters_network_http_connection_manager_v3_Ht // Original file: deps/envoy-api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto -export enum _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_ServerHeaderTransformation { +export const _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_ServerHeaderTransformation = { /** * Overwrite any Server header with the contents of server_name. */ - OVERWRITE = 0, + OVERWRITE: 'OVERWRITE', /** * If no Server header is present, append Server server_name * If a Server header is present, pass it through. */ - APPEND_IF_ABSENT = 1, + APPEND_IF_ABSENT: 'APPEND_IF_ABSENT', /** * Pass through the value of the server header, and do not append a header * if none is present. */ - PASS_THROUGH = 2, -} + PASS_THROUGH: 'PASS_THROUGH', +} as const; + +export type _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_ServerHeaderTransformation = + /** + * Overwrite any Server header with the contents of server_name. + */ + | 'OVERWRITE' + | 0 + /** + * If no Server header is present, append Server server_name + * If a Server header is present, pass it through. + */ + | 'APPEND_IF_ABSENT' + | 1 + /** + * Pass through the value of the server header, and do not append a header + * if none is present. + */ + | 'PASS_THROUGH' + | 2 + +export type _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_ServerHeaderTransformation__Output = typeof _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_ServerHeaderTransformation[keyof typeof _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_ServerHeaderTransformation] /** * [#next-free-field: 7] @@ -693,7 +849,7 @@ export interface HttpConnectionManager { /** * Supplies the type of codec that the connection manager should use. */ - 'codec_type'?: (_envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_CodecType | keyof typeof _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_CodecType); + 'codec_type'?: (_envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_CodecType); /** * The human readable prefix to use when emitting statistics for the * connection manager. See the :ref:`statistics documentation ` for @@ -781,7 +937,7 @@ export interface HttpConnectionManager { * How to handle the :ref:`config_http_conn_man_headers_x-forwarded-client-cert` (XFCC) HTTP * header. */ - 'forward_client_cert_details'?: (_envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_ForwardClientCertDetails | keyof typeof _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_ForwardClientCertDetails); + 'forward_client_cert_details'?: (_envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_ForwardClientCertDetails); /** * This field is valid only when :ref:`forward_client_cert_details * ` @@ -981,7 +1137,7 @@ export interface HttpConnectionManager { * By default, Envoy will overwrite the header with the value specified in * server_name. */ - 'server_header_transformation'?: (_envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_ServerHeaderTransformation | keyof typeof _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_ServerHeaderTransformation); + 'server_header_transformation'?: (_envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_ServerHeaderTransformation); /** * Additional settings for HTTP requests handled by the connection manager. These will be * applicable to both HTTP1 and HTTP2 requests. @@ -1092,7 +1248,7 @@ export interface HttpConnectionManager { * :ref:`header validation configuration ` * is present.] */ - 'path_with_escaped_slashes_action'?: (_envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_PathWithEscapedSlashesAction | keyof typeof _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_PathWithEscapedSlashesAction); + 'path_with_escaped_slashes_action'?: (_envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_PathWithEscapedSlashesAction); /** * The configuration for the original IP detection extensions. * @@ -1190,6 +1346,7 @@ export interface HttpConnectionManager { * Note that if both this field and :ref:`access_log_flush_interval * ` * are specified, the former (deprecated field) is ignored. + * @deprecated */ 'access_log_flush_interval'?: (_google_protobuf_Duration | null); /** @@ -1200,6 +1357,7 @@ export interface HttpConnectionManager { * Note that if both this field and :ref:`flush_access_log_on_new_request * ` * are specified, the former (deprecated field) is ignored. + * @deprecated */ 'flush_access_log_on_new_request'?: (boolean); /** @@ -1217,7 +1375,7 @@ export interface HttpConnectionManager__Output { /** * Supplies the type of codec that the connection manager should use. */ - 'codec_type': (keyof typeof _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_CodecType); + 'codec_type': (_envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_CodecType__Output); /** * The human readable prefix to use when emitting statistics for the * connection manager. See the :ref:`statistics documentation ` for @@ -1305,7 +1463,7 @@ export interface HttpConnectionManager__Output { * How to handle the :ref:`config_http_conn_man_headers_x-forwarded-client-cert` (XFCC) HTTP * header. */ - 'forward_client_cert_details': (keyof typeof _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_ForwardClientCertDetails); + 'forward_client_cert_details': (_envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_ForwardClientCertDetails__Output); /** * This field is valid only when :ref:`forward_client_cert_details * ` @@ -1505,7 +1663,7 @@ export interface HttpConnectionManager__Output { * By default, Envoy will overwrite the header with the value specified in * server_name. */ - 'server_header_transformation': (keyof typeof _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_ServerHeaderTransformation); + 'server_header_transformation': (_envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_ServerHeaderTransformation__Output); /** * Additional settings for HTTP requests handled by the connection manager. These will be * applicable to both HTTP1 and HTTP2 requests. @@ -1616,7 +1774,7 @@ export interface HttpConnectionManager__Output { * :ref:`header validation configuration ` * is present.] */ - 'path_with_escaped_slashes_action': (keyof typeof _envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_PathWithEscapedSlashesAction); + 'path_with_escaped_slashes_action': (_envoy_extensions_filters_network_http_connection_manager_v3_HttpConnectionManager_PathWithEscapedSlashesAction__Output); /** * The configuration for the original IP detection extensions. * @@ -1714,6 +1872,7 @@ export interface HttpConnectionManager__Output { * Note that if both this field and :ref:`access_log_flush_interval * ` * are specified, the former (deprecated field) is ignored. + * @deprecated */ 'access_log_flush_interval': (_google_protobuf_Duration__Output | null); /** @@ -1724,6 +1883,7 @@ export interface HttpConnectionManager__Output { * Note that if both this field and :ref:`flush_access_log_on_new_request * ` * are specified, the former (deprecated field) is ignored. + * @deprecated */ 'flush_access_log_on_new_request': (boolean); /** diff --git a/packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/common/v3/LocalityLbConfig.ts b/packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/common/v3/LocalityLbConfig.ts index d6fecd36b..4e3d9659e 100644 --- a/packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/common/v3/LocalityLbConfig.ts +++ b/packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/common/v3/LocalityLbConfig.ts @@ -2,7 +2,6 @@ import type { Percent as _envoy_type_v3_Percent, Percent__Output as _envoy_type_v3_Percent__Output } from '../../../../../envoy/type/v3/Percent'; import type { UInt64Value as _google_protobuf_UInt64Value, UInt64Value__Output as _google_protobuf_UInt64Value__Output } from '../../../../../google/protobuf/UInt64Value'; -import type { Long } from '@grpc/proto-loader'; /** * Configuration for :ref:`locality weighted load balancing diff --git a/packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/ring_hash/v3/RingHash.ts b/packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/ring_hash/v3/RingHash.ts index 4e2a73031..d8156fe0f 100644 --- a/packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/ring_hash/v3/RingHash.ts +++ b/packages/grpc-js-xds/src/generated/envoy/extensions/load_balancing_policies/ring_hash/v3/RingHash.ts @@ -4,29 +4,55 @@ import type { UInt64Value as _google_protobuf_UInt64Value, UInt64Value__Output a import type { UInt32Value as _google_protobuf_UInt32Value, UInt32Value__Output as _google_protobuf_UInt32Value__Output } from '../../../../../google/protobuf/UInt32Value'; import type { ConsistentHashingLbConfig as _envoy_extensions_load_balancing_policies_common_v3_ConsistentHashingLbConfig, ConsistentHashingLbConfig__Output as _envoy_extensions_load_balancing_policies_common_v3_ConsistentHashingLbConfig__Output } from '../../../../../envoy/extensions/load_balancing_policies/common/v3/ConsistentHashingLbConfig'; import type { _envoy_extensions_load_balancing_policies_common_v3_LocalityLbConfig_LocalityWeightedLbConfig, _envoy_extensions_load_balancing_policies_common_v3_LocalityLbConfig_LocalityWeightedLbConfig__Output } from '../../../../../envoy/extensions/load_balancing_policies/common/v3/LocalityLbConfig'; -import type { Long } from '@grpc/proto-loader'; // Original file: deps/envoy-api/envoy/extensions/load_balancing_policies/ring_hash/v3/ring_hash.proto /** * The hash function used to hash hosts onto the ketama ring. */ -export enum _envoy_extensions_load_balancing_policies_ring_hash_v3_RingHash_HashFunction { +export const _envoy_extensions_load_balancing_policies_ring_hash_v3_RingHash_HashFunction = { /** * Currently defaults to XX_HASH. */ - DEFAULT_HASH = 0, + DEFAULT_HASH: 'DEFAULT_HASH', /** * Use `xxHash `_. */ - XX_HASH = 1, + XX_HASH: 'XX_HASH', /** * Use `MurmurHash2 `_, this is compatible with * std:hash in GNU libstdc++ 3.4.20 or above. This is typically the case when compiled * on Linux and not macOS. */ - MURMUR_HASH_2 = 2, -} + MURMUR_HASH_2: 'MURMUR_HASH_2', +} as const; + +/** + * The hash function used to hash hosts onto the ketama ring. + */ +export type _envoy_extensions_load_balancing_policies_ring_hash_v3_RingHash_HashFunction = + /** + * Currently defaults to XX_HASH. + */ + | 'DEFAULT_HASH' + | 0 + /** + * Use `xxHash `_. + */ + | 'XX_HASH' + | 1 + /** + * Use `MurmurHash2 `_, this is compatible with + * std:hash in GNU libstdc++ 3.4.20 or above. This is typically the case when compiled + * on Linux and not macOS. + */ + | 'MURMUR_HASH_2' + | 2 + +/** + * The hash function used to hash hosts onto the ketama ring. + */ +export type _envoy_extensions_load_balancing_policies_ring_hash_v3_RingHash_HashFunction__Output = typeof _envoy_extensions_load_balancing_policies_ring_hash_v3_RingHash_HashFunction[keyof typeof _envoy_extensions_load_balancing_policies_ring_hash_v3_RingHash_HashFunction] /** * This configuration allows the built-in RING_HASH LB policy to be configured via the LB policy @@ -39,7 +65,7 @@ export interface RingHash { * The hash function used to hash hosts onto the ketama ring. The value defaults to * :ref:`XX_HASH`. */ - 'hash_function'?: (_envoy_extensions_load_balancing_policies_ring_hash_v3_RingHash_HashFunction | keyof typeof _envoy_extensions_load_balancing_policies_ring_hash_v3_RingHash_HashFunction); + 'hash_function'?: (_envoy_extensions_load_balancing_policies_ring_hash_v3_RingHash_HashFunction); /** * Minimum hash ring size. The larger the ring is (that is, the more hashes there are for each * provided host) the better the request distribution will reflect the desired weights. Defaults @@ -60,6 +86,7 @@ export interface RingHash { * ..note:: * This is deprecated and please use :ref:`consistent_hashing_lb_config * ` instead. + * @deprecated */ 'use_hostname_for_hashing'?: (boolean); /** @@ -83,6 +110,7 @@ export interface RingHash { * ..note:: * This is deprecated and please use :ref:`consistent_hashing_lb_config * ` instead. + * @deprecated */ 'hash_balance_factor'?: (_google_protobuf_UInt32Value | null); /** @@ -106,7 +134,7 @@ export interface RingHash__Output { * The hash function used to hash hosts onto the ketama ring. The value defaults to * :ref:`XX_HASH`. */ - 'hash_function': (keyof typeof _envoy_extensions_load_balancing_policies_ring_hash_v3_RingHash_HashFunction); + 'hash_function': (_envoy_extensions_load_balancing_policies_ring_hash_v3_RingHash_HashFunction__Output); /** * Minimum hash ring size. The larger the ring is (that is, the more hashes there are for each * provided host) the better the request distribution will reflect the desired weights. Defaults @@ -127,6 +155,7 @@ export interface RingHash__Output { * ..note:: * This is deprecated and please use :ref:`consistent_hashing_lb_config * ` instead. + * @deprecated */ 'use_hostname_for_hashing': (boolean); /** @@ -150,6 +179,7 @@ export interface RingHash__Output { * ..note:: * This is deprecated and please use :ref:`consistent_hashing_lb_config * ` instead. + * @deprecated */ 'hash_balance_factor': (_google_protobuf_UInt32Value__Output | null); /** diff --git a/packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/CertificateProviderPluginInstance.ts b/packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/CertificateProviderPluginInstance.ts deleted file mode 100644 index 3a3100f55..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/CertificateProviderPluginInstance.ts +++ /dev/null @@ -1,52 +0,0 @@ -// Original file: deps/envoy-api/envoy/extensions/transport_sockets/tls/v3/common.proto - - -/** - * Indicates a certificate to be obtained from a named CertificateProvider plugin instance. - * The plugin instances are defined in the client's bootstrap file. - * The plugin allows certificates to be fetched/refreshed over the network asynchronously with - * respect to the TLS handshake. - * [#not-implemented-hide:] - */ -export interface CertificateProviderPluginInstance { - /** - * Provider instance name. If not present, defaults to "default". - * - * Instance names should generally be defined not in terms of the underlying provider - * implementation (e.g., "file_watcher") but rather in terms of the function of the - * certificates (e.g., "foo_deployment_identity"). - */ - 'instance_name'?: (string); - /** - * Opaque name used to specify certificate instances or types. For example, "ROOTCA" to specify - * a root-certificate (validation context) or "example.com" to specify a certificate for a - * particular domain. Not all provider instances will actually use this field, so the value - * defaults to the empty string. - */ - 'certificate_name'?: (string); -} - -/** - * Indicates a certificate to be obtained from a named CertificateProvider plugin instance. - * The plugin instances are defined in the client's bootstrap file. - * The plugin allows certificates to be fetched/refreshed over the network asynchronously with - * respect to the TLS handshake. - * [#not-implemented-hide:] - */ -export interface CertificateProviderPluginInstance__Output { - /** - * Provider instance name. If not present, defaults to "default". - * - * Instance names should generally be defined not in terms of the underlying provider - * implementation (e.g., "file_watcher") but rather in terms of the function of the - * certificates (e.g., "foo_deployment_identity"). - */ - 'instance_name': (string); - /** - * Opaque name used to specify certificate instances or types. For example, "ROOTCA" to specify - * a root-certificate (validation context) or "example.com" to specify a certificate for a - * particular domain. Not all provider instances will actually use this field, so the value - * defaults to the empty string. - */ - 'certificate_name': (string); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/CertificateValidationContext.ts b/packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/CertificateValidationContext.ts deleted file mode 100644 index 379320086..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/CertificateValidationContext.ts +++ /dev/null @@ -1,372 +0,0 @@ -// Original file: deps/envoy-api/envoy/extensions/transport_sockets/tls/v3/common.proto - -import type { DataSource as _envoy_config_core_v3_DataSource, DataSource__Output as _envoy_config_core_v3_DataSource__Output } from '../../../../../envoy/config/core/v3/DataSource'; -import type { BoolValue as _google_protobuf_BoolValue, BoolValue__Output as _google_protobuf_BoolValue__Output } from '../../../../../google/protobuf/BoolValue'; -import type { StringMatcher as _envoy_type_matcher_v3_StringMatcher, StringMatcher__Output as _envoy_type_matcher_v3_StringMatcher__Output } from '../../../../../envoy/type/matcher/v3/StringMatcher'; -import type { WatchedDirectory as _envoy_config_core_v3_WatchedDirectory, WatchedDirectory__Output as _envoy_config_core_v3_WatchedDirectory__Output } from '../../../../../envoy/config/core/v3/WatchedDirectory'; -import type { TypedExtensionConfig as _envoy_config_core_v3_TypedExtensionConfig, TypedExtensionConfig__Output as _envoy_config_core_v3_TypedExtensionConfig__Output } from '../../../../../envoy/config/core/v3/TypedExtensionConfig'; -import type { CertificateProviderPluginInstance as _envoy_extensions_transport_sockets_tls_v3_CertificateProviderPluginInstance, CertificateProviderPluginInstance__Output as _envoy_extensions_transport_sockets_tls_v3_CertificateProviderPluginInstance__Output } from '../../../../../envoy/extensions/transport_sockets/tls/v3/CertificateProviderPluginInstance'; - -// Original file: deps/envoy-api/envoy/extensions/transport_sockets/tls/v3/common.proto - -/** - * Peer certificate verification mode. - */ -export enum _envoy_extensions_transport_sockets_tls_v3_CertificateValidationContext_TrustChainVerification { - /** - * Perform default certificate verification (e.g., against CA / verification lists) - */ - VERIFY_TRUST_CHAIN = 0, - /** - * Connections where the certificate fails verification will be permitted. - * For HTTP connections, the result of certificate verification can be used in route matching. ( - * see :ref:`validated ` ). - */ - ACCEPT_UNTRUSTED = 1, -} - -/** - * [#next-free-field: 14] - */ -export interface CertificateValidationContext { - /** - * TLS certificate data containing certificate authority certificates to use in verifying - * a presented peer certificate (e.g. server certificate for clusters or client certificate - * for listeners). If not specified and a peer certificate is presented it will not be - * verified. By default, a client certificate is optional, unless one of the additional - * options (:ref:`require_client_certificate - * `, - * :ref:`verify_certificate_spki - * `, - * :ref:`verify_certificate_hash - * `, or - * :ref:`match_subject_alt_names - * `) is also - * specified. - * - * It can optionally contain certificate revocation lists, in which case Envoy will verify - * that the presented peer certificate has not been revoked by one of the included CRLs. Note - * that if a CRL is provided for any certificate authority in a trust chain, a CRL must be - * provided for all certificate authorities in that chain. Failure to do so will result in - * verification failure for both revoked and unrevoked certificates from that chain. - * - * See :ref:`the TLS overview ` for a list of common - * system CA locations. - * - * If *trusted_ca* is a filesystem path, a watch will be added to the parent - * directory for any file moves to support rotation. This currently only - * applies to dynamic secrets, when the *CertificateValidationContext* is - * delivered via SDS. - * - * Only one of *trusted_ca* and *ca_certificate_provider_instance* may be specified. - * - * [#next-major-version: This field and watched_directory below should ideally be moved into a - * separate sub-message, since there's no point in specifying the latter field without this one.] - */ - 'trusted_ca'?: (_envoy_config_core_v3_DataSource | null); - /** - * An optional list of hex-encoded SHA-256 hashes. If specified, Envoy will verify that - * the SHA-256 of the DER-encoded presented certificate matches one of the specified values. - * - * A hex-encoded SHA-256 of the certificate can be generated with the following command: - * - * .. code-block:: bash - * - * $ openssl x509 -in path/to/client.crt -outform DER | openssl dgst -sha256 | cut -d" " -f2 - * df6ff72fe9116521268f6f2dd4966f51df479883fe7037b39f75916ac3049d1a - * - * A long hex-encoded and colon-separated SHA-256 (a.k.a. "fingerprint") of the certificate - * can be generated with the following command: - * - * .. code-block:: bash - * - * $ openssl x509 -in path/to/client.crt -noout -fingerprint -sha256 | cut -d"=" -f2 - * DF:6F:F7:2F:E9:11:65:21:26:8F:6F:2D:D4:96:6F:51:DF:47:98:83:FE:70:37:B3:9F:75:91:6A:C3:04:9D:1A - * - * Both of those formats are acceptable. - * - * When both: - * :ref:`verify_certificate_hash - * ` and - * :ref:`verify_certificate_spki - * ` are specified, - * a hash matching value from either of the lists will result in the certificate being accepted. - */ - 'verify_certificate_hash'?: (string)[]; - /** - * An optional list of base64-encoded SHA-256 hashes. If specified, Envoy will verify that the - * SHA-256 of the DER-encoded Subject Public Key Information (SPKI) of the presented certificate - * matches one of the specified values. - * - * A base64-encoded SHA-256 of the Subject Public Key Information (SPKI) of the certificate - * can be generated with the following command: - * - * .. code-block:: bash - * - * $ openssl x509 -in path/to/client.crt -noout -pubkey - * | openssl pkey -pubin -outform DER - * | openssl dgst -sha256 -binary - * | openssl enc -base64 - * NvqYIYSbgK2vCJpQhObf77vv+bQWtc5ek5RIOwPiC9A= - * - * This is the format used in HTTP Public Key Pinning. - * - * When both: - * :ref:`verify_certificate_hash - * ` and - * :ref:`verify_certificate_spki - * ` are specified, - * a hash matching value from either of the lists will result in the certificate being accepted. - * - * .. attention:: - * - * This option is preferred over :ref:`verify_certificate_hash - * `, - * because SPKI is tied to a private key, so it doesn't change when the certificate - * is renewed using the same private key. - */ - 'verify_certificate_spki'?: (string)[]; - /** - * [#not-implemented-hide:] Must present signed certificate time-stamp. - */ - 'require_signed_certificate_timestamp'?: (_google_protobuf_BoolValue | null); - /** - * An optional `certificate revocation list - * `_ - * (in PEM format). If specified, Envoy will verify that the presented peer - * certificate has not been revoked by this CRL. If this DataSource contains - * multiple CRLs, all of them will be used. Note that if a CRL is provided - * for any certificate authority in a trust chain, a CRL must be provided - * for all certificate authorities in that chain. Failure to do so will - * result in verification failure for both revoked and unrevoked certificates - * from that chain. - */ - 'crl'?: (_envoy_config_core_v3_DataSource | null); - /** - * If specified, Envoy will not reject expired certificates. - */ - 'allow_expired_certificate'?: (boolean); - /** - * An optional list of Subject Alternative name matchers. If specified, Envoy will verify that the - * Subject Alternative Name of the presented certificate matches one of the specified matchers. - * - * When a certificate has wildcard DNS SAN entries, to match a specific client, it should be - * configured with exact match type in the :ref:`string matcher `. - * For example if the certificate has "\*.example.com" as DNS SAN entry, to allow only "api.example.com", - * it should be configured as shown below. - * - * .. code-block:: yaml - * - * match_subject_alt_names: - * exact: "api.example.com" - * - * .. attention:: - * - * Subject Alternative Names are easily spoofable and verifying only them is insecure, - * therefore this option must be used together with :ref:`trusted_ca - * `. - */ - 'match_subject_alt_names'?: (_envoy_type_matcher_v3_StringMatcher)[]; - /** - * Certificate trust chain verification mode. - */ - 'trust_chain_verification'?: (_envoy_extensions_transport_sockets_tls_v3_CertificateValidationContext_TrustChainVerification | keyof typeof _envoy_extensions_transport_sockets_tls_v3_CertificateValidationContext_TrustChainVerification); - /** - * If specified, updates of a file-based *trusted_ca* source will be triggered - * by this watch. This allows explicit control over the path watched, by - * default the parent directory of the filesystem path in *trusted_ca* is - * watched if this field is not specified. This only applies when a - * *CertificateValidationContext* is delivered by SDS with references to - * filesystem paths. See the :ref:`SDS key rotation ` - * documentation for further details. - */ - 'watched_directory'?: (_envoy_config_core_v3_WatchedDirectory | null); - /** - * The configuration of an extension specific certificate validator. - * If specified, all validation is done by the specified validator, - * and the behavior of all other validation settings is defined by the specified validator (and may be entirely ignored, unused, and unvalidated). - * Refer to the documentation for the specified validator. If you do not want a custom validation algorithm, do not set this field. - * [#extension-category: envoy.tls.cert_validator] - */ - 'custom_validator_config'?: (_envoy_config_core_v3_TypedExtensionConfig | null); - /** - * Certificate provider instance for fetching TLS certificates. - * - * Only one of *trusted_ca* and *ca_certificate_provider_instance* may be specified. - * [#not-implemented-hide:] - */ - 'ca_certificate_provider_instance'?: (_envoy_extensions_transport_sockets_tls_v3_CertificateProviderPluginInstance | null); -} - -/** - * [#next-free-field: 14] - */ -export interface CertificateValidationContext__Output { - /** - * TLS certificate data containing certificate authority certificates to use in verifying - * a presented peer certificate (e.g. server certificate for clusters or client certificate - * for listeners). If not specified and a peer certificate is presented it will not be - * verified. By default, a client certificate is optional, unless one of the additional - * options (:ref:`require_client_certificate - * `, - * :ref:`verify_certificate_spki - * `, - * :ref:`verify_certificate_hash - * `, or - * :ref:`match_subject_alt_names - * `) is also - * specified. - * - * It can optionally contain certificate revocation lists, in which case Envoy will verify - * that the presented peer certificate has not been revoked by one of the included CRLs. Note - * that if a CRL is provided for any certificate authority in a trust chain, a CRL must be - * provided for all certificate authorities in that chain. Failure to do so will result in - * verification failure for both revoked and unrevoked certificates from that chain. - * - * See :ref:`the TLS overview ` for a list of common - * system CA locations. - * - * If *trusted_ca* is a filesystem path, a watch will be added to the parent - * directory for any file moves to support rotation. This currently only - * applies to dynamic secrets, when the *CertificateValidationContext* is - * delivered via SDS. - * - * Only one of *trusted_ca* and *ca_certificate_provider_instance* may be specified. - * - * [#next-major-version: This field and watched_directory below should ideally be moved into a - * separate sub-message, since there's no point in specifying the latter field without this one.] - */ - 'trusted_ca': (_envoy_config_core_v3_DataSource__Output | null); - /** - * An optional list of hex-encoded SHA-256 hashes. If specified, Envoy will verify that - * the SHA-256 of the DER-encoded presented certificate matches one of the specified values. - * - * A hex-encoded SHA-256 of the certificate can be generated with the following command: - * - * .. code-block:: bash - * - * $ openssl x509 -in path/to/client.crt -outform DER | openssl dgst -sha256 | cut -d" " -f2 - * df6ff72fe9116521268f6f2dd4966f51df479883fe7037b39f75916ac3049d1a - * - * A long hex-encoded and colon-separated SHA-256 (a.k.a. "fingerprint") of the certificate - * can be generated with the following command: - * - * .. code-block:: bash - * - * $ openssl x509 -in path/to/client.crt -noout -fingerprint -sha256 | cut -d"=" -f2 - * DF:6F:F7:2F:E9:11:65:21:26:8F:6F:2D:D4:96:6F:51:DF:47:98:83:FE:70:37:B3:9F:75:91:6A:C3:04:9D:1A - * - * Both of those formats are acceptable. - * - * When both: - * :ref:`verify_certificate_hash - * ` and - * :ref:`verify_certificate_spki - * ` are specified, - * a hash matching value from either of the lists will result in the certificate being accepted. - */ - 'verify_certificate_hash': (string)[]; - /** - * An optional list of base64-encoded SHA-256 hashes. If specified, Envoy will verify that the - * SHA-256 of the DER-encoded Subject Public Key Information (SPKI) of the presented certificate - * matches one of the specified values. - * - * A base64-encoded SHA-256 of the Subject Public Key Information (SPKI) of the certificate - * can be generated with the following command: - * - * .. code-block:: bash - * - * $ openssl x509 -in path/to/client.crt -noout -pubkey - * | openssl pkey -pubin -outform DER - * | openssl dgst -sha256 -binary - * | openssl enc -base64 - * NvqYIYSbgK2vCJpQhObf77vv+bQWtc5ek5RIOwPiC9A= - * - * This is the format used in HTTP Public Key Pinning. - * - * When both: - * :ref:`verify_certificate_hash - * ` and - * :ref:`verify_certificate_spki - * ` are specified, - * a hash matching value from either of the lists will result in the certificate being accepted. - * - * .. attention:: - * - * This option is preferred over :ref:`verify_certificate_hash - * `, - * because SPKI is tied to a private key, so it doesn't change when the certificate - * is renewed using the same private key. - */ - 'verify_certificate_spki': (string)[]; - /** - * [#not-implemented-hide:] Must present signed certificate time-stamp. - */ - 'require_signed_certificate_timestamp': (_google_protobuf_BoolValue__Output | null); - /** - * An optional `certificate revocation list - * `_ - * (in PEM format). If specified, Envoy will verify that the presented peer - * certificate has not been revoked by this CRL. If this DataSource contains - * multiple CRLs, all of them will be used. Note that if a CRL is provided - * for any certificate authority in a trust chain, a CRL must be provided - * for all certificate authorities in that chain. Failure to do so will - * result in verification failure for both revoked and unrevoked certificates - * from that chain. - */ - 'crl': (_envoy_config_core_v3_DataSource__Output | null); - /** - * If specified, Envoy will not reject expired certificates. - */ - 'allow_expired_certificate': (boolean); - /** - * An optional list of Subject Alternative name matchers. If specified, Envoy will verify that the - * Subject Alternative Name of the presented certificate matches one of the specified matchers. - * - * When a certificate has wildcard DNS SAN entries, to match a specific client, it should be - * configured with exact match type in the :ref:`string matcher `. - * For example if the certificate has "\*.example.com" as DNS SAN entry, to allow only "api.example.com", - * it should be configured as shown below. - * - * .. code-block:: yaml - * - * match_subject_alt_names: - * exact: "api.example.com" - * - * .. attention:: - * - * Subject Alternative Names are easily spoofable and verifying only them is insecure, - * therefore this option must be used together with :ref:`trusted_ca - * `. - */ - 'match_subject_alt_names': (_envoy_type_matcher_v3_StringMatcher__Output)[]; - /** - * Certificate trust chain verification mode. - */ - 'trust_chain_verification': (keyof typeof _envoy_extensions_transport_sockets_tls_v3_CertificateValidationContext_TrustChainVerification); - /** - * If specified, updates of a file-based *trusted_ca* source will be triggered - * by this watch. This allows explicit control over the path watched, by - * default the parent directory of the filesystem path in *trusted_ca* is - * watched if this field is not specified. This only applies when a - * *CertificateValidationContext* is delivered by SDS with references to - * filesystem paths. See the :ref:`SDS key rotation ` - * documentation for further details. - */ - 'watched_directory': (_envoy_config_core_v3_WatchedDirectory__Output | null); - /** - * The configuration of an extension specific certificate validator. - * If specified, all validation is done by the specified validator, - * and the behavior of all other validation settings is defined by the specified validator (and may be entirely ignored, unused, and unvalidated). - * Refer to the documentation for the specified validator. If you do not want a custom validation algorithm, do not set this field. - * [#extension-category: envoy.tls.cert_validator] - */ - 'custom_validator_config': (_envoy_config_core_v3_TypedExtensionConfig__Output | null); - /** - * Certificate provider instance for fetching TLS certificates. - * - * Only one of *trusted_ca* and *ca_certificate_provider_instance* may be specified. - * [#not-implemented-hide:] - */ - 'ca_certificate_provider_instance': (_envoy_extensions_transport_sockets_tls_v3_CertificateProviderPluginInstance__Output | null); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/GenericSecret.ts b/packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/GenericSecret.ts deleted file mode 100644 index b206fb13a..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/GenericSecret.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Original file: deps/envoy-api/envoy/extensions/transport_sockets/tls/v3/secret.proto - -import type { DataSource as _envoy_config_core_v3_DataSource, DataSource__Output as _envoy_config_core_v3_DataSource__Output } from '../../../../../envoy/config/core/v3/DataSource'; - -export interface GenericSecret { - /** - * Secret of generic type and is available to filters. - */ - 'secret'?: (_envoy_config_core_v3_DataSource | null); -} - -export interface GenericSecret__Output { - /** - * Secret of generic type and is available to filters. - */ - 'secret': (_envoy_config_core_v3_DataSource__Output | null); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/PrivateKeyProvider.ts b/packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/PrivateKeyProvider.ts deleted file mode 100644 index b4a2ad933..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/PrivateKeyProvider.ts +++ /dev/null @@ -1,39 +0,0 @@ -// Original file: deps/envoy-api/envoy/extensions/transport_sockets/tls/v3/common.proto - -import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../../../google/protobuf/Any'; - -/** - * BoringSSL private key method configuration. The private key methods are used for external - * (potentially asynchronous) signing and decryption operations. Some use cases for private key - * methods would be TPM support and TLS acceleration. - */ -export interface PrivateKeyProvider { - /** - * Private key method provider name. The name must match a - * supported private key method provider type. - */ - 'provider_name'?: (string); - 'typed_config'?: (_google_protobuf_Any | null); - /** - * Private key method provider specific configuration. - */ - 'config_type'?: "typed_config"; -} - -/** - * BoringSSL private key method configuration. The private key methods are used for external - * (potentially asynchronous) signing and decryption operations. Some use cases for private key - * methods would be TPM support and TLS acceleration. - */ -export interface PrivateKeyProvider__Output { - /** - * Private key method provider name. The name must match a - * supported private key method provider type. - */ - 'provider_name': (string); - 'typed_config'?: (_google_protobuf_Any__Output | null); - /** - * Private key method provider specific configuration. - */ - 'config_type': "typed_config"; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/SdsSecretConfig.ts b/packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/SdsSecretConfig.ts deleted file mode 100644 index 38b850c50..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/SdsSecretConfig.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Original file: deps/envoy-api/envoy/extensions/transport_sockets/tls/v3/secret.proto - -import type { ConfigSource as _envoy_config_core_v3_ConfigSource, ConfigSource__Output as _envoy_config_core_v3_ConfigSource__Output } from '../../../../../envoy/config/core/v3/ConfigSource'; - -export interface SdsSecretConfig { - /** - * Name by which the secret can be uniquely referred to. When both name and config are specified, - * then secret can be fetched and/or reloaded via SDS. When only name is specified, then secret - * will be loaded from static resources. - */ - 'name'?: (string); - 'sds_config'?: (_envoy_config_core_v3_ConfigSource | null); -} - -export interface SdsSecretConfig__Output { - /** - * Name by which the secret can be uniquely referred to. When both name and config are specified, - * then secret can be fetched and/or reloaded via SDS. When only name is specified, then secret - * will be loaded from static resources. - */ - 'name': (string); - 'sds_config': (_envoy_config_core_v3_ConfigSource__Output | null); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/Secret.ts b/packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/Secret.ts deleted file mode 100644 index c86957da5..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/Secret.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Original file: deps/envoy-api/envoy/extensions/transport_sockets/tls/v3/secret.proto - -import type { TlsCertificate as _envoy_extensions_transport_sockets_tls_v3_TlsCertificate, TlsCertificate__Output as _envoy_extensions_transport_sockets_tls_v3_TlsCertificate__Output } from '../../../../../envoy/extensions/transport_sockets/tls/v3/TlsCertificate'; -import type { TlsSessionTicketKeys as _envoy_extensions_transport_sockets_tls_v3_TlsSessionTicketKeys, TlsSessionTicketKeys__Output as _envoy_extensions_transport_sockets_tls_v3_TlsSessionTicketKeys__Output } from '../../../../../envoy/extensions/transport_sockets/tls/v3/TlsSessionTicketKeys'; -import type { CertificateValidationContext as _envoy_extensions_transport_sockets_tls_v3_CertificateValidationContext, CertificateValidationContext__Output as _envoy_extensions_transport_sockets_tls_v3_CertificateValidationContext__Output } from '../../../../../envoy/extensions/transport_sockets/tls/v3/CertificateValidationContext'; -import type { GenericSecret as _envoy_extensions_transport_sockets_tls_v3_GenericSecret, GenericSecret__Output as _envoy_extensions_transport_sockets_tls_v3_GenericSecret__Output } from '../../../../../envoy/extensions/transport_sockets/tls/v3/GenericSecret'; - -/** - * [#next-free-field: 6] - */ -export interface Secret { - /** - * Name (FQDN, UUID, SPKI, SHA256, etc.) by which the secret can be uniquely referred to. - */ - 'name'?: (string); - 'tls_certificate'?: (_envoy_extensions_transport_sockets_tls_v3_TlsCertificate | null); - 'session_ticket_keys'?: (_envoy_extensions_transport_sockets_tls_v3_TlsSessionTicketKeys | null); - 'validation_context'?: (_envoy_extensions_transport_sockets_tls_v3_CertificateValidationContext | null); - 'generic_secret'?: (_envoy_extensions_transport_sockets_tls_v3_GenericSecret | null); - 'type'?: "tls_certificate"|"session_ticket_keys"|"validation_context"|"generic_secret"; -} - -/** - * [#next-free-field: 6] - */ -export interface Secret__Output { - /** - * Name (FQDN, UUID, SPKI, SHA256, etc.) by which the secret can be uniquely referred to. - */ - 'name': (string); - 'tls_certificate'?: (_envoy_extensions_transport_sockets_tls_v3_TlsCertificate__Output | null); - 'session_ticket_keys'?: (_envoy_extensions_transport_sockets_tls_v3_TlsSessionTicketKeys__Output | null); - 'validation_context'?: (_envoy_extensions_transport_sockets_tls_v3_CertificateValidationContext__Output | null); - 'generic_secret'?: (_envoy_extensions_transport_sockets_tls_v3_GenericSecret__Output | null); - 'type': "tls_certificate"|"session_ticket_keys"|"validation_context"|"generic_secret"; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/TlsCertificate.ts b/packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/TlsCertificate.ts deleted file mode 100644 index ce8046e95..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/TlsCertificate.ts +++ /dev/null @@ -1,127 +0,0 @@ -// Original file: deps/envoy-api/envoy/extensions/transport_sockets/tls/v3/common.proto - -import type { DataSource as _envoy_config_core_v3_DataSource, DataSource__Output as _envoy_config_core_v3_DataSource__Output } from '../../../../../envoy/config/core/v3/DataSource'; -import type { PrivateKeyProvider as _envoy_extensions_transport_sockets_tls_v3_PrivateKeyProvider, PrivateKeyProvider__Output as _envoy_extensions_transport_sockets_tls_v3_PrivateKeyProvider__Output } from '../../../../../envoy/extensions/transport_sockets/tls/v3/PrivateKeyProvider'; -import type { WatchedDirectory as _envoy_config_core_v3_WatchedDirectory, WatchedDirectory__Output as _envoy_config_core_v3_WatchedDirectory__Output } from '../../../../../envoy/config/core/v3/WatchedDirectory'; - -/** - * [#next-free-field: 8] - */ -export interface TlsCertificate { - /** - * The TLS certificate chain. - * - * If *certificate_chain* is a filesystem path, a watch will be added to the - * parent directory for any file moves to support rotation. This currently - * only applies to dynamic secrets, when the *TlsCertificate* is delivered via - * SDS. - */ - 'certificate_chain'?: (_envoy_config_core_v3_DataSource | null); - /** - * The TLS private key. - * - * If *private_key* is a filesystem path, a watch will be added to the parent - * directory for any file moves to support rotation. This currently only - * applies to dynamic secrets, when the *TlsCertificate* is delivered via SDS. - */ - 'private_key'?: (_envoy_config_core_v3_DataSource | null); - /** - * The password to decrypt the TLS private key. If this field is not set, it is assumed that the - * TLS private key is not password encrypted. - */ - 'password'?: (_envoy_config_core_v3_DataSource | null); - /** - * The OCSP response to be stapled with this certificate during the handshake. - * The response must be DER-encoded and may only be provided via ``filename`` or - * ``inline_bytes``. The response may pertain to only one certificate. - */ - 'ocsp_staple'?: (_envoy_config_core_v3_DataSource | null); - /** - * [#not-implemented-hide:] - */ - 'signed_certificate_timestamp'?: (_envoy_config_core_v3_DataSource)[]; - /** - * BoringSSL private key method provider. This is an alternative to :ref:`private_key - * ` field. This can't be - * marked as ``oneof`` due to API compatibility reasons. Setting both :ref:`private_key - * ` and - * :ref:`private_key_provider - * ` fields will result in an - * error. - */ - 'private_key_provider'?: (_envoy_extensions_transport_sockets_tls_v3_PrivateKeyProvider | null); - /** - * If specified, updates of file-based *certificate_chain* and *private_key* - * sources will be triggered by this watch. The certificate/key pair will be - * read together and validated for atomic read consistency (i.e. no - * intervening modification occurred between cert/key read, verified by file - * hash comparisons). This allows explicit control over the path watched, by - * default the parent directories of the filesystem paths in - * *certificate_chain* and *private_key* are watched if this field is not - * specified. This only applies when a *TlsCertificate* is delivered by SDS - * with references to filesystem paths. See the :ref:`SDS key rotation - * ` documentation for further details. - */ - 'watched_directory'?: (_envoy_config_core_v3_WatchedDirectory | null); -} - -/** - * [#next-free-field: 8] - */ -export interface TlsCertificate__Output { - /** - * The TLS certificate chain. - * - * If *certificate_chain* is a filesystem path, a watch will be added to the - * parent directory for any file moves to support rotation. This currently - * only applies to dynamic secrets, when the *TlsCertificate* is delivered via - * SDS. - */ - 'certificate_chain': (_envoy_config_core_v3_DataSource__Output | null); - /** - * The TLS private key. - * - * If *private_key* is a filesystem path, a watch will be added to the parent - * directory for any file moves to support rotation. This currently only - * applies to dynamic secrets, when the *TlsCertificate* is delivered via SDS. - */ - 'private_key': (_envoy_config_core_v3_DataSource__Output | null); - /** - * The password to decrypt the TLS private key. If this field is not set, it is assumed that the - * TLS private key is not password encrypted. - */ - 'password': (_envoy_config_core_v3_DataSource__Output | null); - /** - * The OCSP response to be stapled with this certificate during the handshake. - * The response must be DER-encoded and may only be provided via ``filename`` or - * ``inline_bytes``. The response may pertain to only one certificate. - */ - 'ocsp_staple': (_envoy_config_core_v3_DataSource__Output | null); - /** - * [#not-implemented-hide:] - */ - 'signed_certificate_timestamp': (_envoy_config_core_v3_DataSource__Output)[]; - /** - * BoringSSL private key method provider. This is an alternative to :ref:`private_key - * ` field. This can't be - * marked as ``oneof`` due to API compatibility reasons. Setting both :ref:`private_key - * ` and - * :ref:`private_key_provider - * ` fields will result in an - * error. - */ - 'private_key_provider': (_envoy_extensions_transport_sockets_tls_v3_PrivateKeyProvider__Output | null); - /** - * If specified, updates of file-based *certificate_chain* and *private_key* - * sources will be triggered by this watch. The certificate/key pair will be - * read together and validated for atomic read consistency (i.e. no - * intervening modification occurred between cert/key read, verified by file - * hash comparisons). This allows explicit control over the path watched, by - * default the parent directories of the filesystem paths in - * *certificate_chain* and *private_key* are watched if this field is not - * specified. This only applies when a *TlsCertificate* is delivered by SDS - * with references to filesystem paths. See the :ref:`SDS key rotation - * ` documentation for further details. - */ - 'watched_directory': (_envoy_config_core_v3_WatchedDirectory__Output | null); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/TlsParameters.ts b/packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/TlsParameters.ts deleted file mode 100644 index e68464c8b..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/TlsParameters.ts +++ /dev/null @@ -1,211 +0,0 @@ -// Original file: deps/envoy-api/envoy/extensions/transport_sockets/tls/v3/common.proto - - -// Original file: deps/envoy-api/envoy/extensions/transport_sockets/tls/v3/common.proto - -export enum _envoy_extensions_transport_sockets_tls_v3_TlsParameters_TlsProtocol { - /** - * Envoy will choose the optimal TLS version. - */ - TLS_AUTO = 0, - /** - * TLS 1.0 - */ - TLSv1_0 = 1, - /** - * TLS 1.1 - */ - TLSv1_1 = 2, - /** - * TLS 1.2 - */ - TLSv1_2 = 3, - /** - * TLS 1.3 - */ - TLSv1_3 = 4, -} - -export interface TlsParameters { - /** - * Minimum TLS protocol version. By default, it's ``TLSv1_2`` for clients and ``TLSv1_0`` for - * servers. - */ - 'tls_minimum_protocol_version'?: (_envoy_extensions_transport_sockets_tls_v3_TlsParameters_TlsProtocol | keyof typeof _envoy_extensions_transport_sockets_tls_v3_TlsParameters_TlsProtocol); - /** - * Maximum TLS protocol version. By default, it's ``TLSv1_2`` for clients and ``TLSv1_3`` for - * servers. - */ - 'tls_maximum_protocol_version'?: (_envoy_extensions_transport_sockets_tls_v3_TlsParameters_TlsProtocol | keyof typeof _envoy_extensions_transport_sockets_tls_v3_TlsParameters_TlsProtocol); - /** - * If specified, the TLS listener will only support the specified `cipher list - * `_ - * when negotiating TLS 1.0-1.2 (this setting has no effect when negotiating TLS 1.3). - * - * If not specified, a default list will be used. Defaults are different for server (downstream) and - * client (upstream) TLS configurations. - * - * In non-FIPS builds, the default server cipher list is: - * - * .. code-block:: none - * - * [ECDHE-ECDSA-AES128-GCM-SHA256|ECDHE-ECDSA-CHACHA20-POLY1305] - * [ECDHE-RSA-AES128-GCM-SHA256|ECDHE-RSA-CHACHA20-POLY1305] - * ECDHE-ECDSA-AES128-SHA - * ECDHE-RSA-AES128-SHA - * AES128-GCM-SHA256 - * AES128-SHA - * ECDHE-ECDSA-AES256-GCM-SHA384 - * ECDHE-RSA-AES256-GCM-SHA384 - * ECDHE-ECDSA-AES256-SHA - * ECDHE-RSA-AES256-SHA - * AES256-GCM-SHA384 - * AES256-SHA - * - * In builds using :ref:`BoringSSL FIPS `, the default server cipher list is: - * - * .. code-block:: none - * - * ECDHE-ECDSA-AES128-GCM-SHA256 - * ECDHE-RSA-AES128-GCM-SHA256 - * ECDHE-ECDSA-AES128-SHA - * ECDHE-RSA-AES128-SHA - * AES128-GCM-SHA256 - * AES128-SHA - * ECDHE-ECDSA-AES256-GCM-SHA384 - * ECDHE-RSA-AES256-GCM-SHA384 - * ECDHE-ECDSA-AES256-SHA - * ECDHE-RSA-AES256-SHA - * AES256-GCM-SHA384 - * AES256-SHA - * - * In non-FIPS builds, the default client cipher list is: - * - * .. code-block:: none - * - * [ECDHE-ECDSA-AES128-GCM-SHA256|ECDHE-ECDSA-CHACHA20-POLY1305] - * [ECDHE-RSA-AES128-GCM-SHA256|ECDHE-RSA-CHACHA20-POLY1305] - * ECDHE-ECDSA-AES256-GCM-SHA384 - * ECDHE-RSA-AES256-GCM-SHA384 - * - * In builds using :ref:`BoringSSL FIPS `, the default client cipher list is: - * - * .. code-block:: none - * - * ECDHE-ECDSA-AES128-GCM-SHA256 - * ECDHE-RSA-AES128-GCM-SHA256 - * ECDHE-ECDSA-AES256-GCM-SHA384 - * ECDHE-RSA-AES256-GCM-SHA384 - */ - 'cipher_suites'?: (string)[]; - /** - * If specified, the TLS connection will only support the specified ECDH - * curves. If not specified, the default curves will be used. - * - * In non-FIPS builds, the default curves are: - * - * .. code-block:: none - * - * X25519 - * P-256 - * - * In builds using :ref:`BoringSSL FIPS `, the default curve is: - * - * .. code-block:: none - * - * P-256 - */ - 'ecdh_curves'?: (string)[]; -} - -export interface TlsParameters__Output { - /** - * Minimum TLS protocol version. By default, it's ``TLSv1_2`` for clients and ``TLSv1_0`` for - * servers. - */ - 'tls_minimum_protocol_version': (keyof typeof _envoy_extensions_transport_sockets_tls_v3_TlsParameters_TlsProtocol); - /** - * Maximum TLS protocol version. By default, it's ``TLSv1_2`` for clients and ``TLSv1_3`` for - * servers. - */ - 'tls_maximum_protocol_version': (keyof typeof _envoy_extensions_transport_sockets_tls_v3_TlsParameters_TlsProtocol); - /** - * If specified, the TLS listener will only support the specified `cipher list - * `_ - * when negotiating TLS 1.0-1.2 (this setting has no effect when negotiating TLS 1.3). - * - * If not specified, a default list will be used. Defaults are different for server (downstream) and - * client (upstream) TLS configurations. - * - * In non-FIPS builds, the default server cipher list is: - * - * .. code-block:: none - * - * [ECDHE-ECDSA-AES128-GCM-SHA256|ECDHE-ECDSA-CHACHA20-POLY1305] - * [ECDHE-RSA-AES128-GCM-SHA256|ECDHE-RSA-CHACHA20-POLY1305] - * ECDHE-ECDSA-AES128-SHA - * ECDHE-RSA-AES128-SHA - * AES128-GCM-SHA256 - * AES128-SHA - * ECDHE-ECDSA-AES256-GCM-SHA384 - * ECDHE-RSA-AES256-GCM-SHA384 - * ECDHE-ECDSA-AES256-SHA - * ECDHE-RSA-AES256-SHA - * AES256-GCM-SHA384 - * AES256-SHA - * - * In builds using :ref:`BoringSSL FIPS `, the default server cipher list is: - * - * .. code-block:: none - * - * ECDHE-ECDSA-AES128-GCM-SHA256 - * ECDHE-RSA-AES128-GCM-SHA256 - * ECDHE-ECDSA-AES128-SHA - * ECDHE-RSA-AES128-SHA - * AES128-GCM-SHA256 - * AES128-SHA - * ECDHE-ECDSA-AES256-GCM-SHA384 - * ECDHE-RSA-AES256-GCM-SHA384 - * ECDHE-ECDSA-AES256-SHA - * ECDHE-RSA-AES256-SHA - * AES256-GCM-SHA384 - * AES256-SHA - * - * In non-FIPS builds, the default client cipher list is: - * - * .. code-block:: none - * - * [ECDHE-ECDSA-AES128-GCM-SHA256|ECDHE-ECDSA-CHACHA20-POLY1305] - * [ECDHE-RSA-AES128-GCM-SHA256|ECDHE-RSA-CHACHA20-POLY1305] - * ECDHE-ECDSA-AES256-GCM-SHA384 - * ECDHE-RSA-AES256-GCM-SHA384 - * - * In builds using :ref:`BoringSSL FIPS `, the default client cipher list is: - * - * .. code-block:: none - * - * ECDHE-ECDSA-AES128-GCM-SHA256 - * ECDHE-RSA-AES128-GCM-SHA256 - * ECDHE-ECDSA-AES256-GCM-SHA384 - * ECDHE-RSA-AES256-GCM-SHA384 - */ - 'cipher_suites': (string)[]; - /** - * If specified, the TLS connection will only support the specified ECDH - * curves. If not specified, the default curves will be used. - * - * In non-FIPS builds, the default curves are: - * - * .. code-block:: none - * - * X25519 - * P-256 - * - * In builds using :ref:`BoringSSL FIPS `, the default curve is: - * - * .. code-block:: none - * - * P-256 - */ - 'ecdh_curves': (string)[]; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/TlsSessionTicketKeys.ts b/packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/TlsSessionTicketKeys.ts deleted file mode 100644 index 152bccac7..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/extensions/transport_sockets/tls/v3/TlsSessionTicketKeys.ts +++ /dev/null @@ -1,61 +0,0 @@ -// Original file: deps/envoy-api/envoy/extensions/transport_sockets/tls/v3/common.proto - -import type { DataSource as _envoy_config_core_v3_DataSource, DataSource__Output as _envoy_config_core_v3_DataSource__Output } from '../../../../../envoy/config/core/v3/DataSource'; - -export interface TlsSessionTicketKeys { - /** - * Keys for encrypting and decrypting TLS session tickets. The - * first key in the array contains the key to encrypt all new sessions created by this context. - * All keys are candidates for decrypting received tickets. This allows for easy rotation of keys - * by, for example, putting the new key first, and the previous key second. - * - * If :ref:`session_ticket_keys ` - * is not specified, the TLS library will still support resuming sessions via tickets, but it will - * use an internally-generated and managed key, so sessions cannot be resumed across hot restarts - * or on different hosts. - * - * Each key must contain exactly 80 bytes of cryptographically-secure random data. For - * example, the output of ``openssl rand 80``. - * - * .. attention:: - * - * Using this feature has serious security considerations and risks. Improper handling of keys - * may result in loss of secrecy in connections, even if ciphers supporting perfect forward - * secrecy are used. See https://www.imperialviolet.org/2013/06/27/botchingpfs.html for some - * discussion. To minimize the risk, you must: - * - * * Keep the session ticket keys at least as secure as your TLS certificate private keys - * * Rotate session ticket keys at least daily, and preferably hourly - * * Always generate keys using a cryptographically-secure random data source - */ - 'keys'?: (_envoy_config_core_v3_DataSource)[]; -} - -export interface TlsSessionTicketKeys__Output { - /** - * Keys for encrypting and decrypting TLS session tickets. The - * first key in the array contains the key to encrypt all new sessions created by this context. - * All keys are candidates for decrypting received tickets. This allows for easy rotation of keys - * by, for example, putting the new key first, and the previous key second. - * - * If :ref:`session_ticket_keys ` - * is not specified, the TLS library will still support resuming sessions via tickets, but it will - * use an internally-generated and managed key, so sessions cannot be resumed across hot restarts - * or on different hosts. - * - * Each key must contain exactly 80 bytes of cryptographically-secure random data. For - * example, the output of ``openssl rand 80``. - * - * .. attention:: - * - * Using this feature has serious security considerations and risks. Improper handling of keys - * may result in loss of secrecy in connections, even if ciphers supporting perfect forward - * secrecy are used. See https://www.imperialviolet.org/2013/06/27/botchingpfs.html for some - * discussion. To minimize the risk, you must: - * - * * Keep the session ticket keys at least as secure as your TLS certificate private keys - * * Rotate session ticket keys at least daily, and preferably hourly - * * Always generate keys using a cryptographically-secure random data source - */ - 'keys': (_envoy_config_core_v3_DataSource__Output)[]; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/service/status/v3/ClientConfig.ts b/packages/grpc-js-xds/src/generated/envoy/service/status/v3/ClientConfig.ts index ba6b25b4c..506547d1e 100644 --- a/packages/grpc-js-xds/src/generated/envoy/service/status/v3/ClientConfig.ts +++ b/packages/grpc-js-xds/src/generated/envoy/service/status/v3/ClientConfig.ts @@ -4,8 +4,8 @@ import type { Node as _envoy_config_core_v3_Node, Node__Output as _envoy_config_ import type { PerXdsConfig as _envoy_service_status_v3_PerXdsConfig, PerXdsConfig__Output as _envoy_service_status_v3_PerXdsConfig__Output } from '../../../../envoy/service/status/v3/PerXdsConfig'; import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../../google/protobuf/Any'; import type { Timestamp as _google_protobuf_Timestamp, Timestamp__Output as _google_protobuf_Timestamp__Output } from '../../../../google/protobuf/Timestamp'; -import type { ConfigStatus as _envoy_service_status_v3_ConfigStatus } from '../../../../envoy/service/status/v3/ConfigStatus'; -import type { ClientResourceStatus as _envoy_admin_v3_ClientResourceStatus } from '../../../../envoy/admin/v3/ClientResourceStatus'; +import type { ConfigStatus as _envoy_service_status_v3_ConfigStatus, ConfigStatus__Output as _envoy_service_status_v3_ConfigStatus__Output } from '../../../../envoy/service/status/v3/ConfigStatus'; +import type { ClientResourceStatus as _envoy_admin_v3_ClientResourceStatus, ClientResourceStatus__Output as _envoy_admin_v3_ClientResourceStatus__Output } from '../../../../envoy/admin/v3/ClientResourceStatus'; import type { UpdateFailureState as _envoy_admin_v3_UpdateFailureState, UpdateFailureState__Output as _envoy_admin_v3_UpdateFailureState__Output } from '../../../../envoy/admin/v3/UpdateFailureState'; /** @@ -42,11 +42,11 @@ export interface _envoy_service_status_v3_ClientConfig_GenericXdsConfig { * Per xDS resource config status. It is generated by management servers. * It will not be present if the CSDS server is an xDS client. */ - 'config_status'?: (_envoy_service_status_v3_ConfigStatus | keyof typeof _envoy_service_status_v3_ConfigStatus); + 'config_status'?: (_envoy_service_status_v3_ConfigStatus); /** * Per xDS resource status from the view of a xDS client */ - 'client_status'?: (_envoy_admin_v3_ClientResourceStatus | keyof typeof _envoy_admin_v3_ClientResourceStatus); + 'client_status'?: (_envoy_admin_v3_ClientResourceStatus); /** * Set if the last update failed, cleared after the next successful * update. The *error_state* field contains the rejected version of @@ -97,11 +97,11 @@ export interface _envoy_service_status_v3_ClientConfig_GenericXdsConfig__Output * Per xDS resource config status. It is generated by management servers. * It will not be present if the CSDS server is an xDS client. */ - 'config_status': (keyof typeof _envoy_service_status_v3_ConfigStatus); + 'config_status': (_envoy_service_status_v3_ConfigStatus__Output); /** * Per xDS resource status from the view of a xDS client */ - 'client_status': (keyof typeof _envoy_admin_v3_ClientResourceStatus); + 'client_status': (_envoy_admin_v3_ClientResourceStatus__Output); /** * Set if the last update failed, cleared after the next successful * update. The *error_state* field contains the rejected version of @@ -129,6 +129,7 @@ export interface ClientConfig { /** * This field is deprecated in favor of generic_xds_configs which is * much simpler and uniform in structure. + * @deprecated */ 'xds_config'?: (_envoy_service_status_v3_PerXdsConfig)[]; /** @@ -149,6 +150,7 @@ export interface ClientConfig__Output { /** * This field is deprecated in favor of generic_xds_configs which is * much simpler and uniform in structure. + * @deprecated */ 'xds_config': (_envoy_service_status_v3_PerXdsConfig__Output)[]; /** diff --git a/packages/grpc-js-xds/src/generated/envoy/service/status/v3/ClientConfigStatus.ts b/packages/grpc-js-xds/src/generated/envoy/service/status/v3/ClientConfigStatus.ts index 104445a3f..be7a7afd0 100644 --- a/packages/grpc-js-xds/src/generated/envoy/service/status/v3/ClientConfigStatus.ts +++ b/packages/grpc-js-xds/src/generated/envoy/service/status/v3/ClientConfigStatus.ts @@ -3,24 +3,57 @@ /** * Config status from a client-side view. */ -export enum ClientConfigStatus { +export const ClientConfigStatus = { /** * Config status is not available/unknown. */ - CLIENT_UNKNOWN = 0, + CLIENT_UNKNOWN: 'CLIENT_UNKNOWN', /** * Client requested the config but hasn't received any config from management * server yet. */ - CLIENT_REQUESTED = 1, + CLIENT_REQUESTED: 'CLIENT_REQUESTED', /** * Client received the config and replied with ACK. */ - CLIENT_ACKED = 2, + CLIENT_ACKED: 'CLIENT_ACKED', /** * Client received the config and replied with NACK. Notably, the attached * config dump is not the NACKed version, but the most recent accepted one. If * no config is accepted yet, the attached config dump will be empty. */ - CLIENT_NACKED = 3, -} + CLIENT_NACKED: 'CLIENT_NACKED', +} as const; + +/** + * Config status from a client-side view. + */ +export type ClientConfigStatus = + /** + * Config status is not available/unknown. + */ + | 'CLIENT_UNKNOWN' + | 0 + /** + * Client requested the config but hasn't received any config from management + * server yet. + */ + | 'CLIENT_REQUESTED' + | 1 + /** + * Client received the config and replied with ACK. + */ + | 'CLIENT_ACKED' + | 2 + /** + * Client received the config and replied with NACK. Notably, the attached + * config dump is not the NACKed version, but the most recent accepted one. If + * no config is accepted yet, the attached config dump will be empty. + */ + | 'CLIENT_NACKED' + | 3 + +/** + * Config status from a client-side view. + */ +export type ClientConfigStatus__Output = typeof ClientConfigStatus[keyof typeof ClientConfigStatus] diff --git a/packages/grpc-js-xds/src/generated/envoy/service/status/v3/ConfigStatus.ts b/packages/grpc-js-xds/src/generated/envoy/service/status/v3/ConfigStatus.ts index 71db302c3..15a8359e8 100644 --- a/packages/grpc-js-xds/src/generated/envoy/service/status/v3/ConfigStatus.ts +++ b/packages/grpc-js-xds/src/generated/envoy/service/status/v3/ConfigStatus.ts @@ -3,28 +3,66 @@ /** * Status of a config from a management server view. */ -export enum ConfigStatus { +export const ConfigStatus = { /** * Status info is not available/unknown. */ - UNKNOWN = 0, + UNKNOWN: 'UNKNOWN', /** * Management server has sent the config to client and received ACK. */ - SYNCED = 1, + SYNCED: 'SYNCED', /** * Config is not sent. */ - NOT_SENT = 2, + NOT_SENT: 'NOT_SENT', /** * Management server has sent the config to client but hasn’t received * ACK/NACK. */ - STALE = 3, + STALE: 'STALE', /** * Management server has sent the config to client but received NACK. The * attached config dump will be the latest config (the rejected one), since * it is the persisted version in the management server. */ - ERROR = 4, -} + ERROR: 'ERROR', +} as const; + +/** + * Status of a config from a management server view. + */ +export type ConfigStatus = + /** + * Status info is not available/unknown. + */ + | 'UNKNOWN' + | 0 + /** + * Management server has sent the config to client and received ACK. + */ + | 'SYNCED' + | 1 + /** + * Config is not sent. + */ + | 'NOT_SENT' + | 2 + /** + * Management server has sent the config to client but hasn’t received + * ACK/NACK. + */ + | 'STALE' + | 3 + /** + * Management server has sent the config to client but received NACK. The + * attached config dump will be the latest config (the rejected one), since + * it is the persisted version in the management server. + */ + | 'ERROR' + | 4 + +/** + * Status of a config from a management server view. + */ +export type ConfigStatus__Output = typeof ConfigStatus[keyof typeof ConfigStatus] diff --git a/packages/grpc-js-xds/src/generated/envoy/service/status/v3/PerXdsConfig.ts b/packages/grpc-js-xds/src/generated/envoy/service/status/v3/PerXdsConfig.ts index 947f1c81a..d921f3b1c 100644 --- a/packages/grpc-js-xds/src/generated/envoy/service/status/v3/PerXdsConfig.ts +++ b/packages/grpc-js-xds/src/generated/envoy/service/status/v3/PerXdsConfig.ts @@ -1,12 +1,12 @@ // Original file: deps/envoy-api/envoy/service/status/v3/csds.proto -import type { ConfigStatus as _envoy_service_status_v3_ConfigStatus } from '../../../../envoy/service/status/v3/ConfigStatus'; +import type { ConfigStatus as _envoy_service_status_v3_ConfigStatus, ConfigStatus__Output as _envoy_service_status_v3_ConfigStatus__Output } from '../../../../envoy/service/status/v3/ConfigStatus'; import type { ListenersConfigDump as _envoy_admin_v3_ListenersConfigDump, ListenersConfigDump__Output as _envoy_admin_v3_ListenersConfigDump__Output } from '../../../../envoy/admin/v3/ListenersConfigDump'; import type { ClustersConfigDump as _envoy_admin_v3_ClustersConfigDump, ClustersConfigDump__Output as _envoy_admin_v3_ClustersConfigDump__Output } from '../../../../envoy/admin/v3/ClustersConfigDump'; import type { RoutesConfigDump as _envoy_admin_v3_RoutesConfigDump, RoutesConfigDump__Output as _envoy_admin_v3_RoutesConfigDump__Output } from '../../../../envoy/admin/v3/RoutesConfigDump'; import type { ScopedRoutesConfigDump as _envoy_admin_v3_ScopedRoutesConfigDump, ScopedRoutesConfigDump__Output as _envoy_admin_v3_ScopedRoutesConfigDump__Output } from '../../../../envoy/admin/v3/ScopedRoutesConfigDump'; import type { EndpointsConfigDump as _envoy_admin_v3_EndpointsConfigDump, EndpointsConfigDump__Output as _envoy_admin_v3_EndpointsConfigDump__Output } from '../../../../envoy/admin/v3/EndpointsConfigDump'; -import type { ClientConfigStatus as _envoy_service_status_v3_ClientConfigStatus } from '../../../../envoy/service/status/v3/ClientConfigStatus'; +import type { ClientConfigStatus as _envoy_service_status_v3_ClientConfigStatus, ClientConfigStatus__Output as _envoy_service_status_v3_ClientConfigStatus__Output } from '../../../../envoy/service/status/v3/ClientConfigStatus'; /** * Detailed config (per xDS) with status. @@ -17,7 +17,7 @@ export interface PerXdsConfig { * Config status generated by management servers. Will not be present if the * CSDS server is an xDS client. */ - 'status'?: (_envoy_service_status_v3_ConfigStatus | keyof typeof _envoy_service_status_v3_ConfigStatus); + 'status'?: (_envoy_service_status_v3_ConfigStatus); 'listener_config'?: (_envoy_admin_v3_ListenersConfigDump | null); 'cluster_config'?: (_envoy_admin_v3_ClustersConfigDump | null); 'route_config'?: (_envoy_admin_v3_RoutesConfigDump | null); @@ -32,8 +32,9 @@ export interface PerXdsConfig { * This field is deprecated. Use :ref:`ClientResourceStatus * ` for per-resource * config status instead. + * @deprecated */ - 'client_status'?: (_envoy_service_status_v3_ClientConfigStatus | keyof typeof _envoy_service_status_v3_ClientConfigStatus); + 'client_status'?: (_envoy_service_status_v3_ClientConfigStatus); 'per_xds_config'?: "listener_config"|"cluster_config"|"route_config"|"scoped_route_config"|"endpoint_config"; } @@ -46,7 +47,7 @@ export interface PerXdsConfig__Output { * Config status generated by management servers. Will not be present if the * CSDS server is an xDS client. */ - 'status': (keyof typeof _envoy_service_status_v3_ConfigStatus); + 'status': (_envoy_service_status_v3_ConfigStatus__Output); 'listener_config'?: (_envoy_admin_v3_ListenersConfigDump__Output | null); 'cluster_config'?: (_envoy_admin_v3_ClustersConfigDump__Output | null); 'route_config'?: (_envoy_admin_v3_RoutesConfigDump__Output | null); @@ -61,7 +62,8 @@ export interface PerXdsConfig__Output { * This field is deprecated. Use :ref:`ClientResourceStatus * ` for per-resource * config status instead. + * @deprecated */ - 'client_status': (keyof typeof _envoy_service_status_v3_ClientConfigStatus); + 'client_status': (_envoy_service_status_v3_ClientConfigStatus__Output); 'per_xds_config': "listener_config"|"cluster_config"|"route_config"|"scoped_route_config"|"endpoint_config"; } diff --git a/packages/grpc-js-xds/src/generated/envoy/type/matcher/v3/RegexMatcher.ts b/packages/grpc-js-xds/src/generated/envoy/type/matcher/v3/RegexMatcher.ts index c83f8b473..19517678f 100644 --- a/packages/grpc-js-xds/src/generated/envoy/type/matcher/v3/RegexMatcher.ts +++ b/packages/grpc-js-xds/src/generated/envoy/type/matcher/v3/RegexMatcher.ts @@ -31,6 +31,7 @@ export interface _envoy_type_matcher_v3_RegexMatcher_GoogleRE2 { * * Although this field is deprecated, the program size will still be checked against the * global ``re2.max_program_size.error_level`` runtime value. + * @deprecated */ 'max_program_size'?: (_google_protobuf_UInt32Value | null); } @@ -64,6 +65,7 @@ export interface _envoy_type_matcher_v3_RegexMatcher_GoogleRE2__Output { * * Although this field is deprecated, the program size will still be checked against the * global ``re2.max_program_size.error_level`` runtime value. + * @deprecated */ 'max_program_size': (_google_protobuf_UInt32Value__Output | null); } @@ -74,6 +76,7 @@ export interface _envoy_type_matcher_v3_RegexMatcher_GoogleRE2__Output { export interface RegexMatcher { /** * Google's RE2 regex engine. + * @deprecated */ 'google_re2'?: (_envoy_type_matcher_v3_RegexMatcher_GoogleRE2 | null); /** @@ -90,6 +93,7 @@ export interface RegexMatcher { export interface RegexMatcher__Output { /** * Google's RE2 regex engine. + * @deprecated */ 'google_re2'?: (_envoy_type_matcher_v3_RegexMatcher_GoogleRE2__Output | null); /** diff --git a/packages/grpc-js-xds/src/generated/envoy/type/v3/CodecClientType.ts b/packages/grpc-js-xds/src/generated/envoy/type/v3/CodecClientType.ts index 308f14446..e05cdfb96 100644 --- a/packages/grpc-js-xds/src/generated/envoy/type/v3/CodecClientType.ts +++ b/packages/grpc-js-xds/src/generated/envoy/type/v3/CodecClientType.ts @@ -1,12 +1,27 @@ // Original file: deps/envoy-api/envoy/type/v3/http.proto -export enum CodecClientType { - HTTP1 = 0, - HTTP2 = 1, +export const CodecClientType = { + HTTP1: 'HTTP1', + HTTP2: 'HTTP2', /** * [#not-implemented-hide:] QUIC implementation is not production ready yet. Use this enum with * caution to prevent accidental execution of QUIC code. I.e. `!= HTTP2` is no longer sufficient * to distinguish HTTP1 and HTTP2 traffic. */ - HTTP3 = 2, -} + HTTP3: 'HTTP3', +} as const; + +export type CodecClientType = + | 'HTTP1' + | 0 + | 'HTTP2' + | 1 + /** + * [#not-implemented-hide:] QUIC implementation is not production ready yet. Use this enum with + * caution to prevent accidental execution of QUIC code. I.e. `!= HTTP2` is no longer sufficient + * to distinguish HTTP1 and HTTP2 traffic. + */ + | 'HTTP3' + | 2 + +export type CodecClientType__Output = typeof CodecClientType[keyof typeof CodecClientType] diff --git a/packages/grpc-js-xds/src/generated/envoy/type/v3/FractionalPercent.ts b/packages/grpc-js-xds/src/generated/envoy/type/v3/FractionalPercent.ts index 564af9a0f..c45441a79 100644 --- a/packages/grpc-js-xds/src/generated/envoy/type/v3/FractionalPercent.ts +++ b/packages/grpc-js-xds/src/generated/envoy/type/v3/FractionalPercent.ts @@ -6,26 +6,57 @@ /** * Fraction percentages support several fixed denominator values. */ -export enum _envoy_type_v3_FractionalPercent_DenominatorType { +export const _envoy_type_v3_FractionalPercent_DenominatorType = { /** * 100. * * **Example**: 1/100 = 1%. */ - HUNDRED = 0, + HUNDRED: 'HUNDRED', /** * 10,000. * * **Example**: 1/10000 = 0.01%. */ - TEN_THOUSAND = 1, + TEN_THOUSAND: 'TEN_THOUSAND', /** * 1,000,000. * * **Example**: 1/1000000 = 0.0001%. */ - MILLION = 2, -} + MILLION: 'MILLION', +} as const; + +/** + * Fraction percentages support several fixed denominator values. + */ +export type _envoy_type_v3_FractionalPercent_DenominatorType = + /** + * 100. + * + * **Example**: 1/100 = 1%. + */ + | 'HUNDRED' + | 0 + /** + * 10,000. + * + * **Example**: 1/10000 = 0.01%. + */ + | 'TEN_THOUSAND' + | 1 + /** + * 1,000,000. + * + * **Example**: 1/1000000 = 0.0001%. + */ + | 'MILLION' + | 2 + +/** + * Fraction percentages support several fixed denominator values. + */ +export type _envoy_type_v3_FractionalPercent_DenominatorType__Output = typeof _envoy_type_v3_FractionalPercent_DenominatorType[keyof typeof _envoy_type_v3_FractionalPercent_DenominatorType] /** * A fractional percentage is used in cases in which for performance reasons performing floating @@ -44,7 +75,7 @@ export interface FractionalPercent { * Specifies the denominator. If the denominator specified is less than the numerator, the final * fractional percentage is capped at 1 (100%). */ - 'denominator'?: (_envoy_type_v3_FractionalPercent_DenominatorType | keyof typeof _envoy_type_v3_FractionalPercent_DenominatorType); + 'denominator'?: (_envoy_type_v3_FractionalPercent_DenominatorType); } /** @@ -64,5 +95,5 @@ export interface FractionalPercent__Output { * Specifies the denominator. If the denominator specified is less than the numerator, the final * fractional percentage is capped at 1 (100%). */ - 'denominator': (keyof typeof _envoy_type_v3_FractionalPercent_DenominatorType); + 'denominator': (_envoy_type_v3_FractionalPercent_DenominatorType__Output); } diff --git a/packages/grpc-js-xds/src/generated/google/protobuf/FieldDescriptorProto.ts b/packages/grpc-js-xds/src/generated/google/protobuf/FieldDescriptorProto.ts index c511e2eff..4951919fd 100644 --- a/packages/grpc-js-xds/src/generated/google/protobuf/FieldDescriptorProto.ts +++ b/packages/grpc-js-xds/src/generated/google/protobuf/FieldDescriptorProto.ts @@ -4,41 +4,91 @@ import type { FieldOptions as _google_protobuf_FieldOptions, FieldOptions__Outpu // Original file: null -export enum _google_protobuf_FieldDescriptorProto_Label { - LABEL_OPTIONAL = 1, - LABEL_REQUIRED = 2, - LABEL_REPEATED = 3, -} +export const _google_protobuf_FieldDescriptorProto_Label = { + LABEL_OPTIONAL: 'LABEL_OPTIONAL', + LABEL_REQUIRED: 'LABEL_REQUIRED', + LABEL_REPEATED: 'LABEL_REPEATED', +} as const; + +export type _google_protobuf_FieldDescriptorProto_Label = + | 'LABEL_OPTIONAL' + | 1 + | 'LABEL_REQUIRED' + | 2 + | 'LABEL_REPEATED' + | 3 + +export type _google_protobuf_FieldDescriptorProto_Label__Output = typeof _google_protobuf_FieldDescriptorProto_Label[keyof typeof _google_protobuf_FieldDescriptorProto_Label] // Original file: null -export enum _google_protobuf_FieldDescriptorProto_Type { - TYPE_DOUBLE = 1, - TYPE_FLOAT = 2, - TYPE_INT64 = 3, - TYPE_UINT64 = 4, - TYPE_INT32 = 5, - TYPE_FIXED64 = 6, - TYPE_FIXED32 = 7, - TYPE_BOOL = 8, - TYPE_STRING = 9, - TYPE_GROUP = 10, - TYPE_MESSAGE = 11, - TYPE_BYTES = 12, - TYPE_UINT32 = 13, - TYPE_ENUM = 14, - TYPE_SFIXED32 = 15, - TYPE_SFIXED64 = 16, - TYPE_SINT32 = 17, - TYPE_SINT64 = 18, -} +export const _google_protobuf_FieldDescriptorProto_Type = { + TYPE_DOUBLE: 'TYPE_DOUBLE', + TYPE_FLOAT: 'TYPE_FLOAT', + TYPE_INT64: 'TYPE_INT64', + TYPE_UINT64: 'TYPE_UINT64', + TYPE_INT32: 'TYPE_INT32', + TYPE_FIXED64: 'TYPE_FIXED64', + TYPE_FIXED32: 'TYPE_FIXED32', + TYPE_BOOL: 'TYPE_BOOL', + TYPE_STRING: 'TYPE_STRING', + TYPE_GROUP: 'TYPE_GROUP', + TYPE_MESSAGE: 'TYPE_MESSAGE', + TYPE_BYTES: 'TYPE_BYTES', + TYPE_UINT32: 'TYPE_UINT32', + TYPE_ENUM: 'TYPE_ENUM', + TYPE_SFIXED32: 'TYPE_SFIXED32', + TYPE_SFIXED64: 'TYPE_SFIXED64', + TYPE_SINT32: 'TYPE_SINT32', + TYPE_SINT64: 'TYPE_SINT64', +} as const; + +export type _google_protobuf_FieldDescriptorProto_Type = + | 'TYPE_DOUBLE' + | 1 + | 'TYPE_FLOAT' + | 2 + | 'TYPE_INT64' + | 3 + | 'TYPE_UINT64' + | 4 + | 'TYPE_INT32' + | 5 + | 'TYPE_FIXED64' + | 6 + | 'TYPE_FIXED32' + | 7 + | 'TYPE_BOOL' + | 8 + | 'TYPE_STRING' + | 9 + | 'TYPE_GROUP' + | 10 + | 'TYPE_MESSAGE' + | 11 + | 'TYPE_BYTES' + | 12 + | 'TYPE_UINT32' + | 13 + | 'TYPE_ENUM' + | 14 + | 'TYPE_SFIXED32' + | 15 + | 'TYPE_SFIXED64' + | 16 + | 'TYPE_SINT32' + | 17 + | 'TYPE_SINT64' + | 18 + +export type _google_protobuf_FieldDescriptorProto_Type__Output = typeof _google_protobuf_FieldDescriptorProto_Type[keyof typeof _google_protobuf_FieldDescriptorProto_Type] export interface FieldDescriptorProto { 'name'?: (string); 'extendee'?: (string); 'number'?: (number); - 'label'?: (_google_protobuf_FieldDescriptorProto_Label | keyof typeof _google_protobuf_FieldDescriptorProto_Label); - 'type'?: (_google_protobuf_FieldDescriptorProto_Type | keyof typeof _google_protobuf_FieldDescriptorProto_Type); + 'label'?: (_google_protobuf_FieldDescriptorProto_Label); + 'type'?: (_google_protobuf_FieldDescriptorProto_Type); 'typeName'?: (string); 'defaultValue'?: (string); 'options'?: (_google_protobuf_FieldOptions | null); @@ -50,8 +100,8 @@ export interface FieldDescriptorProto__Output { 'name': (string); 'extendee': (string); 'number': (number); - 'label': (keyof typeof _google_protobuf_FieldDescriptorProto_Label); - 'type': (keyof typeof _google_protobuf_FieldDescriptorProto_Type); + 'label': (_google_protobuf_FieldDescriptorProto_Label__Output); + 'type': (_google_protobuf_FieldDescriptorProto_Type__Output); 'typeName': (string); 'defaultValue': (string); 'options': (_google_protobuf_FieldOptions__Output | null); diff --git a/packages/grpc-js-xds/src/generated/google/protobuf/FieldOptions.ts b/packages/grpc-js-xds/src/generated/google/protobuf/FieldOptions.ts index 3c3b446c9..b301f2958 100644 --- a/packages/grpc-js-xds/src/generated/google/protobuf/FieldOptions.ts +++ b/packages/grpc-js-xds/src/generated/google/protobuf/FieldOptions.ts @@ -4,36 +4,56 @@ import type { UninterpretedOption as _google_protobuf_UninterpretedOption, Unint // Original file: null -export enum _google_protobuf_FieldOptions_CType { - STRING = 0, - CORD = 1, - STRING_PIECE = 2, -} +export const _google_protobuf_FieldOptions_CType = { + STRING: 'STRING', + CORD: 'CORD', + STRING_PIECE: 'STRING_PIECE', +} as const; + +export type _google_protobuf_FieldOptions_CType = + | 'STRING' + | 0 + | 'CORD' + | 1 + | 'STRING_PIECE' + | 2 + +export type _google_protobuf_FieldOptions_CType__Output = typeof _google_protobuf_FieldOptions_CType[keyof typeof _google_protobuf_FieldOptions_CType] // Original file: null -export enum _google_protobuf_FieldOptions_JSType { - JS_NORMAL = 0, - JS_STRING = 1, - JS_NUMBER = 2, -} +export const _google_protobuf_FieldOptions_JSType = { + JS_NORMAL: 'JS_NORMAL', + JS_STRING: 'JS_STRING', + JS_NUMBER: 'JS_NUMBER', +} as const; + +export type _google_protobuf_FieldOptions_JSType = + | 'JS_NORMAL' + | 0 + | 'JS_STRING' + | 1 + | 'JS_NUMBER' + | 2 + +export type _google_protobuf_FieldOptions_JSType__Output = typeof _google_protobuf_FieldOptions_JSType[keyof typeof _google_protobuf_FieldOptions_JSType] export interface FieldOptions { - 'ctype'?: (_google_protobuf_FieldOptions_CType | keyof typeof _google_protobuf_FieldOptions_CType); + 'ctype'?: (_google_protobuf_FieldOptions_CType); 'packed'?: (boolean); 'deprecated'?: (boolean); 'lazy'?: (boolean); - 'jstype'?: (_google_protobuf_FieldOptions_JSType | keyof typeof _google_protobuf_FieldOptions_JSType); + 'jstype'?: (_google_protobuf_FieldOptions_JSType); 'weak'?: (boolean); 'uninterpretedOption'?: (_google_protobuf_UninterpretedOption)[]; } export interface FieldOptions__Output { - 'ctype': (keyof typeof _google_protobuf_FieldOptions_CType); + 'ctype': (_google_protobuf_FieldOptions_CType__Output); 'packed': (boolean); 'deprecated': (boolean); 'lazy': (boolean); - 'jstype': (keyof typeof _google_protobuf_FieldOptions_JSType); + 'jstype': (_google_protobuf_FieldOptions_JSType__Output); 'weak': (boolean); 'uninterpretedOption': (_google_protobuf_UninterpretedOption__Output)[]; } diff --git a/packages/grpc-js-xds/src/generated/google/protobuf/FileOptions.ts b/packages/grpc-js-xds/src/generated/google/protobuf/FileOptions.ts index 84500fc30..6fab1a84b 100644 --- a/packages/grpc-js-xds/src/generated/google/protobuf/FileOptions.ts +++ b/packages/grpc-js-xds/src/generated/google/protobuf/FileOptions.ts @@ -5,21 +5,34 @@ import type { StatusAnnotation as _udpa_annotations_StatusAnnotation, StatusAnno // Original file: null -export enum _google_protobuf_FileOptions_OptimizeMode { - SPEED = 1, - CODE_SIZE = 2, - LITE_RUNTIME = 3, -} +export const _google_protobuf_FileOptions_OptimizeMode = { + SPEED: 'SPEED', + CODE_SIZE: 'CODE_SIZE', + LITE_RUNTIME: 'LITE_RUNTIME', +} as const; + +export type _google_protobuf_FileOptions_OptimizeMode = + | 'SPEED' + | 1 + | 'CODE_SIZE' + | 2 + | 'LITE_RUNTIME' + | 3 + +export type _google_protobuf_FileOptions_OptimizeMode__Output = typeof _google_protobuf_FileOptions_OptimizeMode[keyof typeof _google_protobuf_FileOptions_OptimizeMode] export interface FileOptions { 'javaPackage'?: (string); 'javaOuterClassname'?: (string); - 'optimizeFor'?: (_google_protobuf_FileOptions_OptimizeMode | keyof typeof _google_protobuf_FileOptions_OptimizeMode); + 'optimizeFor'?: (_google_protobuf_FileOptions_OptimizeMode); 'javaMultipleFiles'?: (boolean); 'goPackage'?: (string); 'ccGenericServices'?: (boolean); 'javaGenericServices'?: (boolean); 'pyGenericServices'?: (boolean); + /** + * @deprecated + */ 'javaGenerateEqualsAndHash'?: (boolean); 'deprecated'?: (boolean); 'javaStringCheckUtf8'?: (boolean); @@ -33,12 +46,15 @@ export interface FileOptions { export interface FileOptions__Output { 'javaPackage': (string); 'javaOuterClassname': (string); - 'optimizeFor': (keyof typeof _google_protobuf_FileOptions_OptimizeMode); + 'optimizeFor': (_google_protobuf_FileOptions_OptimizeMode__Output); 'javaMultipleFiles': (boolean); 'goPackage': (string); 'ccGenericServices': (boolean); 'javaGenericServices': (boolean); 'pyGenericServices': (boolean); + /** + * @deprecated + */ 'javaGenerateEqualsAndHash': (boolean); 'deprecated': (boolean); 'javaStringCheckUtf8': (boolean); diff --git a/packages/grpc-js-xds/src/generated/google/protobuf/NullValue.ts b/packages/grpc-js-xds/src/generated/google/protobuf/NullValue.ts index 377aab885..c66dacc7b 100644 --- a/packages/grpc-js-xds/src/generated/google/protobuf/NullValue.ts +++ b/packages/grpc-js-xds/src/generated/google/protobuf/NullValue.ts @@ -1,5 +1,11 @@ // Original file: null -export enum NullValue { - NULL_VALUE = 0, -} +export const NullValue = { + NULL_VALUE: 'NULL_VALUE', +} as const; + +export type NullValue = + | 'NULL_VALUE' + | 0 + +export type NullValue__Output = typeof NullValue[keyof typeof NullValue] diff --git a/packages/grpc-js-xds/src/generated/google/protobuf/Value.ts b/packages/grpc-js-xds/src/generated/google/protobuf/Value.ts index b1a942a56..67cc03fff 100644 --- a/packages/grpc-js-xds/src/generated/google/protobuf/Value.ts +++ b/packages/grpc-js-xds/src/generated/google/protobuf/Value.ts @@ -1,11 +1,11 @@ // Original file: null -import type { NullValue as _google_protobuf_NullValue } from '../../google/protobuf/NullValue'; +import type { NullValue as _google_protobuf_NullValue, NullValue__Output as _google_protobuf_NullValue__Output } from '../../google/protobuf/NullValue'; import type { Struct as _google_protobuf_Struct, Struct__Output as _google_protobuf_Struct__Output } from '../../google/protobuf/Struct'; import type { ListValue as _google_protobuf_ListValue, ListValue__Output as _google_protobuf_ListValue__Output } from '../../google/protobuf/ListValue'; export interface Value { - 'nullValue'?: (_google_protobuf_NullValue | keyof typeof _google_protobuf_NullValue); + 'nullValue'?: (_google_protobuf_NullValue); 'numberValue'?: (number | string); 'stringValue'?: (string); 'boolValue'?: (boolean); @@ -15,7 +15,7 @@ export interface Value { } export interface Value__Output { - 'nullValue'?: (keyof typeof _google_protobuf_NullValue); + 'nullValue'?: (_google_protobuf_NullValue__Output); 'numberValue'?: (number); 'stringValue'?: (string); 'boolValue'?: (boolean); diff --git a/packages/grpc-js-xds/src/generated/udpa/annotations/PackageVersionStatus.ts b/packages/grpc-js-xds/src/generated/udpa/annotations/PackageVersionStatus.ts index d0e181aa5..4d15df739 100644 --- a/packages/grpc-js-xds/src/generated/udpa/annotations/PackageVersionStatus.ts +++ b/packages/grpc-js-xds/src/generated/udpa/annotations/PackageVersionStatus.ts @@ -1,21 +1,46 @@ // Original file: deps/xds/udpa/annotations/status.proto -export enum PackageVersionStatus { +export const PackageVersionStatus = { /** * Unknown package version status. */ - UNKNOWN = 0, + UNKNOWN: 'UNKNOWN', /** * This version of the package is frozen. */ - FROZEN = 1, + FROZEN: 'FROZEN', /** * This version of the package is the active development version. */ - ACTIVE = 2, + ACTIVE: 'ACTIVE', /** * This version of the package is the candidate for the next major version. It * is typically machine generated from the active development version. */ - NEXT_MAJOR_VERSION_CANDIDATE = 3, -} + NEXT_MAJOR_VERSION_CANDIDATE: 'NEXT_MAJOR_VERSION_CANDIDATE', +} as const; + +export type PackageVersionStatus = + /** + * Unknown package version status. + */ + | 'UNKNOWN' + | 0 + /** + * This version of the package is frozen. + */ + | 'FROZEN' + | 1 + /** + * This version of the package is the active development version. + */ + | 'ACTIVE' + | 2 + /** + * This version of the package is the candidate for the next major version. It + * is typically machine generated from the active development version. + */ + | 'NEXT_MAJOR_VERSION_CANDIDATE' + | 3 + +export type PackageVersionStatus__Output = typeof PackageVersionStatus[keyof typeof PackageVersionStatus] diff --git a/packages/grpc-js-xds/src/generated/udpa/annotations/StatusAnnotation.ts b/packages/grpc-js-xds/src/generated/udpa/annotations/StatusAnnotation.ts index f01b45063..f129c3c94 100644 --- a/packages/grpc-js-xds/src/generated/udpa/annotations/StatusAnnotation.ts +++ b/packages/grpc-js-xds/src/generated/udpa/annotations/StatusAnnotation.ts @@ -1,6 +1,6 @@ // Original file: deps/xds/udpa/annotations/status.proto -import type { PackageVersionStatus as _udpa_annotations_PackageVersionStatus } from '../../udpa/annotations/PackageVersionStatus'; +import type { PackageVersionStatus as _udpa_annotations_PackageVersionStatus, PackageVersionStatus__Output as _udpa_annotations_PackageVersionStatus__Output } from '../../udpa/annotations/PackageVersionStatus'; export interface StatusAnnotation { /** @@ -10,7 +10,7 @@ export interface StatusAnnotation { /** * The entity belongs to a package with the given version status. */ - 'package_version_status'?: (_udpa_annotations_PackageVersionStatus | keyof typeof _udpa_annotations_PackageVersionStatus); + 'package_version_status'?: (_udpa_annotations_PackageVersionStatus); } export interface StatusAnnotation__Output { @@ -21,5 +21,5 @@ export interface StatusAnnotation__Output { /** * The entity belongs to a package with the given version status. */ - 'package_version_status': (keyof typeof _udpa_annotations_PackageVersionStatus); + 'package_version_status': (_udpa_annotations_PackageVersionStatus__Output); } diff --git a/packages/grpc-js-xds/src/generated/validate/FieldRules.ts b/packages/grpc-js-xds/src/generated/validate/FieldRules.ts index 067125775..ce6f313e7 100644 --- a/packages/grpc-js-xds/src/generated/validate/FieldRules.ts +++ b/packages/grpc-js-xds/src/generated/validate/FieldRules.ts @@ -22,7 +22,6 @@ import type { MapRules as _validate_MapRules, MapRules__Output as _validate_MapR import type { AnyRules as _validate_AnyRules, AnyRules__Output as _validate_AnyRules__Output } from '../validate/AnyRules'; import type { DurationRules as _validate_DurationRules, DurationRules__Output as _validate_DurationRules__Output } from '../validate/DurationRules'; import type { TimestampRules as _validate_TimestampRules, TimestampRules__Output as _validate_TimestampRules__Output } from '../validate/TimestampRules'; -import type { Long } from '@grpc/proto-loader'; /** * FieldRules encapsulates the rules for each type of field. Depending on the diff --git a/packages/grpc-js-xds/src/generated/validate/KnownRegex.ts b/packages/grpc-js-xds/src/generated/validate/KnownRegex.ts index 5880b5baf..8f1e20b4c 100644 --- a/packages/grpc-js-xds/src/generated/validate/KnownRegex.ts +++ b/packages/grpc-js-xds/src/generated/validate/KnownRegex.ts @@ -3,14 +3,36 @@ /** * WellKnownRegex contain some well-known patterns. */ -export enum KnownRegex { - UNKNOWN = 0, +export const KnownRegex = { + UNKNOWN: 'UNKNOWN', /** * HTTP header name as defined by RFC 7230. */ - HTTP_HEADER_NAME = 1, + HTTP_HEADER_NAME: 'HTTP_HEADER_NAME', /** * HTTP header value as defined by RFC 7230. */ - HTTP_HEADER_VALUE = 2, -} + HTTP_HEADER_VALUE: 'HTTP_HEADER_VALUE', +} as const; + +/** + * WellKnownRegex contain some well-known patterns. + */ +export type KnownRegex = + | 'UNKNOWN' + | 0 + /** + * HTTP header name as defined by RFC 7230. + */ + | 'HTTP_HEADER_NAME' + | 1 + /** + * HTTP header value as defined by RFC 7230. + */ + | 'HTTP_HEADER_VALUE' + | 2 + +/** + * WellKnownRegex contain some well-known patterns. + */ +export type KnownRegex__Output = typeof KnownRegex[keyof typeof KnownRegex] diff --git a/packages/grpc-js-xds/src/generated/validate/StringRules.ts b/packages/grpc-js-xds/src/generated/validate/StringRules.ts index b6bb1e460..8bca6dffa 100644 --- a/packages/grpc-js-xds/src/generated/validate/StringRules.ts +++ b/packages/grpc-js-xds/src/generated/validate/StringRules.ts @@ -1,6 +1,6 @@ // Original file: deps/protoc-gen-validate/validate/validate.proto -import type { KnownRegex as _validate_KnownRegex } from '../validate/KnownRegex'; +import type { KnownRegex as _validate_KnownRegex, KnownRegex__Output as _validate_KnownRegex__Output } from '../validate/KnownRegex'; import type { Long } from '@grpc/proto-loader'; /** @@ -129,7 +129,7 @@ export interface StringRules { /** * WellKnownRegex specifies a common well known pattern defined as a regex. */ - 'well_known_regex'?: (_validate_KnownRegex | keyof typeof _validate_KnownRegex); + 'well_known_regex'?: (_validate_KnownRegex); /** * This applies to regexes HTTP_HEADER_NAME and HTTP_HEADER_VALUE to enable * strict header validation. @@ -271,7 +271,7 @@ export interface StringRules__Output { /** * WellKnownRegex specifies a common well known pattern defined as a regex. */ - 'well_known_regex'?: (keyof typeof _validate_KnownRegex); + 'well_known_regex'?: (_validate_KnownRegex__Output); /** * This applies to regexes HTTP_HEADER_NAME and HTTP_HEADER_VALUE to enable * strict header validation. diff --git a/packages/grpc-js-xds/src/generated/xds/annotations/v3/PackageVersionStatus.ts b/packages/grpc-js-xds/src/generated/xds/annotations/v3/PackageVersionStatus.ts index e76e2848f..e85074eae 100644 --- a/packages/grpc-js-xds/src/generated/xds/annotations/v3/PackageVersionStatus.ts +++ b/packages/grpc-js-xds/src/generated/xds/annotations/v3/PackageVersionStatus.ts @@ -1,21 +1,46 @@ // Original file: deps/xds/xds/annotations/v3/status.proto -export enum PackageVersionStatus { +export const PackageVersionStatus = { /** * Unknown package version status. */ - UNKNOWN = 0, + UNKNOWN: 'UNKNOWN', /** * This version of the package is frozen. */ - FROZEN = 1, + FROZEN: 'FROZEN', /** * This version of the package is the active development version. */ - ACTIVE = 2, + ACTIVE: 'ACTIVE', /** * This version of the package is the candidate for the next major version. It * is typically machine generated from the active development version. */ - NEXT_MAJOR_VERSION_CANDIDATE = 3, -} + NEXT_MAJOR_VERSION_CANDIDATE: 'NEXT_MAJOR_VERSION_CANDIDATE', +} as const; + +export type PackageVersionStatus = + /** + * Unknown package version status. + */ + | 'UNKNOWN' + | 0 + /** + * This version of the package is frozen. + */ + | 'FROZEN' + | 1 + /** + * This version of the package is the active development version. + */ + | 'ACTIVE' + | 2 + /** + * This version of the package is the candidate for the next major version. It + * is typically machine generated from the active development version. + */ + | 'NEXT_MAJOR_VERSION_CANDIDATE' + | 3 + +export type PackageVersionStatus__Output = typeof PackageVersionStatus[keyof typeof PackageVersionStatus] diff --git a/packages/grpc-js-xds/src/generated/xds/annotations/v3/StatusAnnotation.ts b/packages/grpc-js-xds/src/generated/xds/annotations/v3/StatusAnnotation.ts index 58efbd8f7..678d6a6bf 100644 --- a/packages/grpc-js-xds/src/generated/xds/annotations/v3/StatusAnnotation.ts +++ b/packages/grpc-js-xds/src/generated/xds/annotations/v3/StatusAnnotation.ts @@ -1,6 +1,6 @@ // Original file: deps/xds/xds/annotations/v3/status.proto -import type { PackageVersionStatus as _xds_annotations_v3_PackageVersionStatus } from '../../../xds/annotations/v3/PackageVersionStatus'; +import type { PackageVersionStatus as _xds_annotations_v3_PackageVersionStatus, PackageVersionStatus__Output as _xds_annotations_v3_PackageVersionStatus__Output } from '../../../xds/annotations/v3/PackageVersionStatus'; export interface StatusAnnotation { /** @@ -10,7 +10,7 @@ export interface StatusAnnotation { /** * The entity belongs to a package with the given version status. */ - 'package_version_status'?: (_xds_annotations_v3_PackageVersionStatus | keyof typeof _xds_annotations_v3_PackageVersionStatus); + 'package_version_status'?: (_xds_annotations_v3_PackageVersionStatus); } export interface StatusAnnotation__Output { @@ -21,5 +21,5 @@ export interface StatusAnnotation__Output { /** * The entity belongs to a package with the given version status. */ - 'package_version_status': (keyof typeof _xds_annotations_v3_PackageVersionStatus); + 'package_version_status': (_xds_annotations_v3_PackageVersionStatus__Output); } diff --git a/packages/grpc-js-xds/src/generated/xds/core/v3/ResourceLocator.ts b/packages/grpc-js-xds/src/generated/xds/core/v3/ResourceLocator.ts index bb1f822b8..28f981dd5 100644 --- a/packages/grpc-js-xds/src/generated/xds/core/v3/ResourceLocator.ts +++ b/packages/grpc-js-xds/src/generated/xds/core/v3/ResourceLocator.ts @@ -97,11 +97,21 @@ export interface _xds_core_v3_ResourceLocator_Directive__Output { // Original file: deps/xds/xds/core/v3/resource_locator.proto -export enum _xds_core_v3_ResourceLocator_Scheme { - XDSTP = 0, - HTTP = 1, - FILE = 2, -} +export const _xds_core_v3_ResourceLocator_Scheme = { + XDSTP: 'XDSTP', + HTTP: 'HTTP', + FILE: 'FILE', +} as const; + +export type _xds_core_v3_ResourceLocator_Scheme = + | 'XDSTP' + | 0 + | 'HTTP' + | 1 + | 'FILE' + | 2 + +export type _xds_core_v3_ResourceLocator_Scheme__Output = typeof _xds_core_v3_ResourceLocator_Scheme[keyof typeof _xds_core_v3_ResourceLocator_Scheme] /** * xDS resource locators identify a xDS resource name and instruct the @@ -125,7 +135,7 @@ export interface ResourceLocator { /** * URI scheme. */ - 'scheme'?: (_xds_core_v3_ResourceLocator_Scheme | keyof typeof _xds_core_v3_ResourceLocator_Scheme); + 'scheme'?: (_xds_core_v3_ResourceLocator_Scheme); /** * Opaque identifier for the resource. Any '/' will not be escaped during URI * encoding and will form part of the URI path. This may end @@ -183,7 +193,7 @@ export interface ResourceLocator__Output { /** * URI scheme. */ - 'scheme': (keyof typeof _xds_core_v3_ResourceLocator_Scheme); + 'scheme': (_xds_core_v3_ResourceLocator_Scheme__Output); /** * Opaque identifier for the resource. Any '/' will not be escaped during URI * encoding and will form part of the URI path. This may end diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 3b8ccf2c1..41594561d 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -66,7 +66,7 @@ "generate-test-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --include-dirs test/fixtures/ -O test/generated/ --grpcLib ../../src/index test_service.proto" }, "dependencies": { - "@grpc/proto-loader": "^0.7.10", + "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" }, "files": [ From d5edf49f6c59fd8f6e9ead2836bc30af8284284e Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 3 May 2024 14:24:44 -0700 Subject: [PATCH 105/109] Merge pull request #2735 from murgatroid99/grpc-js_linkify-it_fix root: Update dependency on jsdoc to avoid linkify-it compilation error --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index a1fd3d59e..a5733f377 100644 --- a/package.json +++ b/package.json @@ -20,14 +20,13 @@ "del": "^3.0.0", "execa": "^0.8.0", "gulp": "^4.0.1", - "gulp-jsdoc3": "^1.0.1", "gulp-jshint": "^2.0.4", "gulp-mocha": "^4.3.1", "gulp-sourcemaps": "^2.6.1", "gulp-tslint": "^8.1.1", "gulp-typescript": "^3.2.2", "gulp-util": "^3.0.8", - "jsdoc": "^3.3.2", + "jsdoc": "^4.0.3", "jshint": "^2.9.5", "make-dir": "^1.1.0", "merge2": "^1.1.0", From fec135a9800ce884b8dd414782f4bd0014821a0c Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 6 May 2024 15:20:11 -0700 Subject: [PATCH 106/109] Merge pull request #2729 from sergiitk/psm-interop-common-prod-tests PSM Interop: simplify Kokoro buildscripts --- .../scripts/psm-interop-build-node.sh | 38 ++++ .../scripts/psm-interop-test-node.sh | 40 ++++ packages/grpc-js-xds/scripts/xds_k8s_lb.sh | 186 ------------------ .../grpc-js-xds/scripts/xds_k8s_url_map.sh | 163 --------------- test/kokoro/xds-interop.cfg | 24 --- test/kokoro/xds_k8s_lb.cfg | 6 +- test/kokoro/xds_k8s_url_map.cfg | 6 +- 7 files changed, 88 insertions(+), 375 deletions(-) create mode 100755 packages/grpc-js-xds/scripts/psm-interop-build-node.sh create mode 100755 packages/grpc-js-xds/scripts/psm-interop-test-node.sh delete mode 100755 packages/grpc-js-xds/scripts/xds_k8s_lb.sh delete mode 100644 packages/grpc-js-xds/scripts/xds_k8s_url_map.sh delete mode 100644 test/kokoro/xds-interop.cfg diff --git a/packages/grpc-js-xds/scripts/psm-interop-build-node.sh b/packages/grpc-js-xds/scripts/psm-interop-build-node.sh new file mode 100755 index 000000000..d52206f0e --- /dev/null +++ b/packages/grpc-js-xds/scripts/psm-interop-build-node.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Copyright 2024 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. +set -eo pipefail + +####################################### +# Builds test app Docker images and pushes them to GCR. +# Called from psm_interop_kokoro_lib.sh. +# +# Globals: +# SRC_DIR: Absolute path to the source repo on Kokoro VM +# SERVER_IMAGE_NAME: Test server Docker image name +# CLIENT_IMAGE_NAME: Test client Docker image name +# GIT_COMMIT: SHA-1 of git commit being built +# DOCKER_REGISTRY: Docker registry to push to +# Outputs: +# Writes the output of docker image build stdout, stderr +####################################### +psm::lang::build_docker_images() { + local client_dockerfile="packages/grpc-js-xds/interop/Dockerfile" + + cd "${SRC_DIR}" + psm::tools::run_verbose git submodule update --init --recursive + psm::tools::run_verbose git submodule status + + psm::build::docker_images_generic "${client_dockerfile}" +} diff --git a/packages/grpc-js-xds/scripts/psm-interop-test-node.sh b/packages/grpc-js-xds/scripts/psm-interop-test-node.sh new file mode 100755 index 000000000..169cf06f2 --- /dev/null +++ b/packages/grpc-js-xds/scripts/psm-interop-test-node.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Copyright 2024 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. +set -eo pipefail + +# Input parameters to psm:: methods of the install script. +readonly GRPC_LANGUAGE="node" +readonly BUILD_SCRIPT_DIR="$(dirname "$0")" + +# Used locally. +readonly TEST_DRIVER_INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/${TEST_DRIVER_REPO_OWNER:-grpc}/psm-interop/${TEST_DRIVER_BRANCH:-main}/.kokoro/psm_interop_kokoro_lib.sh" + +psm::lang::source_install_lib() { + echo "Sourcing test driver install script from: ${TEST_DRIVER_INSTALL_SCRIPT_URL}" + local install_lib + # Download to a tmp file. + install_lib="$(mktemp -d)/psm_interop_kokoro_lib.sh" + curl -s --retry-connrefused --retry 5 -o "${install_lib}" "${TEST_DRIVER_INSTALL_SCRIPT_URL}" + # Checksum. + if command -v sha256sum &> /dev/null; then + echo "Install script checksum:" + sha256sum "${install_lib}" + fi + source "${install_lib}" +} + +psm::lang::source_install_lib +source "${BUILD_SCRIPT_DIR}/psm-interop-build-${GRPC_LANGUAGE}.sh" +psm::run "${PSM_TEST_SUITE}" diff --git a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh b/packages/grpc-js-xds/scripts/xds_k8s_lb.sh deleted file mode 100755 index c900a4ea5..000000000 --- a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh +++ /dev/null @@ -1,186 +0,0 @@ -#!/usr/bin/env bash -# Copyright 2022 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. - -set -eo pipefail - -# Constants -readonly GITHUB_REPOSITORY_NAME="grpc-node" -readonly TEST_DRIVER_INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/${TEST_DRIVER_REPO_OWNER:-grpc}/psm-interop/${TEST_DRIVER_BRANCH:-main}/.kokoro/psm_interop_kokoro_lib.sh" -## xDS test client Docker images -readonly DOCKER_REGISTRY="us-docker.pkg.dev" -readonly SERVER_IMAGE_NAME="us-docker.pkg.dev/grpc-testing/psm-interop/java-server:canonical" -readonly CLIENT_IMAGE_NAME="us-docker.pkg.dev/grpc-testing/psm-interop/node-client" -readonly FORCE_IMAGE_BUILD="${FORCE_IMAGE_BUILD:-0}" -readonly BUILD_APP_PATH="packages/grpc-js-xds/interop/Dockerfile" -readonly LANGUAGE_NAME="Node" - -####################################### -# Builds test app Docker images and pushes them to GCR -# Globals: -# BUILD_APP_PATH -# CLIENT_IMAGE_NAME: Test client Docker image name -# GIT_COMMIT: SHA-1 of git commit being built -# TESTING_VERSION: version branch under test, f.e. v1.42.x, master -# Arguments: -# None -# Outputs: -# Writes the output of `gcloud builds submit` to stdout, stderr -####################################### -build_test_app_docker_images() { - echo "Building ${LANGUAGE_NAME} xDS interop test app Docker images" - - pushd "${SRC_DIR}" - docker build \ - -f "${BUILD_APP_PATH}" \ - -t "${CLIENT_IMAGE_NAME}:${GIT_COMMIT}" \ - . - - gcloud -q auth configure-docker "${DOCKER_REGISTRY}" - docker push "${CLIENT_IMAGE_NAME}:${GIT_COMMIT}" - if is_version_branch "${TESTING_VERSION}"; then - tag_and_push_docker_image "${CLIENT_IMAGE_NAME}" "${GIT_COMMIT}" "${TESTING_VERSION}" - fi - popd -} - -####################################### -# Builds test app and its docker images unless they already exist -# Globals: -# CLIENT_IMAGE_NAME: Test client Docker image name -# GIT_COMMIT: SHA-1 of git commit being built -# FORCE_IMAGE_BUILD -# Arguments: -# None -# Outputs: -# Writes the output to stdout, stderr -####################################### -build_docker_images_if_needed() { - # Check if images already exist - client_tags="$(gcloud_gcr_list_image_tags "${CLIENT_IMAGE_NAME}" "${GIT_COMMIT}")" - printf "Client image: %s:%s\n" "${CLIENT_IMAGE_NAME}" "${GIT_COMMIT}" - echo "${client_tags:-Client image not found}" - - # Build if any of the images are missing, or FORCE_IMAGE_BUILD=1 - if [[ "${FORCE_IMAGE_BUILD}" == "1" || -z "${client_tags}" ]]; then - build_test_app_docker_images - else - echo "Skipping ${LANGUAGE_NAME} test app build" - fi -} - -####################################### -# Executes the test case -# Globals: -# TEST_DRIVER_FLAGFILE: Relative path to test driver flagfile -# KUBE_CONTEXT: The name of kubectl context with GKE cluster access -# SECONDARY_KUBE_CONTEXT: The name of kubectl context with secondary GKE cluster access, if any -# TEST_XML_OUTPUT_DIR: Output directory for the test xUnit XML report -# CLIENT_IMAGE_NAME: Test client Docker image name -# GIT_COMMIT: SHA-1 of git commit being built -# Arguments: -# Test case name -# Outputs: -# Writes the output of test execution to stdout, stderr -# Test xUnit report to ${TEST_XML_OUTPUT_DIR}/${test_name}/sponge_log.xml -####################################### -run_test() { - # Test driver usage: - # https://github.com/grpc/grpc/tree/master/tools/run_tests/xds_k8s_test_driver#basic-usage - local test_name="${1:?Usage: run_test test_name}" - local out_dir="${TEST_XML_OUTPUT_DIR}/${test_name}" - mkdir -pv "${out_dir}" - # testing_version is used by the framework to determine the supported PSM - # features. It's captured from Kokoro job name of the Node repo, which takes - # the form: - # grpc/node// - python3 -m "tests.${test_name}" \ - --flagfile="${TEST_DRIVER_FLAGFILE}" \ - --kube_context="${KUBE_CONTEXT}" \ - --secondary_kube_context="${SECONDARY_KUBE_CONTEXT}" \ - --client_image="${CLIENT_IMAGE_NAME}:${GIT_COMMIT}" \ - --server_image="${SERVER_IMAGE_NAME}" \ - --testing_version="${TESTING_VERSION}" \ - --force_cleanup \ - --collect_app_logs \ - --log_dir="${out_dir}" \ - --xml_output_file="${out_dir}/sponge_log.xml" \ - |& tee "${out_dir}/sponge_log.log" -} - -####################################### -# Main function: provision software necessary to execute tests, and run them -# Globals: -# KOKORO_ARTIFACTS_DIR -# GITHUB_REPOSITORY_NAME -# SRC_DIR: Populated with absolute path to the source repo -# TEST_DRIVER_REPO_DIR: Populated with the path to the repo containing -# the test driver -# TEST_DRIVER_FULL_DIR: Populated with the path to the test driver source code -# TEST_DRIVER_FLAGFILE: Populated with relative path to test driver flagfile -# TEST_XML_OUTPUT_DIR: Populated with the path to test xUnit XML report -# GIT_ORIGIN_URL: Populated with the origin URL of git repo used for the build -# GIT_COMMIT: Populated with the SHA-1 of git commit being built -# GIT_COMMIT_SHORT: Populated with the short SHA-1 of git commit being built -# KUBE_CONTEXT: Populated with name of kubectl context with GKE cluster access -# SECONDARY_KUBE_CONTEXT: Populated with name of kubectl context with secondary GKE cluster access, if any -# Arguments: -# None -# Outputs: -# Writes the output of test execution to stdout, stderr -####################################### -main() { - local script_dir - script_dir="$(dirname "$0")" - - cd "${script_dir}" - - git submodule update --init --recursive - - # Source the test driver from the master branch. - echo "Sourcing test driver install script from: ${TEST_DRIVER_INSTALL_SCRIPT_URL}" - source /dev/stdin <<< "$(curl -s "${TEST_DRIVER_INSTALL_SCRIPT_URL}")" - - activate_gke_cluster GKE_CLUSTER_PSM_LB - activate_secondary_gke_cluster GKE_CLUSTER_PSM_LB - - set -x - if [[ -n "${KOKORO_ARTIFACTS_DIR}" ]]; then - kokoro_setup_test_driver "${GITHUB_REPOSITORY_NAME}" - else - local_setup_test_driver "${script_dir}" - fi - build_docker_images_if_needed - - # Run tests - cd "${TEST_DRIVER_FULL_DIR}" - local failed_tests=0 - test_suites=( - "affinity_test" - "api_listener_test" - "baseline_test" - "change_backend_service_test" - "custom_lb_test" - "failover_test" - "outlier_detection_test" - "remove_neg_test" - "round_robin_test" - ) - for test in "${test_suites[@]}"; do - run_test $test || (( ++failed_tests )) - done - echo "Failed test suites: ${failed_tests}" -} - -main "$@" diff --git a/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh b/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh deleted file mode 100644 index d6e2c7ed4..000000000 --- a/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env bash -# Copyright 2022 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. - -set -eo pipefail - -# Constants -readonly GITHUB_REPOSITORY_NAME="grpc-node" -readonly TEST_DRIVER_INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/${TEST_DRIVER_REPO_OWNER:-grpc}/psm-interop/${TEST_DRIVER_BRANCH:-main}/.kokoro/psm_interop_kokoro_lib.sh" -## xDS test client Docker images -readonly DOCKER_REGISTRY="us-docker.pkg.dev" -readonly CLIENT_IMAGE_NAME="us-docker.pkg.dev/grpc-testing/psm-interop/node-client" -readonly FORCE_IMAGE_BUILD="${FORCE_IMAGE_BUILD:-0}" -readonly BUILD_APP_PATH="packages/grpc-js-xds/interop/Dockerfile" -readonly LANGUAGE_NAME="Node" - -####################################### -# Builds test app Docker images and pushes them to GCR -# Globals: -# BUILD_APP_PATH -# CLIENT_IMAGE_NAME: Test client Docker image name -# GIT_COMMIT: SHA-1 of git commit being built -# TESTING_VERSION: version branch under test, f.e. v1.42.x, master -# Arguments: -# None -# Outputs: -# Writes the output of `gcloud builds submit` to stdout, stderr -####################################### -build_test_app_docker_images() { - echo "Building ${LANGUAGE_NAME} xDS interop test app Docker images" - - pushd "${SRC_DIR}" - docker build \ - -f "${BUILD_APP_PATH}" \ - -t "${CLIENT_IMAGE_NAME}:${GIT_COMMIT}" \ - . - - gcloud -q auth configure-docker "${DOCKER_REGISTRY}" - docker push "${CLIENT_IMAGE_NAME}:${GIT_COMMIT}" - if is_version_branch "${TESTING_VERSION}"; then - tag_and_push_docker_image "${CLIENT_IMAGE_NAME}" "${GIT_COMMIT}" "${TESTING_VERSION}" - fi - popd -} - -####################################### -# Builds test app and its docker images unless they already exist -# Globals: -# CLIENT_IMAGE_NAME: Test client Docker image name -# GIT_COMMIT: SHA-1 of git commit being built -# FORCE_IMAGE_BUILD -# Arguments: -# None -# Outputs: -# Writes the output to stdout, stderr -####################################### -build_docker_images_if_needed() { - # Check if images already exist - client_tags="$(gcloud_gcr_list_image_tags "${CLIENT_IMAGE_NAME}" "${GIT_COMMIT}")" - printf "Client image: %s:%s\n" "${CLIENT_IMAGE_NAME}" "${GIT_COMMIT}" - echo "${client_tags:-Client image not found}" - - # Build if any of the images are missing, or FORCE_IMAGE_BUILD=1 - if [[ "${FORCE_IMAGE_BUILD}" == "1" || -z "${client_tags}" ]]; then - build_test_app_docker_images - else - echo "Skipping ${LANGUAGE_NAME} test app build" - fi -} - -####################################### -# Executes the test case -# Globals: -# TEST_DRIVER_FLAGFILE: Relative path to test driver flagfile -# KUBE_CONTEXT: The name of kubectl context with GKE cluster access -# TEST_XML_OUTPUT_DIR: Output directory for the test xUnit XML report -# CLIENT_IMAGE_NAME: Test client Docker image name -# GIT_COMMIT: SHA-1 of git commit being built -# TESTING_VERSION: version branch under test: used by the framework to determine the supported PSM -# features. -# Arguments: -# Test case name -# Outputs: -# Writes the output of test execution to stdout, stderr -# Test xUnit report to ${TEST_XML_OUTPUT_DIR}/${test_name}/sponge_log.xml -####################################### -run_test() { - # Test driver usage: - # https://github.com/grpc/grpc/tree/master/tools/run_tests/xds_k8s_test_driver#basic-usage - local test_name="${1:?Usage: run_test test_name}" - local out_dir="${TEST_XML_OUTPUT_DIR}/${test_name}" - mkdir -pv "${out_dir}" - set -x - python3 -m "tests.${test_name}" \ - --flagfile="${TEST_DRIVER_FLAGFILE}" \ - --flagfile="config/url-map.cfg" \ - --kube_context="${KUBE_CONTEXT}" \ - --client_image="${CLIENT_IMAGE_NAME}:${GIT_COMMIT}" \ - --testing_version="${TESTING_VERSION}" \ - --collect_app_logs \ - --log_dir="${out_dir}" \ - --xml_output_file="${out_dir}/sponge_log.xml" \ - |& tee "${out_dir}/sponge_log.log" -} - -####################################### -# Main function: provision software necessary to execute tests, and run them -# Globals: -# KOKORO_ARTIFACTS_DIR -# GITHUB_REPOSITORY_NAME -# SRC_DIR: Populated with absolute path to the source repo -# TEST_DRIVER_REPO_DIR: Populated with the path to the repo containing -# the test driver -# TEST_DRIVER_FULL_DIR: Populated with the path to the test driver source code -# TEST_DRIVER_FLAGFILE: Populated with relative path to test driver flagfile -# TEST_XML_OUTPUT_DIR: Populated with the path to test xUnit XML report -# GIT_ORIGIN_URL: Populated with the origin URL of git repo used for the build -# GIT_COMMIT: Populated with the SHA-1 of git commit being built -# GIT_COMMIT_SHORT: Populated with the short SHA-1 of git commit being built -# KUBE_CONTEXT: Populated with name of kubectl context with GKE cluster access -# Arguments: -# None -# Outputs: -# Writes the output of test execution to stdout, stderr -####################################### -main() { - local script_dir - script_dir="$(dirname "$0")" - - cd "${script_dir}" - - git submodule update --init --recursive - - # Source the test driver from the master branch. - echo "Sourcing test driver install script from: ${TEST_DRIVER_INSTALL_SCRIPT_URL}" - source /dev/stdin <<< "$(curl -s "${TEST_DRIVER_INSTALL_SCRIPT_URL}")" - - activate_gke_cluster GKE_CLUSTER_PSM_BASIC - - set -x - if [[ -n "${KOKORO_ARTIFACTS_DIR}" ]]; then - kokoro_setup_test_driver "${GITHUB_REPOSITORY_NAME}" - else - local_setup_test_driver "${script_dir}" - fi - build_docker_images_if_needed - # Run tests - cd "${TEST_DRIVER_FULL_DIR}" - run_test url_map || echo "Failed url_map test" -} - -main "$@" diff --git a/test/kokoro/xds-interop.cfg b/test/kokoro/xds-interop.cfg deleted file mode 100644 index 866cb4b58..000000000 --- a/test/kokoro/xds-interop.cfg +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2017 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. - -# Config file for Kokoro (in protobuf text format) - -# Location of the continuous shell script in repository. -build_file: "grpc-node/packages/grpc-js-xds/scripts/xds.sh" -timeout_mins: 360 -action { - define_artifacts { - regex: "github/grpc/reports/**" - } -} diff --git a/test/kokoro/xds_k8s_lb.cfg b/test/kokoro/xds_k8s_lb.cfg index 09aa3d17d..3efb62f29 100644 --- a/test/kokoro/xds_k8s_lb.cfg +++ b/test/kokoro/xds_k8s_lb.cfg @@ -15,7 +15,7 @@ # Config file for Kokoro (in protobuf text format) # Location of the continuous shell script in repository. -build_file: "grpc-node/packages/grpc-js-xds/scripts/xds_k8s_lb.sh" +build_file: "grpc-node/packages/grpc-js-xds/scripts/psm-interop-test-node.sh" timeout_mins: 180 action { define_artifacts { @@ -24,3 +24,7 @@ action { strip_prefix: "artifacts" } } +env_vars { + key: "PSM_TEST_SUITE" + value: "lb" +} diff --git a/test/kokoro/xds_k8s_url_map.cfg b/test/kokoro/xds_k8s_url_map.cfg index 50d523b66..bb6e6baf1 100644 --- a/test/kokoro/xds_k8s_url_map.cfg +++ b/test/kokoro/xds_k8s_url_map.cfg @@ -15,7 +15,7 @@ # Config file for Kokoro (in protobuf text format) # Location of the continuous shell script in repository. -build_file: "grpc-node/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh" +build_file: "grpc-node/packages/grpc-js-xds/scripts/psm-interop-test-node.sh" timeout_mins: 180 action { define_artifacts { @@ -24,3 +24,7 @@ action { strip_prefix: "artifacts" } } +env_vars { + key: "PSM_TEST_SUITE" + value: "url_map" +} From 87a35414021f627f01591cade9b1f9a7dcaaf5d3 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 14 May 2024 14:47:53 -0700 Subject: [PATCH 107/109] grpc-js: Fix UDS channels not reconnecting after going idle --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/resolver-uds.ts | 2 +- packages/grpc-js/test/common.ts | 51 ++++++++++++++----- packages/grpc-js/test/test-idle-timer.ts | 41 +++++++++++++++ packages/grpc-js/test/test-pick-first.ts | 2 +- .../grpc-js/test/test-server-interceptors.ts | 8 +-- 6 files changed, 86 insertions(+), 20 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index fdfab0d21..f8b070353 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.10.7", + "version": "1.10.8", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/resolver-uds.ts b/packages/grpc-js/src/resolver-uds.ts index 3a42b18c4..4d84de9d5 100644 --- a/packages/grpc-js/src/resolver-uds.ts +++ b/packages/grpc-js/src/resolver-uds.ts @@ -50,7 +50,7 @@ class UdsResolver implements Resolver { } destroy() { - // This resolver owns no resources, so we do nothing here. + this.hasReturnedResult = false; } static getDefaultAuthority(target: GrpcUri): string { diff --git a/packages/grpc-js/test/common.ts b/packages/grpc-js/test/common.ts index fcdbb4500..5efbf9808 100644 --- a/packages/grpc-js/test/common.ts +++ b/packages/grpc-js/test/common.ts @@ -19,6 +19,8 @@ import * as loader from '@grpc/proto-loader'; import * as assert2 from './assert2'; import * as path from 'path'; import * as grpc from '../src'; +import * as fsPromises from 'fs/promises'; +import * as os from 'os'; import { GrpcObject, @@ -71,54 +73,77 @@ const serviceImpl = { export class TestServer { private server: grpc.Server; - public port: number | null = null; + private target: string | null = null; constructor(public useTls: boolean, options?: grpc.ServerOptions) { this.server = new grpc.Server(options); this.server.addService(echoService.service, serviceImpl); } - start(): Promise { - let credentials: grpc.ServerCredentials; + + private getCredentials(): grpc.ServerCredentials { if (this.useTls) { - credentials = grpc.ServerCredentials.createSsl(null, [ + return grpc.ServerCredentials.createSsl(null, [ { private_key: key, cert_chain: cert }, ]); } else { - credentials = grpc.ServerCredentials.createInsecure(); + return grpc.ServerCredentials.createInsecure(); } + } + + start(): Promise { return new Promise((resolve, reject) => { - this.server.bindAsync('localhost:0', credentials, (error, port) => { + this.server.bindAsync('localhost:0', this.getCredentials(), (error, port) => { if (error) { reject(error); return; } - this.port = port; + this.target = `localhost:${port}`; resolve(); }); }); } + startUds(): Promise { + return fsPromises.mkdtemp(path.join(os.tmpdir(), 'uds')).then(dir => { + return new Promise((resolve, reject) => { + const target = `unix://${dir}/socket`; + this.server.bindAsync(target, this.getCredentials(), (error, port) => { + if (error) { + reject(error); + return; + } + this.target = target; + resolve(); + }); + }); + }); + } + shutdown() { this.server.forceShutdown(); } + + getTarget() { + if (this.target === null) { + throw new Error('Server not yet started'); + } + return this.target; + } } export class TestClient { private client: ServiceClient; - constructor(port: number, useTls: boolean, options?: grpc.ChannelOptions) { + constructor(target: string, useTls: boolean, options?: grpc.ChannelOptions) { let credentials: grpc.ChannelCredentials; if (useTls) { credentials = grpc.credentials.createSsl(ca); } else { credentials = grpc.credentials.createInsecure(); } - this.client = new echoService(`localhost:${port}`, credentials, options); + this.client = new echoService(target, credentials, options); } static createFromServer(server: TestServer, options?: grpc.ChannelOptions) { - if (server.port === null) { - throw new Error('Cannot create client, server not started'); - } - return new TestClient(server.port, server.useTls, options); + return new TestClient(server.getTarget(), server.useTls, options); } waitForReady(deadline: grpc.Deadline, callback: (error?: Error) => void) { diff --git a/packages/grpc-js/test/test-idle-timer.ts b/packages/grpc-js/test/test-idle-timer.ts index a8f457e3f..3f2a8ed20 100644 --- a/packages/grpc-js/test/test-idle-timer.ts +++ b/packages/grpc-js/test/test-idle-timer.ts @@ -129,6 +129,47 @@ describe('Channel idle timer', () => { }); }); +describe('Channel idle timer with UDS', () => { + let server: TestServer; + let client: TestClient | null = null; + before(() => { + server = new TestServer(false); + return server.startUds(); + }); + afterEach(() => { + if (client) { + client.close(); + client = null; + } + }); + after(() => { + server.shutdown(); + }); + it('Should be able to make a request after going idle', function (done) { + this.timeout(5000); + client = TestClient.createFromServer(server, { + 'grpc.client_idle_timeout_ms': 1000, + }); + client.sendRequest(error => { + assert.ifError(error); + assert.strictEqual( + client!.getChannelState(), + grpc.connectivityState.READY + ); + setTimeout(() => { + assert.strictEqual( + client!.getChannelState(), + grpc.connectivityState.IDLE + ); + client!.sendRequest(error => { + assert.ifError(error); + done(); + }); + }, 1100); + }); + }); +}); + describe('Server idle timer', () => { let server: TestServer; let client: TestClient | null = null; diff --git a/packages/grpc-js/test/test-pick-first.ts b/packages/grpc-js/test/test-pick-first.ts index 4c2c319e1..9803a5853 100644 --- a/packages/grpc-js/test/test-pick-first.ts +++ b/packages/grpc-js/test/test-pick-first.ts @@ -811,7 +811,7 @@ describe('pick_first load balancing policy', () => { before(async () => { server = new TestServer(false); await server.start(); - client = new TestClient(server.port!, false, { + client = TestClient.createFromServer(server, { 'grpc.service_config': JSON.stringify(serviceConfig), }); }); diff --git a/packages/grpc-js/test/test-server-interceptors.ts b/packages/grpc-js/test/test-server-interceptors.ts index e94169721..5d4038599 100644 --- a/packages/grpc-js/test/test-server-interceptors.ts +++ b/packages/grpc-js/test/test-server-interceptors.ts @@ -153,7 +153,7 @@ describe('Server interceptors', () => { grpc.ServerCredentials.createInsecure(), (error, port) => { assert.ifError(error); - client = new TestClient(port, false); + client = new TestClient(`localhost:${port}`, false); done(); } ); @@ -195,7 +195,7 @@ describe('Server interceptors', () => { grpc.ServerCredentials.createInsecure(), (error, port) => { assert.ifError(error); - client = new TestClient(port, false); + client = new TestClient(`localhost:${port}`, false); done(); } ); @@ -246,7 +246,7 @@ describe('Server interceptors', () => { grpc.ServerCredentials.createInsecure(), (error, port) => { assert.ifError(error); - client = new TestClient(port, false); + client = new TestClient(`localhost:${port}`, false); done(); } ); @@ -292,7 +292,7 @@ describe('Server interceptors', () => { grpc.ServerCredentials.createInsecure(), (error, port) => { assert.ifError(error); - client = new TestClient(port, false); + client = new TestClient(`localhost:${port}`, false); done(); } ); From e64d816d7df6d6cde62314beb67d11f1e0a8c79e Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 5 Jun 2024 11:22:23 -0700 Subject: [PATCH 108/109] grpc-js: Avoid buffering significantly more than max_receive_message_size per received message (1.10.x) --- packages/grpc-js/src/compression-filter.ts | 67 ++++++++++---- packages/grpc-js/src/internal-channel.ts | 2 - .../grpc-js/src/max-message-size-filter.ts | 88 ------------------- packages/grpc-js/src/server-interceptors.ts | 88 ++++++++++++------- packages/grpc-js/src/stream-decoder.ts | 5 ++ packages/grpc-js/src/subchannel-call.ts | 14 ++- packages/grpc-js/src/transport.ts | 7 +- .../grpc-js/test/fixtures/test_service.proto | 1 + packages/grpc-js/test/test-server-errors.ts | 49 ++++++++++- 9 files changed, 173 insertions(+), 148 deletions(-) delete mode 100644 packages/grpc-js/src/max-message-size-filter.ts diff --git a/packages/grpc-js/src/compression-filter.ts b/packages/grpc-js/src/compression-filter.ts index 136311ad5..f1600b36d 100644 --- a/packages/grpc-js/src/compression-filter.ts +++ b/packages/grpc-js/src/compression-filter.ts @@ -21,7 +21,7 @@ import { WriteObject, WriteFlags } from './call-interface'; import { Channel } from './channel'; import { ChannelOptions } from './channel-options'; import { CompressionAlgorithms } from './compression-algorithms'; -import { LogVerbosity } from './constants'; +import { DEFAULT_MAX_RECEIVE_MESSAGE_LENGTH, LogVerbosity, Status } from './constants'; import { BaseFilter, Filter, FilterFactory } from './filter'; import * as logging from './logging'; import { Metadata, MetadataValue } from './metadata'; @@ -98,6 +98,10 @@ class IdentityHandler extends CompressionHandler { } class DeflateHandler extends CompressionHandler { + constructor(private maxRecvMessageLength: number) { + super(); + } + compressMessage(message: Buffer) { return new Promise((resolve, reject) => { zlib.deflate(message, (err, output) => { @@ -112,18 +116,34 @@ class DeflateHandler extends CompressionHandler { decompressMessage(message: Buffer) { return new Promise((resolve, reject) => { - zlib.inflate(message, (err, output) => { - if (err) { - reject(err); - } else { - resolve(output); + let totalLength = 0; + const messageParts: Buffer[] = []; + const decompresser = zlib.createInflate(); + decompresser.on('data', (chunk: Buffer) => { + messageParts.push(chunk); + totalLength += chunk.byteLength; + if (this.maxRecvMessageLength !== -1 && totalLength > this.maxRecvMessageLength) { + decompresser.destroy(); + reject({ + code: Status.RESOURCE_EXHAUSTED, + details: `Received message that decompresses to a size larger than ${this.maxRecvMessageLength}` + }); } }); + decompresser.on('end', () => { + resolve(Buffer.concat(messageParts)); + }); + decompresser.write(message); + decompresser.end(); }); } } class GzipHandler extends CompressionHandler { + constructor(private maxRecvMessageLength: number) { + super(); + } + compressMessage(message: Buffer) { return new Promise((resolve, reject) => { zlib.gzip(message, (err, output) => { @@ -138,13 +158,25 @@ class GzipHandler extends CompressionHandler { decompressMessage(message: Buffer) { return new Promise((resolve, reject) => { - zlib.unzip(message, (err, output) => { - if (err) { - reject(err); - } else { - resolve(output); + let totalLength = 0; + const messageParts: Buffer[] = []; + const decompresser = zlib.createGunzip(); + decompresser.on('data', (chunk: Buffer) => { + messageParts.push(chunk); + totalLength += chunk.byteLength; + if (this.maxRecvMessageLength !== -1 && totalLength > this.maxRecvMessageLength) { + decompresser.destroy(); + reject({ + code: Status.RESOURCE_EXHAUSTED, + details: `Received message that decompresses to a size larger than ${this.maxRecvMessageLength}` + }); } }); + decompresser.on('end', () => { + resolve(Buffer.concat(messageParts)); + }); + decompresser.write(message); + decompresser.end(); }); } } @@ -169,14 +201,14 @@ class UnknownHandler extends CompressionHandler { } } -function getCompressionHandler(compressionName: string): CompressionHandler { +function getCompressionHandler(compressionName: string, maxReceiveMessageSize: number): CompressionHandler { switch (compressionName) { case 'identity': return new IdentityHandler(); case 'deflate': - return new DeflateHandler(); + return new DeflateHandler(maxReceiveMessageSize); case 'gzip': - return new GzipHandler(); + return new GzipHandler(maxReceiveMessageSize); default: return new UnknownHandler(compressionName); } @@ -186,6 +218,7 @@ export class CompressionFilter extends BaseFilter implements Filter { private sendCompression: CompressionHandler = new IdentityHandler(); private receiveCompression: CompressionHandler = new IdentityHandler(); private currentCompressionAlgorithm: CompressionAlgorithm = 'identity'; + private maxReceiveMessageLength: number; constructor( channelOptions: ChannelOptions, @@ -195,6 +228,7 @@ export class CompressionFilter extends BaseFilter implements Filter { const compressionAlgorithmKey = channelOptions['grpc.default_compression_algorithm']; + this.maxReceiveMessageLength = channelOptions['grpc.max_receive_message_length'] ?? DEFAULT_MAX_RECEIVE_MESSAGE_LENGTH if (compressionAlgorithmKey !== undefined) { if (isCompressionAlgorithmKey(compressionAlgorithmKey)) { const clientSelectedEncoding = CompressionAlgorithms[ @@ -215,7 +249,8 @@ export class CompressionFilter extends BaseFilter implements Filter { ) { this.currentCompressionAlgorithm = clientSelectedEncoding; this.sendCompression = getCompressionHandler( - this.currentCompressionAlgorithm + this.currentCompressionAlgorithm, + -1 ); } } else { @@ -247,7 +282,7 @@ export class CompressionFilter extends BaseFilter implements Filter { if (receiveEncoding.length > 0) { const encoding: MetadataValue = receiveEncoding[0]; if (typeof encoding === 'string') { - this.receiveCompression = getCompressionHandler(encoding); + this.receiveCompression = getCompressionHandler(encoding, this.maxReceiveMessageLength); } } metadata.remove('grpc-encoding'); diff --git a/packages/grpc-js/src/internal-channel.ts b/packages/grpc-js/src/internal-channel.ts index 469ace557..e0cebd469 100644 --- a/packages/grpc-js/src/internal-channel.ts +++ b/packages/grpc-js/src/internal-channel.ts @@ -33,7 +33,6 @@ import { } from './resolver'; import { trace } from './logging'; import { SubchannelAddress } from './subchannel-address'; -import { MaxMessageSizeFilterFactory } from './max-message-size-filter'; import { mapProxyName } from './http_proxy'; import { GrpcUri, parseUri, uriToString } from './uri-parser'; import { ServerSurfaceCall } from './server-call'; @@ -402,7 +401,6 @@ export class InternalChannel { } ); this.filterStackFactory = new FilterStackFactory([ - new MaxMessageSizeFilterFactory(this.options), new CompressionFilterFactory(this, this.options), ]); this.trace( diff --git a/packages/grpc-js/src/max-message-size-filter.ts b/packages/grpc-js/src/max-message-size-filter.ts deleted file mode 100644 index b6df374b2..000000000 --- a/packages/grpc-js/src/max-message-size-filter.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2020 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 { BaseFilter, Filter, FilterFactory } from './filter'; -import { WriteObject } from './call-interface'; -import { - Status, - DEFAULT_MAX_SEND_MESSAGE_LENGTH, - DEFAULT_MAX_RECEIVE_MESSAGE_LENGTH, -} from './constants'; -import { ChannelOptions } from './channel-options'; -import { Metadata } from './metadata'; - -export class MaxMessageSizeFilter extends BaseFilter implements Filter { - private maxSendMessageSize: number = DEFAULT_MAX_SEND_MESSAGE_LENGTH; - private maxReceiveMessageSize: number = DEFAULT_MAX_RECEIVE_MESSAGE_LENGTH; - constructor(options: ChannelOptions) { - super(); - if ('grpc.max_send_message_length' in options) { - this.maxSendMessageSize = options['grpc.max_send_message_length']!; - } - if ('grpc.max_receive_message_length' in options) { - this.maxReceiveMessageSize = options['grpc.max_receive_message_length']!; - } - } - - async sendMessage(message: Promise): Promise { - /* A configured size of -1 means that there is no limit, so skip the check - * entirely */ - if (this.maxSendMessageSize === -1) { - return message; - } else { - const concreteMessage = await message; - if (concreteMessage.message.length > this.maxSendMessageSize) { - throw { - code: Status.RESOURCE_EXHAUSTED, - details: `Sent message larger than max (${concreteMessage.message.length} vs. ${this.maxSendMessageSize})`, - metadata: new Metadata(), - }; - } else { - return concreteMessage; - } - } - } - - async receiveMessage(message: Promise): Promise { - /* A configured size of -1 means that there is no limit, so skip the check - * entirely */ - if (this.maxReceiveMessageSize === -1) { - return message; - } else { - const concreteMessage = await message; - if (concreteMessage.length > this.maxReceiveMessageSize) { - throw { - code: Status.RESOURCE_EXHAUSTED, - details: `Received message larger than max (${concreteMessage.length} vs. ${this.maxReceiveMessageSize})`, - metadata: new Metadata(), - }; - } else { - return concreteMessage; - } - } - } -} - -export class MaxMessageSizeFilterFactory - implements FilterFactory -{ - constructor(private readonly options: ChannelOptions) {} - - createFilter(): MaxMessageSizeFilter { - return new MaxMessageSizeFilter(this.options); - } -} diff --git a/packages/grpc-js/src/server-interceptors.ts b/packages/grpc-js/src/server-interceptors.ts index b62d55108..c2d985a6a 100644 --- a/packages/grpc-js/src/server-interceptors.ts +++ b/packages/grpc-js/src/server-interceptors.ts @@ -30,14 +30,10 @@ import { import * as http2 from 'http2'; import { getErrorMessage } from './error'; import * as zlib from 'zlib'; -import { promisify } from 'util'; import { StreamDecoder } from './stream-decoder'; import { CallEventTracker } from './transport'; import * as logging from './logging'; -const unzip = promisify(zlib.unzip); -const inflate = promisify(zlib.inflate); - const TRACER_NAME = 'server_call'; function trace(text: string) { @@ -496,7 +492,7 @@ export class BaseServerInterceptingCall private wantTrailers = false; private cancelNotified = false; private incomingEncoding = 'identity'; - private decoder = new StreamDecoder(); + private decoder: StreamDecoder; private readQueue: ReadQueueEntry[] = []; private isReadPending = false; private receivedHalfClose = false; @@ -554,6 +550,8 @@ export class BaseServerInterceptingCall this.maxReceiveMessageSize = options['grpc.max_receive_message_length']!; } + this.decoder = new StreamDecoder(this.maxReceiveMessageSize); + const metadata = Metadata.fromHttp2Headers(headers); if (logging.isTracerEnabled(TRACER_NAME)) { @@ -674,18 +672,41 @@ export class BaseServerInterceptingCall message: Buffer, encoding: string ): Buffer | Promise { - switch (encoding) { - case 'deflate': - return inflate(message.subarray(5)); - case 'gzip': - return unzip(message.subarray(5)); - case 'identity': - return message.subarray(5); - default: - return Promise.reject({ - code: Status.UNIMPLEMENTED, - details: `Received message compressed with unsupported encoding "${encoding}"`, + const messageContents = message.subarray(5); + if (encoding === 'identity') { + return messageContents; + } else if (encoding === 'deflate' || encoding === 'gzip') { + let decompresser: zlib.Gunzip | zlib.Deflate; + if (encoding === 'deflate') { + decompresser = zlib.createInflate(); + } else { + decompresser = zlib.createGunzip(); + } + return new Promise((resolve, reject) => { + let totalLength = 0 + const messageParts: Buffer[] = []; + decompresser.on('data', (chunk: Buffer) => { + messageParts.push(chunk); + totalLength += chunk.byteLength; + if (this.maxReceiveMessageSize !== -1 && totalLength > this.maxReceiveMessageSize) { + decompresser.destroy(); + reject({ + code: Status.RESOURCE_EXHAUSTED, + details: `Received message that decompresses to a size larger than ${this.maxReceiveMessageSize}` + }); + } + }); + decompresser.on('end', () => { + resolve(Buffer.concat(messageParts)); }); + decompresser.write(messageContents); + decompresser.end(); + }); + } else { + return Promise.reject({ + code: Status.UNIMPLEMENTED, + details: `Received message compressed with unsupported encoding "${encoding}"`, + }); } } @@ -698,10 +719,16 @@ export class BaseServerInterceptingCall const compressedMessageEncoding = compressed ? this.incomingEncoding : 'identity'; - const decompressedMessage = await this.decompressMessage( - queueEntry.compressedMessage!, - compressedMessageEncoding - ); + let decompressedMessage: Buffer; + try { + decompressedMessage = await this.decompressMessage( + queueEntry.compressedMessage!, + compressedMessageEncoding + ); + } catch (err) { + this.sendStatus(err as PartialStatusObject); + return; + } try { queueEntry.parsedMessage = this.handler.deserialize(decompressedMessage); } catch (err) { @@ -743,23 +770,16 @@ export class BaseServerInterceptingCall ' received data frame of size ' + data.length ); - const rawMessages = this.decoder.write(data); + let rawMessages: Buffer[]; + try { + rawMessages = this.decoder.write(data); + } catch (e) { + this.sendStatus({ code: Status.RESOURCE_EXHAUSTED, details: (e as Error).message }); + return; + } for (const messageBytes of rawMessages) { this.stream.pause(); - if ( - this.maxReceiveMessageSize !== -1 && - messageBytes.length - 5 > this.maxReceiveMessageSize - ) { - this.sendStatus({ - code: Status.RESOURCE_EXHAUSTED, - details: `Received message larger than max (${ - messageBytes.length - 5 - } vs. ${this.maxReceiveMessageSize})`, - metadata: null, - }); - return; - } const queueEntry: ReadQueueEntry = { type: 'COMPRESSED', compressedMessage: messageBytes, diff --git a/packages/grpc-js/src/stream-decoder.ts b/packages/grpc-js/src/stream-decoder.ts index 671ad41ae..ea669d14c 100644 --- a/packages/grpc-js/src/stream-decoder.ts +++ b/packages/grpc-js/src/stream-decoder.ts @@ -30,6 +30,8 @@ export class StreamDecoder { private readPartialMessage: Buffer[] = []; private readMessageRemaining = 0; + constructor(private maxReadMessageLength: number) {} + write(data: Buffer): Buffer[] { let readHead = 0; let toRead: number; @@ -60,6 +62,9 @@ export class StreamDecoder { // readSizeRemaining >=0 here if (this.readSizeRemaining === 0) { this.readMessageSize = this.readPartialSize.readUInt32BE(0); + if (this.maxReadMessageLength !== -1 && this.readMessageSize > this.maxReadMessageLength) { + throw new Error(`Received message larger than max (${this.readMessageSize} vs ${this.maxReadMessageLength})`); + } this.readMessageRemaining = this.readMessageSize; if (this.readMessageRemaining > 0) { this.readState = ReadState.READING_MESSAGE; diff --git a/packages/grpc-js/src/subchannel-call.ts b/packages/grpc-js/src/subchannel-call.ts index 0ce7d72cb..bee00119f 100644 --- a/packages/grpc-js/src/subchannel-call.ts +++ b/packages/grpc-js/src/subchannel-call.ts @@ -18,7 +18,7 @@ import * as http2 from 'http2'; import * as os from 'os'; -import { Status } from './constants'; +import { DEFAULT_MAX_RECEIVE_MESSAGE_LENGTH, Status } from './constants'; import { Metadata } from './metadata'; import { StreamDecoder } from './stream-decoder'; import * as logging from './logging'; @@ -116,7 +116,7 @@ function mapHttpStatusCode(code: number): StatusObject { } export class Http2SubchannelCall implements SubchannelCall { - private decoder = new StreamDecoder(); + private decoder: StreamDecoder; private isReadFilterPending = false; private isPushPending = false; @@ -147,6 +147,8 @@ export class Http2SubchannelCall implements SubchannelCall { private readonly transport: Transport, private readonly callId: number ) { + const maxReceiveMessageLength = transport.getOptions()['grpc.max_receive_message_length'] ?? DEFAULT_MAX_RECEIVE_MESSAGE_LENGTH; + this.decoder = new StreamDecoder(maxReceiveMessageLength); http2Stream.on('response', (headers, flags) => { let headersString = ''; for (const header of Object.keys(headers)) { @@ -182,7 +184,13 @@ export class Http2SubchannelCall implements SubchannelCall { return; } this.trace('receive HTTP/2 data frame of length ' + data.length); - const messages = this.decoder.write(data); + let messages: Buffer[]; + try { + messages = this.decoder.write(data); + } catch (e) { + this.cancelWithStatus(Status.RESOURCE_EXHAUSTED, (e as Error).message); + return; + } for (const message of messages) { this.trace('parsed message of length ' + message.length); diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index 66a5d4556..934b62111 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -84,6 +84,7 @@ export interface TransportDisconnectListener { export interface Transport { getChannelzRef(): SocketRef; getPeerName(): string; + getOptions(): ChannelOptions; createCall( metadata: Metadata, host: string, @@ -147,7 +148,7 @@ class Http2Transport implements Transport { constructor( private session: http2.ClientHttp2Session, subchannelAddress: SubchannelAddress, - options: ChannelOptions, + private options: ChannelOptions, /** * Name of the remote server, if it is not the same as the subchannel * address, i.e. if connecting through an HTTP CONNECT proxy. @@ -617,6 +618,10 @@ class Http2Transport implements Transport { return this.subchannelAddressString; } + getOptions() { + return this.options; + } + shutdown() { this.session.close(); unregisterChannelzRef(this.channelzRef); diff --git a/packages/grpc-js/test/fixtures/test_service.proto b/packages/grpc-js/test/fixtures/test_service.proto index 64ce0d378..2a7a303f3 100644 --- a/packages/grpc-js/test/fixtures/test_service.proto +++ b/packages/grpc-js/test/fixtures/test_service.proto @@ -21,6 +21,7 @@ message Request { bool error = 1; string message = 2; int32 errorAfter = 3; + int32 responseLength = 4; } message Response { diff --git a/packages/grpc-js/test/test-server-errors.ts b/packages/grpc-js/test/test-server-errors.ts index 24ccfeef3..243e10918 100644 --- a/packages/grpc-js/test/test-server-errors.ts +++ b/packages/grpc-js/test/test-server-errors.ts @@ -33,6 +33,7 @@ import { } from '../src/server-call'; import { loadProtoFile } from './common'; +import { CompressionAlgorithms } from '../src/compression-algorithms'; const protoFile = join(__dirname, 'fixtures', 'test_service.proto'); const testServiceDef = loadProtoFile(protoFile); @@ -310,7 +311,7 @@ describe('Other conditions', () => { trailerMetadata ); } else { - cb(null, { count: 1 }, trailerMetadata); + cb(null, { count: 1, message: 'a'.repeat(req.responseLength) }, trailerMetadata); } }, @@ -320,6 +321,7 @@ describe('Other conditions', () => { ) { let count = 0; let errored = false; + let responseLength = 0; stream.on('data', (data: any) => { if (data.error) { @@ -327,13 +329,14 @@ describe('Other conditions', () => { errored = true; cb(new Error(message) as ServiceError, null, trailerMetadata); } else { + responseLength += data.responseLength; count++; } }); stream.on('end', () => { if (!errored) { - cb(null, { count }, trailerMetadata); + cb(null, { count, message: 'a'.repeat(responseLength) }, trailerMetadata); } }); }, @@ -349,7 +352,7 @@ describe('Other conditions', () => { }); } else { for (let i = 1; i <= 5; i++) { - stream.write({ count: i }); + stream.write({ count: i, message: 'a'.repeat(req.responseLength) }); if (req.errorAfter && req.errorAfter === i) { stream.emit('error', { code: grpc.status.UNKNOWN, @@ -376,7 +379,7 @@ describe('Other conditions', () => { err.metadata.add('count', '' + count); stream.emit('error', err); } else { - stream.write({ count }); + stream.write({ count, message: 'a'.repeat(data.responseLength) }); count++; } }); @@ -740,6 +743,44 @@ describe('Other conditions', () => { }); }); }); + + describe('Max message size', () => { + const largeMessage = 'a'.repeat(10_000_000); + it('Should be enforced on the server', done => { + client.unary({ message: largeMessage }, (error?: ServiceError) => { + assert(error); + assert.strictEqual(error.code, grpc.status.RESOURCE_EXHAUSTED); + done(); + }); + }); + it('Should be enforced on the client', done => { + client.unary({ responseLength: 10_000_000 }, (error?: ServiceError) => { + assert(error); + assert.strictEqual(error.code, grpc.status.RESOURCE_EXHAUSTED); + done(); + }); + }); + describe('Compressed messages', () => { + it('Should be enforced with gzip', done => { + const compressingClient = new testServiceClient(`localhost:${port}`, clientInsecureCreds, {'grpc.default_compression_algorithm': CompressionAlgorithms.gzip}); + compressingClient.unary({ message: largeMessage }, (error?: ServiceError) => { + assert(error); + assert.strictEqual(error.code, grpc.status.RESOURCE_EXHAUSTED); + assert.match(error.details, /Received message that decompresses to a size larger/); + done(); + }); + }); + it('Should be enforced with deflate', done => { + const compressingClient = new testServiceClient(`localhost:${port}`, clientInsecureCreds, {'grpc.default_compression_algorithm': CompressionAlgorithms.deflate}); + compressingClient.unary({ message: largeMessage }, (error?: ServiceError) => { + assert(error); + assert.strictEqual(error.code, grpc.status.RESOURCE_EXHAUSTED); + assert.match(error.details, /Received message that decompresses to a size larger/); + done(); + }); + }); + }); + }); }); function identity(arg: any): any { From 7ecaa2d2dcaaa49467d41143169212caf55a40cd Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 7 Jun 2024 10:52:50 -0700 Subject: [PATCH 109/109] grpc-js: Bump to 1.10.9 --- packages/grpc-js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index f8b070353..73b63bf7b 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.10.8", + "version": "1.10.9", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js",