Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Amazon EKS Resource Detector #465

Merged
merged 50 commits into from
Dec 4, 2020
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
217dc61
Add EKS Resource Detector
wilguo Nov 20, 2020
96439ac
Remove getK8sCredHeader() from detectorUtils interface
wilguo Nov 20, 2020
fc9da57
Change implementation for getContainerId() to be consistent with ecs …
wilguo Nov 21, 2020
1938d61
Change code to use resource.Empty()
wilguo Nov 21, 2020
52dbceb
Add comments to functions
wilguo Nov 22, 2020
d6135de
Update error handling with useful messages
wilguo Nov 22, 2020
687f99d
Delete unused variables in EKS resource detector tests
wilguo Nov 22, 2020
049a547
Add whitespace around operators
wilguo Nov 22, 2020
0f7a91f
Add space around "-" operator
wilguo Nov 22, 2020
cc784e5
Update comments for functions to be more descriptive
wilguo Nov 22, 2020
1bae800
Update CHANGELOG.md
wilguo Nov 23, 2020
31f05b1
Merge branch 'master' into wilguo-eksdetector
wilguo Nov 23, 2020
26a9129
Fix spaces around operators.
wilguo Nov 23, 2020
8e43f34
Change "GET" to use http.MethodGet
wilguo Nov 27, 2020
be46135
Update error message for retrieving auth configmap
wilguo Nov 27, 2020
1bcfc40
Update error message for HTTP request
wilguo Nov 27, 2020
388dbe2
Update error message for executing HTTP request
wilguo Nov 27, 2020
0cf7144
Update error message for HTTP response
wilguo Nov 27, 2020
a549e85
Update error message for getClusterName
wilguo Nov 27, 2020
d9b7a50
Update error message for parsing JSON for clusterName
wilguo Nov 27, 2020
666ce5f
Update error message for reading file
wilguo Nov 27, 2020
aeb0708
Fix build errors
wilguo Nov 27, 2020
376940d
Change JSON parsing to use map[string]interface{}
wilguo Nov 27, 2020
9d5e833
Add check for HTTP response status code in error handling
wilguo Nov 27, 2020
7cda33b
Refactor millisecondTimeOut variable name to timeoutMillis
wilguo Nov 27, 2020
6d4b650
Update CHANGELOG.md
wilguo Dec 3, 2020
6159f52
Update detectors/aws/eks/go.mod
wilguo Dec 3, 2020
a909236
Update detectors/aws/go.sum
wilguo Dec 3, 2020
2a369d0
Update detectors/aws/eks/go.mod
wilguo Dec 3, 2020
a7101cd
Update detectors/aws/eks/eks_resource_detector.go
wilguo Dec 3, 2020
58cbe99
Update detectors/aws/eks/eks_resource_detector.go
wilguo Dec 3, 2020
2f7cf16
Update detectors/aws/eks/eks_resource_detector.go
wilguo Dec 3, 2020
22268b7
Update detectors/aws/eks/eks_resource_detector.go
wilguo Dec 3, 2020
74229e2
Update detectors/aws/eks/eks_resource_detector.go
wilguo Dec 3, 2020
c23b0a1
Update detectors/aws/eks/eks_resource_detector.go
wilguo Dec 3, 2020
b4181f0
Update detectors/aws/eks/eks_resource_detector.go
wilguo Dec 3, 2020
88b4980
Update detectors/aws/eks/eks_resource_detector.go
wilguo Dec 3, 2020
f2616cb
Update detectors/aws/eks/eks_resource_detector.go
wilguo Dec 3, 2020
7208ab0
Update detectors/aws/eks/eks_resource_detector.go
wilguo Dec 3, 2020
48b9725
Update detectors/aws/eks/eks_resource_detector.go
wilguo Dec 3, 2020
0d6d0c9
Update detectors/aws/eks/eks_resource_detector.go
wilguo Dec 3, 2020
16c436d
Update detectors/aws/eks/eks_resource_detector_test.go
wilguo Dec 3, 2020
6306b4f
Update detectors/aws/eks/eks_resource_detector_test.go
wilguo Dec 3, 2020
4f304a6
Update detectors/aws/eks/eks_resource_detector_test.go
wilguo Dec 3, 2020
9c042fc
Update detectors/aws/eks/eks_resource_detector_test.go
wilguo Dec 3, 2020
ed13bce
Fix build error and address changes to PR
wilguo Dec 3, 2020
52f25a6
Change getContainerID() to use regex
wilguo Dec 3, 2020
ff1170d
Update detectors/aws/eks/detector.go
wilguo Dec 4, 2020
861b905
Update detectors/aws/eks/detector.go
wilguo Dec 4, 2020
1b0ec9c
Merge branch 'master' into wilguo-eksdetector
wilguo Dec 4, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased]

