Skip to content

Commit 4292187

Browse files
authored
Merge branch 'main' into liamcervante/kubernetes-workload-tree
2 parents d680526 + 0ea933e commit 4292187

2 files changed

Lines changed: 99 additions & 4 deletions

File tree

pkg/tree/aws/s3/s3.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,26 +27,46 @@ func (s *S3) PostProcess() {
2727
}
2828
}
2929

30+
// findBucket locates the owning bucket for a sub-resource. It matches on the
31+
// referenced bucket name first, then falls back to the sub-resource's own ID.
32+
// CloudFormation/CDK buckets often have no explicit BucketName (the physical
33+
// name is generated at deploy time), so inline configurations (lifecycle,
34+
// intelligent-tiering, versioning) carry an empty bucket name. For those, the
35+
// sub-resource shares the owning bucket's logical ID, so the ID fallback links
36+
// them correctly. Standalone resources (e.g. a separate lifecycle configuration
37+
// or bucket policy) have their own distinct ID and so never false-match.
38+
findBucket := func(bucketName, id string) (*Bucket, bool) {
39+
if b, ok := bucketMap[bucketName]; ok {
40+
return b, true
41+
}
42+
if id != "" {
43+
if b, ok := bucketMap[id]; ok {
44+
return b, true
45+
}
46+
}
47+
return nil, false
48+
}
49+
3050
for i, lc := range s.LifecycleConfigurations {
31-
if b, ok := bucketMap[lc.BucketName.Value()]; ok {
51+
if b, ok := findBucket(lc.BucketName.Value(), lc.ID); ok {
3252
b.Relationships.LifecycleConfigurations = append(b.Relationships.LifecycleConfigurations, &s.LifecycleConfigurations[i])
3353
}
3454
}
3555

3656
for i, itc := range s.IntelligentTieringConfigurations {
37-
if b, ok := bucketMap[itc.BucketName.Value()]; ok {
57+
if b, ok := findBucket(itc.BucketName.Value(), itc.ID); ok {
3858
b.Relationships.IntelligentTieringConfigurations = append(b.Relationships.IntelligentTieringConfigurations, &s.IntelligentTieringConfigurations[i])
3959
}
4060
}
4161

4262
for i, vc := range s.BucketVersioningConfigurations {
43-
if b, ok := bucketMap[vc.BucketName.Value()]; ok {
63+
if b, ok := findBucket(vc.BucketName.Value(), vc.ID); ok {
4464
b.Relationships.BucketVersioningConfigurations = append(b.Relationships.BucketVersioningConfigurations, &s.BucketVersioningConfigurations[i])
4565
}
4666
}
4767

4868
for i, bp := range s.BucketPolicies {
49-
if b, ok := bucketMap[bp.BucketName.Value()]; ok {
69+
if b, ok := findBucket(bp.BucketName.Value(), bp.ID); ok {
5070
b.Relationships.BucketPolicies = append(b.Relationships.BucketPolicies, &s.BucketPolicies[i])
5171
}
5272
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package s3
2+
3+
import (
4+
"testing"
5+
6+
"github.com/infracost/go-proto/pkg/tree/resource"
7+
"github.com/infracost/go-proto/pkg/tree/value"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
// CloudFormation/CDK buckets frequently have no explicit BucketName (the physical
12+
// name is generated at deploy time). Their inline configurations carry an empty
13+
// bucket name but share the owning bucket's logical ID, so PostProcess must link
14+
// them to the bucket via that ID fallback rather than the (empty) name.
15+
func TestPostProcess_LinksInlineConfigsForNamelessBucketByID(t *testing.T) {
16+
const bucketID = "DevArtifactBucket2651DA98"
17+
18+
s := &S3{
19+
Buckets: []Bucket{
20+
{
21+
Resource: resource.Resource{ID: bucketID},
22+
Name: value.New("", 0, "", nil), // no explicit BucketName
23+
},
24+
},
25+
LifecycleConfigurations: []LifecycleConfiguration{
26+
{
27+
Resource: resource.Resource{ID: bucketID}, // inline config shares the bucket's logical ID
28+
BucketName: value.New("", 0, "", nil),
29+
},
30+
},
31+
IntelligentTieringConfigurations: []IntelligentTieringConfiguration{
32+
{
33+
Resource: resource.Resource{ID: bucketID},
34+
BucketName: value.New("", 0, "", nil),
35+
},
36+
},
37+
BucketVersioningConfigurations: []BucketVersioningConfiguration{
38+
{
39+
Resource: resource.Resource{ID: bucketID},
40+
BucketName: value.New("", 0, "", nil),
41+
},
42+
},
43+
}
44+
45+
s.PostProcess()
46+
47+
assert.Len(t, s.Buckets[0].Relationships.LifecycleConfigurations, 1,
48+
"inline lifecycle configuration should link to a nameless bucket via shared ID")
49+
assert.Len(t, s.Buckets[0].Relationships.IntelligentTieringConfigurations, 1)
50+
assert.Len(t, s.Buckets[0].Relationships.BucketVersioningConfigurations, 1)
51+
}
52+
53+
// A standalone sub-resource with its own distinct ID and a non-matching bucket
54+
// name must NOT be linked to an unrelated bucket by the ID fallback.
55+
func TestPostProcess_DoesNotFalseMatchStandaloneConfig(t *testing.T) {
56+
s := &S3{
57+
Buckets: []Bucket{
58+
{
59+
Resource: resource.Resource{ID: "bucket-1"},
60+
Name: value.New("bucket-1", 0, "", nil),
61+
},
62+
},
63+
LifecycleConfigurations: []LifecycleConfiguration{
64+
{
65+
Resource: resource.Resource{ID: "standalone-lc"},
66+
BucketName: value.New("some-other-bucket", 0, "", nil),
67+
},
68+
},
69+
}
70+
71+
s.PostProcess()
72+
73+
assert.Empty(t, s.Buckets[0].Relationships.LifecycleConfigurations,
74+
"a standalone config pointing at another bucket must not link via the ID fallback")
75+
}

0 commit comments

Comments
 (0)