forked from elastic/apm-agent-go
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Based on the Python reference implementation: elastic/apm-agent-python#826 By default we attempt to sniff metadata for all cloud providers. This can be overridden by setting the environment variable ELASTIC_APM_CLOUD_PROVIDER to one of "none", "aws", "azure", or "gcp". We set a short (100ms) socket connect timeout, and a slightly longer overall timeout (1s) for fetching the cloud metadata. In tests we disable cloud metadata fetching by default. There are currently no functional tests, as that would rely on us running in a known cloud environment.
- Loading branch information
Showing
14 changed files
with
665 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package apmtest | ||
|
||
import "os" | ||
|
||
func init() { | ||
// Disable cloud metadata sniffing by default in tests. | ||
os.Setenv("ELASTIC_APM_CLOUD_PROVIDER", "none") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
package apmcloudutil | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"io/ioutil" | ||
"net/http" | ||
|
||
"go.elastic.co/apm/model" | ||
) | ||
|
||
const ( | ||
ec2TokenURL = "http://169.254.169.254/latest/api/token" | ||
ec2MetadataURL = "http://169.254.169.254/latest/dynamic/instance-identity/document" | ||
) | ||
|
||
// See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html | ||
func getAWSCloudMetadata(ctx context.Context, client *http.Client, out *model.Cloud) error { | ||
token, err := getAWSToken(ctx, client) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
req, err := http.NewRequest("GET", ec2MetadataURL, nil) | ||
if err != nil { | ||
return err | ||
} | ||
if token != "" { | ||
req.Header.Set("X-aws-ec2-metadata-token", token) | ||
} | ||
|
||
resp, err := client.Do(req.WithContext(ctx)) | ||
if err != nil { | ||
return err | ||
} | ||
defer resp.Body.Close() | ||
if resp.StatusCode != http.StatusOK { | ||
return errors.New(resp.Status) | ||
} | ||
|
||
var ec2Metadata struct { | ||
AccountID string `json:"accountId"` | ||
AvailabilityZone string `json:"availabilityZone"` | ||
Region string `json:"region"` | ||
InstanceID string `json:"instanceId"` | ||
InstanceType string `json:"instanceType"` | ||
} | ||
if err := json.NewDecoder(resp.Body).Decode(&ec2Metadata); err != nil { | ||
return err | ||
} | ||
|
||
out.Region = ec2Metadata.Region | ||
out.AvailabilityZone = ec2Metadata.AvailabilityZone | ||
if ec2Metadata.InstanceID != "" { | ||
out.Instance = &model.CloudInstance{ID: ec2Metadata.InstanceID} | ||
} | ||
if ec2Metadata.InstanceType != "" { | ||
out.Machine = &model.CloudMachine{Type: ec2Metadata.InstanceType} | ||
} | ||
if ec2Metadata.AccountID != "" { | ||
out.Account = &model.CloudAccount{ID: ec2Metadata.AccountID} | ||
} | ||
return nil | ||
} | ||
|
||
func getAWSToken(ctx context.Context, client *http.Client) (string, error) { | ||
req, err := http.NewRequest("PUT", ec2TokenURL, nil) | ||
if err != nil { | ||
return "", err | ||
} | ||
req.Header.Set("X-aws-ec2-metadata-token-ttl-seconds", "300") | ||
resp, err := client.Do(req.WithContext(ctx)) | ||
if err != nil { | ||
return "", err | ||
} | ||
defer resp.Body.Close() | ||
token, err := ioutil.ReadAll(resp.Body) | ||
if err != nil { | ||
return "", err | ||
} | ||
return string(token), nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
package apmcloudutil | ||
|
||
import ( | ||
"context" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
|
||
"go.elastic.co/apm/model" | ||
) | ||
|
||
func TestAWSCloudMetadata(t *testing.T) { | ||
srv, client := newAWSMetadataServer() | ||
defer srv.Close() | ||
|
||
for _, provider := range []Provider{Auto, AWS} { | ||
var out model.Cloud | ||
var logger testLogger | ||
assert.True(t, provider.getCloudMetadata(context.Background(), client, &logger, &out)) | ||
assert.Zero(t, logger) | ||
assert.Equal(t, model.Cloud{ | ||
Provider: "aws", | ||
Region: "us-east-2", | ||
AvailabilityZone: "us-east-2a", | ||
Instance: &model.CloudInstance{ | ||
ID: "i-0ae894a7c1c4f2a75", | ||
}, | ||
Machine: &model.CloudMachine{ | ||
Type: "t2.medium", | ||
}, | ||
Account: &model.CloudAccount{ | ||
ID: "946960629917", | ||
}, | ||
}, out) | ||
} | ||
} | ||
|
||
func newAWSMetadataServer() (*httptest.Server, *http.Client) { | ||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
switch r.URL.Path { | ||
case "/latest/api/token": | ||
w.Write([]byte("topsecret")) | ||
return | ||
case "/latest/dynamic/instance-identity/document": | ||
token := r.Header.Get("X-Aws-Ec2-Metadata-Token") | ||
if token != "topsecret" { | ||
w.WriteHeader(http.StatusBadRequest) | ||
w.Write([]byte("invalid token")) | ||
return | ||
} | ||
break | ||
default: | ||
w.WriteHeader(http.StatusNotFound) | ||
return | ||
} | ||
|
||
w.Write([]byte(`{ | ||
"accountId": "946960629917", | ||
"architecture": "x86_64", | ||
"availabilityZone": "us-east-2a", | ||
"billingProducts": null, | ||
"devpayProductCodes": null, | ||
"marketplaceProductCodes": null, | ||
"imageId": "ami-07c1207a9d40bc3bd", | ||
"instanceId": "i-0ae894a7c1c4f2a75", | ||
"instanceType": "t2.medium", | ||
"kernelId": null, | ||
"pendingTime": "2020-06-12T17:46:09Z", | ||
"privateIp": "172.31.0.212", | ||
"ramdiskId": null, | ||
"region": "us-east-2", | ||
"version": "2017-09-30" | ||
}`)) | ||
})) | ||
|
||
client := &http.Client{Transport: newTargetedRoundTripper("169.254.169.254", srv.Listener.Addr().String())} | ||
return srv, client | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
package apmcloudutil | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"net/http" | ||
|
||
"go.elastic.co/apm/model" | ||
) | ||
|
||
const ( | ||
azureMetadataURL = "http://169.254.169.254/metadata/instance/compute?api-version=2019-08-15" | ||
) | ||
|
||
// See: https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service | ||
func getAzureCloudMetadata(ctx context.Context, client *http.Client, out *model.Cloud) error { | ||
req, err := http.NewRequest("GET", azureMetadataURL, nil) | ||
if err != nil { | ||
return err | ||
} | ||
req.Header.Set("Metadata", "true") | ||
|
||
resp, err := client.Do(req.WithContext(ctx)) | ||
if err != nil { | ||
return err | ||
} | ||
defer resp.Body.Close() | ||
if resp.StatusCode != http.StatusOK { | ||
return errors.New(resp.Status) | ||
} | ||
|
||
var azureMetadata struct { | ||
Location string `json:"location"` | ||
Name string `json:"name"` | ||
ResourceGroupName string `json:"resourceGroupName"` | ||
SubscriptionID string `json:"subscriptionId"` | ||
VMID string `json:"vmId"` | ||
VMSize string `json:"vmSize"` | ||
Zone string `json:"zone"` | ||
} | ||
if err := json.NewDecoder(resp.Body).Decode(&azureMetadata); err != nil { | ||
return err | ||
} | ||
|
||
out.Region = azureMetadata.Location | ||
out.AvailabilityZone = azureMetadata.Zone | ||
if azureMetadata.VMID != "" || azureMetadata.Name != "" { | ||
out.Instance = &model.CloudInstance{ID: azureMetadata.VMID, Name: azureMetadata.Name} | ||
} | ||
if azureMetadata.VMSize != "" { | ||
out.Machine = &model.CloudMachine{Type: azureMetadata.VMSize} | ||
} | ||
if azureMetadata.ResourceGroupName != "" { | ||
out.Project = &model.CloudProject{Name: azureMetadata.ResourceGroupName} | ||
} | ||
if azureMetadata.SubscriptionID != "" { | ||
out.Account = &model.CloudAccount{ID: azureMetadata.SubscriptionID} | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
package apmcloudutil | ||
|
||
import ( | ||
"context" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
|
||
"go.elastic.co/apm/model" | ||
) | ||
|
||
func TestAzureCloudMetadata(t *testing.T) { | ||
srv, client := newAzureMetadataServer() | ||
defer srv.Close() | ||
|
||
for _, provider := range []Provider{Auto, Azure} { | ||
var out model.Cloud | ||
var logger testLogger | ||
assert.True(t, provider.getCloudMetadata(context.Background(), client, &logger, &out)) | ||
assert.Zero(t, logger) | ||
assert.Equal(t, model.Cloud{ | ||
Provider: "azure", | ||
Region: "westus2", | ||
Instance: &model.CloudInstance{ | ||
ID: "e11ebedc-019d-427f-84dd-56cd4388d3a8", | ||
Name: "basepi-test", | ||
}, | ||
Machine: &model.CloudMachine{ | ||
Type: "Standard_D2s_v3", | ||
}, | ||
Project: &model.CloudProject{ | ||
Name: "basepi-testing", | ||
}, | ||
Account: &model.CloudAccount{ | ||
ID: "7657426d-c4c3-44ac-88a2-3b2cd59e6dba", | ||
}, | ||
}, out) | ||
} | ||
} | ||
|
||
func newAzureMetadataServer() (*httptest.Server, *http.Client) { | ||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
if r.URL.Path != "/metadata/instance/compute" { | ||
w.WriteHeader(http.StatusNotFound) | ||
return | ||
} | ||
w.Write([]byte(`{ | ||
"location": "westus2", | ||
"name": "basepi-test", | ||
"resourceGroupName": "basepi-testing", | ||
"subscriptionId": "7657426d-c4c3-44ac-88a2-3b2cd59e6dba", | ||
"vmId": "e11ebedc-019d-427f-84dd-56cd4388d3a8", | ||
"vmScaleSetName": "", | ||
"vmSize": "Standard_D2s_v3", | ||
"zone": "" | ||
}`)) | ||
})) | ||
|
||
client := &http.Client{Transport: newTargetedRoundTripper("169.254.169.254", srv.Listener.Addr().String())} | ||
return srv, client | ||
} |
Oops, something went wrong.