github.com/newrelic/go-agent@v3.26.0+incompatible/internal/utilization/gcp.go (about) 1 // Copyright 2020 New Relic Corporation. All rights reserved. 2 // SPDX-License-Identifier: Apache-2.0 3 4 package utilization 5 6 import ( 7 "encoding/json" 8 "fmt" 9 "io/ioutil" 10 "net/http" 11 "strings" 12 ) 13 14 const ( 15 gcpHostname = "metadata.google.internal" 16 gcpEndpointPath = "/computeMetadata/v1/instance/?recursive=true" 17 gcpEndpoint = "http://" + gcpHostname + gcpEndpointPath 18 ) 19 20 func gatherGCP(util *Data, client *http.Client) error { 21 gcp, err := getGCP(client) 22 if err != nil { 23 // Only return the error here if it is unexpected to prevent 24 // warning customers who aren't running GCP about a timeout. 25 if _, ok := err.(unexpectedGCPErr); ok { 26 return err 27 } 28 return nil 29 } 30 util.Vendors.GCP = gcp 31 32 return nil 33 } 34 35 // numericString is used rather than json.Number because we want the output when 36 // marshalled to be a string, rather than a number. 37 type numericString string 38 39 func (ns *numericString) MarshalJSON() ([]byte, error) { 40 return json.Marshal(ns.String()) 41 } 42 43 func (ns *numericString) String() string { 44 return string(*ns) 45 } 46 47 func (ns *numericString) UnmarshalJSON(data []byte) error { 48 var n int64 49 50 // Try to unmarshal as an integer first. 51 if err := json.Unmarshal(data, &n); err == nil { 52 *ns = numericString(fmt.Sprintf("%d", n)) 53 return nil 54 } 55 56 // Otherwise, unmarshal as a string, and verify that it's numeric (for our 57 // definition of numeric, which is actually integral). 58 var s string 59 if err := json.Unmarshal(data, &s); err != nil { 60 return err 61 } 62 63 for _, r := range s { 64 if r < '0' || r > '9' { 65 return fmt.Errorf("invalid numeric character: %c", r) 66 } 67 } 68 69 *ns = numericString(s) 70 return nil 71 } 72 73 type gcp struct { 74 ID numericString `json:"id"` 75 MachineType string `json:"machineType,omitempty"` 76 Name string `json:"name,omitempty"` 77 Zone string `json:"zone,omitempty"` 78 } 79 80 type unexpectedGCPErr struct{ e error } 81 82 func (e unexpectedGCPErr) Error() string { 83 return fmt.Sprintf("unexpected GCP error: %v", e.e) 84 } 85 86 func getGCP(client *http.Client) (*gcp, error) { 87 // GCP's metadata service requires a Metadata-Flavor header because... hell, I 88 // don't know, maybe they really like Guy Fieri? 89 req, err := http.NewRequest("GET", gcpEndpoint, nil) 90 if err != nil { 91 return nil, err 92 } 93 req.Header.Add("Metadata-Flavor", "Google") 94 95 response, err := client.Do(req) 96 if err != nil { 97 return nil, err 98 } 99 defer response.Body.Close() 100 101 if response.StatusCode != 200 { 102 return nil, unexpectedGCPErr{e: fmt.Errorf("response code %d", response.StatusCode)} 103 } 104 105 data, err := ioutil.ReadAll(response.Body) 106 if err != nil { 107 return nil, unexpectedGCPErr{e: err} 108 } 109 110 g := &gcp{} 111 if err := json.Unmarshal(data, g); err != nil { 112 return nil, unexpectedGCPErr{e: err} 113 } 114 115 if err := g.validate(); err != nil { 116 return nil, unexpectedGCPErr{e: err} 117 } 118 119 return g, nil 120 } 121 122 func (g *gcp) validate() (err error) { 123 id, err := normalizeValue(g.ID.String()) 124 if err != nil { 125 return fmt.Errorf("Invalid ID: %v", err) 126 } 127 g.ID = numericString(id) 128 129 mt, err := normalizeValue(g.MachineType) 130 if err != nil { 131 return fmt.Errorf("Invalid machine type: %v", err) 132 } 133 g.MachineType = stripGCPPrefix(mt) 134 135 g.Name, err = normalizeValue(g.Name) 136 if err != nil { 137 return fmt.Errorf("Invalid name: %v", err) 138 } 139 140 zone, err := normalizeValue(g.Zone) 141 if err != nil { 142 return fmt.Errorf("Invalid zone: %v", err) 143 } 144 g.Zone = stripGCPPrefix(zone) 145 146 return 147 } 148 149 // We're only interested in the last element of slash separated paths for the 150 // machine type and zone values, so this function handles stripping the parts 151 // we don't need. 152 func stripGCPPrefix(s string) string { 153 parts := strings.Split(s, "/") 154 return parts[len(parts)-1] 155 }