Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for generic oidc accounts #831

Merged
merged 13 commits into from
Dec 5, 2024
2 changes: 1 addition & 1 deletion docs/data-sources/accounts.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ data "octopusdeploy_accounts" "example" {

### Optional

- `account_type` (String) A filter to search by a list of account types. Valid account types are `AmazonWebServicesAccount`, `AmazonWebServicesRoleAccount`, `AmazonWebServicesOidcAccount`, `AzureServicePrincipal`, `AzureSubscription`, `None`, `SshKeyPair`, `Token`, or `UsernamePassword`.
- `account_type` (String) A filter to search by a list of account types. Valid account types are `AmazonWebServicesAccount`, `AmazonWebServicesRoleAccount`, `AmazonWebServicesOidcAccount`, `AzureServicePrincipal`, `AzureSubscription`, `GenericOidcAccount`, `None`, `SshKeyPair`, `Token`, or `UsernamePassword`.
- `ids` (List of String) A filter to search by a list of IDs.
- `partial_name` (String) A filter to search by the partial match of a name.
- `skip` (Number) A filter to specify the number of items to skip in the response.
Expand Down
51 changes: 51 additions & 0 deletions docs/resources/generic_oidc_account.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "octopusdeploy_generic_oidc_account Resource - terraform-provider-octopusdeploy"
subcategory: ""
description: |-
This resource manages a Generic OIDC Account in Octopus Deploy.
---

# octopusdeploy_generic_oidc_account (Resource)

This resource manages a Generic OIDC Account in Octopus Deploy.

## Example Usage

```terraform
resource "octopusdeploy_generic_oidc_account" "example" {
name = "Generic OpenID Connect Account (OK to Delete)"
execution_subject_keys = ["space", "project"]
audience = "api://default"
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `name` (String) The name of the generic oidc account.

### Optional

- `audience` (String) The audience associated with this resource.
- `description` (String) The description of this generic oidc account.
- `environments` (List of String) A list of environment IDs associated with this resource.
- `execution_subject_keys` (List of String) Keys to include in a deployment or runbook. Valid options are `space`, `environment`, `project`, `tenant`, `runbook`, `account`, `type`.
- `space_id` (String) The space ID associated with this resource.
- `tenant_tags` (List of String) A list of tenant tags associated with this resource.
- `tenanted_deployment_participation` (String) The tenanted deployment mode of the resource. Valid account types are `Untenanted`, `TenantedOrUntenanted`, or `Tenanted`.
- `tenants` (List of String) A list of tenant IDs associated with this resource.

### Read-Only

- `id` (String) The unique ID for this resource.

## Import

Import is supported using the following syntax:

```shell
terraform import [options] octopusdeploy_generic_oidc_account.<name> <account-id>
```
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform import [options] octopusdeploy_generic_oidc_account.<name> <account-id>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
resource "octopusdeploy_generic_oidc_account" "example" {
name = "Generic OpenID Connect Account (OK to Delete)"
execution_subject_keys = ["space", "project"]
audience = "api://default"
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/OctopusDeploy/terraform-provider-octopusdeploy
go 1.21

require (
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.62.2
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.63.0
github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework v0.0.0-20240729041805-46db6fb717b4
github.com/google/uuid v1.6.0
github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ github.com/Microsoft/hcsshim v0.12.4 h1:Ev7YUMHAHoWNm+aDSPzc5W9s6E2jyL1szpVDJeZ/
github.com/Microsoft/hcsshim v0.12.4/go.mod h1:Iyl1WVpZzr+UkzjekHZbV8o5Z9ZkxNGx6CtY2Qg/JVQ=
github.com/OctopusDeploy/go-octodiff v1.0.0 h1:U+ORg6azniwwYo+O44giOw6TiD5USk8S4VDhOQ0Ven0=
github.com/OctopusDeploy/go-octodiff v1.0.0/go.mod h1:Mze0+EkOWTgTmi8++fyUc6r0aLZT7qD9gX+31t8MmIU=
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.62.2 h1:8CexD1Jnf8ng4S6bHilG7s3+iQOraXZY31Dn0SAxjEM=
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.62.2/go.mod h1:ggvOXzMnq+w0pLg6C9zdjz6YBaHfO3B3tqmmB7JQdaw=
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.63.0 h1:TshwN+IqKt21uY9aXzj0ou0Ew92uIi3+ZGTccVd9Z8g=
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.63.0/go.mod h1:ggvOXzMnq+w0pLg6C9zdjz6YBaHfO3B3tqmmB7JQdaw=
github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework v0.0.0-20240729041805-46db6fb717b4 h1:QfbVf0bOIRMp/WHAWsuVDB7KHoWnRsGbvDuOf2ua7k4=
github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework v0.0.0-20240729041805-46db6fb717b4/go.mod h1:Oq9KbiRNDBB5jFmrwnrgLX0urIqR/1ptY18TzkqXm7M=
github.com/ProtonMail/go-crypto v1.1.0-alpha.2 h1:bkyFVUP+ROOARdgCiJzNQo2V2kiB97LyUpzH9P6Hrlg=
Expand Down
3 changes: 2 additions & 1 deletion octopusdeploy/schema_queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (

func getQueryAccountType() *schema.Schema {
return &schema.Schema{
Description: "A filter to search by a list of account types. Valid account types are `AmazonWebServicesAccount`, `AmazonWebServicesRoleAccount`, `AmazonWebServicesOidcAccount`, `AzureServicePrincipal`, `AzureSubscription`, `None`, `SshKeyPair`, `Token`, or `UsernamePassword`.",
Description: "A filter to search by a list of account types. Valid account types are `AmazonWebServicesAccount`, `AmazonWebServicesRoleAccount`, `AmazonWebServicesOidcAccount`, `AzureServicePrincipal`, `AzureSubscription`, `GenericOidcAccount`, `None`, `SshKeyPair`, `Token`, or `UsernamePassword`.",
Optional: true,
Type: schema.TypeString,
ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{
Expand All @@ -17,6 +17,7 @@ func getQueryAccountType() *schema.Schema {
"AzureServicePrincipal",
"AzureOIDC",
"AzureSubscription",
"GenericOidcAccount",
"None",
"SshKeyPair",
"Token",
Expand Down
3 changes: 2 additions & 1 deletion octopusdeploy/schema_utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (

func getAccountTypeSchema(isRequired bool) *schema.Schema {
schema := &schema.Schema{
Description: "Specifies the type of the account. Valid account types are `AmazonWebServicesAccount`, `AmazonWebServicesRoleAccount`, `AzureServicePrincipal`, `AzureOIDC`, `AzureSubscription`, `AmazonWebServicesOidcAccount`, `None`, `SshKeyPair`, `Token`, or `UsernamePassword`.",
Description: "Specifies the type of the account. Valid account types are `AmazonWebServicesAccount`, `AmazonWebServicesRoleAccount`, `AzureServicePrincipal`, `AzureOIDC`, `AzureSubscription`, `AmazonWebServicesOidcAccount`, `GenericOidcAccount`, `None`, `SshKeyPair`, `Token`, or `UsernamePassword`.",
ForceNew: true,
Type: schema.TypeString,
ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{
Expand All @@ -18,6 +18,7 @@ func getAccountTypeSchema(isRequired bool) *schema.Schema {
"AzureServicePrincipal",
"AzureOIDC",
"AzureSubscription",
"GenericOidcAccount",
"None",
"SshKeyPair",
"Token",
Expand Down
1 change: 1 addition & 0 deletions octopusdeploy_framework/framework_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ func (p *octopusDeployFrameworkProvider) Resources(ctx context.Context) []func()
NewDeploymentFreezeResource,
NewDeploymentFreezeProjectResource,
NewServiceAccountOIDCIdentity,
NewGenericOidcResource,
}
}

Expand Down
179 changes: 179 additions & 0 deletions octopusdeploy_framework/resource_generic_oidc_account.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package octopusdeploy_framework

import (
"context"
"fmt"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/accounts"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core"
"github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal/errors"
"github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/schemas"
"github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
)

var _ resource.Resource = &genericOidcAccountResource{}
var _ resource.ResourceWithImportState = &genericOidcAccountResource{}

type genericOidcAccountResource struct {
*Config
}

func NewGenericOidcResource() resource.Resource {
return &genericOidcAccountResource{}
}

func (r *genericOidcAccountResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = util.GetTypeName("generic_oidc_account")
}

func (r *genericOidcAccountResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schemas.GenericOidcAccountSchema{}.GetResourceSchema()
}

func (r *genericOidcAccountResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
r.Config = ResourceConfiguration(req, resp)
}
func (r *genericOidcAccountResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan schemas.GenericOidcAccountResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}

tflog.Debug(ctx, "Creating generic oidc account", map[string]interface{}{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine to leave this as is but in the future, we can now use any instead of interface{} in Go. Makes the code easier to read.

"name": plan.Name.ValueString(),
})

account := expandGenericOidcAccountResource(ctx, plan)
createdAccount, err := accounts.Add(r.Client, account)
if err != nil {
resp.Diagnostics.AddError("Error creating generic oidc account", err.Error())
return
}

state := flattenGenericOidcAccountResource(ctx, createdAccount.(*accounts.GenericOIDCAccount), plan)
resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
}

func (r *genericOidcAccountResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state schemas.GenericOidcAccountResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}

account, err := accounts.GetByID(r.Client, state.SpaceID.ValueString(), state.ID.ValueString())
if err != nil {
if err := errors.ProcessApiErrorV2(ctx, resp, state, err, "genericOidcAccountResource"); err != nil {
resp.Diagnostics.AddError("unable to load generic oidc account", err.Error())
}
return
}

newState := flattenGenericOidcAccountResource(ctx, account.(*accounts.GenericOIDCAccount), state)
resp.Diagnostics.Append(resp.State.Set(ctx, newState)...)
}

func (r *genericOidcAccountResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan schemas.GenericOidcAccountResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}

account := expandGenericOidcAccountResource(ctx, plan)
updatedAccount, err := accounts.Update(r.Client, account)
if err != nil {
resp.Diagnostics.AddError("Error updating generic oidc account", err.Error())
return
}

state := flattenGenericOidcAccountResource(ctx, updatedAccount.(*accounts.GenericOIDCAccount), plan)
resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
}

func (r *genericOidcAccountResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state schemas.GenericOidcAccountResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}

err := accounts.DeleteByID(r.Client, state.SpaceID.ValueString(), state.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Error deleting generic oidc account", err.Error())
return
}
}

func (r *genericOidcAccountResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
accountID := req.ID

account, err := accounts.GetByID(r.Client, r.Client.GetSpaceID(), accountID)
if err != nil {
resp.Diagnostics.AddError(
"Error reading generic oidc account",
fmt.Sprintf("Unable to read generic oidc account with ID %s: %s", accountID, err.Error()),
)
return
}

genericOidcAccount, ok := account.(*accounts.GenericOIDCAccount)
if !ok {
resp.Diagnostics.AddError(
"Unexpected account type",
fmt.Sprintf("Expected generic oidc account, got: %T", account),
)
return
}

state := schemas.GenericOidcAccountResourceModel{
SpaceID: types.StringValue(genericOidcAccount.GetSpaceID()),
Name: types.StringValue(genericOidcAccount.GetName()),
Description: types.StringValue(genericOidcAccount.GetDescription()),
TenantedDeploymentParticipation: types.StringValue(string(genericOidcAccount.GetTenantedDeploymentMode())),
Environments: flattenStringList(genericOidcAccount.GetEnvironmentIDs(), types.ListNull(types.StringType)),
Tenants: flattenStringList(genericOidcAccount.GetTenantIDs(), types.ListNull(types.StringType)),
TenantTags: flattenStringList(genericOidcAccount.TenantTags, types.ListNull(types.StringType)),
ExecutionSubjectKeys: flattenStringList(genericOidcAccount.DeploymentSubjectKeys, types.ListNull(types.StringType)),
Audience: types.StringValue(genericOidcAccount.Audience),
}
state.ID = types.StringValue(genericOidcAccount.ID)

resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}

func expandGenericOidcAccountResource(ctx context.Context, model schemas.GenericOidcAccountResourceModel) *accounts.GenericOIDCAccount {
account, _ := accounts.NewGenericOIDCAccount(model.Name.ValueString())

account.SetID(model.ID.ValueString())
account.SetDescription(model.Description.ValueString())
account.SetSpaceID(model.SpaceID.ValueString())
account.SetEnvironmentIDs(util.ExpandStringList(model.Environments))
account.SetTenantedDeploymentMode(core.TenantedDeploymentMode(model.TenantedDeploymentParticipation.ValueString()))
account.SetTenantIDs(util.ExpandStringList(model.Tenants))
account.SetTenantTags(util.ExpandStringList(model.TenantTags))
account.DeploymentSubjectKeys = util.ExpandStringList(model.ExecutionSubjectKeys)
account.Audience = model.Audience.ValueString()

return account
}

func flattenGenericOidcAccountResource(ctx context.Context, account *accounts.GenericOIDCAccount, model schemas.GenericOidcAccountResourceModel) schemas.GenericOidcAccountResourceModel {
model.ID = types.StringValue(account.GetID())
model.SpaceID = types.StringValue(account.GetSpaceID())
model.Name = types.StringValue(account.GetName())
model.Description = types.StringValue(account.GetDescription())
model.TenantedDeploymentParticipation = types.StringValue(string(account.GetTenantedDeploymentMode()))

model.Environments = util.FlattenStringList(account.GetEnvironmentIDs())
model.Tenants = util.FlattenStringList(account.GetTenantIDs())
model.TenantTags = util.FlattenStringList(account.TenantTags)

model.ExecutionSubjectKeys = util.FlattenStringList(account.DeploymentSubjectKeys)
model.Audience = types.StringValue(account.Audience)

return model
}
78 changes: 78 additions & 0 deletions octopusdeploy_framework/resource_generic_oidc_account_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package octopusdeploy_framework

import (
"fmt"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core"
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"strings"
"testing"
)

func TestAccGenericOidcAccountBasic(t *testing.T) {
localName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)
resourceName := "octopusdeploy_generic_oidc_account." + localName

description := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)
name := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)
tenantedDeploymentParticipation := core.TenantedDeploymentModeTenantedOrUntenanted

executionKeys := []string{"space"}
audience := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)
updatedAudience := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)

config := testGenericOidcAccountBasic(localName, name, description, tenantedDeploymentParticipation, executionKeys, audience)
updateConfig := testGenericOidcAccountBasic(localName, name, description, tenantedDeploymentParticipation, executionKeys, updatedAudience)

resource.Test(t, resource.TestCase{
PreCheck: func() { TestAccPreCheck(t) },
ProtoV6ProviderFactories: ProtoV6ProviderFactories(),
Steps: []resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(
testAccountExists(resourceName),
resource.TestCheckResourceAttr(resourceName, "description", description),
resource.TestCheckResourceAttrSet(resourceName, "id"),
resource.TestCheckResourceAttr(resourceName, "name", name),
resource.TestCheckResourceAttrSet(resourceName, "space_id"),
resource.TestCheckResourceAttr(resourceName, "tenanted_deployment_participation", string(tenantedDeploymentParticipation)),
resource.TestCheckResourceAttr(resourceName, "execution_subject_keys.0", executionKeys[0]),
resource.TestCheckResourceAttr(resourceName, "audience", audience),
),
ResourceName: resourceName,
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you please include a 2nd TestStep so that we can also test update. It will basically be the same step, just with a slight change in the config, like a different description.

{
Config: updateConfig,
Check: resource.ComposeTestCheckFunc(
testAccountExists(resourceName),
resource.TestCheckResourceAttr(resourceName, "description", description),
resource.TestCheckResourceAttrSet(resourceName, "id"),
resource.TestCheckResourceAttr(resourceName, "name", name),
resource.TestCheckResourceAttrSet(resourceName, "space_id"),
resource.TestCheckResourceAttr(resourceName, "tenanted_deployment_participation", string(tenantedDeploymentParticipation)),
resource.TestCheckResourceAttr(resourceName, "execution_subject_keys.0", executionKeys[0]),
resource.TestCheckResourceAttr(resourceName, "audience", updatedAudience),
),
ResourceName: resourceName,
},
},
})
}

func testGenericOidcAccountBasic(localName string, name string, description string, tenantedDeploymentParticipation core.TenantedDeploymentMode, execution_subject_keys []string, audience string) string {

execKeysStr := fmt.Sprintf(`["%s"]`, strings.Join(execution_subject_keys, `", "`))

return fmt.Sprintf(`resource "octopusdeploy_generic_oidc_account" "%s" {
description = "%s"
name = "%s"
tenanted_deployment_participation = "%s"
execution_subject_keys = %s
audience = "%s"
}

data "octopusdeploy_accounts" "test" {
ids = [octopusdeploy_generic_oidc_account.%s.id]
}`, localName, description, name, tenantedDeploymentParticipation, execKeysStr, audience, localName)
}
Loading
Loading