1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package metadata
21
22 import (
23 "context"
24 "encoding/json"
25 "fmt"
26 "io"
27 "net"
28 "net/http"
29 "net/url"
30 "os"
31 "runtime"
32 "strings"
33 "sync"
34 "time"
35 )
36
37 const (
38
39 metadataIP = "169.254.169.254"
40
41
42
43
44
45
46 metadataHostEnv = "GCE_METADATA_HOST"
47
48 userAgent = "gcloud-golang/0.1"
49 )
50
51 type cachedValue struct {
52 k string
53 trim bool
54 mu sync.Mutex
55 v string
56 }
57
58 var (
59 projID = &cachedValue{k: "project/project-id", trim: true}
60 projNum = &cachedValue{k: "project/numeric-project-id", trim: true}
61 instID = &cachedValue{k: "instance/id", trim: true}
62 )
63
64 var defaultClient = &Client{hc: newDefaultHTTPClient()}
65
66 func newDefaultHTTPClient() *http.Client {
67 return &http.Client{
68 Transport: &http.Transport{
69 Dial: (&net.Dialer{
70 Timeout: 2 * time.Second,
71 KeepAlive: 30 * time.Second,
72 }).Dial,
73 IdleConnTimeout: 60 * time.Second,
74 },
75 Timeout: 5 * time.Second,
76 }
77 }
78
79
80
81
82
83
84
85 type NotDefinedError string
86
87 func (suffix NotDefinedError) Error() string {
88 return fmt.Sprintf("metadata: GCE metadata %q not defined", string(suffix))
89 }
90
91 func (c *cachedValue) get(cl *Client) (v string, err error) {
92 defer c.mu.Unlock()
93 c.mu.Lock()
94 if c.v != "" {
95 return c.v, nil
96 }
97 if c.trim {
98 v, err = cl.getTrimmed(context.Background(), c.k)
99 } else {
100 v, err = cl.GetWithContext(context.Background(), c.k)
101 }
102 if err == nil {
103 c.v = v
104 }
105 return
106 }
107
108 var (
109 onGCEOnce sync.Once
110 onGCE bool
111 )
112
113
114 func OnGCE() bool {
115 onGCEOnce.Do(initOnGCE)
116 return onGCE
117 }
118
119 func initOnGCE() {
120 onGCE = testOnGCE()
121 }
122
123 func testOnGCE() bool {
124
125 if os.Getenv(metadataHostEnv) != "" {
126 return true
127 }
128
129 ctx, cancel := context.WithCancel(context.Background())
130 defer cancel()
131
132 resc := make(chan bool, 2)
133
134
135
136 go func() {
137 req, _ := http.NewRequest("GET", "http://"+metadataIP, nil)
138 req.Header.Set("User-Agent", userAgent)
139 res, err := newDefaultHTTPClient().Do(req.WithContext(ctx))
140 if err != nil {
141 resc <- false
142 return
143 }
144 defer res.Body.Close()
145 resc <- res.Header.Get("Metadata-Flavor") == "Google"
146 }()
147
148 go func() {
149 resolver := &net.Resolver{}
150 addrs, err := resolver.LookupHost(ctx, "metadata.google.internal.")
151 if err != nil || len(addrs) == 0 {
152 resc <- false
153 return
154 }
155 resc <- strsContains(addrs, metadataIP)
156 }()
157
158 tryHarder := systemInfoSuggestsGCE()
159 if tryHarder {
160 res := <-resc
161 if res {
162
163 return true
164 }
165
166
167
168
169 timer := time.NewTimer(5 * time.Second)
170 defer timer.Stop()
171 select {
172 case res = <-resc:
173 return res
174 case <-timer.C:
175
176 return false
177 }
178 }
179
180
181
182
183
184
185
186
187
188 return <-resc
189 }
190
191
192
193
194
195 func systemInfoSuggestsGCE() bool {
196 if runtime.GOOS != "linux" {
197
198 return false
199 }
200 slurp, _ := os.ReadFile("/sys/class/dmi/id/product_name")
201 name := strings.TrimSpace(string(slurp))
202 return name == "Google" || name == "Google Compute Engine"
203 }
204
205
206 func Subscribe(suffix string, fn func(v string, ok bool) error) error {
207 return defaultClient.SubscribeWithContext(context.Background(), suffix, func(ctx context.Context, v string, ok bool) error { return fn(v, ok) })
208 }
209
210
211 func SubscribeWithContext(ctx context.Context, suffix string, fn func(ctx context.Context, v string, ok bool) error) error {
212 return defaultClient.SubscribeWithContext(ctx, suffix, fn)
213 }
214
215
216
217
218 func Get(suffix string) (string, error) {
219 return defaultClient.GetWithContext(context.Background(), suffix)
220 }
221
222
223 func GetWithContext(ctx context.Context, suffix string) (string, error) {
224 return defaultClient.GetWithContext(ctx, suffix)
225 }
226
227
228 func ProjectID() (string, error) { return defaultClient.ProjectID() }
229
230
231 func NumericProjectID() (string, error) { return defaultClient.NumericProjectID() }
232
233
234 func InternalIP() (string, error) { return defaultClient.InternalIP() }
235
236
237 func ExternalIP() (string, error) { return defaultClient.ExternalIP() }
238
239
240 func Email(serviceAccount string) (string, error) { return defaultClient.Email(serviceAccount) }
241
242
243
244 func Hostname() (string, error) { return defaultClient.Hostname() }
245
246
247
248 func InstanceTags() ([]string, error) { return defaultClient.InstanceTags() }
249
250
251 func InstanceID() (string, error) { return defaultClient.InstanceID() }
252
253
254 func InstanceName() (string, error) { return defaultClient.InstanceName() }
255
256
257 func Zone() (string, error) { return defaultClient.Zone() }
258
259
260 func InstanceAttributes() ([]string, error) { return defaultClient.InstanceAttributes() }
261
262
263 func ProjectAttributes() ([]string, error) { return defaultClient.ProjectAttributes() }
264
265
266 func InstanceAttributeValue(attr string) (string, error) {
267 return defaultClient.InstanceAttributeValue(attr)
268 }
269
270
271 func ProjectAttributeValue(attr string) (string, error) {
272 return defaultClient.ProjectAttributeValue(attr)
273 }
274
275
276 func Scopes(serviceAccount string) ([]string, error) { return defaultClient.Scopes(serviceAccount) }
277
278 func strsContains(ss []string, s string) bool {
279 for _, v := range ss {
280 if v == s {
281 return true
282 }
283 }
284 return false
285 }
286
287
288 type Client struct {
289 hc *http.Client
290 }
291
292
293
294
295 func NewClient(c *http.Client) *Client {
296 if c == nil {
297 return defaultClient
298 }
299
300 return &Client{hc: c}
301 }
302
303
304
305 func (c *Client) getETag(ctx context.Context, suffix string) (value, etag string, err error) {
306
307
308
309
310
311 host := os.Getenv(metadataHostEnv)
312 if host == "" {
313
314
315
316
317
318 host = metadataIP
319 }
320 suffix = strings.TrimLeft(suffix, "/")
321 u := "http://" + host + "/computeMetadata/v1/" + suffix
322 req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
323 if err != nil {
324 return "", "", err
325 }
326 req.Header.Set("Metadata-Flavor", "Google")
327 req.Header.Set("User-Agent", userAgent)
328 var res *http.Response
329 var reqErr error
330 retryer := newRetryer()
331 for {
332 res, reqErr = c.hc.Do(req)
333 var code int
334 if res != nil {
335 code = res.StatusCode
336 }
337 if delay, shouldRetry := retryer.Retry(code, reqErr); shouldRetry {
338 if err := sleep(ctx, delay); err != nil {
339 return "", "", err
340 }
341 continue
342 }
343 break
344 }
345 if reqErr != nil {
346 return "", "", reqErr
347 }
348 defer res.Body.Close()
349 if res.StatusCode == http.StatusNotFound {
350 return "", "", NotDefinedError(suffix)
351 }
352 all, err := io.ReadAll(res.Body)
353 if err != nil {
354 return "", "", err
355 }
356 if res.StatusCode != 200 {
357 return "", "", &Error{Code: res.StatusCode, Message: string(all)}
358 }
359 return string(all), res.Header.Get("Etag"), nil
360 }
361
362
363
364
365
366
367
368
369
370
371
372 func (c *Client) Get(suffix string) (string, error) {
373 return c.GetWithContext(context.Background(), suffix)
374 }
375
376
377
378
379
380
381
382
383
384 func (c *Client) GetWithContext(ctx context.Context, suffix string) (string, error) {
385 val, _, err := c.getETag(ctx, suffix)
386 return val, err
387 }
388
389 func (c *Client) getTrimmed(ctx context.Context, suffix string) (s string, err error) {
390 s, err = c.GetWithContext(ctx, suffix)
391 s = strings.TrimSpace(s)
392 return
393 }
394
395 func (c *Client) lines(suffix string) ([]string, error) {
396 j, err := c.GetWithContext(context.Background(), suffix)
397 if err != nil {
398 return nil, err
399 }
400 s := strings.Split(strings.TrimSpace(j), "\n")
401 for i := range s {
402 s[i] = strings.TrimSpace(s[i])
403 }
404 return s, nil
405 }
406
407
408 func (c *Client) ProjectID() (string, error) { return projID.get(c) }
409
410
411 func (c *Client) NumericProjectID() (string, error) { return projNum.get(c) }
412
413
414 func (c *Client) InstanceID() (string, error) { return instID.get(c) }
415
416
417 func (c *Client) InternalIP() (string, error) {
418 return c.getTrimmed(context.Background(), "instance/network-interfaces/0/ip")
419 }
420
421
422
423
424 func (c *Client) Email(serviceAccount string) (string, error) {
425 if serviceAccount == "" {
426 serviceAccount = "default"
427 }
428 return c.getTrimmed(context.Background(), "instance/service-accounts/"+serviceAccount+"/email")
429 }
430
431
432 func (c *Client) ExternalIP() (string, error) {
433 return c.getTrimmed(context.Background(), "instance/network-interfaces/0/access-configs/0/external-ip")
434 }
435
436
437
438 func (c *Client) Hostname() (string, error) {
439 return c.getTrimmed(context.Background(), "instance/hostname")
440 }
441
442
443
444 func (c *Client) InstanceTags() ([]string, error) {
445 var s []string
446 j, err := c.GetWithContext(context.Background(), "instance/tags")
447 if err != nil {
448 return nil, err
449 }
450 if err := json.NewDecoder(strings.NewReader(j)).Decode(&s); err != nil {
451 return nil, err
452 }
453 return s, nil
454 }
455
456
457 func (c *Client) InstanceName() (string, error) {
458 return c.getTrimmed(context.Background(), "instance/name")
459 }
460
461
462 func (c *Client) Zone() (string, error) {
463 zone, err := c.getTrimmed(context.Background(), "instance/zone")
464
465 if err != nil {
466 return "", err
467 }
468 return zone[strings.LastIndex(zone, "/")+1:], nil
469 }
470
471
472
473
474 func (c *Client) InstanceAttributes() ([]string, error) { return c.lines("instance/attributes/") }
475
476
477
478
479 func (c *Client) ProjectAttributes() ([]string, error) { return c.lines("project/attributes/") }
480
481
482
483
484
485
486
487
488
489 func (c *Client) InstanceAttributeValue(attr string) (string, error) {
490 return c.GetWithContext(context.Background(), "instance/attributes/"+attr)
491 }
492
493
494
495
496
497
498
499
500
501 func (c *Client) ProjectAttributeValue(attr string) (string, error) {
502 return c.GetWithContext(context.Background(), "project/attributes/"+attr)
503 }
504
505
506
507
508 func (c *Client) Scopes(serviceAccount string) ([]string, error) {
509 if serviceAccount == "" {
510 serviceAccount = "default"
511 }
512 return c.lines("instance/service-accounts/" + serviceAccount + "/scopes")
513 }
514
515
516
517
518
519
520 func (c *Client) Subscribe(suffix string, fn func(v string, ok bool) error) error {
521 return c.SubscribeWithContext(context.Background(), suffix, func(ctx context.Context, v string, ok bool) error { return fn(v, ok) })
522 }
523
524
525
526
527
528
529
530
531
532
533 func (c *Client) SubscribeWithContext(ctx context.Context, suffix string, fn func(ctx context.Context, v string, ok bool) error) error {
534 const failedSubscribeSleep = time.Second * 5
535
536
537 val, lastETag, err := c.getETag(ctx, suffix)
538 if err != nil {
539 return err
540 }
541
542 if err := fn(ctx, val, true); err != nil {
543 return err
544 }
545
546 ok := true
547 if strings.ContainsRune(suffix, '?') {
548 suffix += "&wait_for_change=true&last_etag="
549 } else {
550 suffix += "?wait_for_change=true&last_etag="
551 }
552 for {
553 val, etag, err := c.getETag(ctx, suffix+url.QueryEscape(lastETag))
554 if err != nil {
555 if _, deleted := err.(NotDefinedError); !deleted {
556 time.Sleep(failedSubscribeSleep)
557 continue
558 }
559 ok = false
560 }
561 lastETag = etag
562
563 if err := fn(ctx, val, ok); err != nil || !ok {
564 return err
565 }
566 }
567 }
568
569
570 type Error struct {
571
572 Code int
573
574 Message string
575 }
576
577 func (e *Error) Error() string {
578 return fmt.Sprintf("compute: Received %d `%s`", e.Code, e.Message)
579 }
580
View as plain text