grpc-js-xds: Add HTTP Filters support

This commit is contained in:
Michael Lumish 2021-06-17 09:45:19 -07:00 committed by murgatroid99
parent 49822c8f49
commit 311aca31e4
19 changed files with 596 additions and 48 deletions

View File

@ -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/udpa/ deps/googleapis/ deps/protoc-gen-validate/ -O src/generated/ --grpcLib @grpc/grpc-js envoy/service/discovery/v2/ads.proto envoy/service/load_stats/v2/lrs.proto 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",
"generate-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --includeDirs deps/envoy-api/ deps/udpa/ deps/googleapis/ deps/protoc-gen-validate/ -O src/generated/ --grpcLib @grpc/grpc-js envoy/service/discovery/v2/ads.proto envoy/service/load_stats/v2/lrs.proto 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",
"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"
},
"repository": {

View File

@ -13,4 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
*/
export const EXPERIMENTAL_FAULT_INJECTION = process.env.GRPC_XDS_EXPERIMENTAL_FAULT_INJECTION;

View File

@ -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);
}
export interface EnumOptions__Output {
'allowAlias': (boolean);
'deprecated': (boolean);
'uninterpretedOption': (_google_protobuf_UninterpretedOption__Output)[];
'.udpa.annotations.enum_migrate'?: (_udpa_annotations_MigrateAnnotation__Output);
}

View File

@ -1,18 +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);
}
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);
}

View File

