Skip to content

Commit 9aea4e9

Browse files
committed
feat(discovery-jobs): support for IncludeLinkedAccounts
Signed-off-by: Pablo Osinaga <pablo.osinaga@deepl.com>
1 parent c13b3fb commit 9aea4e9

18 files changed

Lines changed: 627 additions & 44 deletions

pkg/clients/cloudwatch/client.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ type Client interface {
3030
// ListMetrics returns the list of metrics and dimensions for a given namespace
3131
// and metric name. Results pagination is handled automatically: the caller can
3232
// optionally pass a non-nil func in order to handle results pages.
33-
ListMetrics(ctx context.Context, namespace string, metric *model.MetricConfig, recentlyActiveOnly bool, fn func(page []*model.Metric)) error
33+
ListMetrics(ctx context.Context, namespace string, metric *model.MetricConfig, includeLinkedAccounts []string, recentlyActiveOnly bool, fn func(page []*model.Metric)) error
3434

3535
// GetMetricData returns the output of the GetMetricData CloudWatch API.
3636
// Results pagination is handled automatically.
@@ -90,9 +90,9 @@ func (c limitedConcurrencyClient) GetMetricData(ctx context.Context, getMetricDa
9090
return res
9191
}
9292

93-
func (c limitedConcurrencyClient) ListMetrics(ctx context.Context, namespace string, metric *model.MetricConfig, recentlyActiveOnly bool, fn func(page []*model.Metric)) error {
93+
func (c limitedConcurrencyClient) ListMetrics(ctx context.Context, namespace string, metric *model.MetricConfig, includeLinkedAccounts []string, recentlyActiveOnly bool, fn func(page []*model.Metric)) error {
9494
c.limiter.Acquire(listMetricsCall)
95-
err := c.client.ListMetrics(ctx, namespace, metric, recentlyActiveOnly, fn)
95+
err := c.client.ListMetrics(ctx, namespace, metric, includeLinkedAccounts, recentlyActiveOnly, fn)
9696
c.limiter.Release(listMetricsCall)
9797
return err
9898
}

pkg/clients/cloudwatch/v1/client.go

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ package v1
1515
import (
1616
"context"
1717
"log/slog"
18+
"slices"
1819
"time"
1920

2021
"github.com/aws/aws-sdk-go/aws"
@@ -38,11 +39,14 @@ func NewClient(logger *slog.Logger, cloudwatchAPI cloudwatchiface.CloudWatchAPI)
3839
}
3940
}
4041

41-
func (c client) ListMetrics(ctx context.Context, namespace string, metric *model.MetricConfig, recentlyActiveOnly bool, fn func(page []*model.Metric)) error {
42+
func (c client) ListMetrics(ctx context.Context, namespace string, metric *model.MetricConfig, includeLinkedAccounts []string, recentlyActiveOnly bool, fn func(page []*model.Metric)) error {
4243
filter := &cloudwatch.ListMetricsInput{
4344
MetricName: aws.String(metric.Name),
4445
Namespace: aws.String(namespace),
4546
}
47+
if len(includeLinkedAccounts) > 0 {
48+
filter.IncludeLinkedAccounts = aws.Bool(true)
49+
}
4650
if recentlyActiveOnly {
4751
filter.RecentlyActive = aws.String("PT3H")
4852
}
@@ -52,7 +56,7 @@ func (c client) ListMetrics(ctx context.Context, namespace string, metric *model
5256
err := c.cloudwatchAPI.ListMetricsPagesWithContext(ctx, filter, func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
5357
promutil.CloudwatchAPICounter.WithLabelValues("ListMetrics").Inc()
5458

55-
metricsPage := toModelMetric(page)
59+
metricsPage := toModelMetric(page, includeLinkedAccounts)
5660

5761
c.logger.Debug("ListMetrics", "output", metricsPage, "last_page", lastPage)
5862

@@ -68,15 +72,32 @@ func (c client) ListMetrics(ctx context.Context, namespace string, metric *model
6872
return nil
6973
}
7074

71-
func toModelMetric(page *cloudwatch.ListMetricsOutput) []*model.Metric {
75+
func toModelMetric(page *cloudwatch.ListMetricsOutput, includeLinkedAccounts []string) []*model.Metric {
7276
modelMetrics := make([]*model.Metric, 0, len(page.Metrics))
73-
for _, cloudwatchMetric := range page.Metrics {
74-
modelMetric := &model.Metric{
75-
MetricName: *cloudwatchMetric.MetricName,
76-
Namespace: *cloudwatchMetric.Namespace,
77-
Dimensions: toModelDimensions(cloudwatchMetric.Dimensions),
77+
if len(includeLinkedAccounts) > 0 {
78+
includeAll := slices.Contains(includeLinkedAccounts, "*")
79+
for i := 0; i < len(page.Metrics); i++ {
80+
linkedAccountID := *page.OwningAccounts[i]
81+
if !includeAll && !slices.Contains(includeLinkedAccounts, linkedAccountID) {
82+
continue
83+
}
84+
modelMetric := &model.Metric{
85+
MetricName: *page.Metrics[i].MetricName,
86+
Namespace: *page.Metrics[i].Namespace,
87+
Dimensions: toModelDimensions(page.Metrics[i].Dimensions),
88+
LinkedAccountID: linkedAccountID,
89+
}
90+
modelMetrics = append(modelMetrics, modelMetric)
91+
}
92+
} else {
93+
for _, cloudwatchMetric := range page.Metrics {
94+
modelMetric := &model.Metric{
95+
MetricName: *cloudwatchMetric.MetricName,
96+
Namespace: *cloudwatchMetric.Namespace,
97+
Dimensions: toModelDimensions(cloudwatchMetric.Dimensions),
98+
}
99+
modelMetrics = append(modelMetrics, modelMetric)
78100
}
79-
modelMetrics = append(modelMetrics, modelMetric)
80101
}
81102
return modelMetrics
82103
}
@@ -106,12 +127,16 @@ func (c client) GetMetricData(ctx context.Context, getMetricData []*model.Cloudw
106127
Period: &data.GetMetricDataProcessingParams.Period,
107128
Stat: &data.GetMetricDataProcessingParams.Statistic,
108129
}
109-
metricDataQueries = append(metricDataQueries, &cloudwatch.MetricDataQuery{
130+
metricDataQuery := &cloudwatch.MetricDataQuery{
110131
Id: &data.GetMetricDataProcessingParams.QueryID,
111132
MetricStat: metricStat,
112133
ReturnData: aws.Bool(true),
113-
})
134+
}
114135
exportAllDataPoints = exportAllDataPoints || data.MetricMigrationParams.ExportAllDataPoints
136+
if data.LinkedAccountID != "" {
137+
metricDataQuery.AccountId = aws.String(data.LinkedAccountID)
138+
}
139+
metricDataQueries = append(metricDataQueries, metricDataQuery)
115140
}
116141
input := &cloudwatch.GetMetricDataInput{
117142
EndTime: &endTime,

pkg/clients/cloudwatch/v1/client_test.go

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,175 @@ func Test_toMetricDataResult(t *testing.T) {
151151
})
152152
}
153153
}
154+
155+
func Test_toModelMetric(t *testing.T) {
156+
type testCase struct {
157+
name string
158+
listMetricsOutput *cloudwatch.ListMetricsOutput
159+
includeLinkedAccounts []string
160+
expectedMetrics []*model.Metric
161+
}
162+
163+
testCases := []testCase{
164+
{
165+
name: "no linked accounts filter - original behavior",
166+
listMetricsOutput: &cloudwatch.ListMetricsOutput{
167+
Metrics: []*cloudwatch.Metric{
168+
{
169+
MetricName: aws.String("CPUUtilization"),
170+
Namespace: aws.String("AWS/EC2"),
171+
Dimensions: []*cloudwatch.Dimension{
172+
{Name: aws.String("InstanceId"), Value: aws.String("i-12345")},
173+
},
174+
},
175+
{
176+
MetricName: aws.String("NetworkIn"),
177+
Namespace: aws.String("AWS/EC2"),
178+
Dimensions: []*cloudwatch.Dimension{
179+
{Name: aws.String("InstanceId"), Value: aws.String("i-67890")},
180+
},
181+
},
182+
},
183+
},
184+
includeLinkedAccounts: nil,
185+
expectedMetrics: []*model.Metric{
186+
{
187+
MetricName: "CPUUtilization",
188+
Namespace: "AWS/EC2",
189+
Dimensions: []model.Dimension{
190+
{Name: "InstanceId", Value: "i-12345"},
191+
},
192+
},
193+
{
194+
MetricName: "NetworkIn",
195+
Namespace: "AWS/EC2",
196+
Dimensions: []model.Dimension{
197+
{Name: "InstanceId", Value: "i-67890"},
198+
},
199+
},
200+
},
201+
},
202+
{
203+
name: "with wildcard linked accounts - include all",
204+
listMetricsOutput: &cloudwatch.ListMetricsOutput{
205+
Metrics: []*cloudwatch.Metric{
206+
{
207+
MetricName: aws.String("CPUUtilization"),
208+
Namespace: aws.String("AWS/EC2"),
209+
Dimensions: []*cloudwatch.Dimension{
210+
{Name: aws.String("InstanceId"), Value: aws.String("i-12345")},
211+
},
212+
},
213+
{
214+
MetricName: aws.String("NetworkIn"),
215+
Namespace: aws.String("AWS/EC2"),
216+
Dimensions: []*cloudwatch.Dimension{
217+
{Name: aws.String("InstanceId"), Value: aws.String("i-67890")},
218+
},
219+
},
220+
},
221+
OwningAccounts: []*string{
222+
aws.String("111111111111"),
223+
aws.String("222222222222"),
224+
},
225+
},
226+
includeLinkedAccounts: []string{"*"},
227+
expectedMetrics: []*model.Metric{
228+
{
229+
MetricName: "CPUUtilization",
230+
Namespace: "AWS/EC2",
231+
Dimensions: []model.Dimension{
232+
{Name: "InstanceId", Value: "i-12345"},
233+
},
234+
LinkedAccountID: "111111111111",
235+
},
236+
{
237+
MetricName: "NetworkIn",
238+
Namespace: "AWS/EC2",
239+
Dimensions: []model.Dimension{
240+
{Name: "InstanceId", Value: "i-67890"},
241+
},
242+
LinkedAccountID: "222222222222",
243+
},
244+
},
245+
},
246+
{
247+
name: "with specific linked accounts - filter by account ID",
248+
listMetricsOutput: &cloudwatch.ListMetricsOutput{
249+
Metrics: []*cloudwatch.Metric{
250+
{
251+
MetricName: aws.String("CPUUtilization"),
252+
Namespace: aws.String("AWS/EC2"),
253+
Dimensions: []*cloudwatch.Dimension{
254+
{Name: aws.String("InstanceId"), Value: aws.String("i-12345")},
255+
},
256+
},
257+
{
258+
MetricName: aws.String("NetworkIn"),
259+
Namespace: aws.String("AWS/EC2"),
260+
Dimensions: []*cloudwatch.Dimension{
261+
{Name: aws.String("InstanceId"), Value: aws.String("i-67890")},
262+
},
263+
},
264+
{
265+
MetricName: aws.String("DiskReadOps"),
266+
Namespace: aws.String("AWS/EC2"),
267+
Dimensions: []*cloudwatch.Dimension{
268+
{Name: aws.String("InstanceId"), Value: aws.String("i-11111")},
269+
},
270+
},
271+
},
272+
OwningAccounts: []*string{
273+
aws.String("111111111111"),
274+
aws.String("222222222222"),
275+
aws.String("333333333333"),
276+
},
277+
},
278+
includeLinkedAccounts: []string{"111111111111", "333333333333"},
279+
expectedMetrics: []*model.Metric{
280+
{
281+
MetricName: "CPUUtilization",
282+
Namespace: "AWS/EC2",
283+
Dimensions: []model.Dimension{
284+
{Name: "InstanceId", Value: "i-12345"},
285+
},
286+
LinkedAccountID: "111111111111",
287+
},
288+
{
289+
MetricName: "DiskReadOps",
290+
Namespace: "AWS/EC2",
291+
Dimensions: []model.Dimension{
292+
{Name: "InstanceId", Value: "i-11111"},
293+
},
294+
LinkedAccountID: "333333333333",
295+
},
296+
},
297+
},
298+
{
299+
name: "with linked accounts filter - no matches",
300+
listMetricsOutput: &cloudwatch.ListMetricsOutput{
301+
Metrics: []*cloudwatch.Metric{
302+
{
303+
MetricName: aws.String("CPUUtilization"),
304+
Namespace: aws.String("AWS/EC2"),
305+
Dimensions: []*cloudwatch.Dimension{
306+
{Name: aws.String("InstanceId"), Value: aws.String("i-12345")},
307+
},
308+
},
309+
},
310+
OwningAccounts: []*string{
311+
aws.String("111111111111"),
312+
},
313+
},
314+
includeLinkedAccounts: []string{"999999999999"},
315+
expectedMetrics: []*model.Metric{},
316+
},
317+
}
318+
319+
for _, tc := range testCases {
320+
t.Run(tc.name, func(t *testing.T) {
321+
result := toModelMetric(tc.listMetricsOutput, tc.includeLinkedAccounts)
322+
require.Equal(t, tc.expectedMetrics, result)
323+
})
324+
}
325+
}

pkg/clients/cloudwatch/v2/client.go

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ package v2
1515
import (
1616
"context"
1717
"log/slog"
18+
"slices"
1819
"time"
1920

2021
"github.com/aws/aws-sdk-go-v2/aws"
@@ -38,11 +39,14 @@ func NewClient(logger *slog.Logger, cloudwatchAPI *cloudwatch.Client) cloudwatch
3839
}
3940
}
4041

41-
func (c client) ListMetrics(ctx context.Context, namespace string, metric *model.MetricConfig, recentlyActiveOnly bool, fn func(page []*model.Metric)) error {
42+
func (c client) ListMetrics(ctx context.Context, namespace string, metric *model.MetricConfig, includeLinkedAccounts []string, recentlyActiveOnly bool, fn func(page []*model.Metric)) error {
4243
filter := &cloudwatch.ListMetricsInput{
4344
MetricName: aws.String(metric.Name),
4445
Namespace: aws.String(namespace),
4546
}
47+
if len(includeLinkedAccounts) > 0 {
48+
filter.IncludeLinkedAccounts = aws.Bool(true)
49+
}
4650
if recentlyActiveOnly {
4751
filter.RecentlyActive = types.RecentlyActivePt3h
4852
}
@@ -62,7 +66,7 @@ func (c client) ListMetrics(ctx context.Context, namespace string, metric *model
6266
return err
6367
}
6468

65-
metricsPage := toModelMetric(page)
69+
metricsPage := toModelMetric(page, includeLinkedAccounts)
6670
c.logger.Debug("ListMetrics", "output", metricsPage)
6771

6872
fn(metricsPage)
@@ -71,15 +75,32 @@ func (c client) ListMetrics(ctx context.Context, namespace string, metric *model
7175
return nil
7276
}
7377

74-
func toModelMetric(page *cloudwatch.ListMetricsOutput) []*model.Metric {
78+
func toModelMetric(page *cloudwatch.ListMetricsOutput, includeLinkedAccounts []string) []*model.Metric {
7579
modelMetrics := make([]*model.Metric, 0, len(page.Metrics))
76-
for _, cloudwatchMetric := range page.Metrics {
77-
modelMetric := &model.Metric{
78-
MetricName: *cloudwatchMetric.MetricName,
79-
Namespace: *cloudwatchMetric.Namespace,
80-
Dimensions: toModelDimensions(cloudwatchMetric.Dimensions),
80+
if len(includeLinkedAccounts) > 0 {
81+
includeAll := slices.Contains(includeLinkedAccounts, "*")
82+
for i := 0; i < len(page.Metrics); i++ {
83+
linkedAccountID := page.OwningAccounts[i]
84+
if !includeAll && !slices.Contains(includeLinkedAccounts, linkedAccountID) {
85+
continue
86+
}
87+
modelMetric := &model.Metric{
88+
MetricName: *page.Metrics[i].MetricName,
89+
Namespace: *page.Metrics[i].Namespace,
90+
Dimensions: toModelDimensions(page.Metrics[i].Dimensions),
91+
LinkedAccountID: linkedAccountID,
92+
}
93+
modelMetrics = append(modelMetrics, modelMetric)
94+
}
95+
} else {
96+
for _, cloudwatchMetric := range page.Metrics {
97+
modelMetric := &model.Metric{
98+
MetricName: *cloudwatchMetric.MetricName,
99+
Namespace: *cloudwatchMetric.Namespace,
100+
Dimensions: toModelDimensions(cloudwatchMetric.Dimensions),
101+
}
102+
modelMetrics = append(modelMetrics, modelMetric)
81103
}
82-
modelMetrics = append(modelMetrics, modelMetric)
83104
}
84105
return modelMetrics
85106
}
@@ -109,12 +130,16 @@ func (c client) GetMetricData(ctx context.Context, getMetricData []*model.Cloudw
109130
Period: aws.Int32(int32(data.GetMetricDataProcessingParams.Period)),
110131
Stat: &data.GetMetricDataProcessingParams.Statistic,
111132
}
112-
metricDataQueries = append(metricDataQueries, types.MetricDataQuery{
133+
metricDataQuery := types.MetricDataQuery{
113134
Id: &data.GetMetricDataProcessingParams.QueryID,
114135
MetricStat: metricStat,
115136
ReturnData: aws.Bool(true),
116-
})
137+
}
117138
exportAllDataPoints = exportAllDataPoints || data.MetricMigrationParams.ExportAllDataPoints
139+
if data.LinkedAccountID != "" {
140+
metricDataQuery.AccountId = aws.String(data.LinkedAccountID)
141+
}
142+
metricDataQueries = append(metricDataQueries, metricDataQuery)
118143
}
119144

120145
input := &cloudwatch.GetMetricDataInput{

0 commit comments

Comments
 (0)