gcp.go 3.5 KB

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