@@ -27,6 +27,17 @@ const KIND_MAP = new Map([
2727 [ 'configMapKeyRef' , 'ConfigMap' ]
2828] ) ;
2929
30+ const LRU = require ( 'lru-cache' ) ;
31+ const LruOptions = {
32+ maxSize : 100000 , // the max cache size
33+ sizeCalculation : ( r ) => { return ( JSON . stringify ( r ) . length ) ; } , // how to determine the size of a resource added to the cache
34+ ttl : 1000 * 60 * 3 , // max time to cache (LRU does not directly enforce, but maxSize will eventually push them out)
35+ updateAgeOnGet : false , // Don't update ttl when an item is retrieved from cache
36+ updateAgeOnHas : false , // Don't update ttl when an item is checked in cache
37+ } ;
38+ const globalResourceCache = new LRU ( LruOptions ) ;
39+ const globalResourceCacheUsers = new Set ( ) ;
40+
3041module . exports = class FetchEnvs {
3142
3243 get [ Symbol . toStringTag ] ( ) {
@@ -45,6 +56,71 @@ module.exports = class FetchEnvs {
4556 this . updateRazeeLogs = controllerObject . updateRazeeLogs ?
4657 ( ( logLevel , log ) => { controllerObject . updateRazeeLogs ( logLevel , log ) ; } ) :
4758 ( ( ) => { log . debug ( '\'updateRazeeLogs()\' not passed to fetchEnvs. will not update razeeLogs on failure to fetch envs' ) ; } ) ;
59+
60+ const user = this . data ?. object ?. spec ?. clusterAuth ?. impersonateUser ;
61+ if ( process . env . INSTANCE_FETCHENVS_CACHE_ONLY ) {
62+ // Using `user` is not technically necessary for an instance-specific cache, but used for consistency
63+ log . info ( 'FetchEnvs.constructor using instance-specific resource cache' ) ;
64+ this . instanceCache = { } ;
65+ this . resourceCache = {
66+ has : ( key ) => {
67+ const hit = Object . prototype . hasOwnProperty . call ( this . instanceCache , `${ user } /${ key } ` ) ;
68+ log . info ( `FetchEnvs cache ${ hit ?'HIT' :'MISS' } : '${ user } /${ key } '` ) ;
69+ return hit ;
70+ } ,
71+ set : ( key , value ) => { this . instanceCache [ `${ user } /${ key } ` ] = value ; } ,
72+ get : ( key ) => { return this . instanceCache [ `${ user } /${ key } ` ] ; } ,
73+ } ;
74+ }
75+ else {
76+ log . info ( `FetchEnvs.constructor using global resource cache, ${ globalResourceCache . size } resources currently cached (may be TTL expired)` ) ;
77+ this . resourceCache = {
78+ has : ( key ) => {
79+ const hit = globalResourceCache . has ( `${ user } /${ key } ` ) ;
80+ log . info ( `FetchEnvs cache ${ hit ?'HIT' :'MISS' } : '${ user } /${ key } '` ) ;
81+ return hit ;
82+ } ,
83+ set : ( key , value ) => {
84+ // When setting a key, keep track of users to allow later deletion
85+ globalResourceCacheUsers . add ( user ) ;
86+ globalResourceCache . set ( `${ user } /${ key } ` , value ) ;
87+ log . info ( `FetchEnvs cached '${ user } /${ key } '` ) ;
88+ } ,
89+ get : ( key ) => {
90+ return globalResourceCache . get ( `${ user } /${ key } ` ) ;
91+ } ,
92+ } ;
93+ }
94+ }
95+
96+ // This function needs to be called any time a watch on a potentially cached item is triggered by creation/update/poll, e.g. in the ReferencedResourceManager
97+ // If it is not, the old resource may still be served from cache until the TTL expires
98+ static updateInGlobalCache ( resource ) {
99+ const cacheKey = [ resource ?. apiVersion , resource ?. kind , resource ?. metadata ?. namespace , resource ?. metadata ?. name ] . join ( '/' ) ;
100+ let updated = false ;
101+ // When updating a key, updating it for all users
102+ for ( const cacheUser of globalResourceCacheUsers ) {
103+ if ( globalResourceCache . has ( `${ cacheUser } /${ cacheKey } ` ) ) {
104+ globalResourceCache . set ( `${ cacheUser } /${ cacheKey } ` , resource ) ;
105+ updated = true ;
106+ }
107+ }
108+ if ( updated ) log . info ( `FetchEnvs cache updated for "*/${ cacheKey } "` ) ;
109+ }
110+
111+ // This function needs to be called any time a watch on a potentially cached item is triggered by deletion, e.g. in the ReferencedResourceManager
112+ // If it is not, the deleted resource may still be served from cache until the TTL expires
113+ static deleteFromGlobalCache ( resource ) {
114+ const cacheKey = [ resource ?. apiVersion , resource ?. kind , resource ?. metadata ?. namespace , resource ?. metadata ?. name ] . join ( '/' ) ;
115+ let deleted = false ;
116+ // When deleting a key, delete it for all users
117+ for ( const cacheUser of globalResourceCacheUsers ) {
118+ if ( globalResourceCache . has ( `${ cacheUser } /${ cacheKey } ` ) ) {
119+ globalResourceCache . delete ( `${ cacheUser } /${ cacheKey } ` ) ;
120+ deleted = true ;
121+ }
122+ }
123+ if ( deleted ) log . info ( `FetchEnvs cache deleted for "*/${ cacheKey } "` ) ;
48124 }
49125
50126 #secretMapRef( conf ) {
@@ -63,6 +139,13 @@ module.exports = class FetchEnvs {
63139 return this . #genericKeyRef( conf , 'configMapKeyRef' ) ;
64140 }
65141
142+ /*
143+ @param [I] conf An object like `{ configMapRef: { name: 'asdf', namespace: 'asdf' } }`.
144+ @param [I] valueFrom The name of the conf attribute containing resource details, e.g. `configMapRef`.
145+ @param [I] decode A boolean indicating whether to base64 decode the values retrieved, e.g. from Secrets
146+
147+ @return An object like { configMapRef: { name: 'asdf', namespace: 'asdf' }, data: { key1: val1, ... } }
148+ */
66149 async #genericMapRef( conf , valueFrom = 'genericMapRef' , decode = false ) {
67150 let resource ;
68151 let kubeError = ERR_NODATA ;
@@ -76,13 +159,22 @@ module.exports = class FetchEnvs {
76159 name
77160 } = ref ;
78161
79- const krm = await this . kubeClass . getKubeResourceMeta ( apiVersion , kind , 'update' ) ;
162+ const cacheKey = [ apiVersion , kind , namespace , name ] . join ( '/' ) ;
163+ if ( this . resourceCache . has ( cacheKey ) ) {
164+ resource = this . resourceCache . get ( cacheKey ) ;
165+ }
166+ else {
167+ const krm = await this . kubeClass . getKubeResourceMeta ( apiVersion , kind , 'update' ) ;
80168
81- if ( krm ) {
82- try {
83- resource = await krm . get ( name , namespace ) ;
84- } catch ( error ) {
85- kubeError = error ;
169+ if ( krm ) {
170+ try {
171+ resource = await krm . get ( name , namespace ) ;
172+ if ( resource ) {
173+ this . resourceCache . set ( cacheKey , resource ) ; // Cache this resource
174+ }
175+ } catch ( error ) {
176+ kubeError = error ;
177+ }
86178 }
87179 }
88180
@@ -108,6 +200,14 @@ module.exports = class FetchEnvs {
108200 return { ...conf , data } ;
109201 }
110202
203+ /*
204+ @param [I] conf An object like `{ default: '{default:true}', overrideStrategy: 'merge', configMapRef: { name: 'asdf', namespace: 'asdf', key: 'asdf', type: 'json' } }`
205+ - name, namespace, and matchLabels identify the resource
206+ - key identifies the data inside the resource
207+ - type identifies how to typecast the value
208+
209+ @return The discovered value
210+ */
111211 async #genericKeyRef( conf , valueFrom = 'genericKeyRef' , decode = false ) {
112212 let response ;
113213 let kubeError = ERR_NODATA ;
@@ -125,36 +225,52 @@ module.exports = class FetchEnvs {
125225 apiVersion = 'v1'
126226 } = ref ;
127227
128- const krm = await this . kubeClass . getKubeResourceMeta (
129- apiVersion ,
130- kind ,
131- 'update'
132- ) ;
133-
134228 const matchLabelsQS = labelSelectors ( matchLabels ) ;
135229
136- if ( krm ) {
137- try {
138- response = await this . api ( {
139- uri : krm . uri ( { namespace, name } ) ,
140- json : true ,
141- qs : matchLabelsQS
142- } ) ;
143- } catch ( error ) {
144- kubeError = error ;
230+ const cacheKey = [ apiVersion , kind , namespace , name ] . join ( '/' ) ;
231+ // Note: Using `matchLabels` will always result in a kube api call, label-based queries cannot use the resourceCache
232+ if ( ! matchLabelsQS && this . resourceCache . has ( cacheKey ) ) {
233+ response = this . resourceCache . get ( cacheKey ) ;
234+ }
235+ else {
236+ const krm = await this . kubeClass . getKubeResourceMeta ( apiVersion , kind , 'update' ) ;
237+
238+ if ( krm ) {
239+ try {
240+ response = await this . api ( {
241+ uri : krm . uri ( { namespace, name } ) ,
242+ json : true ,
243+ qs : matchLabelsQS
244+ } ) ;
245+ // Note: cache here only if getting a single resource
246+ if ( response ?. data && ! response ?. items ) {
247+ this . resourceCache . set ( cacheKey , response ) ;
248+ }
249+ } catch ( error ) {
250+ kubeError = error ;
251+ }
145252 }
146253 }
147254
148255 let value = response ?. data ?. [ key ] ;
149256
257+ // If matching by labels, there can be multiple matching resources.
258+ // Reduce to a single value via the specified strategy ('merge' combines objects, otherwise a single value is picked).
150259 if ( typeof matchLabelsQS === OBJECT ) {
260+ // Cache here if there are multiple retrieved resources
261+ if ( response ?. items ) {
262+ response . items . forEach ( function ( item ) {
263+ const cacheKey = [ item . apiVersion , item . kind , item . metadata . namespace , item . metadata . name ] . join ( '/' ) ;
264+ this . resourceCache . set ( cacheKey , item ) ;
265+ } , this ) ;
266+ }
151267 const output = response ?. items . reduce (
152268 reduceItemList ( ref , strategy , decode ) ,
153269 Object . create ( null )
154270 ) ;
155271
156272 value = output ?. [ key ] ;
157- decode = false ;
273+ decode = false ; // 'decode' was used in the reduceItemList, set to false to avoid double-decoding.
158274 }
159275
160276 if ( value === undefined ) {
@@ -182,9 +298,15 @@ module.exports = class FetchEnvs {
182298 return typeCast ( name , value , type ) ;
183299 }
184300
301+ /*
302+ Retrieve all values from specified kube resources.
303+
304+ @param [I] envs Array of objects like `[ { configMapRef: { ... }, ... } ]`
185305
186- processEnvFrom ( envFrom ) {
187- return Promise . all ( envFrom . map ( ( element ) => {
306+ @return Array of objects like ``[ { configMapRef: { ... }, data: { key1: val1, key2: val2, ... } }, ... ]``
307+ */
308+ async processEnvFrom ( envFrom ) {
309+ const retVal = await Promise . all ( envFrom . map ( ( element ) => {
188310 const { configMapRef, secretMapRef, genericMapRef } = element ;
189311
190312 if ( ! configMapRef && ! secretMapRef && ! genericMapRef ) {
@@ -195,25 +317,44 @@ module.exports = class FetchEnvs {
195317 if ( secretMapRef ) return this . #secretMapRef( element ) ;
196318 return this . #genericMapRef( element ) ;
197319 } ) ) ;
320+ return ( retVal ) ;
198321 }
199322
200- #processEnv( envs ) {
201- return Promise . all ( envs . map ( async ( env ) => {
202- if ( env . value ) return env ;
203- const valueFrom = env . valueFrom || { } ;
204- const { genericKeyRef, configMapKeyRef, secretKeyRef } = valueFrom ;
323+ /*
324+ Retrieve specific values from specified kube resources.
325+
326+ Each env is retrieved and processed sequentially so that caching can take place.
327+ If Promise.all were used, multiple requests for the same resource would be sent
328+ in parallel and caching would be unable to assist. The return value is an array
329+ as if from Promise.all.
205330
206- if ( ! genericKeyRef && ! configMapKeyRef && ! secretKeyRef ) {
207- throw new Error ( `oneOf genericKeyRef, configMapKeyRef, secretKeyRef must be defined. Got: ${ JSON . stringify ( env ) } ` ) ;
331+ @param [I] envs Array of objects like `[ { configMapKeyRef: { ... }, ... } ]`
332+
333+ @return Array of objects like `[ { configMapKeyRef: { ... }, value: asdf }, ... ]`
334+ */
335+ async #processEnv( envs ) {
336+ const retVal = [ ] ;
337+ for ( const env of envs ) {
338+ if ( env . value ) {
339+ retVal . push ( env ) ;
208340 }
341+ else {
342+ const valueFrom = env . valueFrom || { } ;
343+ const { genericKeyRef, configMapKeyRef, secretKeyRef } = valueFrom ;
209344
210- let value ;
211- if ( secretKeyRef ) value = await this . #secretKeyRef( env ) ;
212- if ( configMapKeyRef ) value = await this . #configMapKeyRef( env ) ;
213- if ( genericKeyRef ) value = await this . #genericKeyRef( env ) ;
345+ if ( ! genericKeyRef && ! configMapKeyRef && ! secretKeyRef ) {
346+ throw new Error ( `oneOf genericKeyRef, configMapKeyRef, secretKeyRef must be defined. Got: ${ JSON . stringify ( env ) } ` ) ;
347+ }
214348
215- return { ...env , value } ;
216- } ) ) ;
349+ let value ;
350+ if ( secretKeyRef ) value = await this . #secretKeyRef( env ) ;
351+ if ( configMapKeyRef ) value = await this . #configMapKeyRef( env ) ;
352+ if ( genericKeyRef ) value = await this . #genericKeyRef( env ) ;
353+
354+ retVal . push ( { ...env , value } ) ;
355+ }
356+ }
357+ return retVal ;
217358 }
218359
219360 #processEnvSourceSimpleLinks( envs ) {
@@ -225,21 +366,31 @@ module.exports = class FetchEnvs {
225366 } ) ) ;
226367 }
227368
369+ /*
370+ Retrieve values specified in spec.envFrom and spec.env elements
371+
372+ @param [I] path path to the env and envFrom elements in the resource
373+
374+ @return A map of keys to values
375+ */
228376 async get ( path = 'spec' ) {
229377 let result = { } ;
230378 // removes any number of '.' at the start and end of the path, and
231379 // removes the '.env' or '.envFrom' if the paths ends in either
232380 path = path . replace ( / ^ \. * | \. * $ | ( \. e n v F r o m \. * $ ) | ( \. e n v \. * $ ) / g, '' ) ;
233381
234382 let envFrom = objectPath . get ( this . data , `object.${ path } .envFrom` , [ ] ) ;
383+
235384 envFrom = await this . processEnvFrom ( envFrom ) ;
236385 for ( const env of envFrom ) {
237386 const data = env ?. data ?? { } ;
238387 result = { ...result , ...data } ;
239388 }
240389
241- const env = objectPath . get ( this . data , `object.${ path } .env` , [ ] ) ;
242- return ( await this . #processEnv( env ) ) . reduce ( reduceEnv , result ) ;
390+ let env = objectPath . get ( this . data , `object.${ path } .env` , [ ] ) ;
391+
392+ env = await this . #processEnv( env ) ;
393+ return ( env ) . reduce ( reduceEnv , result ) ;
243394 }
244395
245396 async getSourceSimpleLinks ( path = 'spec' ) {
@@ -319,6 +470,17 @@ function labelSelectors(query) {
319470 } ;
320471}
321472
473+ /*
474+ Cast the specified value to the indicated type. The value is returned unmodified if:
475+ - 'type' is not specified
476+ - 'value' is null or not a string
477+
478+ @param [I] name The name of the reference from which the value was obtained. Used only in generating error text in case of JSON parsing errors.
479+ @param [I] value The string value to typecast.
480+ @param [I] type How to typecast the value.
481+
482+ @return Value, cast to the indicated type (e.g. number, boolean, json, base64 decoded string )
483+ */
322484function typeCast ( name , value , type ) {
323485 if ( ! type ) return value ;
324486 if ( value == null ) return ;
0 commit comments