- Add Amazon EKS resource detector. (#465)
wilguo marked this conversation as resolved.
Show resolved Hide resolved

## [0.14.0] - 2020-11-20

### Added
Expand Down
242 changes: 242 additions & 0 deletions detectors/aws/eks/eks_resource_detector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
// Copyright The OpenTelemetry Authors
wilguo marked this conversation as resolved.
Show resolved Hide resolved
//
// 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"
"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
)

// Create interface for functions that need to be mocked
wilguo marked this conversation as resolved.
Show resolved Hide resolved
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 function collects associated resource attributes when running in Amazon EKS environment.
wilguo marked this conversation as resolved.
Show resolved Hide resolved
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.New(labels...), nil

}

// isEks checks if the current environment is running in EKS
wilguo marked this conversation as resolved.
Show resolved Hide resolved
func isEks(utils detectorUtils) (bool, error) {
wilguo marked this conversation as resolved.
Show resolved Hide resolved
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
// this function implements the detectorUtils interface
wilguo marked this conversation as resolved.
Show resolved Hide resolved
func (eksUtils eksDetectorUtils) fileExists(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
wilguo marked this conversation as resolved.
Show resolved Hide resolved
}

// fetchString executes an HTTP request with a given HTTP Method and URL string
// this function implements the detectorUtils interface
wilguo marked this conversation as resolved.
Show resolved Hide resolved
func (eksUtils eksDetectorUtils) fetchString(httpMethod string, URL string) (string, error) {

// Create new HTTP request object
wilguo marked this conversation as resolved.
Show resolved Hide resolved
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,
},
},
}

// Execute HTTP request
wilguo marked this conversation as resolved.
Show resolved Hide resolved
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)
}

wilguo marked this conversation as resolved.
Show resolved Hide resolved
// 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
wilguo marked this conversation as resolved.
Show resolved Hide resolved
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)
wilguo marked this conversation as resolved.
Show resolved Hide resolved
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 retrieves the containerID resource attribute
// this function implements the detectorUtils interface
wilguo marked this conversation as resolved.
Show resolved Hide resolved
func (eksUtils eksDetectorUtils) getContainerID() (string, error) {

// Read file
wilguo marked this conversation as resolved.
Show resolved Hide resolved
fileData, err := ioutil.ReadFile(defaultCgroupPath)
if err != nil {
return "", fmt.Errorf("getContainerID() error: cannot read file with path %s: %w", defaultCgroupPath, err)
}

// Retrieve containerID from file
splitData := strings.Split(strings.TrimSpace(string(fileData)), "\n")
for _, str := range splitData {
if len(str) > containerIDLength {
return str[len(str)-containerIDLength:], nil
}
}
wilguo marked this conversation as resolved.
Show resolved Hide resolved
return "", fmt.Errorf("getContainerID() error: cannot read containerID from file %s", defaultCgroupPath)
}
98 changes: 98 additions & 0 deletions detectors/aws/eks/eks_resource_detector_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright The OpenTelemetry Authors
wilguo marked this conversation as resolved.
Show resolved Hide resolved
//
// 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"

"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.New(eksResourceLabels...)

// Call EKS Resource detector to detect resources
eksResourceDetector := ResourceDetector{detectorUtils}
resourceObj, _ := eksResourceDetector.Detect(context.Background())
wilguo marked this conversation as resolved.
Show resolved Hide resolved

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) {
wilguo marked this conversation as resolved.
Show resolved Hide resolved

detectorUtils := new(MockDetectorUtils)

// Set EKS resource detector variables
wilguo marked this conversation as resolved.
Show resolved Hide resolved
k8sTokenPath := "/var/run/secrets/kubernetes.io/serviceaccount/token"

// Mock functions and set expectations
detectorUtils.On("fileExists", k8sTokenPath).Return(false)

// Expected resource object
expectedResource := resource.Empty()

// Call EKS Resource detector to detect resources
eksResourceDetector := ResourceDetector{detectorUtils}
resourceObj, _ := eksResourceDetector.Detect(context.Background())

assert.Equal(t, expectedResource, resourceObj, "Resource object should be empty")
wilguo marked this conversation as resolved.
Show resolved Hide resolved
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 github.com/wilguo/opentelemetry-go-contrib/detectors/aws/eks
wilguo marked this conversation as resolved.
Show resolved Hide resolved

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.13.0
go.opentelemetry.io/otel/sdk v0.13.0
wilguo marked this conversation as resolved.
Show resolved Hide resolved
)
Loading