UNPKG

10.9 kBJavaScriptView Raw
1"use strict";
2/*
3 * Copyright 2019 gRPC authors.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17 */
18Object.defineProperty(exports, "__esModule", { value: true });
19exports.extractAndSelectServiceConfig = exports.validateServiceConfig = void 0;
20/* This file implements gRFC A2 and the service config spec:
21 * https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md
22 * https://github.com/grpc/grpc/blob/master/doc/service_config.md. Each
23 * function here takes an object with unknown structure and returns its
24 * specific object type if the input has the right structure, and throws an
25 * error otherwise. */
26/* The any type is purposely used here. All functions validate their input at
27 * runtime */
28/* eslint-disable @typescript-eslint/no-explicit-any */
29const os = require("os");
30const load_balancer_1 = require("./load-balancer");
31/**
32 * Recognizes a number with up to 9 digits after the decimal point, followed by
33 * an "s", representing a number of seconds.
34 */
35const TIMEOUT_REGEX = /^\d+(\.\d{1,9})?s$/;
36/**
37 * Client language name used for determining whether this client matches a
38 * `ServiceConfigCanaryConfig`'s `clientLanguage` list.
39 */
40const CLIENT_LANGUAGE_STRING = 'node';
41function validateName(obj) {
42 if (!('service' in obj) || typeof obj.service !== 'string') {
43 throw new Error('Invalid method config name: invalid service');
44 }
45 const result = {
46 service: obj.service,
47 };
48 if ('method' in obj) {
49 if (typeof obj.method === 'string') {
50 result.method = obj.method;
51 }
52 else {
53 throw new Error('Invalid method config name: invalid method');
54 }
55 }
56 return result;
57}
58function validateMethodConfig(obj) {
59 var _a;
60 const result = {
61 name: [],
62 };
63 if (!('name' in obj) || !Array.isArray(obj.name)) {
64 throw new Error('Invalid method config: invalid name array');
65 }
66 for (const name of obj.name) {
67 result.name.push(validateName(name));
68 }
69 if ('waitForReady' in obj) {
70 if (typeof obj.waitForReady !== 'boolean') {
71 throw new Error('Invalid method config: invalid waitForReady');
72 }
73 result.waitForReady = obj.waitForReady;
74 }
75 if ('timeout' in obj) {
76 if (typeof obj.timeout === 'object') {
77 if (!('seconds' in obj.timeout) ||
78 !(typeof obj.timeout.seconds === 'number')) {
79 throw new Error('Invalid method config: invalid timeout.seconds');
80 }
81 if (!('nanos' in obj.timeout) ||
82 !(typeof obj.timeout.nanos === 'number')) {
83 throw new Error('Invalid method config: invalid timeout.nanos');
84 }
85 result.timeout = obj.timeout;
86 }
87 else if (typeof obj.timeout === 'string' &&
88 TIMEOUT_REGEX.test(obj.timeout)) {
89 const timeoutParts = obj.timeout
90 .substring(0, obj.timeout.length - 1)
91 .split('.');
92 result.timeout = {
93 seconds: timeoutParts[0] | 0,
94 nanos: ((_a = timeoutParts[1]) !== null && _a !== void 0 ? _a : 0) | 0,
95 };
96 }
97 else {
98 throw new Error('Invalid method config: invalid timeout');
99 }
100 }
101 if ('maxRequestBytes' in obj) {
102 if (typeof obj.maxRequestBytes !== 'number') {
103 throw new Error('Invalid method config: invalid maxRequestBytes');
104 }
105 result.maxRequestBytes = obj.maxRequestBytes;
106 }
107 if ('maxResponseBytes' in obj) {
108 if (typeof obj.maxResponseBytes !== 'number') {
109 throw new Error('Invalid method config: invalid maxRequestBytes');
110 }
111 result.maxResponseBytes = obj.maxResponseBytes;
112 }
113 return result;
114}
115function validateServiceConfig(obj) {
116 const result = {
117 loadBalancingConfig: [],
118 methodConfig: [],
119 };
120 if ('loadBalancingPolicy' in obj) {
121 if (typeof obj.loadBalancingPolicy === 'string') {
122 result.loadBalancingPolicy = obj.loadBalancingPolicy;
123 }
124 else {
125 throw new Error('Invalid service config: invalid loadBalancingPolicy');
126 }
127 }
128 if ('loadBalancingConfig' in obj) {
129 if (Array.isArray(obj.loadBalancingConfig)) {
130 for (const config of obj.loadBalancingConfig) {
131 result.loadBalancingConfig.push(load_balancer_1.validateLoadBalancingConfig(config));
132 }
133 }
134 else {
135 throw new Error('Invalid service config: invalid loadBalancingConfig');
136 }
137 }
138 if ('methodConfig' in obj) {
139 if (Array.isArray(obj.methodConfig)) {
140 for (const methodConfig of obj.methodConfig) {
141 result.methodConfig.push(validateMethodConfig(methodConfig));
142 }
143 }
144 }
145 // Validate method name uniqueness
146 const seenMethodNames = [];
147 for (const methodConfig of result.methodConfig) {
148 for (const name of methodConfig.name) {
149 for (const seenName of seenMethodNames) {
150 if (name.service === seenName.service &&
151 name.method === seenName.method) {
152 throw new Error(`Invalid service config: duplicate name ${name.service}/${name.method}`);
153 }
154 }
155 seenMethodNames.push(name);
156 }
157 }
158 return result;
159}
160exports.validateServiceConfig = validateServiceConfig;
161function validateCanaryConfig(obj) {
162 if (!('serviceConfig' in obj)) {
163 throw new Error('Invalid service config choice: missing service config');
164 }
165 const result = {
166 serviceConfig: validateServiceConfig(obj.serviceConfig),
167 };
168 if ('clientLanguage' in obj) {
169 if (Array.isArray(obj.clientLanguage)) {
170 result.clientLanguage = [];
171 for (const lang of obj.clientLanguage) {
172 if (typeof lang === 'string') {
173 result.clientLanguage.push(lang);
174 }
175 else {
176 throw new Error('Invalid service config choice: invalid clientLanguage');
177 }
178 }
179 }
180 else {
181 throw new Error('Invalid service config choice: invalid clientLanguage');
182 }
183 }
184 if ('clientHostname' in obj) {
185 if (Array.isArray(obj.clientHostname)) {
186 result.clientHostname = [];
187 for (const lang of obj.clientHostname) {
188 if (typeof lang === 'string') {
189 result.clientHostname.push(lang);
190 }
191 else {
192 throw new Error('Invalid service config choice: invalid clientHostname');
193 }
194 }
195 }
196 else {
197 throw new Error('Invalid service config choice: invalid clientHostname');
198 }
199 }
200 if ('percentage' in obj) {
201 if (typeof obj.percentage === 'number' &&
202 0 <= obj.percentage &&
203 obj.percentage <= 100) {
204 result.percentage = obj.percentage;
205 }
206 else {
207 throw new Error('Invalid service config choice: invalid percentage');
208 }
209 }
210 // Validate that no unexpected fields are present
211 const allowedFields = [
212 'clientLanguage',
213 'percentage',
214 'clientHostname',
215 'serviceConfig',
216 ];
217 for (const field in obj) {
218 if (!allowedFields.includes(field)) {
219 throw new Error(`Invalid service config choice: unexpected field ${field}`);
220 }
221 }
222 return result;
223}
224function validateAndSelectCanaryConfig(obj, percentage) {
225 if (!Array.isArray(obj)) {
226 throw new Error('Invalid service config list');
227 }
228 for (const config of obj) {
229 const validatedConfig = validateCanaryConfig(config);
230 /* For each field, we check if it is present, then only discard the
231 * config if the field value does not match the current client */
232 if (typeof validatedConfig.percentage === 'number' &&
233 percentage > validatedConfig.percentage) {
234 continue;
235 }
236 if (Array.isArray(validatedConfig.clientHostname)) {
237 let hostnameMatched = false;
238 for (const hostname of validatedConfig.clientHostname) {
239 if (hostname === os.hostname()) {
240 hostnameMatched = true;
241 }
242 }
243 if (!hostnameMatched) {
244 continue;
245 }
246 }
247 if (Array.isArray(validatedConfig.clientLanguage)) {
248 let languageMatched = false;
249 for (const language of validatedConfig.clientLanguage) {
250 if (language === CLIENT_LANGUAGE_STRING) {
251 languageMatched = true;
252 }
253 }
254 if (!languageMatched) {
255 continue;
256 }
257 }
258 return validatedConfig.serviceConfig;
259 }
260 throw new Error('No matching service config found');
261}
262/**
263 * Find the "grpc_config" record among the TXT records, parse its value as JSON, validate its contents,
264 * and select a service config with selection fields that all match this client. Most of these steps
265 * can fail with an error; the caller must handle any errors thrown this way.
266 * @param txtRecord The TXT record array that is output from a successful call to dns.resolveTxt
267 * @param percentage A number chosen from the range [0, 100) that is used to select which config to use
268 * @return The service configuration to use, given the percentage value, or null if the service config
269 * data has a valid format but none of the options match the current client.
270 */
271function extractAndSelectServiceConfig(txtRecord, percentage) {
272 for (const record of txtRecord) {
273 if (record.length > 0 && record[0].startsWith('grpc_config=')) {
274 /* Treat the list of strings in this record as a single string and remove
275 * "grpc_config=" from the beginning. The rest should be a JSON string */
276 const recordString = record.join('').substring('grpc_config='.length);
277 const recordJson = JSON.parse(recordString);
278 return validateAndSelectCanaryConfig(recordJson, percentage);
279 }
280 }
281 return null;
282}
283exports.extractAndSelectServiceConfig = extractAndSelectServiceConfig;
284//# sourceMappingURL=service-config.js.map
\No newline at end of file