google-cloud-env 1.6.0 → 2.0.0

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