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 }