UNPKG

10.1 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 const result = {
60 name: [],
61 };
62 if (!('name' in obj) || !Array.isArray(obj.name)) {
63 throw new Error('Invalid method config: invalid name array');
64 }
65 for (const name of obj.name) {
66 result.name.push(validateName(name));
67 }
68 if ('waitForReady' in obj) {
69 if (typeof obj.waitForReady !== 'boolean') {
70 throw new Error('Invalid method config: invalid waitForReady');
71 }
72 result.waitForReady = obj.waitForReady;
73 }
74 if ('timeout' in obj) {
75 if (!(typeof obj.timeout === 'string') ||
76 !TIMEOUT_REGEX.test(obj.timeout)) {
77 throw new Error('Invalid method config: invalid timeout');
78 }
79 result.timeout = obj.timeout;
80 }
81 if ('maxRequestBytes' in obj) {
82 if (typeof obj.maxRequestBytes !== 'number') {
83 throw new Error('Invalid method config: invalid maxRequestBytes');
84 }
85 result.maxRequestBytes = obj.maxRequestBytes;
86 }
87 if ('maxResponseBytes' in obj) {
88 if (typeof obj.maxResponseBytes !== 'number') {
89 throw new Error('Invalid method config: invalid maxRequestBytes');
90 }
91 result.maxResponseBytes = obj.maxResponseBytes;
92 }
93 return result;
94}
95function validateServiceConfig(obj) {
96 const result = {
97 loadBalancingConfig: [],
98 methodConfig: [],
99 };
100 if ('loadBalancingPolicy' in obj) {
101 if (typeof obj.loadBalancingPolicy === 'string') {
102 result.loadBalancingPolicy = obj.loadBalancingPolicy;
103 }
104 else {
105 throw new Error('Invalid service config: invalid loadBalancingPolicy');
106 }
107 }
108 if ('loadBalancingConfig' in obj) {
109 if (Array.isArray(obj.loadBalancingConfig)) {
110 for (const config of obj.loadBalancingConfig) {
111 result.loadBalancingConfig.push(load_balancer_1.validateLoadBalancingConfig(config));
112 }
113 }
114 else {
115 throw new Error('Invalid service config: invalid loadBalancingConfig');
116 }
117 }
118 if ('methodConfig' in obj) {
119 if (Array.isArray(obj.methodConfig)) {
120 for (const methodConfig of obj.methodConfig) {
121 result.methodConfig.push(validateMethodConfig(methodConfig));
122 }
123 }
124 }
125 // Validate method name uniqueness
126 const seenMethodNames = [];
127 for (const methodConfig of result.methodConfig) {
128 for (const name of methodConfig.name) {
129 for (const seenName of seenMethodNames) {
130 if (name.service === seenName.service &&
131 name.method === seenName.method) {
132 throw new Error(`Invalid service config: duplicate name ${name.service}/${name.method}`);
133 }
134 }
135 seenMethodNames.push(name);
136 }
137 }
138 return result;
139}
140exports.validateServiceConfig = validateServiceConfig;
141function validateCanaryConfig(obj) {
142 if (!('serviceConfig' in obj)) {
143 throw new Error('Invalid service config choice: missing service config');
144 }
145 const result = {
146 serviceConfig: validateServiceConfig(obj.serviceConfig),
147 };
148 if ('clientLanguage' in obj) {
149 if (Array.isArray(obj.clientLanguage)) {
150 result.clientLanguage = [];
151 for (const lang of obj.clientLanguage) {
152 if (typeof lang === 'string') {
153 result.clientLanguage.push(lang);
154 }
155 else {
156 throw new Error('Invalid service config choice: invalid clientLanguage');
157 }
158 }
159 }
160 else {
161 throw new Error('Invalid service config choice: invalid clientLanguage');
162 }
163 }
164 if ('clientHostname' in obj) {
165 if (Array.isArray(obj.clientHostname)) {
166 result.clientHostname = [];
167 for (const lang of obj.clientHostname) {
168 if (typeof lang === 'string') {
169 result.clientHostname.push(lang);
170 }
171 else {
172 throw new Error('Invalid service config choice: invalid clientHostname');
173 }
174 }
175 }
176 else {
177 throw new Error('Invalid service config choice: invalid clientHostname');
178 }
179 }
180 if ('percentage' in obj) {
181 if (typeof obj.percentage === 'number' &&
182 0 <= obj.percentage &&
183 obj.percentage <= 100) {
184 result.percentage = obj.percentage;
185 }
186 else {
187 throw new Error('Invalid service config choice: invalid percentage');
188 }
189 }
190 // Validate that no unexpected fields are present
191 const allowedFields = [
192 'clientLanguage',
193 'percentage',
194 'clientHostname',
195 'serviceConfig',
196 ];
197 for (const field in obj) {
198 if (!allowedFields.includes(field)) {
199 throw new Error(`Invalid service config choice: unexpected field ${field}`);
200 }
201 }
202 return result;
203}
204function validateAndSelectCanaryConfig(obj, percentage) {
205 if (!Array.isArray(obj)) {
206 throw new Error('Invalid service config list');
207 }
208 for (const config of obj) {
209 const validatedConfig = validateCanaryConfig(config);
210 /* For each field, we check if it is present, then only discard the
211 * config if the field value does not match the current client */
212 if (typeof validatedConfig.percentage === 'number' &&
213 percentage > validatedConfig.percentage) {
214 continue;
215 }
216 if (Array.isArray(validatedConfig.clientHostname)) {
217 let hostnameMatched = false;
218 for (const hostname of validatedConfig.clientHostname) {
219 if (hostname === os.hostname()) {
220 hostnameMatched = true;
221 }
222 }
223 if (!hostnameMatched) {
224 continue;
225 }
226 }
227 if (Array.isArray(validatedConfig.clientLanguage)) {
228 let languageMatched = false;
229 for (const language of validatedConfig.clientLanguage) {
230 if (language === CLIENT_LANGUAGE_STRING) {
231 languageMatched = true;
232 }
233 }
234 if (!languageMatched) {
235 continue;
236 }
237 }
238 return validatedConfig.serviceConfig;
239 }
240 throw new Error('No matching service config found');
241}
242/**
243 * Find the "grpc_config" record among the TXT records, parse its value as JSON, validate its contents,
244 * and select a service config with selection fields that all match this client. Most of these steps
245 * can fail with an error; the caller must handle any errors thrown this way.
246 * @param txtRecord The TXT record array that is output from a successful call to dns.resolveTxt
247 * @param percentage A number chosen from the range [0, 100) that is used to select which config to use
248 * @return The service configuration to use, given the percentage value, or null if the service config
249 * data has a valid format but none of the options match the current client.
250 */
251function extractAndSelectServiceConfig(txtRecord, percentage) {
252 for (const record of txtRecord) {
253 if (record.length > 0 && record[0].startsWith('grpc_config=')) {
254 /* Treat the list of strings in this record as a single string and remove
255 * "grpc_config=" from the beginning. The rest should be a JSON string */
256 const recordString = record.join('').substring('grpc_config='.length);
257 const recordJson = JSON.parse(recordString);
258 return validateAndSelectCanaryConfig(recordJson, percentage);
259 }
260 }
261 return null;
262}
263exports.extractAndSelectServiceConfig = extractAndSelectServiceConfig;
264//# sourceMappingURL=service-config.js.map
\No newline at end of file