github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/util/cloudinfo/cloudinfo.go (about)

     1  // Copyright 2019 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package cloudinfo
    12  
    13  import (
    14  	"context"
    15  	"encoding/json"
    16  	"io/ioutil"
    17  	"regexp"
    18  	"time"
    19  
    20  	"github.com/cockroachdb/cockroach/pkg/util/httputil"
    21  )
    22  
    23  const (
    24  	aws                   = "aws"
    25  	awsMetadataEndpoint   = "http://instance-data.ec2.internal/latest/dynamic/instance-identity/document"
    26  	gcp                   = "gcp"
    27  	gcpMetadataEndpoint   = "http://metadata.google.internal/computeMetadata/v1/instance/"
    28  	azure                 = "azure"
    29  	azureMetadataEndpoint = "http://169.254.169.254/metadata/instance?api-version=2018-10-01"
    30  	instanceClass         = "instanceClass"
    31  	region                = "region"
    32  )
    33  
    34  var enabled = true
    35  
    36  // Disable disables cloud detection until the returned function is called.
    37  // Used for tests that trigger diagnostics updates.
    38  func Disable() (restore func()) {
    39  	enabled = false
    40  	return func() { enabled = true }
    41  }
    42  
    43  // client is necessary to provide a struct for mocking http requests
    44  // in testing.
    45  type client struct {
    46  	httpClient *httputil.Client
    47  }
    48  
    49  type metadataReqHeader struct {
    50  	key   string
    51  	value string
    52  }
    53  
    54  // getAWSInstanceMetadata tries to access the AWS instance metadata
    55  // endpoint to provide metadata about the node. The metadata structure
    56  // is described at:
    57  // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html
    58  func (cli *client) getAWSInstanceMetadata(
    59  	ctx context.Context, metadataElement string,
    60  ) (bool, string, string) {
    61  	body, err := cli.getInstanceMetadata(ctx, awsMetadataEndpoint, []metadataReqHeader{})
    62  
    63  	if err != nil {
    64  		return false, "", ""
    65  	}
    66  
    67  	instanceMetadata := struct {
    68  		InstanceClass string `json:"instanceType"`
    69  		Region        string `json:"Region"`
    70  	}{}
    71  
    72  	if err := json.Unmarshal(body, &instanceMetadata); err != nil {
    73  		return false, "", ""
    74  	}
    75  
    76  	switch metadataElement {
    77  	case instanceClass:
    78  		return true, aws, instanceMetadata.InstanceClass
    79  	case region:
    80  		return true, aws, instanceMetadata.Region
    81  	default:
    82  		return false, "", ""
    83  	}
    84  }
    85  
    86  // getGCPInstanceMetadata tries to access the AWS instance metadata
    87  // endpoint to provide metadata about the node. The metadata structure
    88  // is described at:
    89  // https://cloud.google.com/compute/docs/storing-retrieving-metadata
    90  func (cli *client) getGCPInstanceMetadata(
    91  	ctx context.Context, metadataElement string,
    92  ) (bool, string, string) {
    93  	var endpointPattern string
    94  	var requestEndpoint = gcpMetadataEndpoint
    95  
    96  	switch metadataElement {
    97  	case instanceClass:
    98  		requestEndpoint += "machine-type"
    99  		endpointPattern = `machineTypes\/(.+)$`
   100  	case region:
   101  		requestEndpoint += "zone"
   102  		endpointPattern = `zones\/(.+)$`
   103  	default:
   104  		return false, "", ""
   105  	}
   106  
   107  	body, err := cli.getInstanceMetadata(ctx, requestEndpoint, []metadataReqHeader{{
   108  		"Metadata-Flavor", "Google",
   109  	}})
   110  
   111  	if err != nil {
   112  		return false, "", ""
   113  	}
   114  
   115  	resultRE := regexp.MustCompile(endpointPattern)
   116  
   117  	result := resultRE.FindStringSubmatch(string(body))
   118  
   119  	// Regex should only have 2 values: matched string and
   120  	// capture group containing the machineTypes value.
   121  	if len(result) != 2 {
   122  		return false, "", ""
   123  	}
   124  
   125  	return true, gcp, result[1]
   126  
   127  }
   128  
   129  // getAzureInstanceMetadata tries to access the AWS instance metadata
   130  // endpoint to provide metadata about the node. The metadata structure
   131  // is described at:
   132  // https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service
   133  func (cli *client) getAzureInstanceMetadata(
   134  	ctx context.Context, metadataElement string,
   135  ) (bool, string, string) {
   136  	body, err := cli.getInstanceMetadata(ctx, azureMetadataEndpoint, []metadataReqHeader{{
   137  		"Metadata", "true",
   138  	}})
   139  
   140  	if err != nil {
   141  		return false, "", ""
   142  	}
   143  
   144  	instanceMetadata := struct {
   145  		ComputeEnv struct {
   146  			InstanceClass string `json:"vmSize"`
   147  			Region        string `json:"Location"`
   148  		} `json:"compute"`
   149  	}{}
   150  
   151  	if err := json.Unmarshal(body, &instanceMetadata); err != nil {
   152  		return false, "", ""
   153  	}
   154  
   155  	switch metadataElement {
   156  	case instanceClass:
   157  		return true, azure, instanceMetadata.ComputeEnv.InstanceClass
   158  	case region:
   159  		return true, azure, instanceMetadata.ComputeEnv.Region
   160  	default:
   161  		return false, "", ""
   162  	}
   163  }
   164  
   165  func (cli *client) getInstanceMetadata(
   166  	ctx context.Context, url string, headers []metadataReqHeader,
   167  ) ([]byte, error) {
   168  	req, err := httputil.NewRequestWithContext(ctx, "GET", url, nil)
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  
   173  	for _, header := range headers {
   174  		req.Header.Set(header.key, header.value)
   175  	}
   176  
   177  	resp, err := cli.httpClient.Do(req)
   178  
   179  	if err != nil {
   180  		return nil, err
   181  	}
   182  
   183  	defer resp.Body.Close()
   184  
   185  	return ioutil.ReadAll(resp.Body)
   186  }
   187  
   188  // getCloudInfo provides a generic interface to iterate over the
   189  // defined cloud functions, attempting to determine which platform
   190  // the node is running on, as well as the value of the requested metadata
   191  // element.
   192  func getCloudInfo(ctx context.Context, metadataElement string) (provider string, element string) {
   193  	if !enabled {
   194  		return "", ""
   195  	}
   196  
   197  	const timeout = 500 * time.Millisecond
   198  	cli := client{httputil.NewClientWithTimeout(timeout)}
   199  
   200  	// getCloudMetadata lets us iterate over all of the functions to check
   201  	// the defined clouds for the metadata element we're looking for.
   202  	getCloudMetadata := []struct {
   203  		get func(context.Context, string) (bool, string, string)
   204  	}{
   205  		{cli.getAWSInstanceMetadata},
   206  		{cli.getGCPInstanceMetadata},
   207  		{cli.getAzureInstanceMetadata},
   208  	}
   209  
   210  	var success bool
   211  
   212  	for _, c := range getCloudMetadata {
   213  		success, provider, element = c.get(ctx, metadataElement)
   214  		if success {
   215  			return provider, element
   216  		}
   217  	}
   218  	return "", ""
   219  }
   220  
   221  // GetInstanceClass returns the node's instance provider (e.g. AWS) and
   222  // the name given to its instance class (e.g. m5a.large).
   223  func GetInstanceClass(ctx context.Context) (providerName string, instanceClassName string) {
   224  	return getCloudInfo(ctx, instanceClass)
   225  }
   226  
   227  // GetInstanceRegion returns the node's instance provider (e.g. AWS) and
   228  // the name given to its region (e.g. us-east-1d).
   229  func GetInstanceRegion(ctx context.Context) (providerName string, regionName string) {
   230  	return getCloudInfo(ctx, region)
   231  }