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
+ }
0 commit comments