Skip to content
This repository was archived by the owner on Sep 24, 2021. It is now read-only.

Commit bbbb01a

Browse files
committed
Enable cross account and region support
Support cross account and region by specifying `roleArn` and `region` respectively in the `externalmetric` spec definition. Using a `roleArn` the adapter will assume into the specified role to retrieve metrics. Issue: #8, #30
1 parent fef2835 commit bbbb01a

File tree

14 files changed

+331
-153
lines changed

14 files changed

+331
-153
lines changed

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ $(OUT_DIR)/adapter: $(src_deps)
1919
docker-build: verify-apis test
2020
cp deploy/Dockerfile $(TEMP_DIR)/Dockerfile
2121

22-
docker run -v $(TEMP_DIR):/build -v $(shell pwd):/go/src/github.com/awslabs/k8s-cloudwatch-adapter -e GOARCH=amd64 -e GOFLAGS="$(GOFLAGS)" -w /go/src/github.com/awslabs/k8s-cloudwatch-adapter $(GOIMAGE) /bin/bash -c "\
22+
docker run --rm -v $(TEMP_DIR):/build -v $(shell pwd):/go/src/github.com/awslabs/k8s-cloudwatch-adapter -e GOARCH=amd64 -e GOFLAGS="$(GOFLAGS)" -w /go/src/github.com/awslabs/k8s-cloudwatch-adapter $(GOIMAGE) /bin/bash -c "\
2323
CGO_ENABLED=0 GO111MODULE=on go build -o /build/adapter cmd/adapter/adapter.go"
2424

2525
docker build -t $(REGISTRY)/$(IMAGE):$(VERSION) $(TEMP_DIR)
@@ -44,7 +44,7 @@ else
4444
endif
4545

4646
test:
47-
CGO_ENABLED=0 GO111MODULE=on go test ./pkg/...
47+
CGO_ENABLED=0 GO111MODULE=on go test -cover ./pkg/...
4848

4949
clean:
5050
rm -rf ${OUT_DIR} vendor

cmd/adapter/adapter.go

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ type CloudWatchAdapter struct {
2424
basecmd.AdapterBase
2525
}
2626

