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