diff --git a/client/cloud_credentials.go b/client/cloud_credentials.go index 14b186db..542bf1b7 100644 --- a/client/cloud_credentials.go +++ b/client/cloud_credentials.go @@ -47,10 +47,10 @@ type GoogleCostCredentialsCreatePayload struct { Name string `json:"name"` OrganizationId string `json:"organizationId"` Type GcpCredentialsType `json:"type"` - Value GoogleCostCredentialsValeuPayload `json:"value"` + Value GoogleCostCredentialsValuePayload `json:"value"` } -type GoogleCostCredentialsValeuPayload struct { +type GoogleCostCredentialsValuePayload struct { TableId string `json:"tableid"` Secret string `json:"secret"` } @@ -68,7 +68,7 @@ type GcpCredentialsValuePayload struct { } const ( - GoogleCostCredentiassType GcpCredentialsType = "GCP_CREDENTIALS" + GoogleCostCredentialsType GcpCredentialsType = "GCP_CREDENTIALS" AzureCostCredentialsType AzureCredentialsType = "AZURE_CREDENTIALS" AwsCostCredentialsType AwsCredentialsType = "AWS_ASSUMED_ROLE" AwsAssumedRoleCredentialsType AwsCredentialsType = "AWS_ASSUMED_ROLE_FOR_DEPLOYMENT" diff --git a/client/cloud_credentials_test.go b/client/cloud_credentials_test.go index 680950da..fcd26da4 100644 --- a/client/cloud_credentials_test.go +++ b/client/cloud_credentials_test.go @@ -38,7 +38,7 @@ var _ = Describe("CloudCredentials", func() { BeforeEach(func() { mockOrganizationIdCall(organizationId) - payloadValue := GoogleCostCredentialsValeuPayload{ + payloadValue := GoogleCostCredentialsValuePayload{ TableId: "table", Secret: "secret", } diff --git a/env0/data_cost_credentials.go b/env0/data_cost_credentials.go new file mode 100644 index 00000000..a3b688bd --- /dev/null +++ b/env0/data_cost_credentials.go @@ -0,0 +1,96 @@ +package env0 + +import ( + "context" + + "github.com/env0/terraform-provider-env0/client" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataCostCredentials(credType string) *schema.Resource { + return &schema.Resource{ + ReadContext: dataCostCredentialsRead(credType), + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Description: "the name of the credential", + Optional: true, + ExactlyOneOf: []string{"name", "id"}, + }, + "id": { + Type: schema.TypeString, + Description: "the id of the credential", + Optional: true, + ExactlyOneOf: []string{"name", "id"}, + }, + }, + } +} + +func dataCostCredentialsRead(credType string) func(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return func(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var err diag.Diagnostics + var credentials *client.Credentials + + id, ok := d.GetOk("id") + if ok { + credentials, err = getCostCredentialsById(id.(string), credType, meta) + if err != nil { + return err + } + } else { + name, ok := d.GetOk("name") + if !ok { + return diag.Errorf("Either 'name' or 'id' must be specified") + } + credentials, err = getCostCredentialsByName(name.(string), credType, meta) + if err != nil { + return err + } + } + + errorWhenWriteData := writeResourceData(credentials, d) + if errorWhenWriteData != nil { + return diag.Errorf("Error: %v", errorWhenWriteData) + } + + return nil + } +} + +func getCostCredentialsByName(name interface{}, credType string, meta interface{}) (*client.Credentials, diag.Diagnostics) { + apiClient := meta.(client.ApiClientInterface) + credentialsList, err := apiClient.CloudCredentialsList() + if err != nil { + return &client.Credentials{}, diag.Errorf("Could not query Cost Credentials by name: %v", err) + } + + credentialsByNameAndType := make([]client.Credentials, 0) + for _, candidate := range credentialsList { + if candidate.Name == name.(string) && candidate.Type == credType { + credentialsByNameAndType = append(credentialsByNameAndType, candidate) + } + } + + if len(credentialsByNameAndType) > 1 { + return &client.Credentials{}, diag.Errorf("Found multiple Cost Credentials for name: %s", name) + } + if len(credentialsByNameAndType) == 0 { + return &client.Credentials{}, diag.Errorf("Could not find Cost Credentials with name: %s", name) + } + return &credentialsByNameAndType[0], nil +} + +func getCostCredentialsById(id string, credType string, meta interface{}) (*client.Credentials, diag.Diagnostics) { + apiClient := meta.(client.ApiClientInterface) + credentials, err := apiClient.CloudCredentials(id) + if credentials.Type != credType { + return &client.Credentials{}, diag.Errorf("Found credentials which are not Cost Credentials: %v", credentials) + } + if err != nil { + return &client.Credentials{}, diag.Errorf("Could not query Cost Credentials: %v", err) + } + return &credentials, nil +} diff --git a/env0/data_cost_credentials_test.go b/env0/data_cost_credentials_test.go new file mode 100644 index 00000000..ba5046fc --- /dev/null +++ b/env0/data_cost_credentials_test.go @@ -0,0 +1,304 @@ +package env0 + +import ( + "regexp" + "testing" + + "github.com/env0/terraform-provider-env0/client" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestUnitAzureCostCredentialsDataSource(t *testing.T) { + azureCred := client.Credentials{ + Id: "11111", + Name: "testdata", + OrganizationId: "id", + Type: "AZURE_CREDENTIALS", + } + + credWithInvalidType := client.Credentials{ + Id: azureCred.Id, + Name: azureCred.Name, + OrganizationId: azureCred.OrganizationId, + Type: "Invalid-type", + } + + credWithDiffName := client.Credentials{ + Id: "22222", + Name: "diff name", + OrganizationId: azureCred.OrganizationId, + Type: "AZURE_CREDENTIALS", + } + + AzureCredFieldsByName := map[string]interface{}{"name": azureCred.Name} + AzureCredFieldsById := map[string]interface{}{"id": azureCred.Id} + + resourceType := "env0_azure_cost_credentials" + resourceName := "testdata" + accessor := dataSourceAccessor(resourceType, resourceName) + + getValidTestCase := func(input map[string]interface{}) resource.TestCase { + return resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: dataSourceConfigCreate(resourceType, resourceName, input), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", azureCred.Id), + resource.TestCheckResourceAttr(accessor, "name", azureCred.Name), + ), + }, + }, + } + } + + getErrorTestCase := func(input map[string]interface{}, expectedError string) resource.TestCase { + return resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: dataSourceConfigCreate(resourceType, resourceName, input), + ExpectError: regexp.MustCompile(expectedError), + }, + }, + } + } + + mockGetAzureCredCall := func(returnValue client.Credentials) func(mockFunc *client.MockApiClientInterface) { + return func(mock *client.MockApiClientInterface) { + mock.EXPECT().CloudCredentials(azureCred.Id).AnyTimes().Return(returnValue, nil) + } + } + + mockListAzureCredCall := func(returnValue []client.Credentials) func(mockFunc *client.MockApiClientInterface) { + return func(mock *client.MockApiClientInterface) { + mock.EXPECT().CloudCredentialsList().AnyTimes().Return(returnValue, nil) + } + } + + t.Run("By ID", func(t *testing.T) { + runUnitTest(t, + getValidTestCase(AzureCredFieldsById), + mockGetAzureCredCall(azureCred), + ) + }) + + t.Run("By Name", func(t *testing.T) { + runUnitTest(t, + getValidTestCase(AzureCredFieldsByName), + mockListAzureCredCall([]client.Credentials{azureCred, credWithInvalidType}), + ) + }) + + t.Run("Throw error when no name or id is supplied", func(t *testing.T) { + runUnitTest(t, + getErrorTestCase(map[string]interface{}{}, "one of `id,name` must be specified"), + func(mock *client.MockApiClientInterface) {}, + ) + }) + + t.Run("Throw error when by name and more than one azure-credential exists with the relevant name", func(t *testing.T) { + runUnitTest(t, + getErrorTestCase(AzureCredFieldsByName, "Error: Found multiple Cost Credentials for name: testdata"), + mockListAzureCredCall([]client.Credentials{azureCred, azureCred, azureCred}), + ) + }) + + t.Run("Throw error when by name and no azure-credential exists with the relevant name", func(t *testing.T) { + runUnitTest(t, + getErrorTestCase(AzureCredFieldsByName, "Error: Could not find Cost Credentials with name: testdata"), + mockListAzureCredCall([]client.Credentials{credWithDiffName, credWithDiffName}), + ) + }) + +} + +func TestUnitGoogleCostCredentialsDataSource(t *testing.T) { + gcpCred := client.Credentials{ + Id: "11111", + Name: "testdata", + OrganizationId: "id", + Type: "GCP_CREDENTIALS", + } + + credWithInvalidType := client.Credentials{ + Id: gcpCred.Id, + Name: gcpCred.Name, + OrganizationId: gcpCred.OrganizationId, + Type: "Invalid-type", + } + + GcpCredFieldsByName := map[string]interface{}{"name": gcpCred.Name} + GcpCredFieldsById := map[string]interface{}{"id": gcpCred.Id} + + resourceType := "env0_google_cost_credentials" + resourceName := "testdata" + accessor := dataSourceAccessor(resourceType, resourceName) + + getValidTestCase := func(input map[string]interface{}) resource.TestCase { + return resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: dataSourceConfigCreate(resourceType, resourceName, input), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", gcpCred.Id), + resource.TestCheckResourceAttr(accessor, "name", gcpCred.Name), + ), + }, + }, + } + } + + getErrorTestCase := func(input map[string]interface{}, expectedError string) resource.TestCase { + return resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: dataSourceConfigCreate(resourceType, resourceName, input), + ExpectError: regexp.MustCompile(expectedError), + }, + }, + } + } + + mockGetGcpCredCall := func(returnValue client.Credentials) func(mockFunc *client.MockApiClientInterface) { + return func(mock *client.MockApiClientInterface) { + mock.EXPECT().CloudCredentials(gcpCred.Id).AnyTimes().Return(returnValue, nil) + } + } + + mockListGcpCredCall := func(returnValue []client.Credentials) func(mockFunc *client.MockApiClientInterface) { + return func(mock *client.MockApiClientInterface) { + mock.EXPECT().CloudCredentialsList().AnyTimes().Return(returnValue, nil) + } + } + + t.Run("By ID", func(t *testing.T) { + runUnitTest(t, + getValidTestCase(GcpCredFieldsById), + mockGetGcpCredCall(gcpCred), + ) + }) + + t.Run("By Name", func(t *testing.T) { + runUnitTest(t, + getValidTestCase(GcpCredFieldsByName), + mockListGcpCredCall([]client.Credentials{gcpCred, credWithInvalidType}), + ) + }) + + t.Run("Throw error when no name or id is supplied", func(t *testing.T) { + runUnitTest(t, + getErrorTestCase(map[string]interface{}{}, "one of `id,name` must be specified"), + func(mock *client.MockApiClientInterface) {}, + ) + }) + + t.Run("Throw error when by name and more than one gcp-credential exists with the relevant name", func(t *testing.T) { + runUnitTest(t, + getErrorTestCase(GcpCredFieldsByName, "Error: Found multiple Cost Credentials for name: testdata"), + mockListGcpCredCall([]client.Credentials{gcpCred, gcpCred, gcpCred}), + ) + }) + +} + +func TestUnitAwsCostCredentialsData(t *testing.T) { + awsCred := client.Credentials{ + Id: "11111", + Name: "testdata", + OrganizationId: "id", + Type: "AWS_ASSUMED_ROLE", + } + + credWithInvalidType := client.Credentials{ + Id: awsCred.Id, + Name: awsCred.Name, + OrganizationId: awsCred.OrganizationId, + Type: "Invalid-type", + } + + otherAwsCred := client.Credentials{ + Id: "22222", + Name: "notTestdata", + OrganizationId: "OtherId", + Type: "AWS_ACCESS_KEYS_FOR_DEPLOYMENT", + } + + AwsCredFieldsByName := map[string]interface{}{"name": awsCred.Name} + AwsCredFieldsById := map[string]interface{}{"id": awsCred.Id} + + resourceType := "env0_aws_cost_credentials" + resourceName := "testdata" + accessor := dataSourceAccessor(resourceType, resourceName) + + getValidTestCase := func(input map[string]interface{}) resource.TestCase { + return resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: dataSourceConfigCreate(resourceType, resourceName, input), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", awsCred.Id), + resource.TestCheckResourceAttr(accessor, "name", awsCred.Name), + ), + }, + }, + } + } + + getErrorTestCase := func(input map[string]interface{}, expectedError string) resource.TestCase { + return resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: dataSourceConfigCreate(resourceType, resourceName, input), + ExpectError: regexp.MustCompile(expectedError), + }, + }, + } + } + + mockGetAwsCredCall := func(returnValue client.Credentials) func(mockFunc *client.MockApiClientInterface) { + return func(mock *client.MockApiClientInterface) { + mock.EXPECT().CloudCredentials(awsCred.Id).AnyTimes().Return(returnValue, nil) + } + } + + mockListAwsCredCall := func(returnValue []client.Credentials) func(mockFunc *client.MockApiClientInterface) { + return func(mock *client.MockApiClientInterface) { + mock.EXPECT().CloudCredentialsList().AnyTimes().Return(returnValue, nil) + } + } + + t.Run("By ID", func(t *testing.T) { + runUnitTest(t, + getValidTestCase(AwsCredFieldsById), + mockGetAwsCredCall(awsCred), + ) + }) + + t.Run("By Name", func(t *testing.T) { + runUnitTest(t, + getValidTestCase(AwsCredFieldsByName), + mockListAwsCredCall([]client.Credentials{awsCred, otherAwsCred, credWithInvalidType}), + ) + }) + + t.Run("Throw error when no name or id is supplied", func(t *testing.T) { + runUnitTest(t, + getErrorTestCase(map[string]interface{}{}, "one of `id,name` must be specified"), + func(mock *client.MockApiClientInterface) {}, + ) + }) + + t.Run("Throw error when by name and more than one aws-credential exists with the relevant name", func(t *testing.T) { + runUnitTest(t, + getErrorTestCase(AwsCredFieldsByName, "Found multiple Cost Credentials for name: testdata"), + mockListAwsCredCall([]client.Credentials{awsCred, awsCred, awsCred}), + ) + }) + + t.Run("Throw error when by name and no aws-credential found with that name", func(t *testing.T) { + runUnitTest(t, + getErrorTestCase(AwsCredFieldsByName, "Could not find Cost Credentials with name: testdata"), + mockListAwsCredCall([]client.Credentials{otherAwsCred, credWithInvalidType}), + ) + }) + +} diff --git a/env0/provider.go b/env0/provider.go index 48bb1041..1c3cfdd1 100644 --- a/env0/provider.go +++ b/env0/provider.go @@ -48,23 +48,26 @@ func Provider(version string) plugin.ProviderFunc { }, }, DataSourcesMap: map[string]*schema.Resource{ - "env0_organization": dataOrganization(), - "env0_project": dataProject(), - "env0_project_policy": dataPolicy(), - "env0_configuration_variable": dataConfigurationVariable(), - "env0_template": dataTemplate(), - "env0_ssh_key": dataSshKey(), - "env0_aws_credentials": dataAwsCredentials(), - "env0_gcp_credentials": dataGcpCredentials(), - "env0_azure_credentials": dataAzureCredentials(), - "env0_team": dataTeam(), - "env0_environment": dataEnvironment(), - "env0_workflow_triggers": dataWorkflowTriggers(), - "env0_notification": dataNotification(), - "env0_module": dataModule(), - "env0_git_token": dataGitToken(), - "env0_api_key": dataApiKey(), - "env0_agents": dataAgents(), + "env0_organization": dataOrganization(), + "env0_project": dataProject(), + "env0_project_policy": dataPolicy(), + "env0_configuration_variable": dataConfigurationVariable(), + "env0_template": dataTemplate(), + "env0_ssh_key": dataSshKey(), + "env0_aws_cost_credentials": dataCostCredentials(string(client.AwsCostCredentialsType)), + "env0_azure_cost_credentials": dataCostCredentials(string(client.AzureCostCredentialsType)), + "env0_google_cost_credentials": dataCostCredentials(string(client.GoogleCostCredentialsType)), + "env0_aws_credentials": dataAwsCredentials(), + "env0_gcp_credentials": dataGcpCredentials(), + "env0_azure_credentials": dataAzureCredentials(), + "env0_team": dataTeam(), + "env0_environment": dataEnvironment(), + "env0_workflow_triggers": dataWorkflowTriggers(), + "env0_notification": dataNotification(), + "env0_module": dataModule(), + "env0_git_token": dataGitToken(), + "env0_api_key": dataApiKey(), + "env0_agents": dataAgents(), }, ResourcesMap: map[string]*schema.Resource{ "env0_project": resourceProject(),