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