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>
This commit is contained in:
Wilbert Guo 2020-12-04 12:56:06 -08:00 committed by GitHub
parent d1534b8459
commit b02fae7b25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 387 additions and 0 deletions

View File

@ -27,6 +27,16 @@ updates:
schedule:
interval: "weekly"
day: "sunday"
-
package-ecosystem: "gomod"
directory: "/detectors/aws/eks"
labels:
- dependencies
- go
- "Skip Changelog"
schedule:
interval: "weekly"
day: "sunday"
-
package-ecosystem: "gomod"
directory: "/detectors/aws"

View File

@ -8,6 +8,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
## [Unreleased]
### Added
- Amazon EKS resource detector. (#465)
## [0.14.0] - 2020-11-20
### Added

View File

@ -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)
}

View File

@ -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)
}

11
detectors/aws/eks/go.mod Normal file
View File

@ -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
)

30
detectors/aws/eks/go.sum Normal file
View File

@ -0,0 +1,30 @@
github.com/DataDog/sketches-go v0.0.1/go.mod h1:Q5DbzQ+3AkgGwymQO7aZFNP7ns2lZKGtvRBzRXfdi60=
github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.5.3 h1:x95R7cp+rSeeqAMI2knLtQ0DKlaBhv2NrtrOvafPHRo=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
go.opentelemetry.io/otel v0.14.0 h1:YFBEfjCk9MTjaytCNSUkp9Q8lF7QJezA06T71FbQxLQ=
go.opentelemetry.io/otel v0.14.0/go.mod h1:vH5xEuwy7Rts0GNtsCW3HYQoZDY+OmBJ6t1bFGGlxgw=
go.opentelemetry.io/otel/sdk v0.14.0 h1:Pqgd85y5XhyvHQlOxkKW+FD4DAX7AoeaNIDKC2VhfHQ=
go.opentelemetry.io/otel/sdk v0.14.0/go.mod h1:kGO5pEMSNqSJppHAm8b73zztLxB5fgDQnD56/dl5xqE=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=