Skip to content

Commit d95ea30

Browse files
authored
Merge pull request #2823 from murgatroid99/grpc-js-xds_file_watcher_plugin
grpc-js-xds: Add bootstrap certificate provider config handling
2 parents 3f84a73 + b16e1c9 commit d95ea30

File tree

5 files changed

+119
-8
lines changed

5 files changed

+119
-8
lines changed

Diff for: packages/grpc-js-xds/src/xds-bootstrap.ts

+82-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ import * as fs from 'fs';
1919
import { EXPERIMENTAL_FEDERATION } from './environment';
2020
import { Struct } from './generated/google/protobuf/Struct';
2121
import { Value } from './generated/google/protobuf/Value';
22+
import { experimental } from '@grpc/grpc-js';
23+
24+
import parseDuration = experimental.parseDuration;
25+
import durationToMs = experimental.durationToMs;
26+
import FileWatcherCertificateProviderConfig = experimental.FileWatcherCertificateProviderConfig;
2227

2328
/* eslint-disable @typescript-eslint/no-explicit-any */
2429

@@ -51,12 +56,20 @@ export interface Authority {
5156
xdsServers?: XdsServerConfig[];
5257
}
5358

59+
export type PluginConfig = FileWatcherCertificateProviderConfig;
60+
61+
export interface CertificateProviderConfig {
62+
pluginName: string;
63+
config: PluginConfig;
64+
}
65+
5466
export interface BootstrapInfo {
5567
xdsServers: XdsServerConfig[];
5668
node: Node;
5769
authorities: {[authorityName: string]: Authority};
5870
clientDefaultListenerResourceNameTemplate: string;
5971
serverListenerResourceNameTemplate: string | null;
72+
certificateProviders: {[instanceName: string]: CertificateProviderConfig};
6073
}
6174

6275
const KNOWN_SERVER_FEATURES = ['ignore_resource_deletion'];
@@ -306,6 +319,71 @@ function validateAuthoritiesMap(obj: any): {[authorityName: string]: Authority}
306319
return result;
307320
}
308321

