Skip to content

Commit

Permalink
Feat: add new OIDC credentials creation and assigment (AWS Resource)
Browse files Browse the repository at this point in the history
  • Loading branch information
TomerHeber committed Nov 27, 2023
1 parent 7ef0e10 commit 5d28bb1
Show file tree
Hide file tree
Showing 9 changed files with 358 additions and 4 deletions.
9 changes: 5 additions & 4 deletions client/cloud_credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,17 @@ type AzureCredentialsValuePayload struct {
}

type AwsCredentialsCreatePayload struct {
Name string `json:"name"`
OrganizationId string `json:"organizationId"`
Name string `json:"name,omitempty"`
OrganizationId string `json:"organizationId,omitempty"`
Type AwsCredentialsType `json:"type"`
Value AwsCredentialsValuePayload `json:"value"`
}

type AwsCredentialsValuePayload struct {
RoleArn string `json:"roleArn" tfschema:"arn"`
Duration int `json:"duration,omitempty"`
AccessKeyId string `json:"accessKeyId"`
SecretAccessKey string `json:"secretAccessKey"`
AccessKeyId string `json:"accessKeyId,omitempty"`
SecretAccessKey string `json:"secretAccessKey,omitempty"`
}

type GoogleCostCredentialsCreatePayload struct {
Expand Down Expand Up @@ -101,6 +101,7 @@ const (
AwsCostCredentialsType AwsCredentialsType = "AWS_ASSUMED_ROLE"
AwsAssumedRoleCredentialsType AwsCredentialsType = "AWS_ASSUMED_ROLE_FOR_DEPLOYMENT"
AwsAccessKeysCredentialsType AwsCredentialsType = "AWS_ACCESS_KEYS_FOR_DEPLOYMENT"
AwsOidcCredentialsType AwsCredentialsType = "AWS_OIDC"
GcpServiceAccountCredentialsType GcpCredentialsType = "GCP_SERVICE_ACCOUNT_FOR_DEPLOYMENT"
AzureServicePrincipalCredentialsType AzureCredentialsType = "AZURE_SERVICE_PRINCIPAL_FOR_DEPLOYMENT"
)
Expand Down
2 changes: 2 additions & 0 deletions env0/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const (
GCP_COST_TYPE CloudType = "google_cost"
AZURE_COST_TYPE CloudType = "azure_cost"
AWS_COST_TYPE CloudType = "aws_cost"
AWS_OIDC_TYPE CloudType = "aws_oidc"
)

var credentialsTypeToPrefixList map[CloudType][]string = map[CloudType][]string{
Expand All @@ -30,6 +31,7 @@ var credentialsTypeToPrefixList map[CloudType][]string = map[CloudType][]string{
GCP_COST_TYPE: {string(client.GoogleCostCredentialsType)},
AZURE_COST_TYPE: {string(client.AzureCostCredentialsType)},
AWS_COST_TYPE: {string(client.AwsCostCredentialsType)},
AWS_OIDC_TYPE: {string(client.AwsOidcCredentialsType)},
}

func getCredentialsByName(name string, prefixList []string, meta interface{}) (client.Credentials, error) {
Expand Down
1 change: 1 addition & 0 deletions env0/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ func Provider(version string) plugin.ProviderFunc {
"env0_template": resourceTemplate(),
"env0_ssh_key": resourceSshKey(),
"env0_aws_credentials": resourceAwsCredentials(),
"env0_aws_oidc_credentials": resourceAwsOidcCredentials(),
"env0_aws_cost_credentials": resourceCostCredentials("aws"),
"env0_azure_cost_credentials": resourceCostCredentials("azure"),
"env0_gcp_cost_credentials": resourceCostCredentials("google"),
Expand Down
89 changes: 89 additions & 0 deletions env0/resource_aws_oidc_credentials.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
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 resourceAwsOidcCredentials() *schema.Resource {
return &schema.Resource{
CreateContext: resourceAwsOidcCredentialsCreate,
UpdateContext: resourceAwsOidcCredentialsUpdate,
ReadContext: resourceCredentialsRead(AWS_OIDC_TYPE),
DeleteContext: resourceCredentialsDelete,

Importer: &schema.ResourceImporter{StateContext: resourceCredentialsImport(AWS_OIDC_TYPE)},

Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Description: "name for the oidc credentials",
Required: true,
ForceNew: true,
},
"role_arn": {
Type: schema.TypeString,
Description: "the aws role arn",
Required: true,
},
"duration": {
Type: schema.TypeInt,
Description: "the session duration in seconds. If set must be one of the following: 3600 (1h), 7200 (2h), 14400 (4h), 18000 (5h default), 28800 (8h), 43200 (12h)",
Optional: true,
ValidateDiagFunc: NewIntInValidator([]int{3600, 7200, 14400, 18000, 28800, 43200}),
Default: 18000,
},
},
}
}

func resourceAwsOidcCredentialsCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
apiClient := meta.(client.ApiClientInterface)

value := client.AwsCredentialsValuePayload{}
if err := readResourceData(&value, d); err != nil {
return diag.Errorf("schema resource data deserialization failed: %v", err)
}

value.RoleArn = d.Get("role_arn").(string) // tfschema is set (for older resources) need to manually set the role arn.

request := client.AwsCredentialsCreatePayload{
Name: d.Get("name").(string),
Value: value,
Type: client.AwsOidcCredentialsType,
}

credentials, err := apiClient.CredentialsCreate(&request)
if err != nil {
return diag.Errorf("could not create aws oidc credentials: %v", err)
}

d.SetId(credentials.Id)

return nil
}

func resourceAwsOidcCredentialsUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
apiClient := meta.(client.ApiClientInterface)

value := client.AwsCredentialsValuePayload{}
if err := readResourceData(&value, d); err != nil {
return diag.Errorf("schema resource data deserialization failed: %v", err)
}

value.RoleArn = d.Get("role_arn").(string) // tfschema is set (for older resources) need to manually set the role arn.

request := client.AwsCredentialsCreatePayload{
Value: value,
Type: client.AwsOidcCredentialsType,
}

if _, err := apiClient.CredentialsUpdate(d.Id(), &request); err != nil {
return diag.Errorf("could not update aws oidc credentials: %s %v", d.Id(), err)
}

return nil
}
239 changes: 239 additions & 0 deletions env0/resource_aws_oidc_credentials_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
package env0

