Skip to content

Commit

Permalink
Merge pull request #376 from newrelic/dev
Browse files Browse the repository at this point in the history
9.20 Dev Merge to Main
  • Loading branch information
zsistla committed Mar 10, 2022
2 parents 8536ad5 + 8c410c3 commit 952b9f1
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 49 deletions.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
9.19.0
9.20.0
8 changes: 3 additions & 5 deletions axiom/nr_version.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,15 @@
#endif

/*
* Current version naming scheme is color names that are also food.
* Here's a list:
* https://en.wikipedia.org/wiki/List_of_colors_(compact)
* Current version naming scheme is flowers
*
* ube 29Oct2020 (9.14)
* vanilla 07Dec2020 (9.15)
* watermelon 25Jan2021 (9.16)
* xigua 21Apr2021 (9.17)
* yam 23Aug2021 (9.18)
* zomp 02Mar2022 (9.19)
*/
#define NR_CODENAME "zomp"
#define NR_CODENAME "allium"

const char* nr_version(void) {
return NR_STR2(NR_VERSION);
Expand Down
106 changes: 73 additions & 33 deletions src/newrelic/utilization/aws.go
Original file line number Diff line number Diff line change
@@ -1,100 +1,140 @@
//
// Copyright 2020 New Relic Corporation. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//

package utilization

import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
)

const (
awsHostname = "169.254.169.254"
awsEndpointPath = "/2016-09-02/dynamic/instance-identity/document"
awsEndpoint = "http://" + awsHostname + awsEndpointPath
awsHostname = "169.254.169.254"
awsEndpointPath = "/2016-09-02/dynamic/instance-identity/document"
awsTokenEndpointPath = "/latest/api/token"
awsEndpoint = "http://" + awsHostname + awsEndpointPath
awsTokenEndpoint = "http://" + awsHostname + awsTokenEndpointPath
awsTokenTTL = "60" // seconds this AWS utilization session will last
)

type aws struct {
InstanceID string `json:"instanceId,omitempty"`
InstanceType string `json:"instanceType,omitempty"`
AvailabilityZone string `json:"availabilityZone,omitempty"`

client *http.Client
}

