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 , logVerbosity } 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
+ const TRACER_NAME = 'csds' ;
33
+
34
+ function trace ( text : string ) : void {
35
+ experimental . trace ( logVerbosity . DEBUG , TRACER_NAME , text ) ;
36
+ }
37
+
38
+
39
+ function dateToProtoTimestamp ( date ?: Date | null ) : Timestamp | null {
40
+ if ( ! date ) {
41
+ return null ;
42
+ }
43
+ const millisSinceEpoch = date . getTime ( ) ;
44
+ return {
45
+ seconds : ( millisSinceEpoch / 1000 ) | 0 ,
46
+ nanos : ( millisSinceEpoch % 1000 ) * 1_000_000
47
+ }
48
+ }
49
+
50
+ let clientNode : Node | null = null ;
51
+
52
+ const configStatus = {
53
+ [ EDS_TYPE_URL_V2 ] : new Map < string , GenericXdsConfig > ( ) ,
54
+ [ EDS_TYPE_URL_V3 ] : new Map < string , GenericXdsConfig > ( ) ,
55
+ [ CDS_TYPE_URL_V2 ] : new Map < string , GenericXdsConfig > ( ) ,
56
+ [ CDS_TYPE_URL_V3 ] : new Map < string , GenericXdsConfig > ( ) ,
57
+ [ RDS_TYPE_URL_V2 ] : new Map < string , GenericXdsConfig > ( ) ,
58
+ [ RDS_TYPE_URL_V3 ] : new Map < string , GenericXdsConfig > ( ) ,
59
+ [ LDS_TYPE_URL_V2 ] : new Map < string , GenericXdsConfig > ( ) ,
60
+ [ LDS_TYPE_URL_V3 ] : new Map < string , GenericXdsConfig > ( )
61
+ } ;
62
+
63
+ /**
64
+ * This function only accepts a v3 Node message, because we are only supporting
65
+ * v3 CSDS and it only handles v3 Nodes. If the client is actually using v2 xDS
66
+ * APIs, it should just provide the equivalent v3 Node message.
67
+ * @param node The Node message for the client that is requesting resources
68
+ */
69
+ export function setCsdsClientNode ( node : Node ) {
70
+ clientNode = node ;
71
+ }
72
+
73
+ /**
74
+ * Update the config status maps from the list of names of requested resources
75
+ * for a specific type URL. These lists are the source of truth for determining
76
+ * what resources will be listed in the CSDS response. Any resource that is not
77
+ * in this list will never actually be applied anywhere.
78
+ * @param typeUrl The resource type URL
79
+ * @param names The list of resource names that are being requested
80
+ */
81
+ export function updateCsdsRequestedNameList ( typeUrl : AdsTypeUrl , names : string [ ] ) {
82
+ trace ( 'Update type URL ' + typeUrl + ' with names [' + names + ']' ) ;
83
+ const currentTime = dateToProtoTimestamp ( new Date ( ) ) ;
84
+ const configMap = configStatus [ typeUrl ] ;
85
+ for ( const name of names ) {
86
+ if ( ! configMap . has ( name ) ) {
87
+ configMap . set ( name , {
88
+ type_url : typeUrl ,
89
+ name : name ,
90
+ last_updated : currentTime ,
91
+ client_status : 'REQUESTED'
92
+ } ) ;
93
+ }
94
+ }
95
+ for ( const name of configMap . keys ( ) ) {
96
+ if ( ! names . includes ( name ) ) {
97
+ configMap . delete ( name ) ;
98
+ }
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Update the config status maps from the result of parsing a single ADS
104
+ * response. All resources that validated are considered "ACKED", and all
105
+ * resources that failed validation are considered "NACKED".
106
+ * @param typeUrl The type URL of resources in this response
107
+ * @param versionInfo The version info field from this response
108
+ * @param updates The lists of resources that passed and failed validation
109
+ */
110
+ export function updateCsdsResourceResponse ( typeUrl : AdsTypeUrl , versionInfo : string , updates : HandleResponseResult ) {
111
+ const currentTime = dateToProtoTimestamp ( new Date ( ) ) ;
112
+ const configMap = configStatus [ typeUrl ] ;
113
+ for ( const { name, raw} of updates . accepted ) {
114
+ const mapEntry = configMap . get ( name ) ;
115
+ if ( mapEntry ) {
116
+ trace ( 'Updated ' + typeUrl + ' resource ' + name + ' to state ACKED' ) ;
117
+ mapEntry . client_status = 'ACKED' ;
118
+ mapEntry . version_info = versionInfo ;
119
+ mapEntry . xds_config = raw ;
120
+ mapEntry . error_state = null ;
121
+ mapEntry . last_updated = currentTime ;
122
+ }
123
+ }
124
+ for ( const { name, error, raw} of updates . rejected ) {
125
+ const mapEntry = configMap . get ( name ) ;
126
+ if ( mapEntry ) {
127
+ trace ( 'Updated ' + typeUrl + ' resource ' + name + ' to state NACKED' ) ;
128
+ mapEntry . client_status = 'NACKED' ;
129
+ mapEntry . error_state = {
130
+ failed_configuration : raw ,
131
+ last_update_attempt : currentTime ,
132
+ details : error ,
133
+ version_info : versionInfo
134
+ } ;
135
+ }
136
+ }
137
+ for ( const name of updates . missing ) {
138
+ const mapEntry = configMap . get ( name ) ;
139
+ if ( mapEntry ) {
140
+ trace ( 'Updated ' + typeUrl + ' resource ' + name + ' to state DOES_NOT_EXIST' ) ;
141
+ mapEntry . client_status = 'DOES_NOT_EXIST' ;
142
+ mapEntry . version_info = versionInfo ;
143
+ mapEntry . xds_config = null ;
144
+ mapEntry . error_state = null ;
145
+ mapEntry . last_updated = currentTime ;
146
+ }
147
+ }
148
+ }
149
+
150
+ function getCurrentConfig ( ) : ClientConfig {
151
+ const genericConfigList : GenericXdsConfig [ ] = [ ] ;
152
+ for ( const configMap of Object . values ( configStatus ) ) {
153
+ for ( const configValue of configMap . values ( ) ) {
154
+ genericConfigList . push ( configValue ) ;
155
+ }
156
+ }
157
+ const config = {
158
+ node : clientNode ,
159
+ generic_xds_configs : genericConfigList
160
+ } ;
161
+ trace ( 'Sending curent config ' + JSON . stringify ( config , undefined , 2 ) ) ;
162
+ return config ;
163
+ }
164
+
165
+ const csdsImplementation : ClientStatusDiscoveryServiceHandlers = {
166
+ FetchClientStatus ( call : ServerUnaryCall < ClientStatusRequest__Output , ClientStatusResponse > , callback : sendUnaryData < ClientStatusResponse > ) {
167
+ const request = call . request ;
168
+ if ( request . node_matchers . length > 0 ) {
169
+ callback ( {
170
+ code : status . INVALID_ARGUMENT ,
171
+ details : 'Node matchers not supported'
172
+ } ) ;
173
+ return ;
174
+ }
175
+ callback ( null , {
176
+ config : [ getCurrentConfig ( ) ]
177
+ } ) ;
178
+ } ,
179
+ StreamClientStatus ( call : ServerDuplexStream < ClientStatusRequest__Output , ClientStatusResponse > ) {
180
+ call . on ( 'data' , ( request : ClientStatusRequest__Output ) => {
181
+ if ( request . node_matchers . length > 0 ) {
182
+ call . emit ( 'error' , {
183
+ code : status . INVALID_ARGUMENT ,
184
+ details : 'Node matchers not supported'
185
+ } ) ;
186
+ return ;
187
+ }
188
+ call . write ( {
189
+ config : [ getCurrentConfig ( ) ]
190
+ } ) ;
191
+ } ) ;
192
+ call . on ( 'end' , ( ) => {
193
+ call . end ( ) ;
194
+ } ) ;
195
+ }
196
+ }
197
+
198
+ const loadedProto = loadSync ( 'envoy/service/status/v3/csds.proto' , {
199
+ keepCase : true ,
200
+ longs : String ,
201
+ enums : String ,
202
+ defaults : true ,
203
+ oneofs : true ,
204
+ includeDirs : [
205
+ // Paths are relative to src/build
206
+ __dirname + '/../../deps/envoy-api/' ,
207
+ __dirname + '/../../deps/xds/' ,
208
+ ] ,
209
+ } ) ;
210
+
211
+ const csdsGrpcObject = loadPackageDefinition ( loadedProto ) as unknown as CsdsProtoGrpcType ;
212
+ const csdsServiceDefinition = csdsGrpcObject . envoy . service . status . v3 . ClientStatusDiscoveryService . service ;
213
+
214
+ export function setup ( ) {
215
+ registerAdminService ( ( ) => csdsServiceDefinition , ( ) => csdsImplementation ) ;
216
+ }
0 commit comments