import (
"fmt"
"regexp"
"strconv"
"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"
"go.uber.org/mock/gomock"
)

func TestUnitAwsOidcCredentialsResource(t *testing.T) {
resourceType := "env0_aws_oidc_credentials"
resourceName := "test"
resourceNameImport := resourceType + "." + resourceName
accessor := resourceAccessor(resourceType, resourceName)

duration := 3600
updatedDuration := 2 * duration

awsCredentialsResource := map[string]interface{}{
"name": "test",
"role_arn": "11111",
"duration": strconv.Itoa(duration),
}

updatedAwsCredentialsResource := map[string]interface{}{
"name": "test",
"role_arn": "22222",
"duration": strconv.Itoa(updatedDuration),
}

createPayload := client.AwsCredentialsCreatePayload{
Name: awsCredentialsResource["name"].(string),
Value: client.AwsCredentialsValuePayload{
RoleArn: awsCredentialsResource["role_arn"].(string),
Duration: duration,
},
Type: client.AwsOidcCredentialsType,
}

updatePayload := client.AwsCredentialsCreatePayload{
Value: client.AwsCredentialsValuePayload{
RoleArn: updatedAwsCredentialsResource["role_arn"].(string),
Duration: updatedDuration,
},
Type: client.AwsOidcCredentialsType,
}

returnValues := client.Credentials{
Id: "f595c4b6-0a24-4c22-89f7-7030045de30f",
Name: "test",
OrganizationId: "id",
Type: string(client.AwsOidcCredentialsType),
}

otherTypeReturnValues := client.Credentials{
Id: "f595c4b6-0a24-4c22-89f7-7030045de30a",
Name: "test",
OrganizationId: "id",
Type: "AWS_....",
}

updateReturnValues := client.Credentials{
Id: returnValues.Id,
Name: returnValues.Name,
OrganizationId: "id",
Type: string(client.AwsOidcCredentialsType),
}

testCaseForCreateAndUpdate := resource.TestCase{
Steps: []resource.TestStep{
{
Config: resourceConfigCreate(resourceType, resourceName, awsCredentialsResource),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(accessor, "name", awsCredentialsResource["name"].(string)),
resource.TestCheckResourceAttr(accessor, "role_arn", awsCredentialsResource["role_arn"].(string)),
resource.TestCheckResourceAttr(accessor, "id", returnValues.Id),
resource.TestCheckResourceAttr(accessor, "duration", awsCredentialsResource["duration"].(string)),
),
},
{
Config: resourceConfigCreate(resourceType, resourceName, updatedAwsCredentialsResource),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(accessor, "name", updatedAwsCredentialsResource["name"].(string)),
resource.TestCheckResourceAttr(accessor, "role_arn", updatedAwsCredentialsResource["role_arn"].(string)),
resource.TestCheckResourceAttr(accessor, "id", updateReturnValues.Id),
resource.TestCheckResourceAttr(accessor, "duration", updatedAwsCredentialsResource["duration"].(string)),
),
},
},
}

