diff --git a/docs/resources/username_password_account.md b/docs/resources/username_password_account.md index cd949fa53..bb2a9c698 100644 --- a/docs/resources/username_password_account.md +++ b/docs/resources/username_password_account.md @@ -23,12 +23,12 @@ resource "octopusdeploy_username_password_account" "example" { ### Required -- `name` (String) The name of this resource. +- `name` (String) The name of the username-password account. - `username` (String, Sensitive) The username associated with this resource. ### Optional -- `description` (String) The description of this username/password account. +- `description` (String) The description of this username/password resource. - `environments` (List of String) A list of environment IDs associated with this resource. - `id` (String) The unique ID for this resource. - `password` (String, Sensitive) The password associated with this resource. diff --git a/octopusdeploy/provider.go b/octopusdeploy/provider.go index 07f503374..54b6fdb13 100644 --- a/octopusdeploy/provider.go +++ b/octopusdeploy/provider.go @@ -75,7 +75,6 @@ func Provider() *schema.Provider { "octopusdeploy_token_account": resourceTokenAccount(), "octopusdeploy_user": resourceUser(), "octopusdeploy_user_role": resourceUserRole(), - "octopusdeploy_username_password_account": resourceUsernamePasswordAccount(), }, Schema: map[string]*schema.Schema{ "address": { diff --git a/octopusdeploy/resource_kubernetes_cluster_deployment_target_test.go b/octopusdeploy/resource_kubernetes_cluster_deployment_target_test.go index 0b92199bd..e1973adc0 100644 --- a/octopusdeploy/resource_kubernetes_cluster_deployment_target_test.go +++ b/octopusdeploy/resource_kubernetes_cluster_deployment_target_test.go @@ -149,6 +149,13 @@ func testAccKubernetesClusterDeploymentTargetBasic(accountLocalName string, acco }`, localName, clusterURL, environmentID, name, userRoleID, usernamePasswordAccountID) } +func testUsernamePasswordMinimum(localName string, name string, username string) string { + return fmt.Sprintf(`resource "octopusdeploy_username_password_account" "%s" { + name = "%s" + username = "%s" + }`, localName, name, username) +} + func testAccKubernetesClusterDeploymentTargetGcp( accountLocalName string, accountName string, diff --git a/octopusdeploy/resource_username_password_account.go b/octopusdeploy/resource_username_password_account.go deleted file mode 100644 index 4d6ba8b79..000000000 --- a/octopusdeploy/resource_username_password_account.go +++ /dev/null @@ -1,95 +0,0 @@ -package octopusdeploy - -import ( - "context" - "log" - - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/accounts" - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client" - "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal/errors" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func resourceUsernamePasswordAccount() *schema.Resource { - return &schema.Resource{ - CreateContext: resourceUsernamePasswordAccountCreate, - DeleteContext: resourceUsernamePasswordAccountDelete, - Description: "This resource manages username-password accounts in Octopus Deploy.", - Importer: getImporter(), - ReadContext: resourceUsernamePasswordAccountRead, - Schema: getUsernamePasswordAccountSchema(), - UpdateContext: resourceUsernamePasswordAccountUpdate, - } -} - -func resourceUsernamePasswordAccountCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - account := expandUsernamePasswordAccount(d) - - log.Printf("[INFO] creating username-password account: %#v", account) - - client := m.(*client.Client) - createdAccount, err := accounts.Add(client, account) - if err != nil { - return diag.FromErr(err) - } - - if err := setUsernamePasswordAccount(ctx, d, createdAccount.(*accounts.UsernamePasswordAccount)); err != nil { - return diag.FromErr(err) - } - - d.SetId(createdAccount.GetID()) - - log.Printf("[INFO] username-password account created (%s)", d.Id()) - return nil -} - -func resourceUsernamePasswordAccountDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - log.Printf("[INFO] deleting username-password account (%s)", d.Id()) - - client := m.(*client.Client) - if err := accounts.DeleteByID(client, d.Get("space_id").(string), d.Id()); err != nil { - return diag.FromErr(err) - } - - d.SetId("") - - log.Printf("[INFO] username-password account deleted") - return nil -} - -func resourceUsernamePasswordAccountRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - log.Printf("[INFO] reading username-password account (%s)", d.Id()) - - client := m.(*client.Client) - accountResource, err := accounts.GetByID(client, d.Get("space_id").(string), d.Id()) - if err != nil { - return errors.ProcessApiError(ctx, d, err, "username-password account") - } - - if err := setUsernamePasswordAccount(ctx, d, accountResource.(*accounts.UsernamePasswordAccount)); err != nil { - return diag.FromErr(err) - } - - log.Printf("[INFO] username-password account read: (%s)", d.Id()) - return nil -} - -func resourceUsernamePasswordAccountUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - account := expandUsernamePasswordAccount(d) - - log.Printf("[INFO] updating username-password account: %#v", account) - - client := m.(*client.Client) - updatedAccount, err := accounts.Update(client, account) - if err != nil { - return diag.FromErr(err) - } - - if err := setUsernamePasswordAccount(ctx, d, updatedAccount.(*accounts.UsernamePasswordAccount)); err != nil { - return diag.FromErr(err) - } - - log.Printf("[INFO] username-password account updated (%s)", d.Id()) - return nil -} diff --git a/octopusdeploy/schema_username_password_account.go b/octopusdeploy/schema_username_password_account.go deleted file mode 100644 index 4e0c50b65..000000000 --- a/octopusdeploy/schema_username_password_account.go +++ /dev/null @@ -1,90 +0,0 @@ -package octopusdeploy - -import ( - "context" - "fmt" - - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/accounts" - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func expandUsernamePasswordAccount(d *schema.ResourceData) accounts.IUsernamePasswordAccount { - name := d.Get("name").(string) - - account, _ := accounts.NewUsernamePasswordAccount(name) - account.SetID(d.Id()) - account.SetPassword(core.NewSensitiveValue(d.Get("password").(string))) - - if v, ok := d.GetOk("description"); ok { - account.SetDescription(v.(string)) - } - - if v, ok := d.GetOk("environments"); ok { - account.SetEnvironmentIDs(getSliceFromTerraformTypeList(v)) - } - - if v, ok := d.GetOk("space_id"); ok { - account.SetSpaceID(v.(string)) - } - - if v, ok := d.GetOk("tenanted_deployment_participation"); ok { - account.SetTenantedDeploymentMode(core.TenantedDeploymentMode(v.(string))) - } - - if v, ok := d.GetOk("tenants"); ok { - account.SetTenantIDs(getSliceFromTerraformTypeList(v)) - } - - if v, ok := d.GetOk("tenant_tags"); ok { - account.SetTenantTags(getSliceFromTerraformTypeList(v)) - } - - if v, ok := d.GetOk("username"); ok { - account.SetUsername(v.(string)) - } - - return account -} - -func setUsernamePasswordAccount(ctx context.Context, d *schema.ResourceData, account *accounts.UsernamePasswordAccount) error { - d.Set("description", account.GetDescription()) - - if err := d.Set("environments", account.GetEnvironmentIDs()); err != nil { - return fmt.Errorf("error setting environments: %s", err) - } - - d.Set("id", account.GetID()) - d.Set("name", account.GetName()) - d.Set("space_id", account.GetSpaceID()) - d.Set("tenanted_deployment_participation", account.GetTenantedDeploymentMode()) - - if err := d.Set("tenants", account.GetTenantIDs()); err != nil { - return fmt.Errorf("error setting tenants: %s", err) - } - - if err := d.Set("tenant_tags", account.TenantTags); err != nil { - return fmt.Errorf("error setting tenant_tags: %s", err) - } - - d.Set("username", account.Username) - - d.SetId(account.GetID()) - - return nil -} - -func getUsernamePasswordAccountSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "description": getDescriptionSchema("username/password account"), - "environments": getEnvironmentsSchema(), - "id": getIDSchema(), - "name": getNameSchema(true), - "password": getPasswordSchema(false), - "space_id": getSpaceIDSchema(), - "tenanted_deployment_participation": getTenantedDeploymentSchema(), - "tenants": getTenantsSchema(), - "tenant_tags": getTenantTagsSchema(), - "username": getUsernameSchema(true), - } -} diff --git a/octopusdeploy_framework/framework_provider.go b/octopusdeploy_framework/framework_provider.go index 447f66237..b85b832c3 100644 --- a/octopusdeploy_framework/framework_provider.go +++ b/octopusdeploy_framework/framework_provider.go @@ -93,6 +93,7 @@ func (p *octopusDeployFrameworkProvider) Resources(ctx context.Context) []func() NewVariableResource, NewProjectResource, NewDockerContainerRegistryFeedResource, + NewUsernamePasswordAccountResource, } } diff --git a/octopusdeploy_framework/resource_username_password_account.go b/octopusdeploy_framework/resource_username_password_account.go new file mode 100644 index 000000000..a18422fa2 --- /dev/null +++ b/octopusdeploy_framework/resource_username_password_account.go @@ -0,0 +1,181 @@ +package octopusdeploy_framework + +import ( + "context" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/accounts" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/schemas" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +var _ resource.Resource = &usernamePasswordAccountResource{} + +type usernamePasswordAccountResource struct { + *Config +} + +type usernamePasswordAccountResourceModel struct { + ID types.String `tfsdk:"id"` + SpaceID types.String `tfsdk:"space_id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Environments types.List `tfsdk:"environments"` + Password types.String `tfsdk:"password"` + TenantedDeploymentParticipation types.String `tfsdk:"tenanted_deployment_participation"` + Tenants types.List `tfsdk:"tenants"` + TenantTags types.List `tfsdk:"tenant_tags"` + Username types.String `tfsdk:"username"` +} + +func NewUsernamePasswordAccountResource() resource.Resource { + return &usernamePasswordAccountResource{} +} + +func (r *usernamePasswordAccountResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = util.GetTypeName("username_password_account") +} + +func (r *usernamePasswordAccountResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schemas.GetUsernamePasswordAccountResourceSchema() +} + +func (r *usernamePasswordAccountResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.Config = ResourceConfiguration(req, resp) +} +func (r *usernamePasswordAccountResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan usernamePasswordAccountResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "Creating username password account", map[string]interface{}{ + "name": plan.Name.ValueString(), + }) + + account := expandUsernamePasswordAccount(ctx, plan) + createdAccount, err := accounts.Add(r.Client, account) + if err != nil { + resp.Diagnostics.AddError("Error creating username password account", err.Error()) + return + } + + state := flattenUsernamePasswordAccount(ctx, createdAccount.(*accounts.UsernamePasswordAccount), plan) + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) +} + +func (r *usernamePasswordAccountResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state usernamePasswordAccountResourceModel + 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 { + resp.Diagnostics.AddError("Error reading username password account", err.Error()) + return + } + + newState := flattenUsernamePasswordAccount(ctx, account.(*accounts.UsernamePasswordAccount), state) + resp.Diagnostics.Append(resp.State.Set(ctx, newState)...) +} + +func (r *usernamePasswordAccountResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan usernamePasswordAccountResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + account := expandUsernamePasswordAccount(ctx, plan) + updatedAccount, err := accounts.Update(r.Client, account) + if err != nil { + resp.Diagnostics.AddError("Error updating username password account", err.Error()) + return + } + + state := flattenUsernamePasswordAccount(ctx, updatedAccount.(*accounts.UsernamePasswordAccount), plan) + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) +} + +func (r *usernamePasswordAccountResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state usernamePasswordAccountResourceModel + 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 username password account", err.Error()) + return + } +} + +func expandUsernamePasswordAccount(ctx context.Context, model usernamePasswordAccountResourceModel) *accounts.UsernamePasswordAccount { + account, _ := accounts.NewUsernamePasswordAccount(model.Name.ValueString()) + + account.SetID(model.ID.ValueString()) + account.SetDescription(model.Description.ValueString()) + account.SetSpaceID(model.SpaceID.ValueString()) + account.SetUsername(model.Username.ValueString()) + account.SetPassword(core.NewSensitiveValue(model.Password.ValueString())) + account.SetEnvironmentIDs(expandStringList(ctx, model.Environments)) + account.SetTenantedDeploymentMode(core.TenantedDeploymentMode(model.TenantedDeploymentParticipation.ValueString())) + account.SetTenantIDs(expandStringList(ctx, model.Tenants)) + account.SetTenantTags(expandStringList(ctx, model.TenantTags)) + + return account +} + +func flattenUsernamePasswordAccount(ctx context.Context, account *accounts.UsernamePasswordAccount, model usernamePasswordAccountResourceModel) usernamePasswordAccountResourceModel { + 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.Username = types.StringValue(account.GetUsername()) + model.TenantedDeploymentParticipation = types.StringValue(string(account.GetTenantedDeploymentMode())) + + model.Environments = flattenStringList(ctx, account.GetEnvironmentIDs(), model.Environments) + model.Tenants = flattenStringList(ctx, account.GetTenantIDs(), model.Tenants) + model.TenantTags = flattenStringList(ctx, account.TenantTags, model.TenantTags) + + // Note: We don't flatten the password as it's sensitive and not returned by the API + + return model +} + +func expandStringList(ctx context.Context, list types.List) []string { + if list.IsNull() || list.IsUnknown() { + return nil + } + + var result []string + list.ElementsAs(context.Background(), &result, false) + if len(result) == 0 { + return nil + } + + return result +} + +func flattenStringList(ctx context.Context, slice []string, currentList types.List) types.List { + if len(slice) == 0 && currentList.IsNull() { + return types.ListNull(types.StringType) + } + if slice == nil { + return types.ListNull(types.StringType) + } + + valueSlice := make([]attr.Value, len(slice)) + for i, s := range slice { + valueSlice[i] = types.StringValue(s) + } + + return types.ListValueMust(types.StringType, valueSlice) +} diff --git a/octopusdeploy/resource_username_password_account_integration_test.go b/octopusdeploy_framework/resource_username_password_account_integration_test.go similarity index 98% rename from octopusdeploy/resource_username_password_account_integration_test.go rename to octopusdeploy_framework/resource_username_password_account_integration_test.go index b5ea00af4..a730246f1 100644 --- a/octopusdeploy/resource_username_password_account_integration_test.go +++ b/octopusdeploy_framework/resource_username_password_account_integration_test.go @@ -1,4 +1,4 @@ -package octopusdeploy +package octopusdeploy_framework import ( "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/accounts" diff --git a/octopusdeploy/resource_username_password_account_test.go b/octopusdeploy_framework/resource_username_password_account_test.go similarity index 87% rename from octopusdeploy/resource_username_password_account_test.go rename to octopusdeploy_framework/resource_username_password_account_test.go index e1a29bd9f..5cc58f92d 100644 --- a/octopusdeploy/resource_username_password_account_test.go +++ b/octopusdeploy_framework/resource_username_password_account_test.go @@ -1,16 +1,16 @@ -package octopusdeploy +package octopusdeploy_framework import ( "fmt" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/variables" "github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework/octoclient" "github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework/test" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" "testing" - - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccUsernamePasswordBasic(t *testing.T) { @@ -26,8 +26,7 @@ func TestAccUsernamePasswordBasic(t *testing.T) { config := testUsernamePasswordBasic(localName, description, name, username, password, tenantedDeploymentParticipation) resource.Test(t, resource.TestCase{ - CheckDestroy: testAccountCheckDestroy, - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { TestAccPreCheck(t) }, ProtoV6ProviderFactories: ProtoV6ProviderFactories(), Steps: []resource.TestStep{ { @@ -48,6 +47,17 @@ func TestAccUsernamePasswordBasic(t *testing.T) { }) } +func testAccountExists(prefix string) resource.TestCheckFunc { + return func(s *terraform.State) error { + accountID := s.RootModule().Resources[prefix].Primary.ID + if _, err := octoClient.Accounts.GetByID(accountID); err != nil { + return err + } + + return nil + } +} + func testUsernamePasswordBasic(localName string, description string, name string, username string, password string, tenantedDeploymentParticipation core.TenantedDeploymentMode) string { return fmt.Sprintf(`resource "octopusdeploy_username_password_account" "%s" { description = "%s" @@ -58,13 +68,6 @@ func testUsernamePasswordBasic(localName string, description string, name string }`, localName, description, name, password, tenantedDeploymentParticipation, username) } -func testUsernamePasswordMinimum(localName string, name string, username string) string { - return fmt.Sprintf(`resource "octopusdeploy_username_password_account" "%s" { - name = "%s" - username = "%s" - }`, localName, name, username) -} - // TestUsernamePasswordVariableResource verifies that a project variable referencing a username/password account // can be created func TestUsernamePasswordVariableResource(t *testing.T) { diff --git a/octopusdeploy_framework/schemas/username_password_account.go b/octopusdeploy_framework/schemas/username_password_account.go new file mode 100644 index 000000000..048eecd31 --- /dev/null +++ b/octopusdeploy_framework/schemas/username_password_account.go @@ -0,0 +1,26 @@ +package schemas + +import ( + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func GetUsernamePasswordAccountResourceSchema() schema.Schema { + return schema.Schema{ + Description: "This resource manages username-password accounts in Octopus Deploy.", + Attributes: map[string]schema.Attribute{ + "id": util.ResourceString().Optional().Computed().PlanModifiers(stringplanmodifier.UseStateForUnknown()).Description("The unique ID for this resource.").Build(), + "space_id": util.ResourceString().Optional().Computed().PlanModifiers(stringplanmodifier.UseStateForUnknown()).Description("The space ID associated with this resource.").Build(), + "name": util.ResourceString().Required().Description("The name of the username-password account.").Build(), + "description": util.ResourceString().Optional().Description("The description of this username/password resource.").Build(), + "environments": util.ResourceList(types.StringType).Optional().Description("A list of environment IDs associated with this resource.").Build(), + "password": util.ResourceString().Optional().Sensitive().Description("The password associated with this resource.").Build(), + "tenanted_deployment_participation": util.ResourceString().Optional().Description("The tenanted deployment mode of the resource. Valid account types are `Untenanted`, `TenantedOrUntenanted`, or `Tenanted`.").Build(), + "tenants": util.ResourceList(types.StringType).Optional().Description("A list of tenant IDs associated with this resource.").Build(), + "tenant_tags": util.ResourceList(types.StringType).Optional().Description("A list of tenant tags associated with this resource.").Build(), + "username": util.ResourceString().Required().Sensitive().Description("The username associated with this resource.").Build(), + }, + } +}