Skip to content

Commit 7c0f2a8

Browse files
authored
use sdk pagination for accurate stack status instrumentation (#15)
1 parent 640bb04 commit 7c0f2a8

5 files changed

Lines changed: 172 additions & 210 deletions

File tree

pkg/worker/handler/stack/detail.go

Lines changed: 33 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -2,97 +2,68 @@ package stack
22

33
import (
44
"context"
5-
"regexp"
65

7-
"github.com/aws/aws-sdk-go-v2/aws"
8-
"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi"
9-
tagtypes "github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi/types"
6+
"github.com/aws/aws-sdk-go-v2/service/cloudformation"
7+
"github.com/aws/aws-sdk-go-v2/service/cloudformation/types"
108
"github.com/xh3b4sd/tracer"
119
)
1210

13-
var (
14-
// exp defines a regular expression for the ARN suffixes of nested
15-
// CloudFormation stacks. Note that there is no precise definition for the
16-
// amount of characters that a random hack suffix for the stack names ought to
17-
// look like. So we are testing against at least 10 characters, but also
18-
// accept e.g. 13 or 14.
19-
//
20-
// arn:aws:cloudformation:us-west-2:995626699990:stack/server-test-FargateStack-QGXQ9XZ4J44K/165deb30-30d0-11f0-9eeb-023c6b26cb57
21-
//
22-
exp = regexp.MustCompile(`-[A-Z0-9]{10,}/[a-f0-9-]{36}$`)
23-
)
24-
2511
type detail struct {
26-
// arn is the well defined Amazon Resource Name of the CloudFormation stack.
27-
arn string
12+
// nam is the name of the CloudFormation stack.
13+
nam string
14+
// sta is the raw CloudFormation stack status, e.g. UPDATE_COMPLETE.
15+
sta types.StackStatus
2816
}
2917

30-
// detail finds all CloudFormation stack ARNs that are tagged with the
18+
// detail finds all CloudFormation stack statuses that are tagged with the
3119
// "environment" that matches Specta's runtime configuration. In other words, if
3220
// Specta is running in "staging", then detail() will find all CloudFormation
3321
// stacks labelled with the resource tags environment=staging.
3422
func (h *Handler) detail() ([]detail, error) {
3523
var err error
3624

37-
var inp *resourcegroupstaggingapi.GetResourcesInput
25+
// Create a new paginator for each instrumentation cycle in order to guarantee
26+
// data integrity per loop.
27+
var pag *cloudformation.DescribeStacksPaginator
3828
{
39-
inp = &resourcegroupstaggingapi.GetResourcesInput{
40-
ResourceTypeFilters: []string{"cloudformation:stack"},
41-
TagFilters: []tagtypes.TagFilter{
42-
{
43-
Key: aws.String("environment"),
44-
Values: []string{h.env.Environment},
45-
},
46-
},
47-
}
48-
}
49-
50-
var out *resourcegroupstaggingapi.GetResourcesOutput
51-
{
52-
out, err = h.tag.GetResources(context.Background(), inp)
53-
if err != nil {
54-
return nil, tracer.Mask(err)
55-
}
29+
pag = cloudformation.NewDescribeStacksPaginator(h.cfc, &cloudformation.DescribeStacksInput{})
5630
}
5731

5832
var det []detail
59-
for _, x := range out.ResourceTagMappingList {
60-
var roo bool
33+
for pag.HasMorePages() {
34+
var out *cloudformation.DescribeStacksOutput
6135
{
62-
roo = rooSta(*x.ResourceARN)
36+
out, err = pag.NextPage(context.Background())
37+
if err != nil {
38+
return nil, tracer.Mask(err)
39+
}
6340
}
6441

65-
if !roo {
66-
continue
67-
}
42+
for _, x := range out.Stacks {
43+
if !hasEnv(x.Tags, h.env.Environment) {
44+
continue
45+
}
6846

69-
det = append(det, detail{
70-
arn: *x.ResourceARN,
71-
})
47+
det = append(det, detail{
48+
nam: *x.StackName,
49+
sta: x.StackStatus,
50+
})
51+
}
7252
}
7353

7454
if len(det) == 0 {
75-
return nil, tracer.Maskf(missingRootStackError, "%v", allArn(out.ResourceTagMappingList))
76-
}
77-
if len(det) > 1 {
78-
return nil, tracer.Maskf(tooManyRootStacksError, "%v", det)
55+
return nil, tracer.Maskf(missingRootStackError, "%s", h.env.Environment)
7956
}
8057

8158
return det, nil
8259
}
8360

84-
func allArn(lis []tagtypes.ResourceTagMapping) []string {
85-
var all []string
86-
87-
for _, x := range lis {
88-
all = append(all, *x.ResourceARN)
61+
func hasEnv(tags []types.Tag, env string) bool {
62+
for _, t := range tags {
63+
if t.Key != nil && t.Value != nil && *t.Key == "environment" && *t.Value == env {
64+
return true
65+
}
8966
}
9067

91-
return all
92-
}
93-
94-
// rooSta identifies whether the given CloudFormation stack ARN is considered a
95-
// root stack ARN.
96-
func rooSta(arn string) bool {
97-
return !exp.MatchString(arn)
68+
return false
9869
}

pkg/worker/handler/stack/detail_test.go

Lines changed: 0 additions & 68 deletions
This file was deleted.

pkg/worker/handler/stack/error.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,5 @@ import (
66

77
var missingRootStackError = &tracer.Error{
88
Kind: "missingRootStackError",
9-
Desc: "The exporter expected to find exactly one root stack, but no stack name matched against the internal mapping.",
10-
}
11-
12-
var tooManyRootStacksError = &tracer.Error{
13-
Kind: "tooManyRootStacksError",
14-
Desc: "The exporter expected to find exactly one root stack, but more than one stack names matched against the internal mapping.",
9+
Desc: "The exporter expected to find exactly one root stack, but no stack was found for teh given environment.",
1510
}

pkg/worker/handler/stack/stack.go

Lines changed: 70 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,51 @@
11
package stack
22

33
import (
4-
"context"
4+
"regexp"
55
"strings"
66

7-
"github.com/aws/aws-sdk-go-v2/aws"
8-
"github.com/aws/aws-sdk-go-v2/service/cloudformation"
97
"github.com/aws/aws-sdk-go-v2/service/cloudformation/types"
10-
"github.com/xh3b4sd/tracer"
118
)
129

13-
var status = map[types.ResourceStatus]float64{
14-
types.ResourceStatusCreateFailed: 0.0,
15-
types.ResourceStatusDeleteFailed: 0.0,
16-
types.ResourceStatusDeleteSkipped: 0.0,
17-
types.ResourceStatusExportFailed: 0.0,
18-
types.ResourceStatusExportRollbackComplete: 0.0,
19-
types.ResourceStatusExportRollbackFailed: 0.0,
20-
types.ResourceStatusExportRollbackInProgress: 0.0,
21-
types.ResourceStatusImportFailed: 0.0,
22-
types.ResourceStatusImportRollbackComplete: 0.0,
23-
types.ResourceStatusImportRollbackFailed: 0.0,
24-
types.ResourceStatusImportRollbackInProgress: 0.0,
25-
types.ResourceStatusRollbackComplete: 0.0,
26-
types.ResourceStatusRollbackFailed: 0.0,
27-
types.ResourceStatusRollbackInProgress: 0.0,
28-
types.ResourceStatusUpdateFailed: 0.0,
29-
types.ResourceStatusUpdateRollbackComplete: 0.0,
30-
types.ResourceStatusUpdateRollbackFailed: 0.0,
31-
types.ResourceStatusUpdateRollbackInProgress: 0.0,
32-
33-
types.ResourceStatusCreateInProgress: 0.5,
34-
types.ResourceStatusDeleteInProgress: 0.5,
35-
types.ResourceStatusExportInProgress: 0.5,
36-
types.ResourceStatusImportInProgress: 0.5,
37-
types.ResourceStatusUpdateInProgress: 0.5,
38-
39-
types.ResourceStatusCreateComplete: 1.0,
40-
types.ResourceStatusDeleteComplete: 1.0,
41-
types.ResourceStatusExportComplete: 1.0,
42-
types.ResourceStatusImportComplete: 1.0,
43-
types.ResourceStatusUpdateComplete: 1.0,
44-
}
10+
var (
11+
// exp defines a regular expression for the stack names of CloudFormation
12+
// stacks. Note that there is no precise definition for the amount of
13+
// characters that a random hash suffix for the stack names ought to look
14+
// like. So we are testing against at least 10 characters, but also accept
15+
// e.g. 13 or 14.
16+
//
17+
// server-test-FargateStack-QGXQ9XZ4J44K
18+
//
19+
exp = regexp.MustCompile(`-[A-Z0-9]{10,}$`)
20+
21+
status = map[types.StackStatus]float64{
22+
types.StackStatusCreateFailed: 0.0,
23+
types.StackStatusDeleteFailed: 0.0,
24+
types.StackStatusImportRollbackComplete: 0.0,
25+
types.StackStatusImportRollbackFailed: 0.0,
26+
types.StackStatusImportRollbackInProgress: 0.0,
27+
types.StackStatusRollbackComplete: 0.0,
28+
types.StackStatusRollbackFailed: 0.0,
29+
types.StackStatusRollbackInProgress: 0.0,
30+
types.StackStatusUpdateFailed: 0.0,
31+
types.StackStatusUpdateRollbackComplete: 0.0,
32+
types.StackStatusUpdateRollbackCompleteCleanupInProgress: 0.0,
33+
types.StackStatusUpdateRollbackFailed: 0.0,
34+
types.StackStatusUpdateRollbackInProgress: 0.0,
35+
36+
types.StackStatusCreateInProgress: 0.5,
37+
types.StackStatusDeleteInProgress: 0.5,
38+
types.StackStatusImportInProgress: 0.5,
39+
types.StackStatusReviewInProgress: 0.5,
40+
types.StackStatusUpdateCompleteCleanupInProgress: 0.5,
41+
types.StackStatusUpdateInProgress: 0.5,
42+
43+
types.StackStatusCreateComplete: 1.0,
44+
types.StackStatusDeleteComplete: 1.0,
45+
types.StackStatusImportComplete: 1.0,
46+
types.StackStatusUpdateComplete: 1.0,
47+
}
48+
)
4549

4650
type stack struct {
4751
// hlt is the CloudFormation stack status, either 0.0, 0.5 or 1.0.
@@ -57,65 +61,57 @@ type stack struct {
5761
// assigned the stack status 0.5, otherwise the unhealthy stack status 0 is
5862
// assigned.
5963
func (h *Handler) stack(det []detail) ([]stack, error) {
60-
var err error
61-
6264
var sta []stack
65+
6366
for _, x := range det {
64-
var inp *cloudformation.DescribeStackResourcesInput
67+
var tag string
6568
{
66-
inp = &cloudformation.DescribeStackResourcesInput{
67-
StackName: aws.String(x.arn),
68-
}
69+
tag = staTag(x.nam)
6970
}
7071

71-
var out *cloudformation.DescribeStackResourcesOutput
72-
{
73-
out, err = h.cfc.DescribeStackResources(context.Background(), inp)
74-
if err != nil {
75-
return nil, tracer.Mask(err)
76-
}
77-
}
72+
if tag == "" {
73+
h.log.Log(
74+
"level", "warning",
75+
"message", "skipping instrumentation for CloudFormation stack",
76+
"reason", "CloudFormation stack name pattern is unrecognizable",
77+
"name", x.nam,
78+
)
7879

79-
for _, y := range out.StackResources {
80-
var tag string
8180
{
82-
tag = staTag(*y.LogicalResourceId)
83-
}
84-
85-
if tag == "" {
86-
h.log.Log(
87-
"level", "warning",
88-
"message", "skipping instrumentation for CloudFormation stack",
89-
"reason", "CloudFormation stack name pattern is unrecognizable",
90-
"name", *y.LogicalResourceId,
91-
)
92-
93-
{
94-
continue
95-
}
96-
}
97-
98-
var hlt float64
99-
{
100-
hlt = status[y.ResourceStatus]
81+
continue
10182
}
83+
}
10284

103-
sta = append(sta, stack{
104-
hlt: hlt,
105-
lab: tag,
106-
})
85+
var hlt float64
86+
{
87+
hlt = status[x.sta]
10788
}
89+
90+
sta = append(sta, stack{
91+
hlt: hlt,
92+
lab: tag,
93+
})
10894
}
10995

11096
return sta, nil
11197
}
11298

99+
// rooSta identifies whether the given CloudFormation stack name is considered a
100+
// root stack name.
101+
func rooSta(arn string) bool {
102+
return !exp.MatchString(arn)
103+
}
104+
113105
func staTag(nam string) string {
114106
for k, v := range mapping {
115107
if strings.Contains(nam, k) {
116108
return v
117109
}
118110
}
119111

112+
if rooSta(nam) {
113+
return "root"
114+
}
115+
120116
return ""
121117
}

0 commit comments

Comments
 (0)