t.Run("create and update", func(t *testing.T) {
runUnitTest(t, testCaseForCreateAndUpdate, func(mock *client.MockApiClientInterface) {
gomock.InOrder(
mock.EXPECT().CredentialsCreate(&createPayload).Times(1).Return(returnValues, nil),
mock.EXPECT().CloudCredentials(returnValues.Id).Times(2).Return(returnValues, nil),
mock.EXPECT().CredentialsUpdate(returnValues.Id, &updatePayload).Times(1).Return(updateReturnValues, nil),
mock.EXPECT().CloudCredentials(updateReturnValues.Id).Times(1).Return(updateReturnValues, nil),
mock.EXPECT().CloudCredentialsDelete(returnValues.Id).Times(1).Return(nil),
)
})
})

t.Run("drift", func(t *testing.T) {
stepConfig := resourceConfigCreate(resourceType, resourceName, awsCredentialsResource)

createTestCase := resource.TestCase{
Steps: []resource.TestStep{
{
Config: stepConfig,
},
{
Config: stepConfig,
},
},
}

runUnitTest(t, createTestCase, func(mock *client.MockApiClientInterface) {
gomock.InOrder(
mock.EXPECT().CredentialsCreate(&createPayload).Times(1).Return(returnValues, nil),
mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, nil),
mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, http.NewMockFailedResponseError(404)),
mock.EXPECT().CredentialsCreate(&createPayload).Times(1).Return(returnValues, nil),
mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, nil),
mock.EXPECT().CloudCredentialsDelete(returnValues.Id).Times(1).Return(nil),
)
})
})

t.Run("import by name", func(t *testing.T) {
testCase := resource.TestCase{
Steps: []resource.TestStep{
{
Config: resourceConfigCreate(resourceType, resourceName, awsCredentialsResource),
},
{
ResourceName: resourceNameImport,
ImportState: true,
ImportStateId: awsCredentialsResource["name"].(string),
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"role_arn", "duration"},
},
},
}

runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) {
gomock.InOrder(
mock.EXPECT().CredentialsCreate(&createPayload).Times(1).Return(returnValues, nil),
mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, nil),
mock.EXPECT().CloudCredentialsList().Times(1).Return([]client.Credentials{otherTypeReturnValues, returnValues}, nil),
mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, nil),
mock.EXPECT().CloudCredentialsDelete(returnValues.Id).Times(1).Return(nil),
)
})
})

t.Run("import by id", func(t *testing.T) {
testCase := resource.TestCase{
Steps: []resource.TestStep{
{
Config: resourceConfigCreate(resourceType, resourceName, awsCredentialsResource),
},
{
ResourceName: resourceNameImport,
ImportState: true,
ImportStateId: returnValues.Id,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"role_arn", "duration"},
},
},
}

runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) {
gomock.InOrder(
mock.EXPECT().CredentialsCreate(&createPayload).Times(1).Return(returnValues, nil),
mock.EXPECT().CloudCredentials(returnValues.Id).Times(3).Return(returnValues, nil),
mock.EXPECT().CloudCredentialsDelete(returnValues.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, awsCredentialsResource),
},
{
ResourceName: resourceNameImport,
ImportState: true,
ImportStateId: otherTypeReturnValues.Id,
ImportStateVerify: true,
ExpectError: regexp.MustCompile("credentials not found"),
},
},
}

runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) {
gomock.InOrder(
mock.EXPECT().CredentialsCreate(&createPayload).Times(1).Return(returnValues, nil),
mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, nil),
mock.EXPECT().CloudCredentials(otherTypeReturnValues.Id).Times(1).Return(client.Credentials{}, &client.NotFoundError{}),
mock.EXPECT().CloudCredentialsDelete(returnValues.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, awsCredentialsResource),
},
{
ResourceName: resourceNameImport,
ImportState: true,
ImportStateId: awsCredentialsResource["name"].(string),
ImportStateVerify: true,
ExpectError: regexp.MustCompile(fmt.Sprintf("credentials with name %v not found", awsCredentialsResource["name"].(string))),
},
},
}

runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) {
gomock.InOrder(
mock.EXPECT().CredentialsCreate(&createPayload).Times(1).Return(returnValues, nil),
mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, nil),
mock.EXPECT().CloudCredentialsList().Times(1).Return([]client.Credentials{otherTypeReturnValues}, nil),
mock.EXPECT().CloudCredentialsDelete(returnValues.Id).Times(1).Return(nil),
)
})
})
}
2 changes: 2 additions & 0 deletions examples/resources/env0_aws_oidc_credentials/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
terraform import env0_aws_oidc_credentials.by_id d31a6b30-5f69-4d24-937c-22322754934e
terraform import env0_aws_oidc_credentials.by_name "credentials name"
5 changes: 5 additions & 0 deletions examples/resources/env0_aws_oidc_credentials/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
resource "env0_aws_oidc_credentials" "credentials" {
name = "example"
role_arn = "arn::role::34"
duration = 3600
}
Loading

0 comments on commit 5d28bb1

Please sign in to comment.