google-cloud-env 1.6.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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