Skip to content

Commit 0dc2284

Browse files
authored
SCALRCORE-30924 Manage environment access to Infracost via Terraform … (#173)
* SCALRCORE-30924 Manage environment access to Infracost via Terraform provider * SCALRCORE-30924 Manage environment access to Infracost via Terraform provider * SCALRCORE-30924 Manage environment access to Infracost via Terraform provider
1 parent 0bf31ed commit 0dc2284

File tree

3 files changed

+278
-0
lines changed

3 files changed

+278
-0
lines changed

infracost_integration.go

+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package scalr
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"net/url"
8+
)
9+
10+
// Compile-time proof of interface implementation.
11+
var _ InfracostIntegrations = (*infracostIntegrations)(nil)
12+
13+
type InfracostIntegrations interface {
14+
List(ctx context.Context, options InfracostIntegrationListOptions) (*InfracostIntegrationList, error)
15+
Create(ctx context.Context, options InfracostIntegrationCreateOptions) (*InfracostIntegration, error)
16+
Read(ctx context.Context, id string) (*InfracostIntegration, error)
17+
Update(ctx context.Context, id string, options InfracostIntegrationUpdateOptions) (*InfracostIntegration, error)
18+
Delete(ctx context.Context, id string) error
19+
}
20+
21+
// infracostIntegrations implements InfracostIntegrations.
22+
type infracostIntegrations struct {
23+
client *Client
24+
}
25+
26+
type InfracostIntegrationList struct {
27+
*Pagination
28+
Items []*InfracostIntegration
29+
}
30+
31+
// InfracostIntegration represents a Scalr IACP Infracost integration.
32+
type InfracostIntegration struct {
33+
ID string `jsonapi:"primary,infracost-integration"`
34+
Name string `jsonapi:"attr,name"`
35+
Status IntegrationStatus `jsonapi:"attr,status"`
36+
ApiKey string `jsonapi:"attr,api-key"`
37+
IsShared bool `jsonapi:"attr,is-shared,omitempty"`
38+
ErrMessage string `jsonapi:"attr,err-message,omitempty"`
39+
40+
// Relations
41+
Environments []*Environment `jsonapi:"relation,environments"`
42+
}
43+
44+
type InfracostIntegrationListOptions struct {
45+
ListOptions
46+
47+
InfracostIntegration *string `url:"filter[infracost-integration],omitempty"`
48+
Name *string `url:"filter[name],omitempty"`
49+
}
50+
51+
type InfracostIntegrationCreateOptions struct {
52+
ID string `jsonapi:"primary,infracost-integration"`
53+
Name *string `jsonapi:"attr,name"`
54+
ApiKey *string `jsonapi:"attr,api-key"`
55+
IsShared *bool `jsonapi:"attr,is-shared,omitempty"`
56+
57+
// Relations
58+
Environments []*Environment `jsonapi:"relation,environments"`
59+
}
60+
61+
type InfracostIntegrationUpdateOptions struct {
62+
ID string `jsonapi:"primary,infracost-integration"`
63+
Name *string `jsonapi:"attr,name,omitempty"`
64+
ApiKey *string `jsonapi:"attr,api-key,omitempty"`
65+
IsShared *bool `jsonapi:"attr,is-shared,omitempty"`
66+
67+
// Relations
68+
Environments []*Environment `jsonapi:"relation,environments"`
69+
}
70+
71+
func (s *infracostIntegrations) List(
72+
ctx context.Context, options InfracostIntegrationListOptions,
73+
) (*InfracostIntegrationList, error) {
74+
req, err := s.client.newRequest("GET", "integrations/infracost", &options)
75+
if err != nil {
76+
return nil, err
77+
}
78+
79+
iil := &InfracostIntegrationList{}
80+
err = s.client.do(ctx, req, iil)
81+
if err != nil {
82+
return nil, err
83+
}
84+
85+
return iil, nil
86+
}
87+
88+
func (s *infracostIntegrations) Create(
89+
ctx context.Context, options InfracostIntegrationCreateOptions,
90+
) (*InfracostIntegration, error) {
91+
// Make sure we don't send a user provided ID.
92+
options.ID = ""
93+
94+
req, err := s.client.newRequest("POST", "integrations/infracost", &options)
95+
if err != nil {
96+
return nil, err
97+
}
98+
99+
ii := &InfracostIntegration{}
100+
err = s.client.do(ctx, req, ii)
101+
if err != nil {
102+
return nil, err
103+
}
104+
105+
return ii, nil
106+
}
107+
108+
func (s *infracostIntegrations) Read(ctx context.Context, id string) (*InfracostIntegration, error) {
109+
if !validStringID(&id) {
110+
return nil, errors.New("invalid value for Infracost integration ID")
111+
}
112+
113+
u := fmt.Sprintf("integrations/infracost/%s", url.QueryEscape(id))
114+
req, err := s.client.newRequest("GET", u, nil)
115+
if err != nil {
116+
return nil, err
117+
}
118+
119+
ii := &InfracostIntegration{}
120+
err = s.client.do(ctx, req, ii)
121+
if err != nil {
122+
return nil, err
123+
}
124+
125+
return ii, nil
126+
}
127+
128+
func (s *infracostIntegrations) Update(
129+
ctx context.Context, id string, options InfracostIntegrationUpdateOptions,
130+
) (*InfracostIntegration, error) {
131+
if !validStringID(&id) {
132+
return nil, errors.New("invalid value for Infracost integration ID")
133+
}
134+
135+
// Make sure we don't send a user provided ID.
136+
options.ID = ""
137+
138+
u := fmt.Sprintf("integrations/infracost/%s", url.QueryEscape(id))
139+
req, err := s.client.newRequest("PATCH", u, &options)
140+
if err != nil {
141+
return nil, err
142+
}
143+
144+
ii := &InfracostIntegration{}
145+
err = s.client.do(ctx, req, ii)
146+
if err != nil {
147+
return nil, err
148+
}
149+
150+
return ii, nil
151+
}
152+
153+
func (s *infracostIntegrations) Delete(ctx context.Context, id string) error {
154+
if !validStringID(&id) {
155+
return errors.New("invalid value for Infracost integration ID")
156+
}
157+
158+
u := fmt.Sprintf("integrations/infracost/%s", url.QueryEscape(id))
159+
req, err := s.client.newRequest("DELETE", u, nil)
160+
if err != nil {
161+
return err
162+
}
163+
164+
return s.client.do(ctx, req, nil)
165+
}

