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  }