322+
function validateFileWatcherPluginConfig(obj: any, instanceName: string): FileWatcherCertificateProviderConfig {
323+
if ('certificate_file' in obj && typeof obj.certificate_file !== 'string') {
324+
throw new Error(`certificate_providers[${instanceName}].config.certificate_file: expected string, got ${typeof obj.certificate_file}`);
325+
}
326+
if ('private_key_file' in obj && typeof obj.private_key_file !== 'string') {
327+
throw new Error(`certificate_providers[${instanceName}].config.private_key_file: expected string, got ${typeof obj.private_key_file}`);
328+
}
329+
if ('ca_certificate_file' in obj && typeof obj.ca_certificate_file !== 'string') {
330+
throw new Error(`certificate_providers[${instanceName}].config.ca_certificate_file: expected string, got ${typeof obj.ca_certificate_file}`);
331+
}
332+
if (typeof obj.refresh_interval !== 'string') {
333+
throw new Error(`certificate_providers[${instanceName}].config.refresh_interval: expected string, got ${typeof obj.refresh_interval}`);
334+
}
335+
if (('private_key_file' in obj) !== ('certificate_file' in obj)) {
336+
throw new Error(`certificate_providers[${instanceName}].config: private_key_file and certificate_file must be provided or omitted together`);
337+
}
338+
if (!('private_key_file' in obj) && !('ca_certificate_file' in obj)) {
339+
throw new Error(`certificate_providers[${instanceName}].config: either private_key_file and certificate_file or ca_certificate_file must be set`);
340+
}
341+
const refreshDuration = parseDuration(obj.refresh_interval);
342+
if (!refreshDuration) {
343+
throw new Error(`certificate_providers[${instanceName}].config.refresh_interval: failed to parse duration from value ${obj.refresh_interval}`);
344+
}
345+
return {
346+
certificateFile: obj.certificate_file,
347+
privateKeyFile: obj.private_key_file,
348+
caCertificateFile: obj.caCertificateFile,
349+
refreshIntervalMs: durationToMs(refreshDuration)
350+
};
351+
}
352+
353+
const pluginConfigValidators: {[typeName: string]: (obj: any, instanceName: string) => PluginConfig} = {
354+
'file_watcher': validateFileWatcherPluginConfig
355+
};
356+
357+
function validateCertificateProvider(obj: any, instanceName: string): CertificateProviderConfig {
358+
if (!('plugin_name' in obj) || typeof obj.plugin_name !== 'string') {
359+
throw new Error(`certificate_providers[${instanceName}].plugin_name: expected string, got ${typeof obj.plugin_name}`);
360+
}
361+
if (!(obj.plugin_name in pluginConfigValidators)) {
362+
throw new Error(`certificate_providers[${instanceName}]: unknown plugin_name ${obj.plugin_name}`);
363+
}
364+
if (!obj.config) {
365+
throw new Error(`certificate_providers[${instanceName}].config: expected object, got ${typeof obj.config}`);
366+
}
367+
if (!(obj.plugin_name in pluginConfigValidators)) {
368+
throw new Error(`certificate_providers[${instanceName}].config: unknown plugin_name ${obj.plugin_name}`);
369+
}
370+
return {
371+
pluginName: obj.plugin_name,
372+
config: pluginConfigValidators[obj.plugin_name]!(obj.config, instanceName)
373+
};
374+
}
375+
376+
function validateCertificateProvidersMap(obj: any): {[instanceName: string]: CertificateProviderConfig} {
377+
if (!obj) {
378+
return {};
379+
}
380+
const result: {[instanceName: string]: CertificateProviderConfig} = {};
381+
for (const [name, provider] of Object.entries(obj)) {
382+
result[name] = validateCertificateProvider(provider, name);
383+
}
384+
return result;
385+
}
386+
309387
export function validateBootstrapConfig(obj: any): BootstrapInfo {
310388
const xdsServers = obj.xds_servers.map(validateXdsServerConfig);
311389
const node = validateNode(obj.node);
@@ -325,15 +403,17 @@ export function validateBootstrapConfig(obj: any): BootstrapInfo {
325403
node: node,
326404
authorities: validateAuthoritiesMap(obj.authorities),
327405
clientDefaultListenerResourceNameTemplate: obj.client_default_listener_resource_name_template ?? '%s',
328-
serverListenerResourceNameTemplate: obj.server_listener_resource_name_template ?? null
406+
serverListenerResourceNameTemplate: obj.server_listener_resource_name_template ?? null,
407+
certificateProviders: validateCertificateProvidersMap(obj.certificate_providers)
329408
};
330409
} else {
331410
return {
332411
xdsServers: xdsServers,
333412
node: node,
334413
authorities: {},
335414
clientDefaultListenerResourceNameTemplate: '%s',
336-
serverListenerResourceNameTemplate: obj.server_listener_resource_name_template ?? null
415+
serverListenerResourceNameTemplate: obj.server_listener_resource_name_template ?? null,
416+
certificateProviders: validateCertificateProvidersMap(obj.certificate_providers)
337417
};
338418
}
339419
}

Diff for: packages/grpc-js-xds/src/xds-client.ts

