Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
28 changes: 24 additions & 4 deletions pkg/tree/aws/s3/s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,46 @@ func (s *S3) PostProcess() {
}
}

// findBucket locates the owning bucket for a sub-resource. It matches on the
// referenced bucket name first, then falls back to the sub-resource's own ID.
// CloudFormation/CDK buckets often have no explicit BucketName (the physical
// name is generated at deploy time), so inline configurations (lifecycle,
// intelligent-tiering, versioning) carry an empty bucket name. For those, the
// sub-resource shares the owning bucket's logical ID, so the ID fallback links
// them correctly. Standalone resources (e.g. a separate lifecycle configuration
// or bucket policy) have their own distinct ID and so never false-match.
findBucket := func(bucketName, id string) (*Bucket, bool) {
if b, ok := bucketMap[bucketName]; ok {
return b, true
}
if id != "" {
if b, ok := bucketMap[id]; ok {
return b, true
}
}
return nil, false
}

for i, lc := range s.LifecycleConfigurations {
if b, ok := bucketMap[lc.BucketName.Value()]; ok {
if b, ok := findBucket(lc.BucketName.Value(), lc.ID); ok {
b.Relationships.LifecycleConfigurations = append(b.Relationships.LifecycleConfigurations, &s.LifecycleConfigurations[i])
}
}

for i, itc := range s.IntelligentTieringConfigurations {
if b, ok := bucketMap[itc.BucketName.Value()]; ok {
if b, ok := findBucket(itc.BucketName.Value(), itc.ID); ok {
b.Relationships.IntelligentTieringConfigurations = append(b.Relationships.IntelligentTieringConfigurations, &s.IntelligentTieringConfigurations[i])
}
}

for i, vc := range s.BucketVersioningConfigurations {
if b, ok := bucketMap[vc.BucketName.Value()]; ok {
if b, ok := findBucket(vc.BucketName.Value(), vc.ID); ok {
b.Relationships.BucketVersioningConfigurations = append(b.Relationships.BucketVersioningConfigurations, &s.BucketVersioningConfigurations[i])
}
}

for i, bp := range s.BucketPolicies {
if b, ok := bucketMap[bp.BucketName.Value()]; ok {
if b, ok := findBucket(bp.BucketName.Value(), bp.ID); ok {
b.Relationships.BucketPolicies = append(b.Relationships.BucketPolicies, &s.BucketPolicies[i])
}
}
Expand Down
75 changes: 75 additions & 0 deletions pkg/tree/aws/s3/s3_nameless_bucket_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package s3

import (
"testing"

"github.com/infracost/go-proto/pkg/tree/resource"
"github.com/infracost/go-proto/pkg/tree/value"
"github.com/stretchr/testify/assert"
)

// CloudFormation/CDK buckets frequently have no explicit BucketName (the physical
// name is generated at deploy time). Their inline configurations carry an empty
// bucket name but share the owning bucket's logical ID, so PostProcess must link
// them to the bucket via that ID fallback rather than the (empty) name.
func TestPostProcess_LinksInlineConfigsForNamelessBucketByID(t *testing.T) {
const bucketID = "DevArtifactBucket2651DA98"

s := &S3{
Buckets: []Bucket{
{
Resource: resource.Resource{ID: bucketID},
Name: value.New("", 0, "", nil), // no explicit BucketName
},
},
LifecycleConfigurations: []LifecycleConfiguration{
{
Resource: resource.Resource{ID: bucketID}, // inline config shares the bucket's logical ID
BucketName: value.New("", 0, "", nil),
},
},
IntelligentTieringConfigurations: []IntelligentTieringConfiguration{
{
Resource: resource.Resource{ID: bucketID},
BucketName: value.New("", 0, "", nil),
},
},
BucketVersioningConfigurations: []BucketVersioningConfiguration{
{
Resource: resource.Resource{ID: bucketID},
BucketName: value.New("", 0, "", nil),
},
},
}

s.PostProcess()

assert.Len(t, s.Buckets[0].Relationships.LifecycleConfigurations, 1,
"inline lifecycle configuration should link to a nameless bucket via shared ID")
assert.Len(t, s.Buckets[0].Relationships.IntelligentTieringConfigurations, 1)
assert.Len(t, s.Buckets[0].Relationships.BucketVersioningConfigurations, 1)
}

// A standalone sub-resource with its own distinct ID and a non-matching bucket
// name must NOT be linked to an unrelated bucket by the ID fallback.
func TestPostProcess_DoesNotFalseMatchStandaloneConfig(t *testing.T) {
s := &S3{
Buckets: []Bucket{
{
Resource: resource.Resource{ID: "bucket-1"},
Name: value.New("bucket-1", 0, "", nil),
},
},
LifecycleConfigurations: []LifecycleConfiguration{
{
Resource: resource.Resource{ID: "standalone-lc"},
BucketName: value.New("some-other-bucket", 0, "", nil),
},
},
}

s.PostProcess()

assert.Empty(t, s.Buckets[0].Relationships.LifecycleConfigurations,
"a standalone config pointing at another bucket must not link via the ID fallback")
}
Loading