From fcf27bc2817096e8f1d6ca7725fc2ccc900d3464 Mon Sep 17 00:00:00 2001 From: Tomer Heber Date: Tue, 26 Dec 2023 08:08:14 -0500 Subject: [PATCH] Feat: add new OIDC credentials creation and assigment (Azure data source) (#773) --- env0/data_aws_oidc_credentials.go | 66 ---------- env0/data_aws_oidc_credentials_test.go | 5 - env0/data_azure_oidc_credentials_test.go | 119 ++++++++++++++++++ env0/data_oidc_credentials.go | 55 ++++++++ env0/data_organization.go | 10 ++ env0/data_organization_test.go | 4 + env0/provider.go | 3 +- tests/integration/006_aws_credentials/main.tf | 5 + .../integration/016_azure_credentials/main.tf | 5 + 9 files changed, 200 insertions(+), 72 deletions(-) delete mode 100644 env0/data_aws_oidc_credentials.go create mode 100644 env0/data_azure_oidc_credentials_test.go create mode 100644 env0/data_oidc_credentials.go diff --git a/env0/data_aws_oidc_credentials.go b/env0/data_aws_oidc_credentials.go deleted file mode 100644 index 5eab8ab1..00000000 --- a/env0/data_aws_oidc_credentials.go +++ /dev/null @@ -1,66 +0,0 @@ -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 dataAwsOidcCredentials() *schema.Resource { - return &schema.Resource{ - ReadContext: dataAwsOidcCredentialRead, - - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Description: "the name of the aws oidc credentials", - Optional: true, - ExactlyOneOf: []string{"name", "id"}, - }, - "id": { - Type: schema.TypeString, - Description: "the id of the aws oidc credentials", - Optional: true, - ExactlyOneOf: []string{"name", "id"}, - }, - "oidc_sub": { - Type: schema.TypeString, - Computed: true, - Description: "the jwt oidc sub", - }, - }, - } -} - -func dataAwsOidcCredentialRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - var credentials client.Credentials - var err error - - id, ok := d.GetOk("id") - if ok { - credentials, err = getCredentialsById(id.(string), credentialsTypeToPrefixList[AWS_OIDC_TYPE], meta) - } else { - credentials, err = getCredentialsByName(d.Get("name").(string), credentialsTypeToPrefixList[AWS_OIDC_TYPE], meta) - } - - if err != nil { - return DataGetFailure("aws oidc credentials", id, err) - } - - if err := writeResourceData(&credentials, d); err != nil { - return diag.Errorf("schema resource data serialization failed: %v", err) - } - - apiClient := meta.(client.ApiClientInterface) - - oidcSub, err := apiClient.OidcSub() - if err != nil { - return diag.Errorf("failed to get oidc sub: %v", err) - } - - d.Set("oidc_sub", oidcSub) - - return nil -} diff --git a/env0/data_aws_oidc_credentials_test.go b/env0/data_aws_oidc_credentials_test.go index 03222e0e..40f18c13 100644 --- a/env0/data_aws_oidc_credentials_test.go +++ b/env0/data_aws_oidc_credentials_test.go @@ -29,8 +29,6 @@ func TestAwsOidcCredentialDataSource(t *testing.T) { Type: string(client.AwsAssumedRoleCredentialsType), } - oidcSub := "oidc sub 123345 !!!" - byName := map[string]interface{}{"name": credentials.Name} byId := map[string]interface{}{"id": credentials.Id} @@ -46,7 +44,6 @@ func TestAwsOidcCredentialDataSource(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(accessor, "id", credentials.Id), resource.TestCheckResourceAttr(accessor, "name", credentials.Name), - resource.TestCheckResourceAttr(accessor, "oidc_sub", oidcSub), ), }, }, @@ -67,14 +64,12 @@ func TestAwsOidcCredentialDataSource(t *testing.T) { mockGetCredentials := func(returnValue client.Credentials) func(mockFunc *client.MockApiClientInterface) { return func(mock *client.MockApiClientInterface) { mock.EXPECT().CloudCredentials(credentials.Id).AnyTimes().Return(returnValue, nil) - mock.EXPECT().OidcSub().AnyTimes().Return(oidcSub, nil) } } mockListCredentials := func(returnValue []client.Credentials) func(mockFunc *client.MockApiClientInterface) { return func(mock *client.MockApiClientInterface) { mock.EXPECT().CloudCredentialsList().AnyTimes().Return(returnValue, nil) - mock.EXPECT().OidcSub().AnyTimes().Return(oidcSub, nil) } } diff --git a/env0/data_azure_oidc_credentials_test.go b/env0/data_azure_oidc_credentials_test.go new file mode 100644 index 00000000..3d3165ef --- /dev/null +++ b/env0/data_azure_oidc_credentials_test.go @@ -0,0 +1,119 @@ +package env0 + +import ( + "fmt" + "regexp" + "testing" + + "github.com/env0/terraform-provider-env0/client" + "github.com/env0/terraform-provider-env0/client/http" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAzureOidcCredentialDataSource(t *testing.T) { + credentials := client.Credentials{ + Id: "id0", + Name: "name0", + Type: string(client.AzureOidcCredentialsType), + } + + credentialsOther1 := client.Credentials{ + Id: "id1", + Name: "name1", + Type: string(client.AzureOidcCredentialsType), + } + + credentialsOther2 := client.Credentials{ + Id: "id2", + Name: "name2", + Type: string(client.AzureServicePrincipalCredentialsType), + } + + byName := map[string]interface{}{"name": credentials.Name} + byId := map[string]interface{}{"id": credentials.Id} + + resourceType := "env0_azure_oidc_credentials" + resourceName := "test_azure_oidc_credentials" + 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", credentials.Id), + resource.TestCheckResourceAttr(accessor, "name", credentials.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), + }, + }, + } + } + + mockGetCredentials := func(returnValue client.Credentials) func(mockFunc *client.MockApiClientInterface) { + return func(mock *client.MockApiClientInterface) { + mock.EXPECT().CloudCredentials(credentials.Id).AnyTimes().Return(returnValue, nil) + } + } + + mockListCredentials := 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(byId), + mockGetCredentials(credentials), + ) + }) + + t.Run("by name", func(t *testing.T) { + runUnitTest(t, + getValidTestCase(byName), + mockListCredentials([]client.Credentials{credentials, credentialsOther1, credentialsOther2}), + ) + }) + + 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 is returned", func(t *testing.T) { + runUnitTest(t, + getErrorTestCase(byName, "found multiple credentials"), + mockListCredentials([]client.Credentials{credentials, credentialsOther1, credentialsOther2, credentials}), + ) + }) + + t.Run("Throw error when by name and not found", func(t *testing.T) { + runUnitTest(t, + getErrorTestCase(byName, "not found"), + mockListCredentials([]client.Credentials{credentialsOther1, credentialsOther2}), + ) + }) + + t.Run("Throw error when by id and not found", func(t *testing.T) { + runUnitTest(t, + getErrorTestCase(byId, fmt.Sprintf("id %s not found", credentials.Id)), + func(mock *client.MockApiClientInterface) { + mock.EXPECT().CloudCredentials(credentials.Id).AnyTimes().Return(client.Credentials{}, http.NewMockFailedResponseError(404)) + }, + ) + }) +} diff --git a/env0/data_oidc_credentials.go b/env0/data_oidc_credentials.go new file mode 100644 index 00000000..240e5266 --- /dev/null +++ b/env0/data_oidc_credentials.go @@ -0,0 +1,55 @@ +package env0 + +import ( + "context" + "fmt" + + "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 dataOidcCredentials(credentialsType CloudType) *schema.Resource { + return &schema.Resource{ + ReadContext: dataOidcCredentialRead(credentialsType), + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Description: fmt.Sprintf("the name of the %s oidc credentials", credentialsType), + Optional: true, + ExactlyOneOf: []string{"name", "id"}, + }, + "id": { + Type: schema.TypeString, + Description: fmt.Sprintf("the id of the %s oidc credentials", credentialsType), + Optional: true, + ExactlyOneOf: []string{"name", "id"}, + }, + }, + } +} + +func dataOidcCredentialRead(credentialsType CloudType) func(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return func(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var credentials client.Credentials + var err error + + id, ok := d.GetOk("id") + if ok { + credentials, err = getCredentialsById(id.(string), credentialsTypeToPrefixList[credentialsType], meta) + } else { + credentials, err = getCredentialsByName(d.Get("name").(string), credentialsTypeToPrefixList[credentialsType], meta) + } + + if err != nil { + return DataGetFailure(fmt.Sprintf("%s oidc credentials", credentialsType), id, err) + } + + if err := writeResourceData(&credentials, d); err != nil { + return diag.Errorf("schema resource data serialization failed: %v", err) + } + + return nil + } +} diff --git a/env0/data_organization.go b/env0/data_organization.go index e3a6fb48..9ea9ebc8 100644 --- a/env0/data_organization.go +++ b/env0/data_organization.go @@ -33,6 +33,11 @@ func dataOrganization() *schema.Resource { Description: "is the organization self hosted", Computed: true, }, + "oidc_sub": { + Type: schema.TypeString, + Computed: true, + Description: "the jwt oidc sub", + }, }, } } @@ -49,5 +54,10 @@ func dataOrganizationRead(ctx context.Context, d *schema.ResourceData, meta inte return diag.Errorf("schema resource data serialization failed: %v", err) } + oidcSub, err := apiClient.OidcSub() + if err == nil { + d.Set("oidc_sub", oidcSub) + } + return nil } diff --git a/env0/data_organization_test.go b/env0/data_organization_test.go index 88a79f43..84320f72 100644 --- a/env0/data_organization_test.go +++ b/env0/data_organization_test.go @@ -20,6 +20,8 @@ func TestUnitOrganizationData(t *testing.T) { IsSelfHostedK8s: true, } + oidcSub := "sub" + testCase := resource.TestCase{ Steps: []resource.TestStep{ { @@ -30,6 +32,7 @@ func TestUnitOrganizationData(t *testing.T) { resource.TestCheckResourceAttr(accessor, "created_by", organization.CreatedBy), resource.TestCheckResourceAttr(accessor, "role", organization.Role), resource.TestCheckResourceAttr(accessor, "is_self_hosted", strconv.FormatBool(organization.IsSelfHostedK8s)), + resource.TestCheckResourceAttr(accessor, "oidc_sub", oidcSub), ), }, }, @@ -37,5 +40,6 @@ func TestUnitOrganizationData(t *testing.T) { runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { mock.EXPECT().Organization().AnyTimes().Return(organization, nil) + mock.EXPECT().OidcSub().AnyTimes().Return(oidcSub, nil) }) } diff --git a/env0/provider.go b/env0/provider.go index 39fe5c36..076f8fe7 100644 --- a/env0/provider.go +++ b/env0/provider.go @@ -69,9 +69,10 @@ func Provider(version string) plugin.ProviderFunc { "env0_azure_cost_credentials": dataCredentials(AZURE_COST_TYPE), "env0_google_cost_credentials": dataCredentials(GCP_COST_TYPE), "env0_aws_credentials": dataCredentials(AWS_TYPE), - "env0_aws_oidc_credentials": dataAwsOidcCredentials(), + "env0_aws_oidc_credentials": dataOidcCredentials(AWS_OIDC_TYPE), "env0_gcp_credentials": dataCredentials(GCP_TYPE), "env0_azure_credentials": dataCredentials(AZURE_TYPE), + "env0_azure_oidc_credentials": dataOidcCredentials(AZURE_OIDC_TYPE), "env0_team": dataTeam(), "env0_teams": dataTeams(), "env0_environment": dataEnvironment(), diff --git a/tests/integration/006_aws_credentials/main.tf b/tests/integration/006_aws_credentials/main.tf index 8731cf69..6f7e8a1c 100644 --- a/tests/integration/006_aws_credentials/main.tf +++ b/tests/integration/006_aws_credentials/main.tf @@ -32,6 +32,11 @@ resource "env0_aws_oidc_credentials" "oidc_credentials" { duration = 7200 } +data "env0_aws_oidc_credentials" "oidc_credentials" { + name = "Test Oidc Credentials ${random_string.random.result}" + depends_on = [env0_aws_oidc_credentials.oidc_credentials] +} + output "name_by_arn" { value = replace(data.env0_aws_credentials.my_role_by_arn.name, random_string.random.result, "") } diff --git a/tests/integration/016_azure_credentials/main.tf b/tests/integration/016_azure_credentials/main.tf index 2076de1f..f3495704 100644 --- a/tests/integration/016_azure_credentials/main.tf +++ b/tests/integration/016_azure_credentials/main.tf @@ -19,6 +19,11 @@ resource "env0_azure_oidc_credentials" "oidc_credentials" { tenant_id = "tenant_id" } +data "env0_azure_oidc_credentials" "oidc_credentials" { + name = "test azure oidc credentials ${random_string.random.result}" + depends_on = [env0_azure_oidc_credentials.oidc_credentials] +} + data "env0_azure_credentials" "azure_cred" { name = env0_azure_credentials.azure_cred.name }