etquia
Version:
Dummy package for the grpc-node repository
310 lines (290 loc) • 8.59 kB
text/typescript
/*
* 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 * as fs from 'fs';
import { Struct } from './generated/google/protobuf/Struct';
import { Value } from './generated/google/protobuf/Value';
/* eslint-disable @typescript-eslint/no-explicit-any */
export interface Locality {
region?: string;
zone?: string;
sub_zone?: string;
}
export interface Node {
id: string,
locality: Locality;
cluster?: string;
metadata?: Struct;
}
export interface ChannelCredsConfig {
type: string;
config?: object;
}
export interface XdsServerConfig {
serverUri: string;
channelCreds: ChannelCredsConfig[];
serverFeatures: string[];
}
export interface BootstrapInfo {
xdsServers: XdsServerConfig[];
node: Node;
}
function validateChannelCredsConfig(obj: any): ChannelCredsConfig {
if (!('type' in obj)) {
throw new Error('type field missing in xds_servers.channel_creds element');
}
if (typeof obj.type !== 'string') {
throw new Error(
`xds_servers.channel_creds.type field: expected string, got ${typeof obj.type}`
);
}
if ('config' in obj) {
if (typeof obj.config !== 'object' || obj.config === null) {
throw new Error(
'xds_servers.channel_creds config field must be an object if provided'
);
}
}
return {
type: obj.type,
config: obj.config,
};
}
function validateXdsServerConfig(obj: any): XdsServerConfig {
if (!('server_uri' in obj)) {
throw new Error('server_uri field missing in xds_servers element');
}
if (typeof obj.server_uri !== 'string') {
throw new Error(
`xds_servers.server_uri field: expected string, got ${typeof obj.server_uri}`
);
}
if (!('channel_creds' in obj)) {
throw new Error('channel_creds missing in xds_servers element');
}
if (!Array.isArray(obj.channel_creds)) {
throw new Error(
`xds_servers.channel_creds field: expected array, got ${typeof obj.channel_creds}`
);
}
if (obj.channel_creds.length === 0) {
throw new Error(
'xds_servers.channel_creds field: at least one entry is required'
);
}
if ('server_features' in obj) {
if (!Array.isArray(obj.server_features)) {
throw new Error(
`xds_servers.server_features field: expected array, got ${typeof obj.server_features}`
);
}
for (const feature of obj.server_features) {
if (typeof feature !== 'string') {
`xds_servers.server_features field element: expected string, got ${typeof feature}`
}
}
}
return {
serverUri: obj.server_uri,
channelCreds: obj.channel_creds.map(validateChannelCredsConfig),
serverFeatures: obj.server_features ?? []
};
}
function validateValue(obj: any): Value {
if (Array.isArray(obj)) {
return {
kind: 'listValue',
listValue: {
values: obj.map((value) => validateValue(value)),
},
};
} else {
switch (typeof obj) {
case 'boolean':
return {
kind: 'boolValue',
boolValue: obj,
};
case 'number':
return {
kind: 'numberValue',
numberValue: obj,
};
case 'string':
return {
kind: 'stringValue',
stringValue: obj,
};
case 'object':
if (obj === null) {
return {
kind: 'nullValue',
nullValue: 'NULL_VALUE',
};
} else {
return {
kind: 'structValue',
structValue: getStructFromJson(obj),
};
}
default:
throw new Error(`Could not handle struct value of type ${typeof obj}`);
}
}
}
function getStructFromJson(obj: any): Struct {
if (typeof obj !== 'object' || obj === null) {
throw new Error('Invalid JSON object for Struct field');
}
const fields: { [key: string]: Value } = {};
for (const [fieldName, value] of Object.entries(obj)) {
fields[fieldName] = validateValue(value);
}
return {
fields,
};
}
/**
* Validate that the input obj is a valid Node proto message. Only checks the
* fields we expect to see: id, cluster, locality, and metadata.
* @param obj
*/
function validateNode(obj: any): Node {
const result: Node = {
id: '',
locality: {}
};
if (!('id' in obj)) {
throw new Error('id field missing in node element');
}
if (typeof obj.id !== 'string') {
throw new Error(`node.id field: expected string, got ${typeof obj.id}`);
}
result.id = obj.id;
if (!('locality' in obj)) {
throw new Error('locality field missing in node element');
}
result.locality = {};
if ('region' in obj.locality) {
if (typeof obj.locality.region !== 'string') {
throw new Error(
`node.locality.region field: expected string, got ${typeof obj.locality
.region}`
);
}
result.locality.region = obj.locality.region;
}
if ('zone' in obj.locality) {
if (typeof obj.locality.zone !== 'string') {
throw new Error(
`node.locality.zone field: expected string, got ${typeof obj.locality
.zone}`
);
}
result.locality.zone = obj.locality.zone;
}
if ('sub_zone' in obj.locality) {
if (typeof obj.locality.sub_zone !== 'string') {
throw new Error(
`node.locality.sub_zone field: expected string, got ${typeof obj
.locality.sub_zone}`
);
}
result.locality.sub_zone = obj.locality.sub_zone;
}
if ('cluster' in obj) {
if (typeof obj.cluster !== 'string') {
throw new Error(
`node.cluster field: expected string, got ${typeof obj.cluster}`
);
}
result.cluster = obj.cluster;
}
if ('metadata' in obj) {
result.metadata = getStructFromJson(obj.metadata);
}
return result;
}
function validateBootstrapFile(obj: any): BootstrapInfo {
return {
xdsServers: obj.xds_servers.map(validateXdsServerConfig),
node: validateNode(obj.node),
};
}
let loadedBootstrapInfo: Promise<BootstrapInfo> | null = null;
export async function loadBootstrapInfo(): Promise<BootstrapInfo> {
if (loadedBootstrapInfo !== null) {
return loadedBootstrapInfo;
}
/**
* If GRPC_XDS_BOOTSTRAP exists
* then use its value as the name of the bootstrap file.
*
* If the file is missing or the contents of the file are malformed,
* return an error.
*/
const bootstrapPath = process.env.GRPC_XDS_BOOTSTRAP;
if (bootstrapPath) {
loadedBootstrapInfo = new Promise((resolve, reject) => {
fs.readFile(bootstrapPath, { encoding: 'utf8' }, (err, data) => {
if (err) {
reject(
new Error(
`Failed to read xDS bootstrap file from path ${bootstrapPath} with error ${err.message}`
)
);
}
try {
const parsedFile = JSON.parse(data);
resolve(validateBootstrapFile(parsedFile));
} catch (e) {
reject(
new Error(
`Failed to parse xDS bootstrap file at path ${bootstrapPath} with error ${e.message}`
)
);
}
});
});
return loadedBootstrapInfo;
}
/**
* Else, if GRPC_XDS_BOOTSTRAP_CONFIG exists
* then use its value as the bootstrap config.
*
* If the value is malformed, return an error.
*
* See: https://github.com/grpc/grpc-node/issues/1868
*/
const bootstrapConfig = process.env.GRPC_XDS_BOOTSTRAP_CONFIG;
if (bootstrapConfig) {
try {
const parsedConfig = JSON.parse(bootstrapConfig);
const loadedBootstrapInfoValue = validateBootstrapFile(parsedConfig);
loadedBootstrapInfo = Promise.resolve(loadedBootstrapInfoValue);
} catch (e) {
throw new Error(
`Failed to parse xDS bootstrap config from environment variable GRPC_XDS_BOOTSTRAP_CONFIG with error ${e.message}`
);
}
return loadedBootstrapInfo;
}
return Promise.reject(
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'
)
);
}