google-cloud-env 1.6.0 → 2.0.0

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,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