google-cloud-env 1.6.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,882 @@
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
+ raise MetadataServerNotResponding unless gce_check
393
+ if @overrides
394
+ @mutex.synchronize do
395
+ return lookup_override path, query if @overrides
396
+ end
397
+ end
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
+ result = @overrides.lookup path, query: query
849
+ result ||= Response.new 404, "Not found", FLAVOR_HEADER
850
+ result
851
+ end
852
+ end
853
+
854
+ ##
855
+ # Error raised when the compute metadata server is expected to be
856
+ # present in the current environment, but couldn't be contacted.
857
+ #
858
+ class MetadataServerNotResponding < StandardError
859
+ ##
860
+ # Default message for the error
861
+ # @return [String]
862
+ #
863
+ DEFAULT_MESSAGE =
864
+ "The Google Metadata Server did not respond to queries. This " \
865
+ "could be because no Server is present, the Server has not yet " \
866
+ "finished starting up, the Server is running but overloaded, or " \
867
+ "Server could not be contacted due to networking issues."
868
+
869
+ ##
870
+ # Create a new MetadataServerNotResponding.
871
+ #
872
+ # @param message [String] Error message. If not provided, defaults to
873
+ # {DEFAULT_MESSAGE}.
874
+ #
875
+ def initialize message = nil
876
+ message ||= DEFAULT_MESSAGE
877
+ super message
878
+ end
879
+ end
880
+ end
881
+ end
882
+ end