Skip to content

Commit

Permalink
Add Amazon EKS Resource Detector (#465)
Browse files Browse the repository at this point in the history
* 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
3 people authored Dec 4, 2020
1 parent d1534b8 commit b02fae7
Show file tree
Hide file tree
Showing 6 changed files with 387 additions and 0 deletions.
10 changes: 10 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
237 changes: 237 additions & 0 deletions detectors/aws/eks/detector.go
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)
}
95 changes: 95 additions & 0 deletions detectors/aws/eks/detector_test.go
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)
}
11 changes: 11 additions & 0 deletions detectors/aws/eks/go.mod
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
)
Loading

0 comments on commit b02fae7

Please sign in to comment.