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  }