+24-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { Channel, ChannelCredentials, ClientDuplexStream, Metadata, StatusObject
1919
import { XdsDecodeContext, XdsDecodeResult, XdsResourceType } from "./xds-resource-type/xds-resource-type";
2020
import { XdsResourceName, parseXdsResourceName, xdsResourceNameToString } from "./resources";
2121
import { Node } from "./generated/envoy/config/core/v3/Node";
22-
import { BootstrapInfo, XdsServerConfig, loadBootstrapInfo, serverConfigEqual } from "./xds-bootstrap";
22+
import { BootstrapInfo, CertificateProviderConfig, XdsServerConfig, loadBootstrapInfo, serverConfigEqual } from "./xds-bootstrap";
2323
import BackoffTimeout = experimental.BackoffTimeout;
2424
import { DiscoveryRequest } from "./generated/envoy/service/discovery/v3/DiscoveryRequest";
2525
import { DiscoveryResponse__Output } from "./generated/envoy/service/discovery/v3/DiscoveryResponse";
@@ -35,6 +35,8 @@ import { LoadStatsResponse__Output } from "./generated/envoy/service/load_stats/
3535
import { Locality, Locality__Output } from "./generated/envoy/config/core/v3/Locality";
3636
import { Duration } from "./generated/google/protobuf/Duration";
3737
import { registerXdsClientWithCsds } from "./csds";
38+
import CertificateProvider = experimental.CertificateProvider;
39+
import FileWatcherCertificateProvider = experimental.FileWatcherCertificateProvider;
3840

3941
const TRACER_NAME = 'xds_client';
4042

@@ -1111,6 +1113,15 @@ interface AuthorityState {
11111113

11121114
const userAgentName = 'gRPC Node Pure JS';
11131115

1116+
function createCertificateProvider(config: CertificateProviderConfig) {
1117+
switch (config.pluginName) {
1118+
case 'file_watcher':
1119+
return new FileWatcherCertificateProvider(config.config);
1120+
default:
1121+
throw new Error(`Unexpected certificate provider plugin name ${config.pluginName}`);
1122+
}
1123+
}
1124+
11141125
export class XdsClient {
11151126
/**
11161127
* authority -> authority state
@@ -1119,6 +1130,8 @@ export class XdsClient {
11191130
private clients: ClientMapEntry[] = [];
11201131
private typeRegistry: Map<string, XdsResourceType> = new Map();
11211132
private bootstrapInfo: BootstrapInfo | null = null;
1133+
private certificateProviderRegistry: Map<string, CertificateProvider> = new Map();
1134+
private certificateProviderRegistryPopulated = false;
11221135

11231136
constructor(bootstrapInfoOverride?: BootstrapInfo) {
11241137
if (bootstrapInfoOverride) {
@@ -1298,6 +1311,16 @@ export class XdsClient {
12981311
removeClusterLocalityStats(lrsServer: XdsServerConfig, clusterName: string, edsServiceName: string, locality: Locality__Output) {
12991312
this.getClient(lrsServer)?.removeClusterLocalityStats(clusterName, edsServiceName, locality);
13001313
}
1314+
1315+
getCertificateProvider(instanceName: string): CertificateProvider | undefined {
1316+
if (!this.certificateProviderRegistryPopulated) {
1317+
for (const [name, config] of Object.entries(this.getBootstrapInfo().certificateProviders)) {
1318+
this.certificateProviderRegistry.set(name, createCertificateProvider(config));
1319+
}
1320+
this.certificateProviderRegistryPopulated = true;
1321+
}
1322+
return this.certificateProviderRegistry.get(instanceName);
1323+
}
13011324
}
13021325

13031326
let singletonXdsClient: XdsClient | null = null;

Diff for: packages/grpc-js/src/certificate-provider.ts

-4
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,6 @@ export interface CertificateProvider {
4949
removeIdentityCertificateListener(listener: IdentityCertificateUpdateListener): void;
5050
}
5151

52-
export interface CertificateProviderProvider<Provider> {
53-
getInstance(): Provider;
54-
}
55-
5652
export interface FileWatcherCertificateProviderConfig {
5753
certificateFile?: string | undefined;
5854
privateKeyFile?: string | undefined;

Diff for: packages/grpc-js/src/duration.ts

+12
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,15 @@ export function durationToMs(duration: Duration): number {
3434
export function isDuration(value: any): value is Duration {
3535
return typeof value.seconds === 'number' && typeof value.nanos === 'number';
3636
}
37+
38+
const durationRegex = /^(\d+)(?:\.(\d+))?s$/;
39+
export function parseDuration(value: string): Duration | null {
40+
const match = value.match(durationRegex);
41+
if (!match) {
42+
return null;
43+
}
44+
return {
45+
seconds: Number.parseInt(match[1], 10),
46+
nanos: Number.parseInt(match[2].padEnd(9, '0'), 10)
47+
};
48+
}

Diff for: packages/grpc-js/src/experimental.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export {
77
createResolver,
88
} from './resolver';
99
export { GrpcUri, uriToString, splitHostPort, HostPort } from './uri-parser';
10-
export { Duration, durationToMs } from './duration';
10+
export { Duration, durationToMs, parseDuration } from './duration';
1111
export { BackoffTimeout } from './backoff-timeout';
1212
export {
1313
LoadBalancer,

0 commit comments

Comments
 (0)