Skip to content

Commit 78c8ce8

Browse files
authored
feat: add acm tree types (#7)
1 parent 2a95675 commit 78c8ce8

9 files changed

Lines changed: 328 additions & 4 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.25.2
44

55
require (
66
github.com/hashicorp/hcl/v2 v2.24.0
7-
github.com/infracost/proto v1.25.0
7+
github.com/infracost/proto v1.26.0
88
github.com/stretchr/testify v1.11.1
99
github.com/zclconf/go-cty v1.17.0
1010
)

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
2222
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
2323
github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
2424
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
25-
github.com/infracost/proto v1.25.0 h1:hnwZKhZrOfoEel7x0BcsoKLXRnQbyQiyNRfQhxVUyzg=
26-
github.com/infracost/proto v1.25.0/go.mod h1:BO6ew0skDYqM8lxl3rKKMA7WRM7VG1dIUKErbTdfGPk=
25+
github.com/infracost/proto v1.26.0 h1:eHXmZcHRudOObyi2SiJVVxUt82Vmlo+QtNn4SGyElec=
26+
github.com/infracost/proto v1.26.0/go.mod h1:BO6ew0skDYqM8lxl3rKKMA7WRM7VG1dIUKErbTdfGPk=
2727
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
2828
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
2929
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

pkg/tree/aws/acm/acm.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package acm
2+
3+
import (
4+
"strings"
5+
6+
"github.com/infracost/go-proto/pkg/tree/aws/acmpca"
7+
)
8+
9+
type CertificateManager struct {
10+
Certificates []Certificate
11+
}
12+
13+
func (cm *CertificateManager) AddCertificateAuthorities(cas *acmpca.PCACertificateAuthority) {
14+
// reset the certificate relationships
15+
for i := range cm.Certificates {
16+
cm.Certificates[i].Relationships = CertificateRelationships{}
17+
}
18+
19+
// iterate over the acmpca CertificateAuthorities and add them.
20+
for i, cert := range cm.Certificates {
21+
if cert.CertificateAuthorityARN.IsEmpty() {
22+
continue
23+
}
24+
25+
for _, ca := range cas.CertificateAuthorities {
26+
if strings.Contains(cert.CertificateAuthorityARN.Value(), ca.ID) {
27+
caCopy := ca
28+
cm.Certificates[i].Relationships.CertificateAuthority = &caCopy
29+
}
30+
}
31+
}
32+
}

pkg/tree/aws/acm/acm_test.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package acm
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/infracost/go-proto/pkg/tree/aws/acmpca"
10+
"github.com/infracost/go-proto/pkg/tree/resource"
11+
"github.com/infracost/go-proto/pkg/tree/value"
12+
)
13+
14+
func TestCertificateManager_AddCertificateAuthorities(t *testing.T) {
15+
tests := []struct {
16+
name string
17+
certificates []Certificate
18+
cas []acmpca.CertificateAuthority
19+
wantLinkedCA []string // expected linked CA ID per cert, empty string means no link
20+
}{
21+
{
22+
name: "no certificates or CAs",
23+
certificates: nil,
24+
cas: nil,
25+
wantLinkedCA: nil,
26+
},
27+
{
28+
name: "certificate with empty ARN is not linked",
29+
certificates: []Certificate{
30+
{CertificateAuthorityARN: value.EmptyString},
31+
},
32+
cas: []acmpca.CertificateAuthority{
33+
{Resource: resource.Resource{ID: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/abc-123"}},
34+
},
35+
wantLinkedCA: []string{""},
36+
},
37+
{
38+
name: "certificate with matching ARN is linked",
39+
certificates: []Certificate{
40+
{CertificateAuthorityARN: value.New("arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/abc-123", 0, "", nil)},
41+
},
42+
cas: []acmpca.CertificateAuthority{
43+
{Resource: resource.Resource{ID: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/abc-123"}},
44+
},
45+
wantLinkedCA: []string{"arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/abc-123"},
46+
},
47+
{
48+
name: "certificate with non-matching ARN is not linked",
49+
certificates: []Certificate{
50+
{CertificateAuthorityARN: value.New("arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/xyz-999", 0, "", nil)},
51+
},
52+
cas: []acmpca.CertificateAuthority{
53+
{Resource: resource.Resource{ID: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/abc-123"}},
54+
},
55+
wantLinkedCA: []string{""},
56+
},
57+
{
58+
name: "multiple certificates with mixed matching",
59+
certificates: []Certificate{
60+
{CertificateAuthorityARN: value.New("arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/abc-123", 0, "", nil)},
61+
{CertificateAuthorityARN: value.EmptyString},
62+
{CertificateAuthorityARN: value.New("arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/def-456", 0, "", nil)},
63+
},
64+
cas: []acmpca.CertificateAuthority{
65+
{Resource: resource.Resource{ID: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/abc-123"}},
66+
{Resource: resource.Resource{ID: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/def-456"}},
67+
},
68+
wantLinkedCA: []string{
69+
"arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/abc-123",
70+
"",
71+
"arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/def-456",
72+
},
73+
},
74+
{
75+
name: "existing relationships are reset",
76+
certificates: []Certificate{
77+
{
78+
CertificateAuthorityARN: value.EmptyString,
79+
Relationships: CertificateRelationships{
80+
CertificateAuthority: &acmpca.CertificateAuthority{
81+
Resource: resource.Resource{ID: "stale-ca"},
82+
},
83+
},
84+
},
85+
},
86+
cas: []acmpca.CertificateAuthority{
87+
{Resource: resource.Resource{ID: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/abc-123"}},
88+
},
89+
wantLinkedCA: []string{""},
90+
},
91+
{
92+
name: "linked CA is a copy not a reference to the slice element",
93+
certificates: []Certificate{
94+
{CertificateAuthorityARN: value.New("arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/abc-123", 0, "", nil)},
95+
},
96+
cas: []acmpca.CertificateAuthority{
97+
{Resource: resource.Resource{ID: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/abc-123"}},
98+
},
99+
wantLinkedCA: []string{"arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/abc-123"},
100+
},
101+
}
102+
103+
for _, tt := range tests {
104+
t.Run(tt.name, func(t *testing.T) {
105+
cm := &CertificateManager{Certificates: tt.certificates}
106+
pca := &acmpca.PCACertificateAuthority{CertificateAuthorities: tt.cas}
107+
108+
cm.AddCertificateAuthorities(pca)
109+
110+
require.Len(t, cm.Certificates, len(tt.wantLinkedCA))
111+
for i, wantID := range tt.wantLinkedCA {
112+
cert := cm.Certificates[i]
113+
if wantID == "" {
114+
assert.Nil(t, cert.Relationships.CertificateAuthority, "cert[%d] should not have a linked CA", i)
115+
} else {
116+
require.NotNil(t, cert.Relationships.CertificateAuthority, "cert[%d] should have a linked CA", i)
117+
assert.Equal(t, wantID, cert.Relationships.CertificateAuthority.ID, "cert[%d] linked to wrong CA", i)
118+
}
119+
}
120+
})
121+
}
122+
}

pkg/tree/aws/acm/certificate.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package acm
2+
3+
import (
4+
"github.com/infracost/go-proto/pkg/tree/aws/acmpca"
5+
"github.com/infracost/go-proto/pkg/tree/resource"
6+
"github.com/infracost/go-proto/pkg/tree/value"
7+
)
8+
9+
type Certificate struct {
10+
resource.Resource `tree:"-"`
11+
CertificateAuthorityARN value.String `tree:"certificate_authority_arn"`
12+
13+
Relationships CertificateRelationships `tree:"-"`
14+
}
15+
16+
type CertificateRelationships struct {
17+
CertificateAuthority *acmpca.CertificateAuthority
18+
}

pkg/tree/aws/acmpca/acmpca.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package acmpca
2+
3+
type PCACertificateAuthority struct {
4+
CertificateAuthorities []CertificateAuthority `tree:"certificate_authorities"`
5+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package acmpca
2+
3+
import (
4+
"github.com/infracost/go-proto/pkg/tree/resource"
5+
"github.com/infracost/go-proto/pkg/tree/value"
6+
)
7+
8+
type CertificateAuthority struct {
9+
resource.Resource `tree:"-"`
10+
11+
UsageMode value.String
12+
MonthlyRequests value.Int
13+
}

pkg/tree/aws/aws.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
package aws
22

33
import (
4+
"github.com/infracost/go-proto/pkg/tree/aws/acm"
5+
"github.com/infracost/go-proto/pkg/tree/aws/acmpca"
46
"github.com/infracost/go-proto/pkg/tree/aws/ec2"
57
)
68

79
type AWS struct {
8-
EC2 ec2.EC2 `tree:"ec2"`
10+
EC2 ec2.EC2 `tree:"ec2"`
11+
CertificateManager acm.CertificateManager `tree:"acm"`
12+
PCACertificateAuthority acmpca.PCACertificateAuthority `tree:"acmpca"`
13+
}
14+
15+
func (aws *AWS) PostProcess() {
16+
// acm
17+
// add the cert authorities to the certificate manager so that they can be linked to certificates
18+
aws.CertificateManager.AddCertificateAuthorities(&aws.PCACertificateAuthority)
919
}

pkg/tree/aws/aws_test.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package aws
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/infracost/go-proto/pkg/tree/aws/acm"
10+
"github.com/infracost/go-proto/pkg/tree/aws/acmpca"
11+
"github.com/infracost/go-proto/pkg/tree/resource"
12+
"github.com/infracost/go-proto/pkg/tree/value"
13+
)
14+
15+
func TestAWS_PostProcess_ACM(t *testing.T) {
16+
tests := []struct {
17+
name string
18+
certificates []acm.Certificate
19+
cas []acmpca.CertificateAuthority
20+
wantLinkedCA []bool // whether each cert should have a linked CA
21+
}{
22+
{
23+
name: "no certificates or CAs",
24+
certificates: nil,
25+
cas: nil,
26+
wantLinkedCA: nil,
27+
},
28+
{
29+
name: "certificate with empty ARN is not linked",
30+
certificates: []acm.Certificate{
31+
{CertificateAuthorityARN: value.EmptyString},
32+
},
33+
cas: []acmpca.CertificateAuthority{
34+
{Resource: resource.Resource{ID: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/abc-123"}},
35+
},
36+
wantLinkedCA: []bool{false},
37+
},
38+
{
39+
name: "certificate with matching ARN is linked",
40+
certificates: []acm.Certificate{
41+
{
42+
CertificateAuthorityARN: value.New("arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/abc-123", 0, "", nil),
43+
},
44+
},
45+
cas: []acmpca.CertificateAuthority{
46+
{Resource: resource.Resource{ID: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/abc-123"}},
47+
},
48+
wantLinkedCA: []bool{true},
49+
},
50+
{
51+
name: "certificate with non-matching ARN is not linked",
52+
certificates: []acm.Certificate{
53+
{
54+
CertificateAuthorityARN: value.New("arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/xyz-999", 0, "", nil),
55+
},
56+
},
57+
cas: []acmpca.CertificateAuthority{
58+
{Resource: resource.Resource{ID: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/abc-123"}},
59+
},
60+
wantLinkedCA: []bool{false},
61+
},
62+
{
63+
name: "multiple certificates with mixed matching",
64+
certificates: []acm.Certificate{
65+
{
66+
CertificateAuthorityARN: value.New("arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/abc-123", 0, "", nil),
67+
},
68+
{
69+
CertificateAuthorityARN: value.EmptyString,
70+
},
71+
{
72+
CertificateAuthorityARN: value.New("arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/def-456", 0, "", nil),
73+
},
74+
},
75+
cas: []acmpca.CertificateAuthority{
76+
{Resource: resource.Resource{ID: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/abc-123"}},
77+
{Resource: resource.Resource{ID: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/def-456"}},
78+
},
79+
wantLinkedCA: []bool{true, false, true},
80+
},
81+
{
82+
name: "existing relationships are reset before linking",
83+
certificates: []acm.Certificate{
84+
{
85+
CertificateAuthorityARN: value.EmptyString,
86+
Relationships: acm.CertificateRelationships{
87+
CertificateAuthority: &acmpca.CertificateAuthority{
88+
Resource: resource.Resource{ID: "stale-ca"},
89+
},
90+
},
91+
},
92+
},
93+
cas: []acmpca.CertificateAuthority{
94+
{Resource: resource.Resource{ID: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/abc-123"}},
95+
},
96+
wantLinkedCA: []bool{false},
97+
},
98+
}
99+
100+
for _, tt := range tests {
101+
t.Run(tt.name, func(t *testing.T) {
102+
aws := &AWS{
103+
CertificateManager: acm.CertificateManager{
104+
Certificates: tt.certificates,
105+
},
106+
PCACertificateAuthority: acmpca.PCACertificateAuthority{
107+
CertificateAuthorities: tt.cas,
108+
},
109+
}
110+
111+
aws.PostProcess()
112+
113+
require.Len(t, aws.CertificateManager.Certificates, len(tt.wantLinkedCA))
114+
for i, wantLinked := range tt.wantLinkedCA {
115+
cert := aws.CertificateManager.Certificates[i]
116+
if wantLinked {
117+
assert.NotNil(t, cert.Relationships.CertificateAuthority, "cert[%d] should have a linked CA", i)
118+
} else {
119+
assert.Nil(t, cert.Relationships.CertificateAuthority, "cert[%d] should not have a linked CA", i)
120+
}
121+
}
122+
})
123+
}
124+
}

0 commit comments

Comments
 (0)