Skip to content

Commit 858d1b6

Browse files
committed
grpc-js-xds: Implement CSDS
1 parent dca3670 commit 858d1b6

File tree

2 files changed

+206
-0
lines changed

2 files changed

+206
-0
lines changed

packages/grpc-js-xds/src/csds.ts

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/*
2+
* Copyright 2021 gRPC authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
import { Node } from "./generated/envoy/config/core/v3/Node";
19+
import { ClientConfig, _envoy_service_status_v3_ClientConfig_GenericXdsConfig as GenericXdsConfig } from "./generated/envoy/service/status/v3/ClientConfig";
20+
import { ClientStatusDiscoveryServiceHandlers } from "./generated/envoy/service/status/v3/ClientStatusDiscoveryService";
21+
import { ClientStatusRequest__Output } from "./generated/envoy/service/status/v3/ClientStatusRequest";
22+
import { ClientStatusResponse } from "./generated/envoy/service/status/v3/ClientStatusResponse";
23+
import { Timestamp } from "./generated/google/protobuf/Timestamp";
24+
import { AdsTypeUrl, CDS_TYPE_URL_V2, CDS_TYPE_URL_V3, EDS_TYPE_URL_V2, EDS_TYPE_URL_V3, LDS_TYPE_URL_V2, LDS_TYPE_URL_V3, RDS_TYPE_URL_V2, RDS_TYPE_URL_V3 } from "./resources";
25+
import { HandleResponseResult } from "./xds-stream-state/xds-stream-state";
26+
import { sendUnaryData, ServerDuplexStream, ServerUnaryCall, status, experimental, loadPackageDefinition } from '@grpc/grpc-js';
27+
import { loadSync } from "@grpc/proto-loader";
28+
import { ProtoGrpcType as CsdsProtoGrpcType } from "./generated/csds";
29+
30+
import registerAdminService = experimental.registerAdminService;
31+
32+
33+
function dateToProtoTimestamp(date?: Date | null): Timestamp | null {
34+
if (!date) {
35+
return null;
36+
}
37+
const millisSinceEpoch = date.getTime();
38+
return {
39+
seconds: (millisSinceEpoch / 1000) | 0,
40+
nanos: (millisSinceEpoch % 1000) * 1_000_000
41+
}
42+
}
43+
44+
let clientNode: Node | null = null;
45+
46+
const configStatus = {
47+
[EDS_TYPE_URL_V2]: new Map<string, GenericXdsConfig>(),
48+
[EDS_TYPE_URL_V3]: new Map<string, GenericXdsConfig>(),
49+
[CDS_TYPE_URL_V2]: new Map<string, GenericXdsConfig>(),
50+
[CDS_TYPE_URL_V3]: new Map<string, GenericXdsConfig>(),
51+
[RDS_TYPE_URL_V2]: new Map<string, GenericXdsConfig>(),
52+
[RDS_TYPE_URL_V3]: new Map<string, GenericXdsConfig>(),
53+
[LDS_TYPE_URL_V2]: new Map<string, GenericXdsConfig>(),
54+
[LDS_TYPE_URL_V3]: new Map<string, GenericXdsConfig>()
55+
};
56+
57+
/**
58+
* This function only accepts a v3 Node message, because we are only supporting
59+
* v3 CSDS and it only handles v3 Nodes. If the client is actually using v2 xDS
60+
* APIs, it should just provide the equivalent v3 Node message.
61+
* @param node The Node message for the client that is requesting resources
62+
*/
63+
export function setCsdsClientNode(node: Node) {
64+
clientNode = node;
65+
}
66+
67+
/**
68+
* Update the config status maps from the list of names of requested resources
69+
* for a specific type URL. These lists are the source of truth for determining
70+
* what resources will be listed in the CSDS response. Any resource that is not
71+
* in this list will never actually be applied anywhere.
72+
* @param typeUrl The resource type URL
73+
* @param names The list of resource names that are being requested
74+
*/
75+
export function updateRequestedNameList(typeUrl: AdsTypeUrl, names: string[]) {
76+
const currentTime = dateToProtoTimestamp(new Date());
77+
const configMap = configStatus[typeUrl];
78+
for (const name of names) {
79+
if (!configMap.has(name)) {
80+
configMap.set(name, {
81+
type_url: typeUrl,
82+
name: name,
83+
last_updated: currentTime,
84+
client_status: 'REQUESTED'
85+
});
86+
}
87+
}
88+
for (const name of configMap.keys()) {
89+
if (!names.includes(name)) {
90+
configMap.delete(name);
91+
}
92+
}
93+
}
94+
95+
/**
96+
* Update the config status maps from the result of parsing a single ADS
97+
* response. All resources that validated are considered "ACKED", and all
98+
* resources that failed validation are considered "NACKED".
99+
* @param typeUrl The type URL of resources in this response
100+
* @param versionInfo The version info field from this response
101+
* @param updates The lists of resources that passed and failed validation
102+
*/
103+
export function updateResourceResponse(typeUrl: AdsTypeUrl, versionInfo: string, updates: HandleResponseResult) {
104+
const currentTime = dateToProtoTimestamp(new Date());
105+
const configMap = configStatus[typeUrl];
106+
for (const {name, raw} of updates.accepted) {
107+
const mapEntry = configMap.get(name);
108+
if (mapEntry) {
109+
mapEntry.client_status = 'ACKED';
110+
mapEntry.version_info = versionInfo;
111+
mapEntry.xds_config = raw;
112+
mapEntry.error_state = null;
113+
mapEntry.last_updated = currentTime;
114+
}
115+
}
116+
for (const {name, error, raw} of updates.rejected) {
117+
const mapEntry = configMap.get(name);
118+
if (mapEntry) {
119+
mapEntry.client_status = 'NACKED';
120+
mapEntry.error_state = {
121+
failed_configuration: raw,
122+
last_update_attempt: currentTime,
123+
details: error,
124+
version_info: versionInfo
125+
};
126+
}
127+
}
128+
for (const name of updates.missing) {
129+
const mapEntry = configMap.get(name);
130+
if (mapEntry) {
131+
mapEntry.client_status = 'DOES_NOT_EXIST';
132+
mapEntry.version_info = versionInfo;
133+
mapEntry.xds_config = null;
134+
mapEntry.error_state = null;
135+
mapEntry.last_updated = currentTime;
136+
}
137+
}
138+
}
139+
140+
function getCurrentConfig(): ClientConfig {
141+
const genericConfigList: GenericXdsConfig[] = [];
142+
for (const configMap of Object.values(configStatus)) {
143+
for (const configValue of configMap.values()) {
144+
genericConfigList.push(configValue);
145+
}
146+
}
147+
return {
148+
node: clientNode,
149+
generic_xds_configs: genericConfigList
150+
};
151+
}
152+
153+
const csdsImplementation: ClientStatusDiscoveryServiceHandlers = {
154+
FetchClientStatus(call: ServerUnaryCall<ClientStatusRequest__Output, ClientStatusResponse>, callback: sendUnaryData<ClientStatusResponse>) {
155+
const request = call.request;
156+
if (request.node_matchers !== null) {
157+
callback({
158+
code: status.INVALID_ARGUMENT,
159+
details: 'Node matchers not supported'
160+
});
161+
return;
162+
}
163+
callback(null, {
164+
config: [getCurrentConfig()]
165+
});
166+
},
167+
StreamClientStatus(call: ServerDuplexStream<ClientStatusRequest__Output, ClientStatusResponse>) {
168+
call.on('data', (request: ClientStatusRequest__Output) => {
169+
if (request.node_matchers !== null) {
170+
call.emit('error', {
171+
code: status.INVALID_ARGUMENT,
172+
details: 'Node matchers not supported'
173+
});
174+
return;
175+
}
176+
call.write({
177+
config: [getCurrentConfig()]
178+
});
179+
});
180+
call.on('end', () => {
181+
call.end();
182+
});
183+
}
184+
}
185+
186+
const loadedProto = loadSync('envoy/service/status/v3/csds.proto', {
187+
keepCase: true,
188+
longs: String,
189+
enums: String,
190+
defaults: true,
191+
oneofs: true,
192+
includeDirs: [
193+
// Paths are relative to src/build
194+
__dirname + '/../../deps/envoy-api/',
195+
__dirname + '/../../deps/xds/',
196+
],
197+
});
198+
199+
const csdsGrpcObject = loadPackageDefinition(loadedProto) as unknown as CsdsProtoGrpcType;
200+
const csdsServiceDefinition = csdsGrpcObject.envoy.service.status.v3.ClientStatusDiscoveryService.service;
201+
202+
export function setup() {
203+
registerAdminService(() => csdsServiceDefinition, () => csdsImplementation);
204+
}

packages/grpc-js-xds/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import * as load_balancer_weighted_target from './load-balancer-weighted-target'
2424
import * as load_balancer_xds_cluster_manager from './load-balancer-xds-cluster-manager';
2525
import * as router_filter from './http-filter/router-filter';
2626
import * as fault_injection_filter from './http-filter/fault-injection-filter';
27+
import * as csds from './csds';
2728

2829
/**
2930
* Register the "xds:" name scheme with the @grpc/grpc-js library.
@@ -38,4 +39,5 @@ export function register() {
3839
load_balancer_xds_cluster_manager.setup();
3940
router_filter.setup();
4041
fault_injection_filter.setup();
42+
csds.setup();
4143
}

0 commit comments

Comments
 (0)