-
Notifications
You must be signed in to change notification settings - Fork 573
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Amazon EKS Resource Detector (#465)
* Add EKS Resource Detector * Remove getK8sCredHeader() from detectorUtils interface * Change implementation for getContainerId() to be consistent with ecs resource detector * Change code to use resource.Empty() * Add comments to functions * Update error handling with useful messages * Delete unused variables in EKS resource detector tests * Add whitespace around operators * Add space around "-" operator * Update comments for functions to be more descriptive * Update CHANGELOG.md * Fix spaces around operators. * Change "GET" to use http.MethodGet Co-authored-by: Anthony Mirabella <a9@aneurysm9.com> * Update error message for retrieving auth configmap Co-authored-by: Anthony Mirabella <a9@aneurysm9.com> * Update error message for HTTP request Co-authored-by: Anthony Mirabella <a9@aneurysm9.com> * Update error message for executing HTTP request Co-authored-by: Anthony Mirabella <a9@aneurysm9.com> * Update error message for HTTP response Co-authored-by: Anthony Mirabella <a9@aneurysm9.com> * Update error message for getClusterName Co-authored-by: Anthony Mirabella <a9@aneurysm9.com> * Update error message for parsing JSON for clusterName Co-authored-by: Anthony Mirabella <a9@aneurysm9.com> * Update error message for reading file Co-authored-by: Anthony Mirabella <a9@aneurysm9.com> * Fix build errors * Change JSON parsing to use map[string]interface{} * Add check for HTTP response status code in error handling * Refactor millisecondTimeOut variable name to timeoutMillis * Update CHANGELOG.md Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * Update detectors/aws/eks/go.mod Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * Update detectors/aws/go.sum Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * Update detectors/aws/eks/go.mod Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * Update detectors/aws/eks/eks_resource_detector.go Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * Update detectors/aws/eks/eks_resource_detector.go Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * Update detectors/aws/eks/eks_resource_detector.go Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * Update detectors/aws/eks/eks_resource_detector.go Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * Update detectors/aws/eks/eks_resource_detector.go Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * Update detectors/aws/eks/eks_resource_detector.go Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * Update detectors/aws/eks/eks_resource_detector.go Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * Update detectors/aws/eks/eks_resource_detector.go Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * Update detectors/aws/eks/eks_resource_detector.go Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * Update detectors/aws/eks/eks_resource_detector.go Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * Update detectors/aws/eks/eks_resource_detector.go Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * Update detectors/aws/eks/eks_resource_detector.go Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * Update detectors/aws/eks/eks_resource_detector_test.go Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * Update detectors/aws/eks/eks_resource_detector_test.go Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * Update detectors/aws/eks/eks_resource_detector_test.go Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * Update detectors/aws/eks/eks_resource_detector_test.go Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * Fix build error and address changes to PR * Change getContainerID() to use regex * Update detectors/aws/eks/detector.go Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * Update detectors/aws/eks/detector.go Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> Co-authored-by: Anthony Mirabella <a9@aneurysm9.com> Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>
- Loading branch information
1 parent
d1534b8
commit b02fae7
Showing
6 changed files
with
387 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
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,237 @@ | ||
// Copyright The OpenTelemetry Authors | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package eks | ||
|
||
import ( | ||
"context" | ||
"crypto/tls" | ||
"crypto/x509" | ||
"encoding/json" | ||
"fmt" | ||
"io/ioutil" | ||
"net/http" | ||
"os" | ||
"regexp" | ||
"strings" | ||
"time" | ||
|
||
"go.opentelemetry.io/otel/label" | ||
"go.opentelemetry.io/otel/sdk/resource" | ||
"go.opentelemetry.io/otel/semconv" | ||
) | ||
|
||
const ( | ||
k8sSvcURL = "https://kubernetes.default.svc" | ||
k8sTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" | ||
k8sCertPath = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" | ||
authConfigmapPath = "/api/v1/namespaces/kube-system/configmaps/aws-auth" | ||
cwConfigmapPath = "/api/v1/namespaces/amazon-cloudwatch/configmaps/cluster-info" | ||
defaultCgroupPath = "/proc/self/cgroup" | ||
containerIDLength = 64 | ||
timeoutMillis = 2000 | ||
) | ||
|
||
// detectorUtils is used for testing the ResourceDetector by abstracting functions that rely on external systems. | ||
type detectorUtils interface { | ||
fileExists(filename string) bool | ||
fetchString(httpMethod string, URL string) (string, error) | ||
getContainerID() (string, error) | ||
} | ||
|
||
// This struct will implement the detectorUtils interface | ||
type eksDetectorUtils struct{} | ||
|
||
// ResourceDetector for detecting resources running on Amazon EKS | ||
type ResourceDetector struct { | ||
utils detectorUtils | ||
} | ||
|
||
// This struct will help unmarshal clustername from JSON response | ||
type data struct { | ||
ClusterName string `json:"cluster.name"` | ||
} | ||
|
||
// Compile time assertion that ResourceDetector implements the resource.Detector interface. | ||
var _ resource.Detector = (*ResourceDetector)(nil) | ||
|
||
// Compile time assertion that eksDetectorUtils implements the detectorUtils interface. | ||
var _ detectorUtils = (*eksDetectorUtils)(nil) | ||
|
||
// Detect returns a Resource describing the Amazon EKS environment being run in. | ||
func (detector *ResourceDetector) Detect(ctx context.Context) (*resource.Resource, error) { | ||
|
||
isEks, err := isEKS(detector.utils) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// Return empty resource object if not running in EKS | ||
if !isEks { | ||
return resource.Empty(), nil | ||
} | ||
|
||
// Create variable to hold resource attributes | ||
labels := []label.KeyValue{} | ||
|
||
// Get clusterName and append to labels | ||
clusterName, err := getClusterName(detector.utils) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if clusterName != "" { | ||
labels = append(labels, semconv.K8SClusterNameKey.String(clusterName)) | ||
} | ||
|
||
// Get containerID and append to labels | ||
containerID, err := detector.utils.getContainerID() | ||
if err != nil { | ||
return nil, err | ||
} | ||
if containerID != "" { | ||
labels = append(labels, semconv.ContainerIDKey.String(containerID)) | ||
} | ||
|
||
// Return new resource object with clusterName and containerID as attributes | ||
return resource.NewWithAttributes(labels...), nil | ||
|
||
} | ||
|
||
// isEKS checks if the current environment is running in EKS. | ||
func isEKS(utils detectorUtils) (bool, error) { | ||
if !isK8s(utils) { | ||
return false, nil | ||
} | ||
|
||
// Make HTTP GET request | ||
awsAuth, err := utils.fetchString(http.MethodGet, k8sSvcURL+authConfigmapPath) | ||
if err != nil { | ||
return false, fmt.Errorf("isEks() error retrieving auth configmap: %w", err) | ||
} | ||
|
||
return awsAuth != "", nil | ||
} | ||
|
||
// isK8s checks if the current environment is running in a Kubernetes environment | ||
func isK8s(utils detectorUtils) bool { | ||
return utils.fileExists(k8sTokenPath) && utils.fileExists(k8sCertPath) | ||
} | ||
|
||
// fileExists checks if a file with a given filename exists. | ||
func (eksUtils eksDetectorUtils) fileExists(filename string) bool { | ||
info, err := os.Stat(filename) | ||
return err == nil && !info.IsDir() | ||
} | ||
|
||
// fetchString executes an HTTP request with a given HTTP Method and URL string. | ||
func (eksUtils eksDetectorUtils) fetchString(httpMethod string, URL string) (string, error) { | ||
request, err := http.NewRequest(httpMethod, URL, nil) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to create new HTTP request with method=%s, URL=%s: %w", httpMethod, URL, err) | ||
} | ||
|
||
// Set HTTP request header with authentication credentials | ||
authHeader, err := getK8sCredHeader() | ||
if err != nil { | ||
return "", err | ||
} | ||
request.Header.Set("Authorization", authHeader) | ||
|
||
// Get certificate | ||
caCert, err := ioutil.ReadFile(k8sCertPath) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to read file with path %s", k8sCertPath) | ||
} | ||
caCertPool := x509.NewCertPool() | ||
caCertPool.AppendCertsFromPEM(caCert) | ||
|
||
// Set HTTP request timeout and add certificate | ||
client := &http.Client{ | ||
Timeout: timeoutMillis * time.Millisecond, | ||
Transport: &http.Transport{ | ||
TLSClientConfig: &tls.Config{ | ||
RootCAs: caCertPool, | ||
}, | ||
}, | ||
} | ||
|
||
response, err := client.Do(request) | ||
if err != nil || response.StatusCode != http.StatusOK { | ||
return "", fmt.Errorf("failed to execute HTTP request with method=%s, URL=%s, Status Code=%d: %w", httpMethod, URL, response.StatusCode, err) | ||
} | ||
|
||
// Retrieve response body from HTTP request | ||
body, err := ioutil.ReadAll(response.Body) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to read response from HTTP request with method=%s, URL=%s: %w", httpMethod, URL, err) | ||
} | ||
|
||
return string(body), nil | ||
} | ||
|
||
// getK8sCredHeader retrieves the kubernetes credential information. | ||
func getK8sCredHeader() (string, error) { | ||
content, err := ioutil.ReadFile(k8sTokenPath) | ||
if err != nil { | ||
return "", fmt.Errorf("getK8sCredHeader() error: cannot read file with path %s", k8sTokenPath) | ||
} | ||
|
||
return "Bearer " + string(content), nil | ||
} | ||
|
||
// getClusterName retrieves the clusterName resource attribute | ||
func getClusterName(utils detectorUtils) (string, error) { | ||
resp, err := utils.fetchString("GET", k8sSvcURL+cwConfigmapPath) | ||
if err != nil { | ||
return "", fmt.Errorf("getClusterName() error: %w", err) | ||
} | ||
|
||
// parse JSON object returned from HTTP request | ||
var respmap map[string]json.RawMessage | ||
err = json.Unmarshal([]byte(resp), &respmap) | ||
if err != nil { | ||
return "", fmt.Errorf("getClusterName() error: cannot parse JSON: %w", err) | ||
} | ||
var d data | ||
err = json.Unmarshal(respmap["data"], &d) | ||
if err != nil { | ||
return "", fmt.Errorf("getClusterName() error: cannot parse JSON: %w", err) | ||
} | ||
|
||
clusterName := d.ClusterName | ||
|
||
return clusterName, nil | ||
} | ||
|
||
// getContainerID returns the containerID if currently running within a container. | ||
func (eksUtils eksDetectorUtils) getContainerID() (string, error) { | ||
fileData, err := ioutil.ReadFile(defaultCgroupPath) | ||
if err != nil { | ||
return "", fmt.Errorf("getContainerID() error: cannot read file with path %s: %w", defaultCgroupPath, err) | ||
} | ||
|
||
r, err := regexp.Compile(`^.*/docker/(.+)$`) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
// Retrieve containerID from file | ||
splitData := strings.Split(strings.TrimSpace(string(fileData)), "\n") | ||
for _, str := range splitData { | ||
if r.MatchString(str) { | ||
return str[len(str)-containerIDLength:], nil | ||
} | ||
} | ||
return "", fmt.Errorf("getContainerID() error: cannot read containerID from file %s", defaultCgroupPath) | ||
} |
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,95 @@ | ||
// Copyright The OpenTelemetry Authors | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package eks | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/bmizerany/assert" | ||
"github.com/stretchr/testify/mock" | ||
"github.com/stretchr/testify/require" | ||
|
||
"go.opentelemetry.io/otel/label" | ||
"go.opentelemetry.io/otel/sdk/resource" | ||
"go.opentelemetry.io/otel/semconv" | ||
) | ||
|
||
type MockDetectorUtils struct { | ||
mock.Mock | ||
} | ||
|
||
// Mock function for fileExists() | ||
func (detectorUtils *MockDetectorUtils) fileExists(filename string) bool { | ||
args := detectorUtils.Called(filename) | ||
return args.Bool(0) | ||
} | ||
|
||
// Mock function for fetchString() | ||
func (detectorUtils *MockDetectorUtils) fetchString(httpMethod string, URL string) (string, error) { | ||
args := detectorUtils.Called(httpMethod, URL) | ||
return args.String(0), args.Error(1) | ||
} | ||
|
||
// Mock function for getContainerID() | ||
func (detectorUtils *MockDetectorUtils) getContainerID() (string, error) { | ||
args := detectorUtils.Called() | ||
return args.String(0), args.Error(1) | ||
} | ||
|
||
// Tests EKS resource detector running in EKS environment | ||
func TestEks(t *testing.T) { | ||
|
||
detectorUtils := new(MockDetectorUtils) | ||
|
||
// Mock functions and set expectations | ||
detectorUtils.On("fileExists", k8sTokenPath).Return(true) | ||
detectorUtils.On("fileExists", k8sCertPath).Return(true) | ||
detectorUtils.On("fetchString", "GET", k8sSvcURL+authConfigmapPath).Return("not empty", nil) | ||
detectorUtils.On("fetchString", "GET", k8sSvcURL+cwConfigmapPath).Return(`{"data":{"cluster.name":"my-cluster"}}`, nil) | ||
detectorUtils.On("getContainerID").Return("0123456789A", nil) | ||
|
||
// Expected resource object | ||
eksResourceLabels := []label.KeyValue{ | ||
semconv.K8SClusterNameKey.String("my-cluster"), | ||
semconv.ContainerIDKey.String("0123456789A"), | ||
} | ||
expectedResource := resource.NewWithAttributes(eksResourceLabels...) | ||
|
||
// Call EKS Resource detector to detect resources | ||
eksResourceDetector := ResourceDetector{detectorUtils} | ||
resourceObj, err := eksResourceDetector.Detect(context.Background()) | ||
require.NoError(t, err) | ||
|
||
assert.Equal(t, expectedResource, resourceObj, "Resource object returned is incorrect") | ||
detectorUtils.AssertExpectations(t) | ||
} | ||
|
||
// Tests EKS resource detector not running in EKS environment | ||
func TestNotEKS(t *testing.T) { | ||
|
||
detectorUtils := new(MockDetectorUtils) | ||
|
||
k8sTokenPath := "/var/run/secrets/kubernetes.io/serviceaccount/token" | ||
|
||
// Mock functions and set expectations | ||
detectorUtils.On("fileExists", k8sTokenPath).Return(false) | ||
|
||
detector := ResourceDetector{detectorUtils} | ||
r, err := detector.Detect(context.Background()) | ||
require.NoError(t, err) | ||
assert.Equal(t, resource.Empty(), r, "Resource object should be empty") | ||
detectorUtils.AssertExpectations(t) | ||
} |
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,11 @@ | ||
module go.opentelemetry.io/contrib/detectors/aws/eks | ||
|
||
go 1.15 | ||
|
||
require ( | ||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 | ||
github.com/kr/pretty v0.2.1 // indirect | ||
github.com/stretchr/testify v1.6.1 | ||
go.opentelemetry.io/otel v0.14.0 | ||
go.opentelemetry.io/otel/sdk v0.14.0 | ||
) |
Oops, something went wrong.