diff --git a/client/api_client.go b/client/api_client.go index 71821af2..5d109252 100644 --- a/client/api_client.go +++ b/client/api_client.go @@ -161,6 +161,11 @@ type ApiClientInterface interface { AssignConfigurationSets(scope string, scopeId string, sets []string) error UnassignConfigurationSets(scope string, scopeId string, sets []string) error ConfigurationSetsAssignments(scope string, scopeId string) ([]ConfigurationSet, error) + CloudAccountCreate(payload *CloudAccountCreatePayload) (*CloudAccount, error) + CloudAccountUpdate(id string, payload *CloudAccountUpdatePayload) (*CloudAccount, error) + CloudAccountDelete(id string) error + CloudAccount(id string) (*CloudAccount, error) + CloudAccounts() ([]CloudAccount, error) } func NewApiClient(client http.HttpClientInterface, defaultOrganizationId string) ApiClientInterface { diff --git a/client/api_client_mock.go b/client/api_client_mock.go index fe67e0e5..54a918b2 100644 --- a/client/api_client_mock.go +++ b/client/api_client_mock.go @@ -304,6 +304,80 @@ func (mr *MockApiClientInterfaceMockRecorder) AssignUserToProject(arg0, arg1 any return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssignUserToProject", reflect.TypeOf((*MockApiClientInterface)(nil).AssignUserToProject), arg0, arg1) } +// CloudAccount mocks base method. +func (m *MockApiClientInterface) CloudAccount(arg0 string) (*CloudAccount, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CloudAccount", arg0) + ret0, _ := ret[0].(*CloudAccount) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CloudAccount indicates an expected call of CloudAccount. +func (mr *MockApiClientInterfaceMockRecorder) CloudAccount(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloudAccount", reflect.TypeOf((*MockApiClientInterface)(nil).CloudAccount), arg0) +} + +// CloudAccountCreate mocks base method. +func (m *MockApiClientInterface) CloudAccountCreate(arg0 *CloudAccountCreatePayload) (*CloudAccount, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CloudAccountCreate", arg0) + ret0, _ := ret[0].(*CloudAccount) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CloudAccountCreate indicates an expected call of CloudAccountCreate. +func (mr *MockApiClientInterfaceMockRecorder) CloudAccountCreate(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloudAccountCreate", reflect.TypeOf((*MockApiClientInterface)(nil).CloudAccountCreate), arg0) +} + +// CloudAccountDelete mocks base method. +func (m *MockApiClientInterface) CloudAccountDelete(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CloudAccountDelete", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// CloudAccountDelete indicates an expected call of CloudAccountDelete. +func (mr *MockApiClientInterfaceMockRecorder) CloudAccountDelete(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloudAccountDelete", reflect.TypeOf((*MockApiClientInterface)(nil).CloudAccountDelete), arg0) +} + +// CloudAccountUpdate mocks base method. +func (m *MockApiClientInterface) CloudAccountUpdate(arg0 string, arg1 *CloudAccountUpdatePayload) (*CloudAccount, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CloudAccountUpdate", arg0, arg1) + ret0, _ := ret[0].(*CloudAccount) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CloudAccountUpdate indicates an expected call of CloudAccountUpdate. +func (mr *MockApiClientInterfaceMockRecorder) CloudAccountUpdate(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloudAccountUpdate", reflect.TypeOf((*MockApiClientInterface)(nil).CloudAccountUpdate), arg0, arg1) +} + +// CloudAccounts mocks base method. +func (m *MockApiClientInterface) CloudAccounts() ([]CloudAccount, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CloudAccounts") + ret0, _ := ret[0].([]CloudAccount) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CloudAccounts indicates an expected call of CloudAccounts. +func (mr *MockApiClientInterfaceMockRecorder) CloudAccounts() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloudAccounts", reflect.TypeOf((*MockApiClientInterface)(nil).CloudAccounts)) +} + // CloudCredentialIdsInProject mocks base method. func (m *MockApiClientInterface) CloudCredentialIdsInProject(arg0 string) ([]string, error) { m.ctrl.T.Helper() diff --git a/client/cloud_account.go b/client/cloud_account.go new file mode 100644 index 00000000..013c1a4e --- /dev/null +++ b/client/cloud_account.go @@ -0,0 +1,88 @@ +package client + +import "fmt" + +type CloudAccountCreatePayload struct { + Provider string `json:"provider"` + Name string `json:"name"` + Configuration interface{} `json:"configuration" tfschema:"-"` +} + +type CloudAccountUpdatePayload struct { + Name string `json:"name"` + Configuration interface{} `json:"configuration" tfschema:"-"` +} + +type AWSCloudAccountConfiguration struct { + AccountId string `json:"accountId"` + BucketName string `json:"bucketName"` + Prefix string `json:"prefix,omitempty"` + Regions []string `json:"regions"` +} + +type CloudAccount struct { + Id string `json:"id"` + Provider string `json:"provider"` + Name string `json:"name"` + Health bool `json:"health"` + Configuration interface{} `json:"configuration" tfschema:"-"` +} + +func (client *ApiClient) CloudAccountCreate(payload *CloudAccountCreatePayload) (*CloudAccount, error) { + organizationId, err := client.OrganizationId() + if err != nil { + return nil, fmt.Errorf("failed to get organization id: %w", err) + } + + payloadWithOrganizationId := struct { + *CloudAccountCreatePayload + OrganizationId string `json:"organizationId"` + }{ + payload, + organizationId, + } + + var cloudAccount CloudAccount + if err := client.http.Post("/cloud/configurations", &payloadWithOrganizationId, &cloudAccount); err != nil { + return nil, err + } + + return &cloudAccount, nil +} + +func (client *ApiClient) CloudAccountUpdate(id string, payload *CloudAccountUpdatePayload) (*CloudAccount, error) { + var cloudAccount CloudAccount + if err := client.http.Put("/cloud/configurations/"+id, payload, &cloudAccount); err != nil { + return nil, err + } + + return &cloudAccount, nil +} + +func (client *ApiClient) CloudAccountDelete(id string) error { + return client.http.Delete("/cloud/configurations/"+id, nil) +} + +func (client *ApiClient) CloudAccount(id string) (*CloudAccount, error) { + var cloudAccount CloudAccount + + if err := client.http.Get("/cloud/configurations/"+id, nil, &cloudAccount); err != nil { + return nil, err + } + + return &cloudAccount, nil +} + +func (client *ApiClient) CloudAccounts() ([]CloudAccount, error) { + organizationId, err := client.OrganizationId() + if err != nil { + return nil, fmt.Errorf("failed to get organization id: %w", err) + } + + var cloudAccounts []CloudAccount + if err := client.http.Get("/cloud/configurations", map[string]string{"organizationId": organizationId}, &cloudAccounts); err != nil { + return nil, err + } + + return cloudAccounts, nil +} diff --git a/client/cloud_account_test.go b/client/cloud_account_test.go new file mode 100644 index 00000000..acbf78cf --- /dev/null +++ b/client/cloud_account_test.go @@ -0,0 +1,173 @@ +package client_test + +import ( + . "github.com/env0/terraform-provider-env0/client" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "go.uber.org/mock/gomock" +) + +var _ = Describe("CloudAccount", func() { + var account *CloudAccount + var accounts []CloudAccount + var err error + + awsConfiguration := AWSCloudAccountConfiguration{ + AccountId: "a", + BucketName: "b", + Prefix: "prefix", + Regions: []string{"us-east-10", "us-west-24"}, + } + + awsConfigurationUpdated := AWSCloudAccountConfiguration{ + AccountId: "c", + BucketName: "d", + Prefix: "prefix2", + Regions: []string{"us-east-10"}, + } + + account1 := CloudAccount{ + Id: "id1", + Provider: "AWS", + Name: "name1", + Health: true, + Configuration: &awsConfiguration, + } + + account1Updated := account1 + account1Updated.Name = "updatedname1" + account1Updated.Configuration = awsConfigurationUpdated + + account2 := CloudAccount{ + Id: "id2", + Provider: "GCP", + Name: "name2", + Configuration: []string{"some random configuration"}, + } + + Describe("create", func() { + BeforeEach(func() { + mockOrganizationIdCall(organizationId) + + payload := CloudAccountCreatePayload{ + Provider: account1.Provider, + Name: account1.Name, + Configuration: account1.Configuration, + } + + payloadWithOrganizationId := struct { + *CloudAccountCreatePayload + OrganizationId string `json:"organizationId"` + }{ + &payload, + organizationId, + } + + httpCall = mockHttpClient.EXPECT(). + Post("/cloud/configurations", &payloadWithOrganizationId, gomock.Any()). + Do(func(path string, request interface{}, response *CloudAccount) { + *response = account1 + }).Times(1) + + account, err = apiClient.CloudAccountCreate(&payload) + }) + + It("should get organization id", func() { + organizationIdCall.Times(1) + }) + + It("should return account", func() { + Expect(*account).To(Equal(account1)) + }) + + It("should not return error", func() { + Expect(err).To(BeNil()) + }) + }) + + Describe("update", func() { + BeforeEach(func() { + payload := CloudAccountUpdatePayload{ + Name: account1Updated.Name, + Configuration: account1Updated.Configuration, + } + + httpCall = mockHttpClient.EXPECT(). + Put("/cloud/configurations/"+account.Id, &payload, gomock.Any()). + Do(func(path string, request interface{}, response *CloudAccount) { + *response = account1Updated + }).Times(1) + + account, err = apiClient.CloudAccountUpdate(account.Id, &payload) + }) + + It("should return updated account", func() { + Expect(*account).To(Equal(account1Updated)) + }) + + It("should not return error", func() { + Expect(err).To(BeNil()) + }) + }) + + Describe("delete", func() { + BeforeEach(func() { + httpCall = mockHttpClient.EXPECT(). + Delete("/cloud/configurations/"+account.Id, nil). + Do(func(path string, request interface{}) {}).Times(1) + + err = apiClient.CloudAccountDelete(account.Id) + }) + + It("should not return error", func() { + Expect(err).To(BeNil()) + }) + }) + + Describe("get", func() { + BeforeEach(func() { + httpCall = mockHttpClient.EXPECT(). + Get("/cloud/configurations/"+account.Id, nil, gomock.Any()). + Do(func(path string, request interface{}, response *CloudAccount) { + *response = account1 + }).Times(1) + + account, err = apiClient.CloudAccount(account.Id) + }) + + It("should return account", func() { + Expect(*account).To(Equal(account1)) + }) + + It("should not return error", func() { + Expect(err).To(BeNil()) + }) + }) + + Describe("list", func() { + mockedAccounts := []CloudAccount{ + account1, + account2, + } + + BeforeEach(func() { + mockOrganizationIdCall(organizationId) + + httpCall = mockHttpClient.EXPECT(). + Get("/cloud/configurations", map[string]string{"organizationId": organizationId}, gomock.Any()). + Do(func(path string, request interface{}, response *[]CloudAccount) { + *response = mockedAccounts + }).Times(1) + + accounts, err = apiClient.CloudAccounts() + }) + + It("should return accounts", func() { + Expect(accounts).To(Equal(mockedAccounts)) + }) + + It("should not return error", func() { + Expect(err).To(BeNil()) + }) + }) +}) diff --git a/env0/cloud_configuration.go b/env0/cloud_configuration.go new file mode 100644 index 00000000..18ed5d23 --- /dev/null +++ b/env0/cloud_configuration.go @@ -0,0 +1,193 @@ +package env0 + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/env0/terraform-provider-env0/client" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func getCloudConfigurationFromSchema(d *schema.ResourceData, provider string) (interface{}, error) { + var configuration interface{} + + // Find the correct type, create an instance of this type, and deserialize from the schema to the instance. + + switch provider { + case "AWS": + configuration = &client.AWSCloudAccountConfiguration{} + default: + return nil, fmt.Errorf("unhandled provider: %s", provider) + } + + if err := readResourceData(configuration, d); err != nil { + return nil, fmt.Errorf("schema resource data deserialization failed: %v", err) + } + + return configuration, nil +} + +func getCloudConfigurationByNameFromApi(apiClient client.ApiClientInterface, name string) (*client.CloudAccount, error) { + cloudAccounts, err := apiClient.CloudAccounts() + if err != nil { + return nil, err + } + + for i, cloudAccount := range cloudAccounts { + if cloudAccount.Name == name { + return &cloudAccounts[i], nil + } + } + + return nil, fmt.Errorf("cloud configuration called '%s' was not found", name) +} + +func getCloudConfigurationFromApi(apiClient client.ApiClientInterface, id string) (*client.CloudAccount, error) { + var err error + var cloudAccount *client.CloudAccount + + if _, parseErr := uuid.Parse(id); parseErr != nil { + // Get by name (used by import). + cloudAccount, err = getCloudConfigurationByNameFromApi(apiClient, id) + } else { + // Get by id. + cloudAccount, err = apiClient.CloudAccount(id) + } + + if err != nil { + return nil, fmt.Errorf("failed to get clound configuration: %w", err) + } + + var configuration interface{} + + // Find the correct type, marshal the interface to bytes, and unmarshal the bytes back to an instance of the correct type. + + switch cloudAccount.Provider { + case "AWS": + configuration = &client.AWSCloudAccountConfiguration{} + default: + return nil, fmt.Errorf("unhandled provider: %s", cloudAccount.Provider) + } + + b, err := json.Marshal(cloudAccount.Configuration) + if err != nil { + return nil, fmt.Errorf("failed to json marshal %s configuration: %w", cloudAccount.Provider, err) + } + + if err := json.Unmarshal(b, configuration); err != nil { + return nil, fmt.Errorf("failed to json unmarshal %s configuration: %w", cloudAccount.Provider, err) + } + + cloudAccount.Configuration = configuration + + return cloudAccount, nil +} + +func createCloudConfiguration(d *schema.ResourceData, meta interface{}, provider string) diag.Diagnostics { + apiClient := meta.(client.ApiClientInterface) + + var createPayload client.CloudAccountCreatePayload + + if err := readResourceData(&createPayload, d); err != nil { + return diag.Errorf("schema resource data deserialization failed: %v", err) + } + + configuration, err := getCloudConfigurationFromSchema(d, provider) + if err != nil { + return diag.FromErr(err) + } + + createPayload.Configuration = configuration + createPayload.Provider = provider + + cloudAccount, err := apiClient.CloudAccountCreate(&createPayload) + if err != nil { + return diag.Errorf("failed to create a cloud configuration: %v", err) + } + + if err := d.Set("health", cloudAccount.Health); err != nil { + return diag.Errorf("failed to set health: %v", err) + } + d.SetId(cloudAccount.Id) + + return nil +} + +func readCloudConfiguration(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + apiClient := meta.(client.ApiClientInterface) + + cloudAccount, err := getCloudConfigurationFromApi(apiClient, d.Id()) + if err != nil { + return ResourceGetFailure(ctx, "cloud_configuration", d, err) + } + + if err := writeResourceData(cloudAccount, d); err != nil { + return diag.Errorf("schema resource data serialization failed: %v", err) + } + + if err := writeResourceData(cloudAccount.Configuration, d); err != nil { + return diag.Errorf("schema resource data serialization failed: %v", err) + } + + return nil +} + +func updateCloudConfiguration(d *schema.ResourceData, meta interface{}, provider string) diag.Diagnostics { + apiClient := meta.(client.ApiClientInterface) + + var updatePayload client.CloudAccountUpdatePayload + + if err := readResourceData(&updatePayload, d); err != nil { + return diag.Errorf("schema resource data deserialization failed: %v", err) + } + + configuration, err := getCloudConfigurationFromSchema(d, provider) + if err != nil { + return diag.FromErr(err) + } + + updatePayload.Configuration = configuration + + cloudAccount, err := apiClient.CloudAccountUpdate(d.Id(), &updatePayload) + if err != nil { + return diag.Errorf("failed to update cloud configuration: %v", err) + } + + if err := d.Set("health", cloudAccount.Health); err != nil { + return diag.Errorf("failed to set health: %v", err) + } + + return nil +} + +func deleteCloudConfiguration(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + apiClient := meta.(client.ApiClientInterface) + + if err := apiClient.CloudAccountDelete(d.Id()); err != nil { + return diag.Errorf("failed to delete cloud configuration: %v", err) + } + + return nil +} + +func importCloudConfiguration(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + apiClient := meta.(client.ApiClientInterface) + + cloudAccount, err := getCloudConfigurationFromApi(apiClient, d.Id()) + if err != nil { + return nil, err + } + + if err := writeResourceData(cloudAccount, d); err != nil { + return nil, fmt.Errorf("schema resource data serialization failed: %w", err) + } + + if err := writeResourceData(cloudAccount.Configuration, d); err != nil { + return nil, fmt.Errorf("schema resource data serialization failed: %w", err) + } + + return []*schema.ResourceData{d}, nil +} diff --git a/env0/errors.go b/env0/errors.go index 0df19189..07bea238 100644 --- a/env0/errors.go +++ b/env0/errors.go @@ -2,6 +2,7 @@ package env0 import ( "context" + "errors" "github.com/env0/terraform-provider-env0/client" "github.com/env0/terraform-provider-env0/client/http" @@ -11,15 +12,14 @@ import ( ) func driftDetected(err error) bool { - if frerr, ok := err.(*http.FailedResponseError); ok && frerr.NotFound() { + var failedResponseError *http.FailedResponseError + if errors.As(err, &failedResponseError) && failedResponseError.NotFound() { return true } - if _, ok := err.(*client.NotFoundError); ok { - return true - } + var notfoundError *client.NotFoundError - return false + return errors.As(err, ¬foundError) } func ResourceGetFailure(ctx context.Context, resourceName string, d *schema.ResourceData, err error) diag.Diagnostics { diff --git a/env0/provider.go b/env0/provider.go index f0a68e89..7568a0cd 100644 --- a/env0/provider.go +++ b/env0/provider.go @@ -160,6 +160,7 @@ func Provider(version string) plugin.ProviderFunc { "env0_variable_set": resourceVariableSet(), "env0_variable_set_assignment": resourceVariableSetAssignment(), "env0_environment_output_configuration_variable": resourceEnvironmentOutputConfigurationVariable(), + "env0_aws_cloud_configuration": resourceAwsCloudConfiguration(), }, } diff --git a/env0/resource_aws_cloud_configuration.go b/env0/resource_aws_cloud_configuration.go new file mode 100644 index 00000000..5f871c67 --- /dev/null +++ b/env0/resource_aws_cloud_configuration.go @@ -0,0 +1,66 @@ +package env0 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceAwsCloudConfiguration() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceAwsCloudConfigurationCreate, + UpdateContext: resourceAwsCloudConfigurationUpdate, + ReadContext: readCloudConfiguration, + DeleteContext: deleteCloudConfiguration, + + Importer: &schema.ResourceImporter{StateContext: importCloudConfiguration}, + + Description: "configure an AWS cloud account (Cloud Compass)", + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Description: "name for the cloud configuration for insights", + Required: true, + }, + "account_id": { + Type: schema.TypeString, + Description: "the AWS account id", + Required: true, + }, + "bucket_name": { + Type: schema.TypeString, + Description: "the CloudTrail bucket name", + Required: true, + }, + "regions": { + Type: schema.TypeList, + Description: "a list of AWS regions", + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "prefix": { + Type: schema.TypeString, + Optional: true, + Default: "", + Description: "an optional bucket prefix (folder)", + }, + "health": { + Type: schema.TypeBool, + Computed: true, + Description: "an indicator if the configuration is valid", + }, + }, + } +} + +func resourceAwsCloudConfigurationCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return createCloudConfiguration(d, meta, "AWS") +} + +func resourceAwsCloudConfigurationUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return updateCloudConfiguration(d, meta, "AWS") +} diff --git a/env0/resource_aws_cloud_configuration_test.go b/env0/resource_aws_cloud_configuration_test.go new file mode 100644 index 00000000..a3e5c6a3 --- /dev/null +++ b/env0/resource_aws_cloud_configuration_test.go @@ -0,0 +1,280 @@ +package env0 + +import ( + "errors" + "fmt" + "regexp" + "testing" + + "github.com/env0/terraform-provider-env0/client" + "github.com/env0/terraform-provider-env0/client/http" + "github.com/google/uuid" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "go.uber.org/mock/gomock" +) + +func TestUnitAwsCloudConfigurationResource(t *testing.T) { + resourceType := "env0_aws_cloud_configuration" + resourceName := "test" + resourceNameImport := resourceType + "." + resourceName + accessor := resourceAccessor(resourceType, resourceName) + + awsConfig := client.AWSCloudAccountConfiguration{ + AccountId: "acc1", + BucketName: "b1", + Regions: []string{"us-west-1", "us-east-1000"}, + } + + updatedAwsConfig := client.AWSCloudAccountConfiguration{ + AccountId: "acc2", + BucketName: "b2", + Regions: []string{"us-west-12", "us-east-1040"}, + Prefix: "////", + } + + cloudConfig := client.CloudAccount{ + Id: uuid.NewString(), + Provider: "AWS", + Name: "name1", + Health: false, + Configuration: &awsConfig, + } + + updatedCloudConfig := cloudConfig + updatedCloudConfig.Name = "name2" + updatedCloudConfig.Configuration = &updatedAwsConfig + updatedCloudConfig.Health = true + + createPayload := client.CloudAccountCreatePayload{ + Name: cloudConfig.Name, + Provider: "AWS", + Configuration: &awsConfig, + } + + updatePayload := client.CloudAccountUpdatePayload{ + Name: updatedCloudConfig.Name, + Configuration: &updatedAwsConfig, + } + + otherCloudConfig := client.CloudAccount{ + Id: uuid.NewString(), + Provider: "AWS", + Name: "other_name", + } + + getFields := func(cloudConfig *client.CloudAccount) map[string]interface{} { + awsConfig := cloudConfig.Configuration.(*client.AWSCloudAccountConfiguration) + + fields := map[string]interface{}{ + "name": cloudConfig.Name, + "account_id": awsConfig.AccountId, + "bucket_name": awsConfig.BucketName, + "regions": awsConfig.Regions, + } + + if awsConfig.Prefix != "" { + fields["prefix"] = awsConfig.Prefix + } + + return fields + } + + t.Run("create and update", func(t *testing.T) { + runUnitTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, getFields(&cloudConfig)), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "name", cloudConfig.Name), + resource.TestCheckResourceAttr(accessor, "account_id", awsConfig.AccountId), + resource.TestCheckResourceAttr(accessor, "bucket_name", awsConfig.BucketName), + resource.TestCheckResourceAttr(accessor, "regions.0", awsConfig.Regions[0]), + resource.TestCheckResourceAttr(accessor, "regions.1", awsConfig.Regions[1]), + resource.TestCheckResourceAttr(accessor, "health", "false"), + ), + }, + { + Config: resourceConfigCreate(resourceType, resourceName, getFields(&updatedCloudConfig)), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "name", updatedCloudConfig.Name), + resource.TestCheckResourceAttr(accessor, "account_id", updatedAwsConfig.AccountId), + resource.TestCheckResourceAttr(accessor, "bucket_name", updatedAwsConfig.BucketName), + resource.TestCheckResourceAttr(accessor, "regions.0", updatedAwsConfig.Regions[0]), + resource.TestCheckResourceAttr(accessor, "regions.1", updatedAwsConfig.Regions[1]), + resource.TestCheckResourceAttr(accessor, "prefix", updatedAwsConfig.Prefix), + resource.TestCheckResourceAttr(accessor, "health", "true"), + ), + }, + }, + }, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().CloudAccountCreate(&createPayload).Times(1).Return(&cloudConfig, nil), + mock.EXPECT().CloudAccount(cloudConfig.Id).Times(2).Return(&cloudConfig, nil), + mock.EXPECT().CloudAccountUpdate(cloudConfig.Id, &updatePayload).Times(1).Return(&updatedCloudConfig, nil), + mock.EXPECT().CloudAccount(updatedCloudConfig.Id).Times(1).Return(&updatedCloudConfig, nil), + mock.EXPECT().CloudAccountDelete(updatedCloudConfig.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("import by id", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, getFields(&cloudConfig)), + }, + { + ResourceName: resourceNameImport, + ImportState: true, + ImportStateId: cloudConfig.Id, + ImportStateVerify: true, + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().CloudAccountCreate(&createPayload).Times(1).Return(&cloudConfig, nil), + mock.EXPECT().CloudAccount(cloudConfig.Id).Times(3).Return(&cloudConfig, nil), + mock.EXPECT().CloudAccountDelete(cloudConfig.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("import by name", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, getFields(&cloudConfig)), + }, + { + ResourceName: resourceNameImport, + ImportState: true, + ImportStateId: cloudConfig.Name, + ImportStateVerify: true, + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().CloudAccountCreate(&createPayload).Times(1).Return(&cloudConfig, nil), + mock.EXPECT().CloudAccount(cloudConfig.Id).Times(1).Return(&cloudConfig, nil), + mock.EXPECT().CloudAccounts().Times(1).Return([]client.CloudAccount{otherCloudConfig, cloudConfig}, nil), + mock.EXPECT().CloudAccount(cloudConfig.Id).Times(1).Return(&cloudConfig, nil), + mock.EXPECT().CloudAccountDelete(cloudConfig.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("import by name not found", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, getFields(&cloudConfig)), + }, + { + ResourceName: resourceNameImport, + ImportState: true, + ImportStateId: cloudConfig.Name, + ImportStateVerify: true, + ExpectError: regexp.MustCompile(fmt.Sprintf("cloud configuration called '%s' was not found", cloudConfig.Name)), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().CloudAccountCreate(&createPayload).Times(1).Return(&cloudConfig, nil), + mock.EXPECT().CloudAccount(cloudConfig.Id).Times(1).Return(&cloudConfig, nil), + mock.EXPECT().CloudAccounts().Times(1).Return([]client.CloudAccount{otherCloudConfig}, nil), + mock.EXPECT().CloudAccountDelete(cloudConfig.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("import by id not found", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, getFields(&cloudConfig)), + }, + { + ResourceName: resourceNameImport, + ImportState: true, + ImportStateId: cloudConfig.Id, + ImportStateVerify: true, + ExpectError: regexp.MustCompile("failed to get clound configuration: not found"), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().CloudAccountCreate(&createPayload).Times(1).Return(&cloudConfig, nil), + mock.EXPECT().CloudAccount(cloudConfig.Id).Times(1).Return(&cloudConfig, nil), + mock.EXPECT().CloudAccount(cloudConfig.Id).Times(1).Return(nil, &client.NotFoundError{}), + mock.EXPECT().CloudAccountDelete(cloudConfig.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("drift", func(t *testing.T) { + runUnitTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, getFields(&cloudConfig)), + }, + { + Config: resourceConfigCreate(resourceType, resourceName, getFields(&cloudConfig)), + }, + }, + }, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().CloudAccountCreate(&createPayload).Times(1).Return(&cloudConfig, nil), + mock.EXPECT().CloudAccount(cloudConfig.Id).Times(1).Return(&cloudConfig, nil), + mock.EXPECT().CloudAccount(cloudConfig.Id).Times(1).Return(nil, http.NewMockFailedResponseError(404)), + mock.EXPECT().CloudAccountCreate(&createPayload).Times(1).Return(&cloudConfig, nil), + mock.EXPECT().CloudAccount(cloudConfig.Id).Times(1).Return(&cloudConfig, nil), + mock.EXPECT().CloudAccountDelete(cloudConfig.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("create failed", func(t *testing.T) { + runUnitTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, getFields(&cloudConfig)), + ExpectError: regexp.MustCompile("failed to create a cloud configuration: error"), + }, + }, + }, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().CloudAccountCreate(&createPayload).Times(1).Return(nil, errors.New("error")), + ) + }) + }) + + t.Run("update failed", func(t *testing.T) { + runUnitTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, getFields(&cloudConfig)), + }, + { + Config: resourceConfigCreate(resourceType, resourceName, getFields(&updatedCloudConfig)), + ExpectError: regexp.MustCompile("failed to update cloud configuration: error"), + }, + }, + }, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().CloudAccountCreate(&createPayload).Times(1).Return(&cloudConfig, nil), + mock.EXPECT().CloudAccount(cloudConfig.Id).Times(2).Return(&cloudConfig, nil), + mock.EXPECT().CloudAccountUpdate(cloudConfig.Id, &updatePayload).Times(1).Return(nil, errors.New("error")), + mock.EXPECT().CloudAccountDelete(updatedCloudConfig.Id).Times(1).Return(nil), + ) + }) + }) +} diff --git a/examples/resources/env0_aws_cloud_configuration/import.sh b/examples/resources/env0_aws_cloud_configuration/import.sh new file mode 100644 index 00000000..3f5973ef --- /dev/null +++ b/examples/resources/env0_aws_cloud_configuration/import.sh @@ -0,0 +1,2 @@ +terraform import env0_aws_cloud_configuration.by_id d31a6b30-5f69-4d24-937c-22322754934e +terraform import env0_aws_cloud_configuration.by_name "cloud configuration name" diff --git a/examples/resources/env0_aws_cloud_configuration/resource.tf b/examples/resources/env0_aws_cloud_configuration/resource.tf new file mode 100644 index 00000000..579c4200 --- /dev/null +++ b/examples/resources/env0_aws_cloud_configuration/resource.tf @@ -0,0 +1,6 @@ +resource "env0_aws_cloud_configuration" "example" { + name = "example" + account_id = "242345678901" + bucket_name = "a_bucket_name" + regions = ["us-east-1", "us-west-2"] +} diff --git a/tests/integration/035_aws_cloud_configuration/conf.tf b/tests/integration/035_aws_cloud_configuration/conf.tf new file mode 100644 index 00000000..8d6d2954 --- /dev/null +++ b/tests/integration/035_aws_cloud_configuration/conf.tf @@ -0,0 +1,15 @@ +terraform { + backend "local" { + } + required_providers { + env0 = { + source = "terraform-registry.env0.com/env0/env0" + } + } +} + +provider "env0" {} + +variable "second_run" { + default = false +} diff --git a/tests/integration/035_aws_cloud_configuration/expected_outputs.json b/tests/integration/035_aws_cloud_configuration/expected_outputs.json new file mode 100644 index 00000000..2c63c085 --- /dev/null +++ b/tests/integration/035_aws_cloud_configuration/expected_outputs.json @@ -0,0 +1,2 @@ +{ +} diff --git a/tests/integration/035_aws_cloud_configuration/main.tf b/tests/integration/035_aws_cloud_configuration/main.tf new file mode 100644 index 00000000..a7012883 --- /dev/null +++ b/tests/integration/035_aws_cloud_configuration/main.tf @@ -0,0 +1,14 @@ +provider "random" {} + +resource "random_string" "random" { + length = 8 + special = false + min_lower = 8 +} + +resource "env0_aws_cloud_configuration" "aws_cloud_configuration" { + name = "aws-${random_string.random.result}" + account_id = var.second_run ? "012345678901" : "242345678901" + bucket_name = var.second_run ? "my_bucket_name2" : "my_bucket_name1" + regions = var.second_run ? ["us-west-2"] : ["us-east-1", "us-west-2"] +}