27-
func (a *CloudWatchAdapter) makeCloudWatchClient() (aws.Client, error) {
28-
client := aws.NewCloudWatchClient()
29-
return client, nil
27+
func (a *CloudWatchAdapter) makeCloudWatchManager() (aws.CloudWatchManager, error) {
28+
manager := aws.NewCloudWatchManager()
29+
return manager, nil
3030
}
3131

32-
func (a *CloudWatchAdapter) newController(metriccache *metriccache.MetricCache) (*controller.Controller, informers.SharedInformerFactory) {
32+
func (a *CloudWatchAdapter) newController(cache *metriccache.MetricCache) (*controller.Controller, informers.SharedInformerFactory) {
3333
clientConfig, err := a.ClientConfig()
3434
if err != nil {
3535
klog.Fatalf("unable to construct client config: %v", err)
@@ -42,14 +42,14 @@ func (a *CloudWatchAdapter) newController(metriccache *metriccache.MetricCache)
4242
adapterInformerFactory := informers.NewSharedInformerFactory(adapterClientSet, time.Second*30)
4343
handler := controller.NewHandler(
4444
adapterInformerFactory.Metrics().V1alpha1().ExternalMetrics().Lister(),
45-
metriccache)
45+
cache)
4646

47-
controller := controller.NewController(adapterInformerFactory.Metrics().V1alpha1().ExternalMetrics(), &handler)
47+
ctrl := controller.NewController(adapterInformerFactory.Metrics().V1alpha1().ExternalMetrics(), &handler)
4848

49-
return controller, adapterInformerFactory
49+
return ctrl, adapterInformerFactory
5050
}
5151

52-
func (a *CloudWatchAdapter) makeProvider(cwClient aws.Client, metriccache *metriccache.MetricCache) (provider.ExternalMetricsProvider, error) {
52+
func (a *CloudWatchAdapter) makeProvider(cwManager aws.CloudWatchManager, cache *metriccache.MetricCache) (provider.ExternalMetricsProvider, error) {
5353
client, err := a.DynamicClient()
5454
if err != nil {
5555
return nil, errors.Wrap(err, "unable to construct Kubernetes client")
@@ -60,7 +60,7 @@ func (a *CloudWatchAdapter) makeProvider(cwClient aws.Client, metriccache *metri
6060
return nil, errors.Wrap(err, "unable to construct RESTMapper")
6161
}
6262

63-
cwProvider := cwprov.NewCloudWatchProvider(client, mapper, cwClient, metriccache)
63+
cwProvider := cwprov.NewCloudWatchProvider(client, mapper, cwManager, cache)
6464
return cwProvider, nil
6565
}
6666

@@ -70,28 +70,28 @@ func main() {
7070

7171
// set up flags
7272
cmd := &CloudWatchAdapter{}
73-
cmd.Name = "cloudwatch-metrics-adapter"
73+
cmd.Name = "k8s-cloudwatch-adapter"
7474
cmd.Flags().AddGoFlagSet(flag.CommandLine) // make sure we get the klog flags
7575
cmd.Flags().Parse(os.Args)
7676

7777
stopCh := make(chan struct{})
7878
defer close(stopCh)
7979

80-
metriccache := metriccache.NewMetricCache()
80+
cache := metriccache.NewMetricCache()
8181

82-
// start and run contoller components
83-
controller, adapterInformerFactory := cmd.newController(metriccache)
82+
// start and run ctrl components
83+
ctrl, adapterInformerFactory := cmd.newController(cache)
8484
go adapterInformerFactory.Start(stopCh)
85-
go controller.Run(2, time.Second, stopCh)
85+
go ctrl.Run(2, time.Second, stopCh)
8686

8787
// create CloudWatch client
88-
cwClient, err := cmd.makeCloudWatchClient()
88+
cwClient, err := cmd.makeCloudWatchManager()
8989
if err != nil {
9090
klog.Fatalf("unable to construct CloudWatch client: %v", err)
9191
}
9292

9393
// construct the provider
94-
cwProvider, err := cmd.makeProvider(cwClient, metriccache)
94+
cwProvider, err := cmd.makeProvider(cwClient, cache)
9595
if err != nil {
9696
klog.Fatalf("unable to construct CloudWatch metrics provider: %v", err)
9797
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ require (
66
github.com/aws/aws-sdk-go v1.33.5
77
github.com/kubernetes-incubator/custom-metrics-apiserver v0.0.0-20200323093244-5046ce1afe6b
88
github.com/pkg/errors v0.9.1
9-
gopkg.in/yaml.v2 v2.2.8
9+
gopkg.in/yaml.v2 v2.2.8 // indirect
1010
k8s.io/apimachinery v0.17.7
1111
k8s.io/apiserver v0.17.7 // indirect
1212
k8s.io/client-go v0.17.7

pkg/apis/metrics/v1alpha1/externalmetric.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ type MetricSeriesSpec struct {
2727
// Name specifies the series name.
2828
Name string `json:"name"`
2929

30+
// RoleARN indicate the ARN of IAM role to assume, this metric will be retrieved using this role.
31+
RoleARN string `json:"roleArn"`
32+
33+
// Region specifies the region where metrics should be retrieved.
34+
Region string `json:"region"`
35+
3036
// Queries specify the CloudWatch metrics query to retrieve data for this series.
3137
Queries []MetricDataQuery `json:"queries"`
3238
}

pkg/aws/client.go

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,45 @@ import (
55
"os"
66
"time"
77

8+
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
9+
10+
"github.com/aws/aws-sdk-go/aws/endpoints"
11+
12+
"github.com/awslabs/k8s-cloudwatch-adapter/pkg/apis/metrics/v1alpha1"
13+
814
"github.com/aws/aws-sdk-go/aws"
915
"github.com/aws/aws-sdk-go/aws/session"
1016
"github.com/aws/aws-sdk-go/service/cloudwatch"
1117
"k8s.io/klog"
1218
)
1319

14-
// NewCloudWatchClient creates a new CloudWatch client.
15-
func NewCloudWatchClient() Client {
20+
func NewCloudWatchManager() CloudWatchManager {
21+
return &cloudwatchManager{}
22+
}
23+
24+
type cloudwatchManager struct {
25+
}
26+
27+
func (c *cloudwatchManager) getClient(role, region string) *cloudwatch.CloudWatch {
28+
// Using the Config value, create the CloudWatch client
29+
sess := session.Must(session.NewSession())
30+
1631
// Using the SDK's default configuration, loading additional config
1732
// and credentials values from the environment variables, shared
1833
// credentials, and shared configuration files
19-
cfg := aws.NewConfig()
34+
cfg := aws.NewConfig().WithSTSRegionalEndpoint(endpoints.RegionalSTSEndpoint)
35+
36+
// check if roleARN is passed
37+
if role != "" {
38+
creds := stscreds.NewCredentials(sess, role)
39+
cfg = cfg.WithCredentials(creds)
40+
klog.Infof("using IAM role ARN: %s", role)
41+
}
2042

2143
// check if region is set
22-
if aws.StringValue(cfg.Region) == "" {
44+
if region != "" {
45+
cfg = cfg.WithRegion(region)
46+
} else if aws.StringValue(cfg.Region) == "" {
2347
cfg.Region = aws.String(GetLocalRegion())
2448
}
2549
klog.Infof("using AWS Region: %s", aws.StringValue(cfg.Region))
@@ -28,17 +52,14 @@ func NewCloudWatchClient() Client {
2852
cfg = cfg.WithLogLevel(aws.LogDebugWithHTTPBody)
2953
}
3054

31-
// Using the Config value, create the CloudWatch client
32-
sess := session.Must(session.NewSession(cfg))
33-
svc := cloudwatch.New(sess)
34-
return &cloudwatchClient{client: svc}
35-
}
36-
37-
type cloudwatchClient struct {
38-
client *cloudwatch.CloudWatch
55+
svc := cloudwatch.New(sess, cfg)
56+
return svc
3957
}
4058

41-
func (c *cloudwatchClient) QueryCloudWatch(cwQuery cloudwatch.GetMetricDataInput) ([]*cloudwatch.MetricDataResult, error) {
59+
func (c *cloudwatchManager) QueryCloudWatch(request v1alpha1.ExternalMetric) ([]*cloudwatch.MetricDataResult, error) {
60+
role := request.Spec.RoleARN
61+
region := request.Spec.Region
62+
cwQuery := toCloudWatchQuery(&request)
4263
now := time.Now()
4364
endTime := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), 0, 0, now.Location())
4465
// CloudWatch metrics have latency, we will grab in a 5 minute window and extract the latest value
@@ -48,7 +69,7 @@ func (c *cloudwatchClient) QueryCloudWatch(cwQuery cloudwatch.GetMetricDataInput
4869
cwQuery.StartTime = &startTime
4970
cwQuery.ScanBy = aws.String("TimestampDescending")
5071

51-
req, resp := c.client.GetMetricDataRequest(&cwQuery)
72+
req, resp := c.getClient(role, region).GetMetricDataRequest(&cwQuery)
5273
req.SetContext(context.Background())
5374

5475
if err := req.Send(); err != nil {

pkg/aws/interface.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ package aws
22

33
import (
44
"github.com/aws/aws-sdk-go/service/cloudwatch"
5+
"github.com/awslabs/k8s-cloudwatch-adapter/pkg/apis/metrics/v1alpha1"
56
)
67

7-
// Client represents a client for Amazon CloudWatch.
8-
type Client interface {
9-
8+
// CloudWatchManager manages clients for Amazon CloudWatch.
9+
type CloudWatchManager interface {
1010
// Query sends a CloudWatch GetMetricDataInput to CloudWatch API for metric results.
11-
QueryCloudWatch(query cloudwatch.GetMetricDataInput) ([]*cloudwatch.MetricDataResult, error)
11+
QueryCloudWatch(request v1alpha1.ExternalMetric) ([]*cloudwatch.MetricDataResult, error)
1212
}

pkg/aws/util.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import (
44
"io/ioutil"
55
"net/http"
66

7+
"github.com/aws/aws-sdk-go/aws"
8+
"github.com/aws/aws-sdk-go/service/cloudwatch"
9+
"github.com/awslabs/k8s-cloudwatch-adapter/pkg/apis/metrics/v1alpha1"
10+
711
"k8s.io/klog"
812
)
913

@@ -24,3 +28,49 @@ func GetLocalRegion() string {
2428
// strip the last character from AZ to get region ID
2529
return string(body[0 : len(body)-1])
2630
}
31+
32+
func toCloudWatchQuery(externalMetric *v1alpha1.ExternalMetric) cloudwatch.GetMetricDataInput {
33+
queries := externalMetric.Spec.Queries
34+
35+
cwMetricQueries := make([]*cloudwatch.MetricDataQuery, len(queries))
36+
for i, q := range queries {
37+
q := q
38+
mdq := &cloudwatch.MetricDataQuery{
39+
Id: &q.ID,
40+
Label: &q.Label,
41+
ReturnData: &q.ReturnData,
42+
}
43+
44+
if len(q.Expression) == 0 {
45+
dimensions := make([]*cloudwatch.Dimension, len(q.MetricStat.Metric.Dimensions))
46+
for j := range q.MetricStat.Metric.Dimensions {
47+
dimensions[j] = &cloudwatch.Dimension{
48+
Name: &q.MetricStat.Metric.Dimensions[j].Name,
49+
Value: &q.MetricStat.Metric.Dimensions[j].Value,
50+
}
51+
}
52+
53+
metric := &cloudwatch.Metric{
54+
Dimensions: dimensions,
55+
MetricName: &q.MetricStat.Metric.MetricName,
56+
Namespace: &q.MetricStat.Metric.Namespace,
57+
}
58+
59+
mdq.MetricStat = &cloudwatch.MetricStat{
60+
Metric: metric,
61+
Period: &q.MetricStat.Period,
62+
Stat: &q.MetricStat.Stat,
63+
Unit: aws.String(q.MetricStat.Unit),
64+
}
65+
} else {
66+
mdq.Expression = &q.Expression
67+
}
68+
69+
cwMetricQueries[i] = mdq
70+
}
71+
cwQuery := cloudwatch.GetMetricDataInput{
72+
MetricDataQueries: cwMetricQueries,
73+
}
74+
75+
return cwQuery
76+
}

0 commit comments

Comments
 (0)