@ -2,8 +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';
// Original file: null
@ -30,10 +28,6 @@ export interface FieldOptions {
'weak'?: (boolean);
'uninterpretedOption'?: (_google_protobuf_UninterpretedOption)[];
'.validate.rules'?: (_validate_FieldRules);
'.udpa.annotations.security'?: (_udpa_annotations_FieldSecurityAnnotation);
'.udpa.annotations.sensitive'?: (boolean);
'.udpa.annotations.field_migrate'?: (_udpa_annotations_FieldMigrateAnnotation);
'.envoy.annotations.disallowed_by_default'?: (boolean);
}
export interface FieldOptions__Output {
@ -45,8 +39,4 @@ export interface FieldOptions__Output {
'weak': (boolean);
'uninterpretedOption': (_google_protobuf_UninterpretedOption__Output)[];
'.validate.rules'?: (_validate_FieldRules__Output);
'.udpa.annotations.security'?: (_udpa_annotations_FieldSecurityAnnotation__Output);
'.udpa.annotations.sensitive': (boolean);
'.udpa.annotations.field_migrate'?: (_udpa_annotations_FieldMigrateAnnotation__Output);
'.envoy.annotations.disallowed_by_default': (boolean);
}

View File

@ -1,8 +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 { 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';
// Original file: null
@ -28,8 +26,6 @@ export interface FileOptions {
'objcClassPrefix'?: (string);
'csharpNamespace'?: (string);
'uninterpretedOption'?: (_google_protobuf_UninterpretedOption)[];
'.udpa.annotations.file_migrate'?: (_udpa_annotations_FileMigrateAnnotation);
'.udpa.annotations.file_status'?: (_udpa_annotations_StatusAnnotation);
}
export interface FileOptions__Output {
@ -48,6 +44,4 @@ export interface FileOptions__Output {
'objcClassPrefix': (string);
'csharpNamespace': (string);
'uninterpretedOption': (_google_protobuf_UninterpretedOption__Output)[];
'.udpa.annotations.file_migrate'?: (_udpa_annotations_FileMigrateAnnotation__Output);
'.udpa.annotations.file_status'?: (_udpa_annotations_StatusAnnotation__Output);
}

View File

@ -1,8 +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';
export interface MessageOptions {
'messageSetWireFormat'?: (boolean);
@ -11,8 +9,6 @@ export interface MessageOptions {
'mapEntry'?: (boolean);
'uninterpretedOption'?: (_google_protobuf_UninterpretedOption)[];
'.validate.disabled'?: (boolean);
'.udpa.annotations.versioning'?: (_udpa_annotations_VersioningAnnotation);
'.udpa.annotations.message_migrate'?: (_udpa_annotations_MigrateAnnotation);
}
export interface MessageOptions__Output {
@ -22,6 +18,4 @@ export interface MessageOptions__Output {
'mapEntry': (boolean);
'uninterpretedOption': (_google_protobuf_UninterpretedOption__Output)[];
'.validate.disabled': (boolean);
'.udpa.annotations.versioning'?: (_udpa_annotations_VersioningAnnotation__Output);
'.udpa.annotations.message_migrate'?: (_udpa_annotations_MigrateAnnotation__Output);
}

View File

@ -0,0 +1,74 @@
import type * as grpc from '@grpc/grpc-js';
import type { ServiceDefinition, EnumTypeDefinition, MessageTypeDefinition } from '@grpc/proto-loader';
type SubtypeConstructor<Constructor extends new (...args: any) => any, Subtype> = {
new(...args: ConstructorParameters<Constructor>): Subtype;
};
export interface ProtoGrpcType {
google: {
protobuf: {
DescriptorProto: MessageTypeDefinition
Duration: MessageTypeDefinition
EnumDescriptorProto: MessageTypeDefinition
EnumOptions: MessageTypeDefinition
EnumValueDescriptorProto: MessageTypeDefinition
EnumValueOptions: MessageTypeDefinition
FieldDescriptorProto: MessageTypeDefinition
FieldOptions: MessageTypeDefinition
FileDescriptorProto: MessageTypeDefinition
FileDescriptorSet: MessageTypeDefinition
FileOptions: MessageTypeDefinition
GeneratedCodeInfo: MessageTypeDefinition
ListValue: MessageTypeDefinition
MessageOptions: MessageTypeDefinition
MethodDescriptorProto: MessageTypeDefinition
MethodOptions: MessageTypeDefinition
NullValue: EnumTypeDefinition
OneofDescriptorProto: MessageTypeDefinition
OneofOptions: MessageTypeDefinition
ServiceDescriptorProto: MessageTypeDefinition
ServiceOptions: MessageTypeDefinition
SourceCodeInfo: MessageTypeDefinition
Struct: MessageTypeDefinition
Timestamp: MessageTypeDefinition
UninterpretedOption: MessageTypeDefinition
Value: MessageTypeDefinition
}
}
udpa: {
type: {
v1: {
TypedStruct: 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
}
}

View File

@ -0,0 +1,77 @@
// Original file: deps/udpa/udpa/type/v1/typed_struct.proto
import type { Struct as _google_protobuf_Struct, Struct__Output as _google_protobuf_Struct__Output } from '../../../google/protobuf/Struct';
/**
* A TypedStruct contains an arbitrary JSON serialized protocol buffer message with a URL that
* describes the type of the serialized message. This is very similar to google.protobuf.Any,
* instead of having protocol buffer binary, this employs google.protobuf.Struct as value.
*
* This message is intended to be embedded inside Any, so it shouldn't be directly referred
* from other UDPA messages.
*
* When packing an opaque extension config, packing the expected type into Any is preferred
* wherever possible for its efficiency. TypedStruct should be used only if a proto descriptor
* is not available, for example if:
* - A control plane sends opaque message that is originally from external source in human readable
* format such as JSON or YAML.
* - The control plane doesn't have the knowledge of the protocol buffer schema hence it cannot
* serialize the message in protocol buffer binary format.
* - The DPLB doesn't have have the knowledge of the protocol buffer schema its plugin or extension
* uses. This has to be indicated in the DPLB capability negotiation.
*
* When a DPLB receives a TypedStruct in Any, it should:
* - Check if the type_url of the TypedStruct matches the type the extension expects.
* - Convert value to the type described in type_url and perform validation.
* TODO(lizan): Figure out how TypeStruct should be used with DPLB extensions that doesn't link
* protobuf descriptor with DPLB itself, (e.g. gRPC LB Plugin, Envoy WASM extensions).
*/
export interface TypedStruct {
/**
* A URL that uniquely identifies the type of the serialize protocol buffer message.
* This has same semantics and format described in google.protobuf.Any:
* https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/any.proto
*/
'type_url'?: (string);
/**
* A JSON representation of the above specified type.
*/
'value'?: (_google_protobuf_Struct);
}
/**
* A TypedStruct contains an arbitrary JSON serialized protocol buffer message with a URL that
* describes the type of the serialized message. This is very similar to google.protobuf.Any,
* instead of having protocol buffer binary, this employs google.protobuf.Struct as value.
*
* This message is intended to be embedded inside Any, so it shouldn't be directly referred
* from other UDPA messages.
*
* When packing an opaque extension config, packing the expected type into Any is preferred
* wherever possible for its efficiency. TypedStruct should be used only if a proto descriptor
* is not available, for example if:
* - A control plane sends opaque message that is originally from external source in human readable
* format such as JSON or YAML.
* - The control plane doesn't have the knowledge of the protocol buffer schema hence it cannot
* serialize the message in protocol buffer binary format.
* - The DPLB doesn't have have the knowledge of the protocol buffer schema its plugin or extension
* uses. This has to be indicated in the DPLB capability negotiation.
*
* When a DPLB receives a TypedStruct in Any, it should:
* - Check if the type_url of the TypedStruct matches the type the extension expects.
* - Convert value to the type described in type_url and perform validation.
* TODO(lizan): Figure out how TypeStruct should be used with DPLB extensions that doesn't link
* protobuf descriptor with DPLB itself, (e.g. gRPC LB Plugin, Envoy WASM extensions).
*/
export interface TypedStruct__Output {
/**
* A URL that uniquely identifies the type of the serialize protocol buffer message.
* This has same semantics and format described in google.protobuf.Any:
* https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/any.proto
*/
'type_url': (string);
/**
* A JSON representation of the above specified type.
*/
'value'?: (_google_protobuf_Struct__Output);
}

View File

@ -0,0 +1,221 @@
/*
* Copyright 2021 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.
*/
// This is a non-public, unstable API, but it's very convenient
import { loadProtosWithOptionsSync } from '@grpc/proto-loader/build/src/util';
import { experimental } from '@grpc/grpc-js';
import { Any__Output } from './generated/google/protobuf/Any';
import Filter = experimental.Filter;
import FilterFactory = experimental.FilterFactory;
import { TypedStruct__Output } from './generated/udpa/type/v1/TypedStruct';
import { FilterConfig__Output } from './generated/envoy/config/route/v3/FilterConfig';
import { HttpFilter__Output } from './generated/envoy/extensions/filters/network/http_connection_manager/v3/HttpFilter';
const TYPED_STRUCT_URL = 'type.googleapis.com/udpa.type.v1.TypedStruct';
const TYPED_STRUCT_NAME = 'udpa.type.v1.TypedStruct';
const FILTER_CONFIG_URL = 'type.googleapis.com/envoy.config.route.v3.FilterConfig';
const FILTER_CONFIG_NAME = 'envoy.config.route.v3.FilterConfig';
const resourceRoot = loadProtosWithOptionsSync([
'udpa/type/v1/typed_struct.proto',
'envoy/config/route/v3/route_components.proto'], {
keepCase: true,
includeDirs: [
// Paths are relative to src/build
__dirname + '/../../deps/udpa/',
__dirname + '/../../deps/envoy-api/'
],
}
);
export interface HttpFilterConfig {
typeUrl: string;
config: any;
}
export interface HttpFilterFactoryConstructor<FilterType extends Filter> {
new(config: HttpFilterConfig, overrideConfig?: HttpFilterConfig): FilterFactory<FilterType>;
}
export interface HttpFilterRegistryEntry {
parseTopLevelFilterConfig(encodedConfig: Any__Output): HttpFilterConfig | null;
parseOverrideFilterConfig(encodedConfig: Any__Output): HttpFilterConfig | null;
httpFilterConstructor: HttpFilterFactoryConstructor<Filter>;
}
const FILTER_REGISTRY = new Map<string, HttpFilterRegistryEntry>();
export function registerHttpFilter(typeName: string, entry: HttpFilterRegistryEntry) {
FILTER_REGISTRY.set(typeName, entry);
}
const toObjectOptions = {
longs: String,
enums: String,
defaults: true,
oneofs: true
}
function parseAnyMessage<MessageType>(message: Any__Output): MessageType | null {
const messageType = resourceRoot.lookup(message.type_url);
if (messageType) {
const decodedMessage = (messageType as any).decode(message.value);
return decodedMessage.$type.toObject(decodedMessage, toObjectOptions) as MessageType;
} else {
return null;
}
}
function getTopLevelFilterUrl(encodedConfig: Any__Output): string {
let typeUrl: string;
if (encodedConfig.type_url === TYPED_STRUCT_URL) {
const typedStruct = parseAnyMessage<TypedStruct__Output>(encodedConfig)
if (typedStruct) {
return typedStruct.type_url;
} else {
throw new Error('Failed to parse TypedStruct');
}
} else {
return encodedConfig.type_url;
}
}
export function validateTopLevelFilter(httpFilter: HttpFilter__Output): boolean {
if (!httpFilter.typed_config) {
return false;
}
const encodedConfig = httpFilter.typed_config;
let typeUrl: string;
try {
typeUrl = getTopLevelFilterUrl(encodedConfig);
} catch (e) {
return false;
}
const registryEntry = FILTER_REGISTRY.get(typeUrl);
if (registryEntry) {
const parsedConfig = registryEntry.parseTopLevelFilterConfig(encodedConfig);
return parsedConfig !== null;
} else {
if (httpFilter.is_optional) {
return true;
} else {
return false;
}
}
}
export function validateOverrideFilter(encodedConfig: Any__Output): boolean {
let typeUrl: string;
let realConfig: Any__Output;
let isOptional = false;
if (encodedConfig.type_url === FILTER_CONFIG_URL) {
const filterConfig = parseAnyMessage<FilterConfig__Output>(encodedConfig);
if (filterConfig) {
isOptional = filterConfig.is_optional;
if (filterConfig.config) {
realConfig = filterConfig.config;
} else {
return false;
}
} else {
return false;
}
} else {
realConfig = encodedConfig;
}
if (realConfig.type_url === TYPED_STRUCT_URL) {
const typedStruct = parseAnyMessage<TypedStruct__Output>(encodedConfig);
if (typedStruct) {
typeUrl = typedStruct.type_url;
} else {
return false;
}
} else {
typeUrl = realConfig.type_url;
}
const registryEntry = FILTER_REGISTRY.get(typeUrl);
if (registryEntry) {
const parsedConfig = registryEntry.parseOverrideFilterConfig(encodedConfig);
return parsedConfig !== null;
} else {
if (isOptional) {
return true;
} else {
return false;
}
}
}
export function parseTopLevelFilterConfig(encodedConfig: Any__Output) {
let typeUrl: string;
try {
typeUrl = getTopLevelFilterUrl(encodedConfig);
} catch (e) {
return null;
}
const registryEntry = FILTER_REGISTRY.get(typeUrl);
if (registryEntry) {
return registryEntry.parseTopLevelFilterConfig(encodedConfig);
} else {
// Filter type URL not found in registry
return null;
}
}
export function parseOverrideFilterConfig(encodedConfig: Any__Output) {
let typeUrl: string;
let realConfig: Any__Output;
if (encodedConfig.type_url === FILTER_CONFIG_URL) {
const filterConfig = parseAnyMessage<FilterConfig__Output>(encodedConfig);
if (filterConfig) {
if (filterConfig.config) {
realConfig = filterConfig.config;
} else {
return null;
}
} else {
return null;
}
} else {
realConfig = encodedConfig;
}
if (realConfig.type_url === TYPED_STRUCT_URL) {
const typedStruct = parseAnyMessage<TypedStruct__Output>(encodedConfig);
if (typedStruct) {
typeUrl = typedStruct.type_url;
} else {
return null;
}
} else {
typeUrl = realConfig.type_url;
}
const registryEntry = FILTER_REGISTRY.get(typeUrl);
if (registryEntry) {
return registryEntry.parseOverrideFilterConfig(encodedConfig);
} else {
return null;
}
}
export function createHttpFilter(config: HttpFilterConfig, overrideConfig?: HttpFilterConfig): FilterFactory<Filter> | null {
const registryEntry = FILTER_REGISTRY.get(config.typeUrl);
if (registryEntry) {
return new registryEntry.httpFilterConstructor(config, overrideConfig);
} else {
return null;
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2021 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.
*/
// This is a non-public, unstable API, but it's very convenient
import { loadProtosWithOptionsSync } from '@grpc/proto-loader/build/src/util';
import { Any__Output } from './generated/google/protobuf/Any';
function parseAnyMessage(encodedMessage: Any__Output) {
}

View File

@ -42,6 +42,12 @@ import { RouteAction, SingleClusterRouteAction, WeightedCluster, WeightedCluster
import { decodeSingleResource, HTTP_CONNECTION_MANGER_TYPE_URL_V3 } 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 } from './environment';
import Filter = experimental.Filter;
import FilterFactory = experimental.FilterFactory;
import BaseFilter = experimental.BaseFilter;
import CallStream = experimental.CallStream;
const TRACER_NAME = 'xds_resolver';
@ -203,6 +209,25 @@ function protoDurationToDuration(duration: Duration__Output): Duration {
}
}
class NoRouterFilter extends BaseFilter implements Filter {
constructor(private call: CallStream) {
super();
}
sendMetadata(metadata: Promise<Metadata>): Promise<Metadata> {
this.call.cancelWithStatus(status.UNAVAILABLE, 'no xDS HTTP router filter configured');
return Promise.reject<Metadata>(new Error('no xDS HTTP router filter configured'));
}
}
class NoRouterFilterFactory implements FilterFactory<NoRouterFilter> {
createFilter(callStream: CallStream): NoRouterFilter {
return new NoRouterFilter(callStream);
}
}
const ROUTER_FILTER_URL = 'type.googleapis.com/envoy.extensions.filters.http.router.v3.Router';
class XdsResolver implements Resolver {
private hasReportedSuccess = false;
@ -221,6 +246,9 @@ class XdsResolver implements Resolver {
private latestDefaultTimeout: Duration | undefined = undefined;
private ldsHttpFilterConfigs: {name: string, config: HttpFilterConfig}[] = [];
private hasRouterFilter = false;
constructor(
private target: GrpcUri,
private listener: ResolverListener,
@ -235,6 +263,21 @@ class XdsResolver implements Resolver {
} else {
this.latestDefaultTimeout = protoDurationToDuration(defaultTimeout);
}
if (EXPERIMENTAL_FAULT_INJECTION) {
this.ldsHttpFilterConfigs = [];
this.hasRouterFilter = false;
for (const filter of httpConnectionManager.http_filters) {
if (filter.typed_config?.type_url === ROUTER_FILTER_URL) {
this.hasRouterFilter = true;
break;
}
// typed_config must be set here, or validation would have failed
const filterConfig = parseTopLevelFilterConfig(filter.typed_config!);
if (filterConfig) {
this.ldsHttpFilterConfigs.push({name: filter.name, config: filterConfig});
}
}
}
switch (httpConnectionManager.route_specifier) {
case 'rds': {
const routeConfigName = httpConnectionManager.rds!.route_config_name;
@ -314,6 +357,15 @@ class XdsResolver implements Resolver {
this.reportResolutionError('No matching route found');
return;
}
const virtualHostHttpFilterOverrides = new Map<string, HttpFilterConfig>();
if (EXPERIMENTAL_FAULT_INJECTION) {
for (const [name, filter] of Object.entries(virtualHost.typed_per_filter_config ?? {})) {
const parsedConfig = parseOverrideFilterConfig(filter);
if (parsedConfig) {
virtualHostHttpFilterOverrides.set(name, parsedConfig);
}
}
}
trace('Received virtual host config ' + JSON.stringify(virtualHost, undefined, 2));
const allConfigClusters = new Set<string>();
const matchList: {matcher: Matcher, action: RouteAction}[] = [];
@ -334,20 +386,89 @@ class XdsResolver implements Resolver {
if (timeout?.seconds === 0 && timeout.nanos === 0) {
timeout = undefined;
}
const routeHttpFilterOverrides = new Map<string, HttpFilterConfig>();
if (EXPERIMENTAL_FAULT_INJECTION) {
for (const [name, filter] of Object.entries(route.typed_per_filter_config ?? {})) {
const parsedConfig = parseOverrideFilterConfig(filter);
if (parsedConfig) {
routeHttpFilterOverrides.set(name, parsedConfig);
}
}
}
switch (route.route!.cluster_specifier) {
case 'cluster_header':
continue;
case 'cluster':{
const cluster = route.route!.cluster!;
allConfigClusters.add(cluster);
routeAction = new SingleClusterRouteAction(cluster, timeout);
const extraFilterFactories: FilterFactory<Filter>[] = [];
if (EXPERIMENTAL_FAULT_INJECTION) {
for (const filterConfig of this.ldsHttpFilterConfigs) {
if (routeHttpFilterOverrides.has(filterConfig.name)) {
const filter = createHttpFilter(filterConfig.config, routeHttpFilterOverrides.get(filterConfig.name)!);
if (filter) {
extraFilterFactories.push(filter);
}
} else if (virtualHostHttpFilterOverrides.has(filterConfig.name)) {
const filter = createHttpFilter(filterConfig.config, virtualHostHttpFilterOverrides.get(filterConfig.name)!);
if (filter) {
extraFilterFactories.push(filter);
}
} else {
const filter = createHttpFilter(filterConfig.config);
if (filter) {
extraFilterFactories.push(filter);
}
}
}
if (!this.hasRouterFilter) {
extraFilterFactories.push(new NoRouterFilterFactory());
}
}
routeAction = new SingleClusterRouteAction(cluster, timeout, extraFilterFactories);
break;
}
case 'weighted_clusters': {
const weightedClusters: WeightedCluster[] = [];
for (const clusterWeight of route.route!.weighted_clusters!.clusters) {
allConfigClusters.add(clusterWeight.name);
weightedClusters.push({name: clusterWeight.name, weight: clusterWeight.weight?.value ?? 0});
const extraFilterFactories: FilterFactory<Filter>[] = [];
const clusterHttpFilterOverrides = new Map<string, HttpFilterConfig>();
if (EXPERIMENTAL_FAULT_INJECTION) {
for (const [name, filter] of Object.entries(clusterWeight.typed_per_filter_config ?? {})) {
const parsedConfig = parseOverrideFilterConfig(filter);
if (parsedConfig) {
clusterHttpFilterOverrides.set(name, parsedConfig);
}
}
for (const filterConfig of this.ldsHttpFilterConfigs) {
if (clusterHttpFilterOverrides.has(filterConfig.name)) {
const filter = createHttpFilter(filterConfig.config, clusterHttpFilterOverrides.get(filterConfig.name)!);
if (filter) {
extraFilterFactories.push(filter);
}
} else if (routeHttpFilterOverrides.has(filterConfig.name)) {
const filter = createHttpFilter(filterConfig.config, routeHttpFilterOverrides.get(filterConfig.name)!);
if (filter) {
extraFilterFactories.push(filter);
}
} else if (virtualHostHttpFilterOverrides.has(filterConfig.name)) {
const filter = createHttpFilter(filterConfig.config, virtualHostHttpFilterOverrides.get(filterConfig.name)!);
if (filter) {
extraFilterFactories.push(filter);
}
} else {
const filter = createHttpFilter(filterConfig.config);
if (filter) {
extraFilterFactories.push(filter);
}
}
}
if (!this.hasRouterFilter) {
extraFilterFactories.push(new NoRouterFilterFactory());
}
}
weightedClusters.push({name: clusterWeight.name, weight: clusterWeight.weight?.value ?? 0, extraFilterFactories: extraFilterFactories});
}
routeAction = new WeightedClusterRouteAction(weightedClusters, route.route!.weighted_clusters!.total_weight?.value ?? 100, timeout);
}
@ -376,16 +497,17 @@ class XdsResolver implements Resolver {
const configSelector: ConfigSelector = (methodName, metadata) => {
for (const {matcher, action} of matchList) {
if (matcher.apply(methodName, metadata)) {
const clusterName = action.getCluster();
this.refCluster(clusterName);
const clusterResult = action.getCluster();
this.refCluster(clusterResult.name);
const onCommitted = () => {
this.unrefCluster(clusterName);
this.unrefCluster(clusterResult.name);
}
return {
methodConfig: {name: [], timeout: action.getTimeout()},
onCommitted: onCommitted,
pickInformation: {cluster: clusterName},
status: status.OK
pickInformation: {cluster: clusterResult.name},
status: status.OK,
extraFilterFactories: clusterResult.extraFilterFactories
};
}
}
@ -393,7 +515,8 @@ class XdsResolver implements Resolver {
methodConfig: {name: []},
// cluster won't be used here, but it's set because of some TypeScript weirdness
pickInformation: {cluster: ''},
status: status.UNAVAILABLE
status: status.UNAVAILABLE,
extraFilterFactories: []
};
};
trace('Created ConfigSelector with configuration:');

View File

@ -16,10 +16,17 @@
import { experimental } from '@grpc/grpc-js';
import Duration = experimental.Duration;
import Filter = experimental.Filter;
import FilterFactory = experimental.FilterFactory;
export interface ClusterResult {
name: string;
extraFilterFactories: FilterFactory<Filter>[];
}
export interface RouteAction {
toString(): string;
getCluster(): string;
getCluster(): ClusterResult;
getTimeout(): Duration | undefined;
}
@ -33,10 +40,13 @@ function durationToLogString(duration: Duration) {
}
export class SingleClusterRouteAction implements RouteAction {
constructor(private cluster: string, private timeout: Duration | undefined) {}
constructor(private cluster: string, private timeout: Duration | undefined, private extraFilterFactories: FilterFactory<Filter>[]) {}
getCluster() {
return this.cluster;
return {
name: this.cluster,
extraFilterFactories: this.extraFilterFactories
};
}
toString() {
@ -55,11 +65,13 @@ export class SingleClusterRouteAction implements RouteAction {
export interface WeightedCluster {
name: string;
weight: number;
extraFilterFactories: FilterFactory<Filter>[];
}
interface ClusterChoice {
name: string;
numerator: number;
extraFilterFactories: FilterFactory<Filter>[];
}
export class WeightedClusterRouteAction implements RouteAction {
@ -72,7 +84,7 @@ export class WeightedClusterRouteAction implements RouteAction {
let lastNumerator = 0;
for (const clusterWeight of clusters) {
lastNumerator += clusterWeight.weight;
this.clusterChoices.push({name: clusterWeight.name, numerator: lastNumerator});
this.clusterChoices.push({name: clusterWeight.name, numerator: lastNumerator, extraFilterFactories: clusterWeight.extraFilterFactories});
}
}
@ -80,11 +92,14 @@ export class WeightedClusterRouteAction implements RouteAction {
const randomNumber = Math.random() * this.totalWeight;
for (const choice of this.clusterChoices) {
if (randomNumber < choice.numerator) {
return choice.name;
return {
name: choice.name,
extraFilterFactories: choice.extraFilterFactories
};
}
}
// This should be prevented by the validation rules
return '';
return {name: '', extraFilterFactories: []};
}
toString() {

View File

@ -22,6 +22,8 @@ import { RdsState } from "./rds-state";
import { Watcher, XdsStreamState } from "./xds-stream-state";
import { HttpConnectionManager__Output } from '../generated/envoy/extensions/filters/network/http_connection_manager/v3/HttpConnectionManager';
import { decodeSingleResource, HTTP_CONNECTION_MANGER_TYPE_URL_V2, HTTP_CONNECTION_MANGER_TYPE_URL_V3 } from '../resources';
import { validateTopLevelFilter } from '../http-filter';
import { EXPERIMENTAL_FAULT_INJECTION } from '../environment';
const TRACER_NAME = 'xds_client';
@ -100,6 +102,13 @@ export class LdsState implements XdsStreamState<Listener__Output> {
return false;
}
const httpConnectionManager = decodeSingleResource(HTTP_CONNECTION_MANGER_TYPE_URL_V3, message.api_listener!.api_listener.value);
if (EXPERIMENTAL_FAULT_INJECTION) {
for (const httpFilter of httpConnectionManager.http_filters) {
if (!validateTopLevelFilter(httpFilter)) {
return false;
}
}
}
switch (httpConnectionManager.route_specifier) {
case 'rds':
return !!httpConnectionManager.rds?.config_source?.ads;

View File

@ -16,7 +16,9 @@
*/
import { experimental, logVerbosity, StatusObject } from "@grpc/grpc-js";
import { EXPERIMENTAL_FAULT_INJECTION } from "../environment";
import { RouteConfiguration__Output } from "../generated/envoy/config/route/v3/RouteConfiguration";
import { validateOverrideFilter } from "../http-filter";
import { CdsLoadBalancingConfig } from "../load-balancer-cds";
import { Watcher, XdsStreamState } from "./xds-stream-state";
import ServiceConfig = experimental.ServiceConfig;
@ -112,6 +114,13 @@ export class RdsState implements XdsStreamState<RouteConfiguration__Output> {
return false;
}
}
if (EXPERIMENTAL_FAULT_INJECTION) {
for (const filterConfig of Object.values(virtualHost.typed_per_filter_config ?? {})) {
if (!validateOverrideFilter(filterConfig)) {
return false;
}
}
}
for (const route of virtualHost.routes) {
const match = route.match;
if (!match) {
@ -131,6 +140,13 @@ export class RdsState implements XdsStreamState<RouteConfiguration__Output> {
if ((route.route === undefined) || SUPPORTED_CLUSTER_SPECIFIERS.indexOf(route.route.cluster_specifier) < 0) {
return false;
}
if (EXPERIMENTAL_FAULT_INJECTION) {
for (const filterConfig of Object.values(route.typed_per_filter_config ?? {})) {
if (!validateOverrideFilter(filterConfig)) {
return false;
}
}
}
if (route.route!.cluster_specifier === 'weighted_clusters') {
let weightSum = 0;
for (const clusterWeight of route.route.weighted_clusters!.clusters) {
@ -139,6 +155,15 @@ export class RdsState implements XdsStreamState<RouteConfiguration__Output> {
if (weightSum !== route.route.weighted_clusters!.total_weight?.value ?? 100) {
return false;
}
if (EXPERIMENTAL_FAULT_INJECTION) {
for (const weightedCluster of route.route!.weighted_clusters!.clusters) {
for (const filterConfig of Object.values(weightedCluster.typed_per_filter_config ?? {})) {
if (!validateOverrideFilter(filterConfig)) {
return false;
}
}
}
}
}
}
}

View File

@ -721,6 +721,10 @@ export class Http2CallStream implements Call {
this.configDeadline = configDeadline;
}
addFilterFactories(extraFilterFactories: FilterFactory<Filter>[]) {
this.filterStack.push(extraFilterFactories.map(filterFactory => filterFactory.createFilter(this)));
}
startRead() {
/* If the stream has ended with an error, we should not emit any more
* messages and we should communicate that the stream has ended */

View File

@ -536,6 +536,7 @@ export class ChannelImplementation implements Channel {
// Refreshing the filters makes the deadline filter pick up the new deadline
stream.filterStack.refresh();
}
stream.addFilterFactories(callConfig.extraFilterFactories);
this.tryPick(stream, metadata, callConfig);
} else {
stream.cancelWithStatus(callConfig.status, "Failed to route call to method " + stream.getMethod());

View File

@ -24,12 +24,14 @@ import { GrpcUri, uriToString } from './uri-parser';
import { ChannelOptions } from './channel-options';
import { Metadata } from './metadata';
import { Status } from './constants';
import { Filter, FilterFactory } from './filter';
export interface CallConfig {
methodConfig: MethodConfig;
onCommitted?: () => void;
pickInformation: {[key: string]: string};
status: Status;
extraFilterFactories: FilterFactory<Filter>[];
}
/**

View File

@ -58,7 +58,8 @@ function getDefaultConfigSelector(serviceConfig: ServiceConfig | null): ConfigSe
return {
methodConfig: methodConfig,
pickInformation: {},
status: Status.OK
status: Status.OK,
extraFilterFactories: []
};
}
}
@ -67,7 +68,8 @@ function getDefaultConfigSelector(serviceConfig: ServiceConfig | null): ConfigSe
return {
methodConfig: {name: []},
pickInformation: {},
status: Status.OK
status: Status.OK,
extraFilterFactories: []
};
}
}