Skip to content

Commit

Permalink
Merge pull request #2823 from murgatroid99/grpc-js-xds_file_watcher_p…
Browse files Browse the repository at this point in the history
…lugin

grpc-js-xds: Add bootstrap certificate provider config handling
  • Loading branch information
murgatroid99 authored Sep 17, 2024
2 parents 3f84a73 + b16e1c9 commit d95ea30
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 8 deletions.
84 changes: 82 additions & 2 deletions packages/grpc-js-xds/src/xds-bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ import * as fs from 'fs';
import { EXPERIMENTAL_FEDERATION } from './environment';
import { Struct } from './generated/google/protobuf/Struct';
import { Value } from './generated/google/protobuf/Value';
import { experimental } from '@grpc/grpc-js';

import parseDuration = experimental.parseDuration;
import durationToMs = experimental.durationToMs;
import FileWatcherCertificateProviderConfig = experimental.FileWatcherCertificateProviderConfig;

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

Expand Down Expand Up @@ -51,12 +56,20 @@ export interface Authority {
xdsServers?: XdsServerConfig[];
}

export type PluginConfig = FileWatcherCertificateProviderConfig;

export interface CertificateProviderConfig {
pluginName: string;
config: PluginConfig;
}

export interface BootstrapInfo {
xdsServers: XdsServerConfig[];
node: Node;
authorities: {[authorityName: string]: Authority};
clientDefaultListenerResourceNameTemplate: string;
serverListenerResourceNameTemplate: string | null;
certificateProviders: {[instanceName: string]: CertificateProviderConfig};
}

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

function validateFileWatcherPluginConfig(obj: any, instanceName: string): FileWatcherCertificateProviderConfig {
if ('certificate_file' in obj && typeof obj.certificate_file !== 'string') {
throw new Error(`certificate_providers[${instanceName}].config.certificate_file: expected string, got ${typeof obj.certificate_file}`);
}
if ('private_key_file' in obj && typeof obj.private_key_file !== 'string') {
throw new Error(`certificate_providers[${instanceName}].config.private_key_file: expected string, got ${typeof obj.private_key_file}`);
}
if ('ca_certificate_file' in obj && typeof obj.ca_certificate_file !== 'string') {
throw new Error(`certificate_providers[${instanceName}].config.ca_certificate_file: expected string, got ${typeof obj.ca_certificate_file}`);
}
if (typeof obj.refresh_interval !== 'string') {
throw new Error(`certificate_providers[${instanceName}].config.refresh_interval: expected string, got ${typeof obj.refresh_interval}`);
}
if (('private_key_file' in obj) !== ('certificate_file' in obj)) {
throw new Error(`certificate_providers[${instanceName}].config: private_key_file and certificate_file must be provided or omitted together`);
}
if (!('private_key_file' in obj) && !('ca_certificate_file' in obj)) {
throw new Error(`certificate_providers[${instanceName}].config: either private_key_file and certificate_file or ca_certificate_file must be set`);
}
const refreshDuration = parseDuration(obj.refresh_interval);
if (!refreshDuration) {
throw new Error(`certificate_providers[${instanceName}].config.refresh_interval: failed to parse duration from value ${obj.refresh_interval}`);
}
return {
certificateFile: obj.certificate_file,
privateKeyFile: obj.private_key_file,
caCertificateFile: obj.caCertificateFile,
refreshIntervalMs: durationToMs(refreshDuration)
};
}

const pluginConfigValidators: {[typeName: string]: (obj: any, instanceName: string) => PluginConfig} = {
'file_watcher': validateFileWatcherPluginConfig
};

function validateCertificateProvider(obj: any, instanceName: string): CertificateProviderConfig {
if (!('plugin_name' in obj) || typeof obj.plugin_name !== 'string') {
throw new Error(`certificate_providers[${instanceName}].plugin_name: expected string, got ${typeof obj.plugin_name}`);
}
if (!(obj.plugin_name in pluginConfigValidators)) {
throw new Error(`certificate_providers[${instanceName}]: unknown plugin_name ${obj.plugin_name}`);
}
if (!obj.config) {
throw new Error(`certificate_providers[${instanceName}].config: expected object, got ${typeof obj.config}`);
}
if (!(obj.plugin_name in pluginConfigValidators)) {
throw new Error(`certificate_providers[${instanceName}].config: unknown plugin_name ${obj.plugin_name}`);
}
return {
pluginName: obj.plugin_name,
config: pluginConfigValidators[obj.plugin_name]!(obj.config, instanceName)
};
}

function validateCertificateProvidersMap(obj: any): {[instanceName: string]: CertificateProviderConfig} {
if (!obj) {
return {};
}
const result: {[instanceName: string]: CertificateProviderConfig} = {};
for (const [name, provider] of Object.entries(obj)) {
result[name] = validateCertificateProvider(provider, name);
}
return result;
}

