github.com/hairyhenderson/gomplate/v4@v4.0.0-pre-2.0.20240520121557-362f058f0c93/gcp/meta.go (about) 1 package gcp 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "net/http" 8 "strconv" 9 "strings" 10 "sync" 11 "time" 12 13 "github.com/hairyhenderson/gomplate/v4/env" 14 ) 15 16 // DefaultEndpoint is the DNS name for the default GCP compute instance metadata service. 17 var DefaultEndpoint = "http://metadata.google.internal" 18 19 var ( 20 // co is a ClientOptions populated from the environment. 21 co ClientOptions 22 // coInit ensures that `co` is only set once. 23 coInit sync.Once 24 ) 25 26 // ClientOptions contains various user-specifiable options for a MetaClient. 27 type ClientOptions struct { 28 Timeout time.Duration 29 } 30 31 // GetClientOptions - Centralised reading of GCP_TIMEOUT 32 // ... but cannot use in vault/auth.go as different strconv.Atoi error handling 33 func GetClientOptions() ClientOptions { 34 coInit.Do(func() { 35 timeout := env.Getenv("GCP_TIMEOUT") 36 if timeout == "" { 37 timeout = "500" 38 } 39 40 t, err := strconv.Atoi(timeout) 41 if err != nil { 42 panic(fmt.Errorf("invalid GCP_TIMEOUT value '%s' - must be an integer: %w", timeout, err)) 43 } 44 45 co.Timeout = time.Duration(t) * time.Millisecond 46 }) 47 return co 48 } 49 50 // MetaClient is used to access metadata accessible via the GCP compute instance 51 // metadata service version 1. 52 type MetaClient struct { 53 ctx context.Context 54 client *http.Client 55 cache map[string]string 56 endpoint string 57 options ClientOptions 58 } 59 60 // NewMetaClient constructs a new MetaClient with the given ClientOptions. If the environment 61 // contains a variable named `GCP_META_ENDPOINT`, the client will address that, if not the 62 // value of `DefaultEndpoint` is used. 63 func NewMetaClient(ctx context.Context, options ClientOptions) *MetaClient { 64 endpoint := env.Getenv("GCP_META_ENDPOINT") 65 if endpoint == "" { 66 endpoint = DefaultEndpoint 67 } 68 69 return &MetaClient{ 70 ctx: ctx, 71 cache: make(map[string]string), 72 endpoint: endpoint, 73 options: options, 74 } 75 } 76 77 // Meta retrieves a value from the GCP Instance Metadata Service, returning the given default 78 // if the service is unavailable or the requested URL does not exist. 79 func (c *MetaClient) Meta(key string, def ...string) (string, error) { 80 url := c.endpoint + "/computeMetadata/v1/instance/" + key 81 return c.retrieveMetadata(c.ctx, url, def...) 82 } 83 84 // retrieveMetadata executes an HTTP request to the GCP Instance Metadata Service with the 85 // correct headers set, and extracts the returned value. 86 func (c *MetaClient) retrieveMetadata(ctx context.Context, url string, def ...string) (string, error) { 87 if value, ok := c.cache[url]; ok { 88 return value, nil 89 } 90 91 if c.client == nil { 92 timeout := c.options.Timeout 93 if timeout == 0 { 94 timeout = 500 * time.Millisecond 95 } 96 c.client = &http.Client{Timeout: timeout} 97 } 98 99 request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 100 if err != nil { 101 return returnDefault(def), nil 102 } 103 request.Header.Add("Metadata-Flavor", "Google") 104 105 resp, err := c.client.Do(request) 106 if err != nil { 107 return returnDefault(def), nil 108 } 109 110 defer resp.Body.Close() 111 if resp.StatusCode > 399 { 112 return returnDefault(def), nil 113 } 114 115 body, err := io.ReadAll(resp.Body) 116 if err != nil { 117 return "", fmt.Errorf("failed to read response body from %s: %w", url, err) 118 } 119 value := strings.TrimSpace(string(body)) 120 c.cache[url] = value 121 122 return value, nil 123 } 124 125 // returnDefault returns the first element of the given slice (often taken from varargs) 126 // if there is one, or returns an empty string if the slice has no elements. 127 func returnDefault(def []string) string { 128 if len(def) > 0 { 129 return def[0] 130 } 131 return "" 132 }