forked from newrelic/go-agent
-
Notifications
You must be signed in to change notification settings - Fork 0
/
gcp.go
155 lines (126 loc) · 3.61 KB
/
gcp.go
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]
}