google-cloud-env 1.6.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,883 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2023 Google LLC
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
+ # https://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
+ require "faraday"
18
+ require "json"
19
+
20
+ require "google/cloud/env/compute_smbios"
21
+ require "google/cloud/env/lazy_value"
22
+ require "google/cloud/env/variables"
23
+
24
+ module Google
25
+ module Cloud
26
+ class Env
27
+ ##
28
+ # A client for the Google metadata service.
29
+ #
30
+ class ComputeMetadata
31
+ ##
32
+ # The default host for the metadata server
33
+ # @return [String]
34
+ #
35
+ DEFAULT_HOST = "http://169.254.169.254"
36
+
37
+ ##
38
+ # The default timeout in seconds for opening http connections
39
+ # @return [Numeric]
40
+ #
41
+ DEFAULT_OPEN_TIMEOUT = 0.1
42
+
43
+ ##
44
+ # The default timeout in seconds for request responses
45
+ # @return [Numeric]
46
+ #
47
+ DEFAULT_REQUEST_TIMEOUT = 0.5
48
+
49
+ ##
50
+ # The default number of retries
51
+ # @return [Integer]
52
+ #
53
+ DEFAULT_RETRY_COUNT = 2
54
+
55
+ ##
56
+ # The default timeout across retries
57
+ # @return [nil]
58
+ #
59
+ DEFAULT_RETRY_TIMEOUT = nil
60
+
61
+ ##
62
+ # The default interval between retries, in seconds
63
+ # @return [Numeric]
64
+ #
65
+ DEFAULT_RETRY_INTERVAL = 0.5
66
+
67
+ ##
68
+ # The default time in seconds to wait for environment warmup.
69
+ # @return [Numeric]
70
+ #
71
+ DEFAULT_WARMUP_TIME = 60
72
+
73
+ ##
74
+ # @private
75
+ # The base path of metadata server queries.
76
+ # @return [String]
77
+ #
78
+ PATH_BASE = "/computeMetadata/v1"
79
+
80
+ ##
81
+ # @private
82
+ # The standard set of headers
83
+ # @return [Hash{String=>String}]
84
+ #
85
+ FLAVOR_HEADER = { "Metadata-Flavor" => "Google" }.freeze
86
+
87
+ ##
88
+ # Basic HTTP response object, returned by
89
+ # {ComputeMetadata#lookup_response}.
90
+ #
91
+ # This object duck-types the `status`, `body`, and `headers` fields of
92
+ # `Faraday::Response`. It also includes the CLOCK_MONOTONIC time when
93
+ # the data was retrieved.
94
+ #
95
+ class Response
96
+ ##
97
+ # Create a response object.
98
+ #
99
+ # @param status [Integer] The HTTP status, normally 200
100
+ # @param body [String] The HTTP body as a string
101
+ # @param headers [Hash{String=>String}] The HTTP response headers.
102
+ # Normally, the `Metadata-Flavor` header must be set to the value
103
+ # `Google`.
104
+ #
105
+ def initialize status, body, headers
106
+ @status = status
107
+ @body = body
108
+ @headers = headers
109
+ @retrieval_monotonic_time = Process.clock_gettime Process::CLOCK_MONOTONIC
110
+ end
111
+
112
+ ##
113
+ # The HTTP status code
114
+ # @return [Integer]
115
+ #
116
+ attr_reader :status
117
+
118
+ ##
119
+ # The HTTP response body
120
+ # @return [String]
121
+ #
122
+ attr_reader :body
123
+
124
+ ##
125
+ # The HTTP response headers
126
+ # @return [Hash{String=>String}]
127
+ #
128
+ attr_reader :headers
129
+
130
+ # The CLOCK_MONOTONIC time at which this response was retrieved.
131
+ # @return [Numeric]
132
+ #
133
+ attr_reader :retrieval_monotonic_time
134
+
135
+ ##
136
+ # Returns true if the metadata-flavor is correct for Google Cloud
137
+ # @return [boolean]
138
+ #
139
+ def google_flavor?
140
+ headers["Metadata-Flavor"] == "Google"
141
+ end
142
+ end
143
+
144
+ ##
145
+ # A set of overrides for metadata access. This is used in
146
+ # {ComputeMetadata#overrides=} and {ComputeMetadata#with_overrides}.
147
+ # Generally, you should create and populate an overrides object, then
148
+ # set it using one of those methods.
149
+ #
150
+ # An empty overrides object that contains no data is interpreted as a
151
+ # metadata server that does not respond and raises
152
+ # MetadataServerNotResponding. Otherwise, the overrides specifies what
153
+ # responses are returned for specified queries, and any query not
154
+ # explicitly set will result in a 404.
155
+ #
156
+ class Overrides
157
+ ##
158
+ # Create an empty overrides object.
159
+ #
160
+ def initialize
161
+ clear
162
+ end
163
+
164
+ ##
165
+ # Add an override to the object, providing a full response.
166
+ #
167
+ # @param path [String] The key path (e.g. `project/project-id`)
168
+ # @param response [Response] The response object to return.
169
+ # @param query [Hash{String => String}] Any additional query
170
+ # parameters for the request.
171
+ #
172
+ # @return [self] for chaining
173
+ #
174
+ def add_response path, response, query: nil
175
+ @data[[path, query || {}]] = response
176
+ self
177
+ end
178
+
179
+ ##
180
+ # Add an override to the object, providing just a body string.
181
+ #
182
+ # @param path [String] The key path (e.g. `project/project-id`)
183
+ # @param string [String] The response string to return.
184
+ # @param query [Hash{String => String}] Any additional query
185
+ # parameters for the request.
186
+ #
187
+ # @return [self] for chaining
188
+ #
189
+ def add path, string, query: nil, headers: nil
190
+ headers = (headers || {}).merge FLAVOR_HEADER
191
+ response = Response.new 200, string, headers
192
+ add_response path, response, query: query
193
+ end
194
+
195
+ ##
196
+ # Add an override for the ping request.
197
+ #
198
+ # @return [self] for chaining
199
+ #
200
+ def add_ping
201
+ add nil, "computeMetadata/\n"
202
+ end
203
+
204
+ ##
205
+ # Clear all data from these overrides
206
+ #
207
+ # @return [self] for chaining
208
+ #
209
+ def clear
210
+ @data = {}
211
+ self
212
+ end
213
+
214
+ ##
215
+ # Look up a response from the override data.
216
+ #
217
+ # @param path [String] The key path (e.g. `project/project-id`)
218
+ # @param query [Hash{String => String}] Any additional query
219
+ # parameters for the request.
220
+ #
221
+ # @return [String] The response
222
+ # @return [nil] if there is no data for the given query
223
+ #
224
+ def lookup path, query: nil
225
+ @data[[path, query || {}]]
226
+ end
227
+
228
+ ##
229
+ # Returns true if there is at least one override present
230
+ #
231
+ # @return [true, false]
232
+ #
233
+ def empty?
234
+ @data.empty?
235
+ end
236
+ end
237
+
238
+ ##
239
+ # Create a compute metadata access object.
240
+ #
241
+ # @param variables [Google::Cloud::Env::Variables] Access object for
242
+ # environment variables. If not provided, a default is created.
243
+ # @param compute_smbios [Google::Cloud::Env::ComputeSMBIOS] Access
244
+ # object for SMBIOS information. If not provided, a default is
245
+ # created.
246
+ #
247
+ def initialize variables: nil,
248
+ compute_smbios: nil
249
+ @variables = variables || Variables.new
250
+ @compute_smbios = compute_smbios || ComputeSMBIOS.new
251
+ # This mutex protects the overrides and existence settings.
252
+ # Those values won't change within a synchronize block.
253
+ @mutex = Thread::Mutex.new
254
+ reset!
255
+ end
256
+
257
+ ##
258
+ # The host URL for the metadata server, including `http://`.
259
+ #
260
+ # @return [String]
261
+ #
262
+ attr_reader :host
263
+
264
+ ##
265
+ # The host URL for the metadata server, including `http://`.
266
+ #
267
+ # @param new_host [String]
268
+ #
269
+ def host= new_host
270
+ new_host ||= @variables["GCE_METADATA_HOST"] || DEFAULT_HOST
271
+ new_host = "http://#{new_host}" unless new_host.start_with? "http://"
272
+ @host = new_host
273
+ end
274
+
275
+ ##
276
+ # The default maximum number of times to retry a query for a key.
277
+ # A value of 1 means 2 attempts (i.e. 1 retry). A value of nil means
278
+ # there is no limit to the number of retries, although there could be
279
+ # an overall timeout.
280
+ #
281
+ # Defaults to {DEFAULT_RETRY_COUNT}.
282
+ #
283
+ # @return [Integer,nil]
284
+ #
285
+ attr_accessor :retry_count
286
+
287
+ ##
288
+ # The default overall timeout across all retries of a lookup, in
289
+ # seconds. A value of nil means there is no timeout, although there
290
+ # could be a limit to the number of retries.
291
+ #
292
+ # Defaults to {DEFAULT_RETRY_TIMEOUT}.
293
+ #
294
+ # @return [Numeric,nil]
295
+ #
296
+ attr_accessor :retry_timeout
297
+
298
+ ##
299
+ # The time in seconds between retries. This time includes the time
300
+ # spent by the previous attempt.
301
+ #
302
+ # Defaults to {DEFAULT_RETRY_INTERVAL}.
303
+ #
304
+ # @return [Numeric]
305
+ #
306
+ attr_accessor :retry_interval
307
+
308
+ ##
309
+ # A time in seconds allotted to environment warmup, during which
310
+ # retries will not be ended. This handles certain environments in which
311
+ # the Metadata Server might not be fully awake until some time after
312
+ # application startup. A value of nil disables this warmup period.
313
+ #
314
+ # Defaults to {DEFAULT_WARMUP_TIME}.
315
+ #
316
+ # @return [Numeric,nil]
317
+ #
318
+ attr_accessor :warmup_time
319
+
320
+ ##
321
+ # The timeout for opening http connections in seconds.
322
+ #
323
+ # @return [Numeric]
324
+ #
325
+ def open_timeout
326
+ connection.options.open_timeout
327
+ end
328
+
329
+ ##
330
+ # The timeout for opening http connections in seconds.
331
+ #
332
+ # @param timeout [Numeric]
333
+ #
334
+ def open_timeout= timeout
335
+ connection.options[:open_timeout] = timeout
336
+ end
337
+
338
+ ##
339
+ # The total timeout for an HTTP request in seconds.
340
+ #
341
+ # @return [Numeric]
342
+ #
343
+ def request_timeout
344
+ connection.options.timeout
345
+ end
346
+
347
+ ##
348
+ # The total timeout for an HTTP request in seconds.
349
+ #
350
+ # @param timeout [Numeric]
351
+ #
352
+ def request_timeout= timeout
353
+ connection.options[:timeout] = timeout
354
+ end
355
+
356
+ ##
357
+ # Look up a particular key from the metadata server, and return a full
358
+ # {Response} object. Could return a cached value if the key has been
359
+ # queried before, otherwise this could block while trying to contact
360
+ # the server through the given timeouts and retries.
361
+ #
362
+ # This returns a Response object even if the HTTP status is 404, so be
363
+ # sure to check the status code to determine whether the key actually
364
+ # exists. Unlike {#lookup}, this method does not return nil.
365
+ #
366
+ # @param path [String] The key path (e.g. `project/project-id`)
367
+ # @param query [Hash{String => String}] Any additional query parameters
368
+ # to send with the request.
369
+ # @param open_timeout [Numeric] Timeout for opening http connections.
370
+ # Defaults to {#open_timeout}.
371
+ # @param request_timeout [Numeric] Timeout for entire http requests.
372
+ # Defaults to {#request_timeout}.
373
+ # @param retry_count [Integer,nil] Number of times to retry. A value of
374
+ # 1 means 2 attempts (i.e. 1 retry). A value of nil indicates
375
+ # retries are limited only by the timeout. Defaults to
376
+ # {#retry_count}.
377
+ # @param retry_timeout [Numeric,nil] Total timeout for retries. A value
378
+ # of nil indicates no time limit, and retries are limited only by
379
+ # count. Defaults to {#retry_timeout}.
380
+ #
381
+ # @return [Response] the data from the metadata server
382
+ # @raise [MetadataServerNotResponding] if the Metadata Server is not
383
+ # responding
384
+ #
385
+ def lookup_response path,
386
+ query: nil,
387
+ open_timeout: nil,
388
+ request_timeout: nil,
389
+ retry_count: :default,
390
+ retry_timeout: :default
391
+ query = canonicalize_query query
392
+ if @overrides
393
+ @mutex.synchronize do
394
+ return lookup_override path, query if @overrides
395
+ end
396
+ end
397
+ raise MetadataServerNotResponding unless gce_check
398
+ retry_count = self.retry_count if retry_count == :default
399
+ retry_count += 1 if retry_count
400
+ retry_timeout = self.retry_timeout if retry_timeout == :default
401
+ @cache.await [path, query], open_timeout, request_timeout,
402
+ transient_errors: [MetadataServerNotResponding],
403
+ max_tries: retry_count,
404
+ max_time: retry_timeout
405
+ end
406
+
407
+ ##
408
+ # Look up a particular key from the metadata server and return the data
409
+ # as a string. Could return a cached value if the key has been queried
410
+ # before, otherwise this could block while trying to contact the server
411
+ # through the given timeouts and retries.
412
+ #
413
+ # This returns the HTTP body as a string, only if the call succeeds. If
414
+ # the key is inaccessible or missing (i.e. the HTTP status was not 200)
415
+ # or does not have the correct `Metadata-Flavor` header, then nil is
416
+ # returned. If you need more detailed information, use
417
+ # {#lookup_response}.
418
+ #
419
+ # @param path [String] The key path (e.g. `project/project-id`)
420
+ # @param query [Hash{String => String}] Any additional query parameters
421
+ # to send with the request.
422
+ # @param open_timeout [Numeric] Timeout for opening http connections.
423
+ # Defaults to {#open_timeout}.
424
+ # @param request_timeout [Numeric] Timeout for entire http requests.
425
+ # Defaults to {#request_timeout}.
426
+ # @param retry_count [Integer,nil] Number of times to retry. A value of
427
+ # 1 means 2 attempts (i.e. 1 retry). A value of nil indicates
428
+ # retries are limited only by the timeout. Defaults to
429
+ # {#retry_count}.
430
+ # @param retry_timeout [Numeric,nil] Total timeout for retries. A value
431
+ # of nil indicates no time limit, and retries are limited only by
432
+ # count. Defaults to {#retry_timeout}.
433
+ #
434
+ # @return [String] the data from the metadata server
435
+ # @return [nil] if the key is not present
436
+ # @raise [MetadataServerNotResponding] if the Metadata Server is not
437
+ # responding
438
+ #
439
+ def lookup path,
440
+ query: nil,
441
+ open_timeout: nil,
442
+ request_timeout: nil,
443
+ retry_count: :default,
444
+ retry_timeout: :default
445
+ response = lookup_response path,
446
+ query: query,
447
+ open_timeout: open_timeout,
448
+ request_timeout: request_timeout,
449
+ retry_count: retry_count,
450
+ retry_timeout: retry_timeout
451
+ return nil unless response.status == 200 && response.google_flavor?
452
+ response.body
453
+ end
454
+
455
+ ##
456
+ # Return detailed information about whether we think Metadata is
457
+ # available. If we have not previously confirmed existence one way or
458
+ # another, this could block while trying to contact the server through
459
+ # the given timeouts and retries.
460
+ #
461
+ # @param open_timeout [Numeric] Timeout for opening http connections.
462
+ # Defaults to {#open_timeout}.
463
+ # @param request_timeout [Numeric] Timeout for entire http requests.
464
+ # Defaults to {#request_timeout}.
465
+ # @param retry_count [Integer,nil] Number of times to retry. A value of
466
+ # 1 means 2 attempts (i.e. 1 retry). A value of nil indicates
467
+ # retries are limited only by the timeout. Defaults to
468
+ # {#retry_count}.
469
+ # @param retry_timeout [Numeric,nil] Total timeout for retries. A value
470
+ # of nil indicates no time limit, and retries are limited only by
471
+ # count. Defaults to {#retry_timeout}.
472
+ #
473
+ # @return [:no] if we know the metadata server is not present
474
+ # @return [:unconfirmed] if we believe metadata should be present but we
475
+ # haven't gotten a confirmed response from it. This can happen if
476
+ # SMBIOS says we're on GCE but we can't contact the Metadata Server
477
+ # even through retries.
478
+ # @return [:confirmed] if we have a confirmed response from metadata.
479
+ #
480
+ def check_existence open_timeout: nil,
481
+ request_timeout: nil,
482
+ retry_count: :default,
483
+ retry_timeout: :default
484
+ current = @existence
485
+ return current if [:no, :confirmed].include? @existence
486
+ begin
487
+ lookup nil,
488
+ open_timeout: open_timeout,
489
+ request_timeout: request_timeout,
490
+ retry_count: retry_count,
491
+ retry_timeout: retry_timeout
492
+ rescue MetadataServerNotResponding
493
+ # Do nothing
494
+ end
495
+ @existence
496
+ end
497
+
498
+ ##
499
+ # The current detailed existence status, without blocking on any
500
+ # attempt to contact the metadata server.
501
+ #
502
+ # @return [nil] if we have no information at all yet
503
+ # @return [:no] if we know the metadata server is not present
504
+ # @return [:unconfirmed] if we believe metadata should be present but we
505
+ # haven't gotten a confirmed response from it.
506
+ # @return [:confirmed] if we have a confirmed response from metadata.
507
+ #
508
+ def existence_immediate
509
+ @existence
510
+ end
511
+
512
+ ##
513
+ # Assert that the Metadata Server should be present, and wait for a
514
+ # confirmed connection to ensure it is up. This will generally run
515
+ # at most {#warmup_time} seconds to wait out the expected maximum
516
+ # warmup time, but a shorter timeout can be provided.
517
+ #
518
+ # @param timeout [Numeric,nil] a timeout in seconds, or nil to wait
519
+ # until we have conclusively decided one way or the other.
520
+ # @return [:confirmed] if we were able to confirm connection.
521
+ # @raise [MetadataServerNotResponding] if we were unable to confirm
522
+ # connection with the Metadata Server, either because the timeout
523
+ # expired or because the server seems to be down
524
+ #
525
+ def ensure_existence timeout: nil
526
+ timeout ||= @startup_time + warmup_time - Process.clock_gettime(Process::CLOCK_MONOTONIC)
527
+ timeout = 1.0 if timeout < 1.0
528
+ check_existence retry_count: nil, retry_timeout: timeout
529
+ raise MetadataServerNotResponding unless @existence == :confirmed
530
+ @existence
531
+ end
532
+
533
+ ##
534
+ # Get the expiration time for the given path. Returns the monotonic
535
+ # time if the data has been retrieved and has an expiration, nil if the
536
+ # data has been retrieved but has no expiration, or false if the data
537
+ # has not yet been retrieved.
538
+ #
539
+ # @return [Numeric,nil,false]
540
+ #
541
+ def expiration_time_of path, query: nil
542
+ state = @cache.internal_state [path, query]
543
+ return false unless state[0] == :success
544
+ state[2]
545
+ end
546
+
547
+ ##
548
+ # The overrides, or nil if overrides are not present.
549
+ # If present, overrides will answer all metadata queries, and actual
550
+ # calls to the metadata server will be blocked.
551
+ #
552
+ # @return [Overrides,nil]
553
+ #
554
+ attr_reader :overrides
555
+
556
+ ##
557
+ # Set the overrides. You can also set nil to disable overrides.
558
+ # If present, overrides will answer all metadata queries, and actual
559
+ # calls to the metadata server will be blocked.
560
+ #
561
+ # @param new_overrides [Overrides,nil]
562
+ #
563
+ def overrides= new_overrides
564
+ @mutex.synchronize do
565
+ @existence = nil
566
+ @overrides = new_overrides
567
+ end
568
+ end
569
+
570
+ ##
571
+ # Run the given block with the overrides replaced with the given set
572
+ # (or nil to disable overrides in the block). The original overrides
573
+ # setting is restored at the end of the block. This is used for
574
+ # debugging/testing/mocking.
575
+ #
576
+ # @param temp_overrides [Overrides,nil]
577
+ #
578
+ def with_overrides temp_overrides
579
+ old_overrides, old_existence = @mutex.synchronize do
580
+ [@overrides, @existence]
581
+ end
582
+ begin
583
+ @mutex.synchronize do
584
+ @existence = nil
585
+ @overrides = temp_overrides
586
+ end
587
+ yield
588
+ ensure
589
+ @mutex.synchronize do
590
+ @existence = old_existence
591
+ @overrides = old_overrides
592
+ end
593
+ end
594
+ end
595
+
596
+ ##
597
+ # @private
598
+ # The underlying Faraday connection. Can be used to customize the
599
+ # connection for testing.
600
+ # @return [Faraday::Connection]
601
+ #
602
+ attr_reader :connection
603
+
604
+ ##
605
+ # @private
606
+ # The underlying LazyDict. Can be used to customize the cache for
607
+ # testing.
608
+ # @return [Google::Cloud::Env::LazyDict]
609
+ #
610
+ attr_reader :cache
611
+
612
+ ##
613
+ # @private
614
+ # The variables access object
615
+ # @return [Google::Cloud::Env::Variables]
616
+ #
617
+ attr_reader :variables
618
+
619
+ ##
620
+ # @private
621
+ # The compute SMBIOS access object
622
+ # @return [Google::Cloud::Env::ComputeSMBIOS]
623
+ #
624
+ attr_reader :compute_smbios
625
+
626
+ ##
627
+ # @private
628
+ # Reset the cache, overrides, and all settings to default, for testing.
629
+ #
630
+ def reset!
631
+ @mutex.synchronize do
632
+ self.host = nil
633
+ @connection = Faraday.new url: host
634
+ self.open_timeout = DEFAULT_OPEN_TIMEOUT
635
+ self.request_timeout = DEFAULT_REQUEST_TIMEOUT
636
+ self.retry_count = DEFAULT_RETRY_COUNT
637
+ self.retry_timeout = DEFAULT_RETRY_TIMEOUT
638
+ self.retry_interval = DEFAULT_RETRY_INTERVAL
639
+ self.warmup_time = DEFAULT_WARMUP_TIME
640
+ @cache = create_cache
641
+ @overrides = nil
642
+ end
643
+ reset_existence!
644
+ end
645
+
646
+ ##
647
+ # @private
648
+ # Clear the existence cache, for testing.
649
+ #
650
+ def reset_existence!
651
+ @mutex.synchronize do
652
+ @existence = nil
653
+ @startup_time = Process.clock_gettime Process::CLOCK_MONOTONIC
654
+ end
655
+ self
656
+ end
657
+
658
+ private
659
+
660
+ ##
661
+ # @private
662
+ # A list of exceptions that are considered transient. They trigger a
663
+ # retry if received from an HTTP attempt, and they are not cached (i.e.
664
+ # the cache lifetime is set to 0.)
665
+ #
666
+ TRANSIENT_EXCEPTIONS = [
667
+ Faraday::TimeoutError,
668
+ Faraday::ConnectionFailed,
669
+ Errno::EHOSTDOWN,
670
+ Errno::ETIMEDOUT,
671
+ Timeout::Error
672
+ ].freeze
673
+
674
+ ##
675
+ # @private
676
+ #
677
+ # A buffer in seconds for token expiry. Our cache for the token will
678
+ # expire approximately this many seconds before the declared expiry
679
+ # time of the token itself.
680
+ #
681
+ # We want this value to be positive so that we provide some buffer to
682
+ # offset any clock skew and Metadata Server latency that might affect
683
+ # our calculation of the expiry time, but more importantly so that a
684
+ # client has approximately this amount of time to use a token we give
685
+ # them before it expires.
686
+ #
687
+ # We don't want this to be much higher, however, to keep the load down
688
+ # on the Metadata Server. We've been advised by the compute/serverless
689
+ # engineering teams to set this value less than 4 minutes because the
690
+ # Metadata Server can refresh the token as late as 4 minutes before the
691
+ # actual expiry of the previous token. If our cache expires and we
692
+ # request a new token, we actually want to receive a new token rather
693
+ # than the previous old token. See internal issue b/311414224.
694
+ #
695
+ TOKEN_EXPIRY_BUFFER = 210
696
+
697
+ ##
698
+ # @private
699
+ #
700
+ # Attempt to determine if we're on GCE (if we haven't previously), and
701
+ # update the existence flag. Return true if we *could* be on GCE, or
702
+ # false if we're definitely not.
703
+ #
704
+ def gce_check
705
+ if @existence.nil?
706
+ @mutex.synchronize do
707
+ @existence ||=
708
+ if @compute_smbios.google_compute? || maybe_gcf || maybe_gcr || maybe_gae
709
+ :unconfirmed
710
+ else
711
+ :no
712
+ end
713
+ end
714
+ end
715
+ @existence != :no
716
+ end
717
+
718
+ # @private
719
+ def maybe_gcf
720
+ @variables["K_SERVICE"] && @variables["K_REVISION"] && @variables["GAE_RUNTIME"]
721
+ end
722
+
723
+ # @private
724
+ def maybe_gcr
725
+ @variables["K_SERVICE"] && @variables["K_REVISION"] && @variables["K_CONFIGURATION"]
726
+ end
727
+
728
+ # @private
729
+ def maybe_gae
730
+ @variables["GAE_SERVICE"] && @variables["GAE_RUNTIME"]
731
+ end
732
+
733
+ ##
734
+ # @private
735
+ # Create and return a new LazyDict cache for the metadata
736
+ #
737
+ def create_cache
738
+ retries = proc do
739
+ Google::Cloud::Env::Retries.new max_tries: nil,
740
+ initial_delay: retry_interval,
741
+ delay_includes_time_elapsed: true
742
+ end
743
+ Google::Cloud::Env::LazyDict.new retries: retries do |(path, query), open_timeout, request_timeout|
744
+ internal_lookup path, query, open_timeout, request_timeout
745
+ end
746
+ end
747
+
748
+ ##
749
+ # @private
750
+ # Look up the given path, without using the cache.
751
+ #
752
+ def internal_lookup path, query, open_timeout, request_timeout
753
+ full_path = path ? "#{PATH_BASE}/#{path}" : ""
754
+ http_response = connection.get full_path do |req|
755
+ req.params = query if query
756
+ req.headers = FLAVOR_HEADER
757
+ req.options.timeout = request_timeout if request_timeout
758
+ req.options.open_timeout = open_timeout if open_timeout
759
+ end
760
+ response = Response.new http_response.status, http_response.body, http_response.headers
761
+ if path.nil?
762
+ post_update_existence(response.status == 200 && response.google_flavor?, response.retrieval_monotonic_time)
763
+ elsif response.google_flavor?
764
+ post_update_existence true, response.retrieval_monotonic_time
765
+ end
766
+ lifetime = determine_data_lifetime path, response.body.strip
767
+ LazyValue.expiring_value lifetime, response
768
+ rescue *TRANSIENT_EXCEPTIONS
769
+ post_update_existence false
770
+ raise MetadataServerNotResponding
771
+ end
772
+
773
+ ##
774
+ # @private
775
+ # Update existence based on a received result
776
+ #
777
+ def post_update_existence success, current_time = nil
778
+ return if @existence == :confirmed
779
+ @mutex.synchronize do
780
+ if success
781
+ @existence = :confirmed
782
+ elsif @existence != :confirmed
783
+ current_time ||= Process.clock_gettime Process::CLOCK_MONOTONIC
784
+ @existence = :no if current_time > @startup_time + warmup_time
785
+ end
786
+ end
787
+ end
788
+
789
+ ##
790
+ # @private
791
+ # Compute the lifetime of data, given the path and data. Returns the
792
+ # value in seconds, or nil for nonexpiring data.
793
+ #
794
+ def determine_data_lifetime path, data
795
+ case path
796
+ when %r{instance/service-accounts/[^/]+/token}
797
+ access_token_lifetime data
798
+ when %r{instance/service-accounts/[^/]+/identity}
799
+ identity_token_lifetime data
800
+ end
801
+ end
802
+
803
+ ##
804
+ # @private
805
+ # Extract the lifetime of an access token
806
+ #
807
+ def access_token_lifetime data
808
+ json = JSON.parse data rescue nil
809
+ return 0 unless json&.key? "expires_in"
810
+ lifetime = json["expires_in"].to_i - TOKEN_EXPIRY_BUFFER
811
+ lifetime = 0 if lifetime.negative?
812
+ lifetime
813
+ end
814
+
815
+ ##
816
+ # @private
817
+ # Extract the lifetime of an identity token
818
+ #
819
+ def identity_token_lifetime data
820
+ return 0 unless data =~ /^[\w=-]+\.([\w=-]+)\.[\w=-]+$/
821
+ base64 = Base64.decode64 Regexp.last_match[1]
822
+ json = JSON.parse base64 rescue nil
823
+ return 0 unless json&.key? "exp"
824
+ lifetime = json["exp"].to_i - Time.now.to_i - TOKEN_EXPIRY_BUFFER
825
+ lifetime = 0 if lifetime.negative?
826
+ lifetime
827
+ end
828
+
829
+ ##
830
+ # @private
831
+ # Stringify keys in a query hash
832
+ #
833
+ def canonicalize_query query
834
+ query&.transform_keys(&:to_s)
835
+ end
836
+
837
+ ##
838
+ # @private
839
+ # Lookup from overrides and return the result or raise.
840
+ # This must be called from within the mutex, and assumes that
841
+ # overrides is non-nil.
842
+ #
843
+ def lookup_override path, query
844
+ if @overrides.empty?
845
+ @existence = :no
846
+ raise MetadataServerNotResponding
847
+ end
848
+ @existence = :confirmed
849
+ result = @overrides.lookup path, query: query
850
+ result ||= Response.new 404, "Not found", FLAVOR_HEADER
851
+ result
852
+ end
853
+ end
854
+
855
+ ##
856
+ # Error raised when the compute metadata server is expected to be
857
+ # present in the current environment, but couldn't be contacted.
858
+ #
859
+ class MetadataServerNotResponding < StandardError
860
+ ##
861
+ # Default message for the error
862
+ # @return [String]
863
+ #
864
+ DEFAULT_MESSAGE =
865
+ "The Google Metadata Server did not respond to queries. This " \
866
+ "could be because no Server is present, the Server has not yet " \
867
+ "finished starting up, the Server is running but overloaded, or " \
868
+ "Server could not be contacted due to networking issues."
869
+
870
+ ##
871
+ # Create a new MetadataServerNotResponding.
872
+ #
873
+ # @param message [String] Error message. If not provided, defaults to
874
+ # {DEFAULT_MESSAGE}.
875
+ #
876
+ def initialize message = nil
877
+ message ||= DEFAULT_MESSAGE
878
+ super message
879
+ end
880
+ end
881
+ end
882
+ end
883
+ end