infracost_integration_test.go

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package scalr
2+
3+
import (
4+
"context"
5+
"github.com/stretchr/testify/assert"
6+
"github.com/stretchr/testify/require"
7+
"os"
8+
"testing"
9+
)
10+
11+
func TestInfracostIntegrations_Create(t *testing.T) {
12+
client := testClient(t)
13+
ctx := context.Background()
14+
apiKey := os.Getenv("TEST_INFRACOST_API_KEY")
15+
if len(apiKey) == 0 {
16+
t.Skip("Please set TEST_INFRACOST_API_KEY to run this test.")
17+
}
18+
19+
t.Run("with valid options", func(t *testing.T) {
20+
options := InfracostIntegrationCreateOptions{
21+
Name: String("test-" + randomString(t)),
22+
ApiKey: &apiKey,
23+
}
24+
25+
ii, err := client.InfracostIntegrations.Create(ctx, options)
26+
require.NoError(t, err)
27+
defer func() { client.InfracostIntegrations.Delete(ctx, ii.ID) }()
28+
29+
refreshed, err := client.InfracostIntegrations.Read(ctx, ii.ID)
30+
require.NoError(t, err)
31+
32+
for _, item := range []*InfracostIntegration{
33+
ii,
34+
refreshed,
35+
} {
36+
assert.NotEmpty(t, item.ID)
37+
assert.Equal(t, *options.Name, item.Name)
38+
assert.Empty(t, item.ApiKey)
39+
}
40+
})
41+
}
42+
43+
func TestInfracostIntegrations_Update(t *testing.T) {
44+
client := testClient(t)
45+
ctx := context.Background()
46+
apiKey := os.Getenv("TEST_INFRACOST_API_KEY")
47+
if len(apiKey) == 0 {
48+
t.Skip("Please set TEST_INFRACOST_API_KEY to run this test.")
49+
}
50+
51+
createOptions := InfracostIntegrationCreateOptions{
52+
Name: String("test-" + randomString(t)),
53+
ApiKey: &apiKey,
54+
}
55+
56+
ii, err := client.InfracostIntegrations.Create(ctx, createOptions)
57+
require.NoError(t, err)
58+
defer func() { client.InfracostIntegrations.Delete(ctx, ii.ID) }()
59+
60+
t.Run("with valid options", func(t *testing.T) {
61+
options := InfracostIntegrationUpdateOptions{
62+
Name: String("test-" + randomString(t)),
63+
ApiKey: &apiKey,
64+
//Status: IntegrationStatusDisabled,
65+
}
66+
67+
ii, err := client.InfracostIntegrations.Update(ctx, ii.ID, options)
68+
require.NoError(t, err)
69+
70+
refreshed, err := client.InfracostIntegrations.Read(ctx, ii.ID)
71+
require.NoError(t, err)
72+
73+
for _, item := range []*InfracostIntegration{
74+
ii,
75+
refreshed,
76+
} {
77+
assert.NotEmpty(t, item.ID)
78+
assert.Equal(t, *options.Name, item.Name)
79+
}
80+
})
81+
}
82+
83+
func TestInfracostIntegrations_List(t *testing.T) {
84+
client := testClient(t)
85+
ctx := context.Background()
86+
apiKey := os.Getenv("TEST_INFRACOST_API_KEY")
87+
if len(apiKey) == 0 {
88+
t.Skip("Please set TEST_INFRACOST_API_KEY to run this test.")
89+
}
90+
91+
createOptions := InfracostIntegrationCreateOptions{
92+
Name: String("test-" + randomString(t)),
93+
ApiKey: &apiKey,
94+
}
95+
96+
ii, err := client.InfracostIntegrations.Create(ctx, createOptions)
97+
require.NoError(t, err)
98+
defer func() { client.InfracostIntegrations.Delete(ctx, ii.ID) }()
99+
100+
t.Run("with valid options", func(t *testing.T) {
101+
iil, err := client.InfracostIntegrations.List(ctx, InfracostIntegrationListOptions{})
102+
require.NoError(t, err)
103+
assert.Equal(t, 1, iil.TotalCount)
104+
expectedIDs := []string{ii.ID}
105+
actualIDs := make([]string, len(iil.Items))
106+
for i, s := range iil.Items {
107+
actualIDs[i] = s.ID
108+
}
109+
assert.ElementsMatch(t, expectedIDs, actualIDs)
110+
})
111+
}

scalr.go

+2
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ type Client struct {
122122
AgentPoolTokens AgentPoolTokens
123123
AgentPools AgentPools
124124
EventBridgeIntegrations EventBridgeIntegrations
125+
InfracostIntegrations InfracostIntegrations
125126
ConfigurationVersions ConfigurationVersions
126127
EnvironmentTags EnvironmentTags
127128
Environments Environments
@@ -253,6 +254,7 @@ func NewClient(cfg *Config) (*Client, error) {
253254
client.SSHKeys = &sshKeys{client: client}
254255
client.SSHKeysLinks = &sshKeysLinks{client: client}
255256
client.RemoteStateConsumers = &remoteStateConsumers{client: client}
257+
client.InfracostIntegrations = &infracostIntegrations{client: client}
256258
return client, nil
257259
}
258260

0 commit comments

Comments
 (0)