export function validateBootstrapConfig(obj: any): BootstrapInfo {
const xdsServers = obj.xds_servers.map(validateXdsServerConfig);
const node = validateNode(obj.node);
Expand All @@ -325,15 +403,17 @@ export function validateBootstrapConfig(obj: any): BootstrapInfo {
node: node,
authorities: validateAuthoritiesMap(obj.authorities),
clientDefaultListenerResourceNameTemplate: obj.client_default_listener_resource_name_template ?? '%s',
serverListenerResourceNameTemplate: obj.server_listener_resource_name_template ?? null
serverListenerResourceNameTemplate: obj.server_listener_resource_name_template ?? null,
certificateProviders: validateCertificateProvidersMap(obj.certificate_providers)
};
} else {
return {
xdsServers: xdsServers,
node: node,
authorities: {},
clientDefaultListenerResourceNameTemplate: '%s',
serverListenerResourceNameTemplate: obj.server_listener_resource_name_template ?? null
serverListenerResourceNameTemplate: obj.server_listener_resource_name_template ?? null,
certificateProviders: validateCertificateProvidersMap(obj.certificate_providers)
};
}
}
Expand Down
25 changes: 24 additions & 1 deletion packages/grpc-js-xds/src/xds-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { Channel, ChannelCredentials, ClientDuplexStream, Metadata, StatusObject
import { XdsDecodeContext, XdsDecodeResult, XdsResourceType } from "./xds-resource-type/xds-resource-type";
import { XdsResourceName, parseXdsResourceName, xdsResourceNameToString } from "./resources";
import { Node } from "./generated/envoy/config/core/v3/Node";
import { BootstrapInfo, XdsServerConfig, loadBootstrapInfo, serverConfigEqual } from "./xds-bootstrap";
import { BootstrapInfo, CertificateProviderConfig, XdsServerConfig, loadBootstrapInfo, serverConfigEqual } from "./xds-bootstrap";
import BackoffTimeout = experimental.BackoffTimeout;
import { DiscoveryRequest } from "./generated/envoy/service/discovery/v3/DiscoveryRequest";
import { DiscoveryResponse__Output } from "./generated/envoy/service/discovery/v3/DiscoveryResponse";
Expand All @@ -35,6 +35,8 @@ import { LoadStatsResponse__Output } from "./generated/envoy/service/load_stats/
import { Locality, Locality__Output } from "./generated/envoy/config/core/v3/Locality";
import { Duration } from "./generated/google/protobuf/Duration";
import { registerXdsClientWithCsds } from "./csds";
import CertificateProvider = experimental.CertificateProvider;
import FileWatcherCertificateProvider = experimental.FileWatcherCertificateProvider;

const TRACER_NAME = 'xds_client';

Expand Down Expand Up @@ -1111,6 +1113,15 @@ interface AuthorityState {

const userAgentName = 'gRPC Node Pure JS';

function createCertificateProvider(config: CertificateProviderConfig) {
switch (config.pluginName) {
case 'file_watcher':
return new FileWatcherCertificateProvider(config.config);
default:
throw new Error(`Unexpected certificate provider plugin name ${config.pluginName}`);
}
}

export class XdsClient {
/**
* authority -> authority state
Expand All @@ -1119,6 +1130,8 @@ export class XdsClient {
private clients: ClientMapEntry[] = [];
private typeRegistry: Map<string, XdsResourceType> = new Map();
private bootstrapInfo: BootstrapInfo | null = null;
private certificateProviderRegistry: Map<string, CertificateProvider> = new Map();
private certificateProviderRegistryPopulated = false;

constructor(bootstrapInfoOverride?: BootstrapInfo) {
if (bootstrapInfoOverride) {
Expand Down Expand Up @@ -1298,6 +1311,16 @@ export class XdsClient {
removeClusterLocalityStats(lrsServer: XdsServerConfig, clusterName: string, edsServiceName: string, locality: Locality__Output) {
this.getClient(lrsServer)?.removeClusterLocalityStats(clusterName, edsServiceName, locality);
}

getCertificateProvider(instanceName: string): CertificateProvider | undefined {
if (!this.certificateProviderRegistryPopulated) {
for (const [name, config] of Object.entries(this.getBootstrapInfo().certificateProviders)) {
this.certificateProviderRegistry.set(name, createCertificateProvider(config));
}
this.certificateProviderRegistryPopulated = true;
}
return this.certificateProviderRegistry.get(instanceName);
}
}

let singletonXdsClient: XdsClient | null = null;
Expand Down
4 changes: 0 additions & 4 deletions packages/grpc-js/src/certificate-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,6 @@ export interface CertificateProvider {
removeIdentityCertificateListener(listener: IdentityCertificateUpdateListener): void;
}

export interface CertificateProviderProvider<Provider> {
getInstance(): Provider;
}

export interface FileWatcherCertificateProviderConfig {
certificateFile?: string | undefined;
privateKeyFile?: string | undefined;
Expand Down
12 changes: 12 additions & 0 deletions packages/grpc-js/src/duration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,15 @@ export function durationToMs(duration: Duration): number {
export function isDuration(value: any): value is Duration {
return typeof value.seconds === 'number' && typeof value.nanos === 'number';
}

const durationRegex = /^(\d+)(?:\.(\d+))?s$/;
export function parseDuration(value: string): Duration | null {
const match = value.match(durationRegex);
if (!match) {
return null;
}
return {
seconds: Number.parseInt(match[1], 10),
nanos: Number.parseInt(match[2].padEnd(9, '0'), 10)
};
}
2 changes: 1 addition & 1 deletion packages/grpc-js/src/experimental.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export {
createResolver,
} from './resolver';
export { GrpcUri, uriToString, splitHostPort, HostPort } from './uri-parser';
export { Duration, durationToMs } from './duration';
export { Duration, durationToMs, parseDuration } from './duration';
export { BackoffTimeout } from './backoff-timeout';
export {
LoadBalancer,
Expand Down

0 comments on commit d95ea30

Please sign in to comment.