google-cloud-env 1.6.0 → 2.0.1

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