func GatherAWS(util *Data) error {
aws := newAWS()
if err := aws.Gather(); err != nil {
return fmt.Errorf("AWS not detected: %s", err)
} else {
util.Vendors.AWS = aws
// http client is shared between auth token and metatdata fetches
client := &http.Client{
Timeout: providerTimeout,
}
aws, err := getAWS(client)
if err != nil {
// Only return the error here if it is unexpected to prevent
// warning customers who aren't running AWS about a timeout.
if _, ok := err.(unexpectedAWSErr); ok {
return err
}
return nil
}
util.Vendors.AWS = aws

return nil
}

func newAWS() *aws {
return &aws{
client: &http.Client{Timeout: providerTimeout},
type unexpectedAWSErr struct{ e error }

func (e unexpectedAWSErr) Error() string {
return fmt.Sprintf("unexpected AWS error: %v", e.e)
}

// getAWSToken attempts to get the IMDSv2 token within the providerTimeout set
// provider.go.
func getAWSToken(client *http.Client) (token string, err error) {
request, err := http.NewRequest("PUT", awsTokenEndpoint, nil)
request.Header.Add("X-aws-ec2-metadata-token-ttl-seconds", awsTokenTTL)
response, err := client.Do(request)
if err != nil {
return "", err
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return "", err
}

return string(body), nil
}

func (a *aws) Gather() (ret error) {
func getAWS(client *http.Client) (ret *aws, err error) {
// In some cases, 3rd party providers might block requests to metadata
// endpoints in such a way that causes a panic in the underlying
// net/http library's (*Transport).getConn() function. To mitigate that
// possibility, we preemptively setup a recovery deferral.
defer func() {
if r := recover(); r != nil {
ret = fmt.Errorf("AWS utilization check error: %v", r)
ret = nil
err = unexpectedAWSErr{e: errors.New("panic contacting AWS metadata endpoint")}
}
}()

response, err := a.client.Get(awsEndpoint)
// AWS' IMDSv2 requires us to get a token before requesting metadata.
awsToken, err := getAWSToken(client)
if err != nil {
return err
// No unexpectedAWSErr here: A timeout is usually going to
// happen.
return nil, err
}

//Add the header to the outbound request.
request, err := http.NewRequest("GET", awsEndpoint, nil)
request.Header.Add("X-aws-ec2-metadata-token", awsToken)

response, err := client.Do(request)
if err != nil {
// No unexpectedAWSErr here: A timeout is usually going to
// happen.
return nil, err
}
defer response.Body.Close()

if response.StatusCode != 200 {
return fmt.Errorf("got response code %d", response.StatusCode)
return nil, unexpectedAWSErr{e: fmt.Errorf("response code %d", response.StatusCode)}
}

data, err := ioutil.ReadAll(response.Body)
if err != nil {
return err
return nil, unexpectedAWSErr{e: err}
}

a := &aws{}
if err := json.Unmarshal(data, a); err != nil {
return err
return nil, unexpectedAWSErr{e: err}
}

if err := a.validate(); err != nil {
*a = aws{client: a.client}
return err
return nil, unexpectedAWSErr{e: err}
}

return nil
return a, nil
}

func (aws *aws) validate() (err error) {
aws.InstanceID, err = normalizeValue(aws.InstanceID)
func (a *aws) validate() (err error) {
a.InstanceID, err = normalizeValue(a.InstanceID)
if err != nil {
return fmt.Errorf("Invalid AWS instance ID: %v", err)
return fmt.Errorf("invalid instance ID: %v", err)
}

aws.InstanceType, err = normalizeValue(aws.InstanceType)
a.InstanceType, err = normalizeValue(a.InstanceType)
if err != nil {
return fmt.Errorf("Invalid AWS instance type: %v", err)
return fmt.Errorf("invalid instance type: %v", err)
}

aws.AvailabilityZone, err = normalizeValue(aws.AvailabilityZone)
a.AvailabilityZone, err = normalizeValue(a.AvailabilityZone)
if err != nil {
return fmt.Errorf("Invalid AWS availability zone: %v", err)
return fmt.Errorf("invalid availability zone: %v", err)
}

return
Expand Down
18 changes: 10 additions & 8 deletions src/newrelic/utilization/aws_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
//
// Copyright 2020 New Relic Corporation. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//

package utilization

import (
"net/http"
"testing"

"newrelic/crossagent"
Expand All @@ -20,18 +19,21 @@ func TestCrossAgentAWS(t *testing.T) {
}

for _, testCase := range testCases {
aws := newAWS()
aws.client.Transport = &mockTransport{
t: t,
responses: testCase.URIs,
client := &http.Client{
Transport: &mockTransport{
t: t,
responses: testCase.URIs,
},
}

aws, err := getAWS(client)

if testCase.ExpectedVendorsHash.AWS == nil {
if err := aws.Gather(); err == nil {
if err == nil {
t.Fatalf("%s: expected error; got nil", testCase.TestName)
}
} else {
if err := aws.Gather(); err != nil {
if err != nil {
t.Fatalf("%s: expected no error; got %v", testCase.TestName, err)
}

Expand Down
5 changes: 4 additions & 1 deletion src/newrelic/utilization/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,11 @@ type mockBody struct {
}

func (m *mockTransport) RoundTrip(r *http.Request) (*http.Response, error) {
// Half the requests are going to the test's endpoint, while the other half
// are going to the AWS IMDSv2 token endpoint. Accept both.
for match, response := range m.responses {
if r.URL.String() == match {
if (r.URL.String() == match) ||
(r.URL.String() == awsTokenEndpoint) {
return m.respond(response)
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/newrelic/utilization/utilization_hash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ func TestVendorsIsEmpty(t *testing.T) {
t.Fatal("default vendors does not register as empty")
}

v.AWS = newAWS()
v.AWS = &aws{}
if v.isEmpty() {
t.Fatal("non-empty vendors registers as empty")
}
Expand Down

0 comments on commit 952b9f1

Please sign in to comment.