From c9bab2c18159a8ec05dcccd0a540fbdc830679a5 Mon Sep 17 00:00:00 2001 From: grace-rehn Date: Fri, 22 Nov 2024 13:03:15 +1000 Subject: [PATCH 1/4] feat: Add ability to disable tenants (#820) * feat: Add ability to disable tenants * chore: fix doc changes * fix: wrong schema attr * fix: add UseStateForUnknown to is_disabled field * chore: update resource tenant tests --- docs/data-sources/tenants.md | 2 ++ docs/resources/tenant.md | 1 + octopusdeploy_framework/datasource_tenants.go | 1 + octopusdeploy_framework/resource_tenant.go | 2 ++ .../resource_tenant_test.go | 12 +++++--- octopusdeploy_framework/schemas/tenant.go | 29 ++++++++++++++++--- 6 files changed, 39 insertions(+), 8 deletions(-) diff --git a/docs/data-sources/tenants.md b/docs/data-sources/tenants.md index c95b3b896..40b737117 100644 --- a/docs/data-sources/tenants.md +++ b/docs/data-sources/tenants.md @@ -20,6 +20,7 @@ Provides information about existing tenants. - `cloned_from_tenant_id` (String) A filter to search for a cloned tenant by its ID. - `ids` (List of String) A filter to search by a list of IDs. - `is_clone` (Boolean) A filter to search for cloned resources. +- `is_disabled` (Boolean) A filter to search by the disabled status of a resource. - `name` (String) A filter to search by name. - `partial_name` (String) A filter to search by a partial name. - `project_id` (String) A filter to search by a project ID. @@ -41,6 +42,7 @@ Read-Only: - `cloned_from_tenant_id` (String) The ID of the tenant from which this tenant was cloned. - `description` (String) The description of this tenants. - `id` (String) The unique ID for this resource. +- `is_disabled` (Boolean) The disabled status of this tenant. - `name` (String) The name of this resource. - `space_id` (String) The space ID associated with this tenant. - `tenant_tags` (Set of String) A list of tenant tags associated with this resource. diff --git a/docs/resources/tenant.md b/docs/resources/tenant.md index 02fb8feb7..733859606 100644 --- a/docs/resources/tenant.md +++ b/docs/resources/tenant.md @@ -20,6 +20,7 @@ This resource manages tenants in Octopus Deploy. - `cloned_from_tenant_id` (String) The ID of the tenant from which this tenant was cloned. - `description` (String) The description of this tenant. +- `is_disabled` (Boolean) The disabled status of this tenant. - `space_id` (String) The space ID associated with this tenant. - `tenant_tags` (Set of String) A list of tenant tags associated with this resource. diff --git a/octopusdeploy_framework/datasource_tenants.go b/octopusdeploy_framework/datasource_tenants.go index d7bcf71da..ec8cf6e1f 100644 --- a/octopusdeploy_framework/datasource_tenants.go +++ b/octopusdeploy_framework/datasource_tenants.go @@ -42,6 +42,7 @@ func (b *tenantsDataSource) Read(ctx context.Context, req datasource.ReadRequest ClonedFromTenantID: data.ClonedFromTenantId.ValueString(), IDs: util.ExpandStringList(data.IDs), IsClone: data.IsClone.ValueBool(), + IsDisabled: data.IsDisabled.ValueBool(), Name: data.Name.ValueString(), PartialName: data.PartialName.ValueString(), ProjectID: data.ProjectId.ValueString(), diff --git a/octopusdeploy_framework/resource_tenant.go b/octopusdeploy_framework/resource_tenant.go index 3fa5c5103..73bfc5c42 100644 --- a/octopusdeploy_framework/resource_tenant.go +++ b/octopusdeploy_framework/resource_tenant.go @@ -151,6 +151,7 @@ func mapStateToTenant(ctx context.Context, data *schemas.TenantModel) (*tenants. tenant.ID = data.ID.ValueString() tenant.ClonedFromTenantID = data.ClonedFromTenantId.ValueString() tenant.Description = data.Description.ValueString() + tenant.IsDisabled = data.IsDisabled.ValueBool() tenant.SpaceID = data.SpaceID.ValueString() convertedTenantTags, diags := util.SetToStringArray(ctx, data.TenantTags) @@ -167,6 +168,7 @@ func mapTenantToState(ctx context.Context, data *schemas.TenantModel, tenant *te data.ID = types.StringValue(tenant.ID) data.ClonedFromTenantId = types.StringValue(tenant.ClonedFromTenantID) data.Description = types.StringValue(tenant.Description) + data.IsDisabled = types.BoolValue(tenant.IsDisabled) data.SpaceID = types.StringValue(tenant.SpaceID) data.Name = types.StringValue(tenant.Name) diff --git a/octopusdeploy_framework/resource_tenant_test.go b/octopusdeploy_framework/resource_tenant_test.go index 3d99614c5..5d2f1ca34 100644 --- a/octopusdeploy_framework/resource_tenant_test.go +++ b/octopusdeploy_framework/resource_tenant_test.go @@ -6,6 +6,7 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" "path/filepath" + "strconv" "testing" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tenants" @@ -41,22 +42,24 @@ func TestAccTenantBasic(t *testing.T) { testTenantExists(resourceName), resource.TestCheckResourceAttr(resourceName, "name", name), resource.TestCheckResourceAttr(resourceName, "description", description), + resource.TestCheckResourceAttr(resourceName, "is_disabled", strconv.FormatBool(false)), ), - Config: testAccTenantBasic(lifecycleLocalName, lifecycleName, projectGroupLocalName, projectGroupName, projectLocalName, projectName, projectDescription, environmentLocalName, environmentName, localName, name, description), + Config: testAccTenantBasic(lifecycleLocalName, lifecycleName, projectGroupLocalName, projectGroupName, projectLocalName, projectName, projectDescription, environmentLocalName, environmentName, localName, name, description, false), }, { Check: resource.ComposeTestCheckFunc( testTenantExists(resourceName), resource.TestCheckResourceAttr(resourceName, "name", name), resource.TestCheckResourceAttr(resourceName, "description", newDescription), + resource.TestCheckResourceAttr(resourceName, "is_disabled", strconv.FormatBool(true)), ), - Config: testAccTenantBasic(lifecycleLocalName, lifecycleName, projectGroupLocalName, projectGroupName, projectLocalName, projectName, projectDescription, environmentLocalName, environmentName, localName, name, newDescription), + Config: testAccTenantBasic(lifecycleLocalName, lifecycleName, projectGroupLocalName, projectGroupName, projectLocalName, projectName, projectDescription, environmentLocalName, environmentName, localName, name, newDescription, true), }, }, }) } -func testAccTenantBasic(lifecycleLocalName string, lifecycleName string, projectGroupLocalName string, projectGroupName string, projectLocalName string, projectName string, projectDescription string, environmentLocalName string, environmentName string, localName string, name string, description string) string { +func testAccTenantBasic(lifecycleLocalName string, lifecycleName string, projectGroupLocalName string, projectGroupName string, projectLocalName string, projectName string, projectDescription string, environmentLocalName string, environmentName string, localName string, name string, description string, isDisabled bool) string { allowDynamicInfrastructure := false environmentDescription := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) sortOrder := acctest.RandIntRange(0, 10) @@ -67,13 +70,14 @@ func testAccTenantBasic(lifecycleLocalName string, lifecycleName string, project resource "octopusdeploy_tenant" "%s" { description = "%s" name = "%s" + is_disabled = %v } resource "octopusdeploy_tenant_project" "project_environment" { tenant_id = octopusdeploy_tenant.%s.id project_id = "${octopusdeploy_project.%s.id}" environment_ids = ["${octopusdeploy_environment.%s.id}"] - }`, localName, description, name, localName, projectLocalName, environmentLocalName) + }`, localName, description, name, isDisabled, localName, projectLocalName, environmentLocalName) } func testTenantExists(prefix string) resource.TestCheckFunc { diff --git a/octopusdeploy_framework/schemas/tenant.go b/octopusdeploy_framework/schemas/tenant.go index 5b0c9f695..f0cc07bcb 100644 --- a/octopusdeploy_framework/schemas/tenant.go +++ b/octopusdeploy_framework/schemas/tenant.go @@ -5,6 +5,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" datasourceSchema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" resourceSchema "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" @@ -14,6 +15,7 @@ import ( type TenantModel struct { ClonedFromTenantId types.String `tfsdk:"cloned_from_tenant_id"` Description types.String `tfsdk:"description"` + IsDisabled types.Bool `tfsdk:"is_disabled"` Name types.String `tfsdk:"name"` SpaceID types.String `tfsdk:"space_id"` TenantTags types.Set `tfsdk:"tenant_tags"` @@ -26,6 +28,7 @@ type TenantsModel struct { ID types.String `tfsdk:"id"` IDs types.List `tfsdk:"ids"` IsClone types.Bool `tfsdk:"is_clone"` + IsDisabled types.Bool `tfsdk:"is_disabled"` Name types.String `tfsdk:"name"` PartialName types.String `tfsdk:"partial_name"` ProjectId types.String `tfsdk:"project_id"` @@ -45,6 +48,7 @@ func TenantObjectType() map[string]attr.Type { "cloned_from_tenant_id": types.StringType, "description": types.StringType, "id": types.StringType, + "is_disabled": types.BoolType, "name": types.StringType, "space_id": types.StringType, "tenant_tags": types.SetType{ElemType: types.StringType}, @@ -62,6 +66,7 @@ func FlattenTenant(tenant *tenants.Tenant) attr.Value { "cloned_from_tenant_id": types.StringValue(tenant.ClonedFromTenantID), "description": types.StringValue(tenant.Description), "id": types.StringValue(tenant.GetID()), + "is_disabled": types.BoolValue(tenant.IsDisabled), "name": types.StringValue(tenant.Name), "space_id": types.StringValue(tenant.SpaceID), "tenant_tags": tenantTagsSet, @@ -82,6 +87,10 @@ func (t TenantSchema) GetDatasourceSchema() datasourceSchema.Schema { Description: "A filter to search for cloned resources.", Optional: true, }, + "is_disabled": datasourceSchema.BoolAttribute{ + Description: "A filter to search by the disabled status of a resource.", + Optional: true, + }, "name": datasourceSchema.StringAttribute{ Description: "A filter to search by name.", Optional: true, @@ -106,8 +115,12 @@ func (t TenantSchema) GetDatasourceSchema() datasourceSchema.Schema { }, "description": GetDescriptionDatasourceSchema("tenants"), "id": GetIdDatasourceSchema(true), - "name": GetReadonlyNameDatasourceSchema(), - "space_id": GetSpaceIdDatasourceSchema("tenant", true), + "is_disabled": datasourceSchema.BoolAttribute{ + Description: "The disabled status of this tenant.", + Computed: true, + }, + "name": GetReadonlyNameDatasourceSchema(), + "space_id": GetSpaceIdDatasourceSchema("tenant", true), "tenant_tags": datasourceSchema.SetAttribute{ Computed: true, Description: "A list of tenant tags associated with this resource.", @@ -132,8 +145,16 @@ func (t TenantSchema) GetResourceSchema() resourceSchema.Schema { }, "description": GetDescriptionResourceSchema("tenant"), "id": GetIdResourceSchema(), - "name": GetNameResourceSchema(true), - "space_id": GetSpaceIdResourceSchema("tenant"), + "is_disabled": resourceSchema.BoolAttribute{ + Description: "The disabled status of this tenant.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, + }, + "name": GetNameResourceSchema(true), + "space_id": GetSpaceIdResourceSchema("tenant"), "tenant_tags": resourceSchema.SetAttribute{ Description: "A list of tenant tags associated with this resource.", ElementType: types.StringType, From df9187af4292004fb79e2aea0f1c0cd7b42a4cfa Mon Sep 17 00:00:00 2001 From: Robert Wagner Date: Thu, 28 Nov 2024 11:56:42 +1000 Subject: [PATCH 2/4] chore: fix missing quote in documentation guide --- docs/guides/2-provider-configuration.md | 4 ++-- templates/guides/2-provider-configuration.md.tmpl | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/guides/2-provider-configuration.md b/docs/guides/2-provider-configuration.md index 158c59df3..94deaf8af 100644 --- a/docs/guides/2-provider-configuration.md +++ b/docs/guides/2-provider-configuration.md @@ -15,7 +15,7 @@ subcategory: "Guides" terraform { required_providers { octopusdeploy = { - source = OctopusDeployLabs/octopusdeploy + source = "OctopusDeployLabs/octopusdeploy" } } } @@ -39,7 +39,7 @@ The environment variable fallback values that the Terraform Provider search for terraform { required_providers { octopusdeploy = { - source = OctopusDeployLabs/octopusdeploy + source = "OctopusDeployLabs/octopusdeploy" } } } diff --git a/templates/guides/2-provider-configuration.md.tmpl b/templates/guides/2-provider-configuration.md.tmpl index 158c59df3..94deaf8af 100644 --- a/templates/guides/2-provider-configuration.md.tmpl +++ b/templates/guides/2-provider-configuration.md.tmpl @@ -15,7 +15,7 @@ subcategory: "Guides" terraform { required_providers { octopusdeploy = { - source = OctopusDeployLabs/octopusdeploy + source = "OctopusDeployLabs/octopusdeploy" } } } @@ -39,7 +39,7 @@ The environment variable fallback values that the Terraform Provider search for terraform { required_providers { octopusdeploy = { - source = OctopusDeployLabs/octopusdeploy + source = "OctopusDeployLabs/octopusdeploy" } } } From 7432680e0410ad9573b3992e822486aca2f41fa9 Mon Sep 17 00:00:00 2001 From: domenicsim1 <87625140+domenicsim1@users.noreply.github.com> Date: Thu, 28 Nov 2024 17:55:57 +1100 Subject: [PATCH 3/4] fix: panic on resource tenant project error handling (#824) --- .../resource_tenant_project.go | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/octopusdeploy_framework/resource_tenant_project.go b/octopusdeploy_framework/resource_tenant_project.go index a5f68eac7..408128023 100644 --- a/octopusdeploy_framework/resource_tenant_project.go +++ b/octopusdeploy_framework/resource_tenant_project.go @@ -2,7 +2,9 @@ package octopusdeploy_framework import ( "context" + "errors" "fmt" + internalErrors "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal/errors" "net/http" "strings" @@ -90,11 +92,10 @@ func (t *tenantProjectResource) Read(ctx context.Context, req resource.ReadReque tenant, err := tenants.GetByID(t.Client, spaceID, tenantID) if err != nil { - apiError := err.(*core.APIError) - if apiError.StatusCode != http.StatusNotFound { + if err := internalErrors.ProcessApiErrorV2(ctx, resp, data, err, "tenant"); err != nil { resp.Diagnostics.AddError("unable to load tenant", err.Error()) - return } + return } data.EnvironmentIDs = util.FlattenStringList(tenant.ProjectEnvironments[projectID]) @@ -162,10 +163,12 @@ func (t *tenantProjectResource) Delete(ctx context.Context, req resource.DeleteR tenant, err := tenants.GetByID(t.Client, spaceId, data.TenantID.ValueString()) if err != nil { - apiError := err.(*core.APIError) - if apiError.StatusCode == http.StatusNotFound { - tflog.Info(ctx, fmt.Sprintf("tenant (%s) no longer exists", data.TenantID.ValueString())) - return + var apiError *core.APIError + if errors.As(err, &apiError) { + if apiError.StatusCode == http.StatusNotFound { + tflog.Info(ctx, fmt.Sprintf("tenant (%s) no longer exists", data.TenantID.ValueString())) + return + } } else { resp.Diagnostics.AddError("cannot load tenant", err.Error()) return From e709e02d2111d3204d54375c0547faa63f094451 Mon Sep 17 00:00:00 2001 From: domenicsim1 <87625140+domenicsim1@users.noreply.github.com> Date: Fri, 29 Nov 2024 09:08:53 +1100 Subject: [PATCH 4/4] fix: small step template fixes --- .../resource_step_template.go | 43 +++++++++++-------- .../schemas/step_template.go | 3 +- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/octopusdeploy_framework/resource_step_template.go b/octopusdeploy_framework/resource_step_template.go index 01bf45428..009805903 100644 --- a/octopusdeploy_framework/resource_step_template.go +++ b/octopusdeploy_framework/resource_step_template.go @@ -210,19 +210,7 @@ func mapStepTemplateResourceModelToActionTemplate(ctx context.Context, data sche at.Packages = make([]packages.PackageReference, len(pkgs)) if len(pkgs) > 0 { for i, val := range pkgs { - pkgProps := make(map[string]string, len(val.Properties.Attributes())) - for key, prop := range val.Properties.Attributes() { - if prop.Type(ctx) == types.StringType { - pkgProps[key] = prop.(types.String).ValueString() - } else { - // We should not get this error unless we add a field to package properties in the schema that is not a string - resp.AddError("Unexpected value type in package properties.", - fmt.Sprintf("Expected [%s] to have value of string but got [%s].", key, prop.String())) - } - } - if resp.HasError() { - return at, resp - } + pkgProps := convertAttributeStepTemplatePackageProperty(val.Properties.Attributes()) pkgRef := packages.PackageReference{ AcquisitionLocation: val.AcquisitionLocation.ValueString(), FeedID: val.FeedID.ValueString(), @@ -335,28 +323,28 @@ func convertStepTemplatePackagePropertyAttribute(atpp map[string]string) (types. diags := diag.Diagnostics{} // We need to manually convert the string map to ensure all fields are set. - if extract, ok := atpp["extract"]; ok { + if extract, ok := atpp["Extract"]; ok { prop["extract"] = types.StringValue(extract) } else { diags.AddWarning("Package property missing value.", "extract value missing from package property") prop["extract"] = types.StringNull() } - if purpose, ok := atpp["purpose"]; ok { + if purpose, ok := atpp["Purpose"]; ok { prop["purpose"] = types.StringValue(purpose) } else { diags.AddWarning("Package property missing value.", "purpose value missing from package property") prop["purpose"] = types.StringNull() } - if purpose, ok := atpp["package_parameter_name"]; ok { + if purpose, ok := atpp["PackageParameterName"]; ok { prop["package_parameter_name"] = types.StringValue(purpose) } else { diags.AddWarning("Package property missing value.", "package_parameter_name value missing from package property") prop["package_parameter_name"] = types.StringNull() } - if selectionMode, ok := atpp["selection_mode"]; ok { + if selectionMode, ok := atpp["SelectionMode"]; ok { prop["selection_mode"] = types.StringValue(selectionMode) } else { diags.AddWarning("Package property missing value.", "selection_mode value missing from package property") @@ -370,3 +358,24 @@ func convertStepTemplatePackagePropertyAttribute(atpp map[string]string) (types. } return propMap, diags } + +func convertAttributeStepTemplatePackageProperty(prop map[string]attr.Value) map[string]string { + atpp := make(map[string]string) + + if extract, ok := prop["extract"]; ok { + atpp["Extract"] = extract.(types.String).ValueString() + } + + if purpose, ok := prop["purpose"]; ok { + atpp["Purpose"] = purpose.(types.String).ValueString() + } + + if purpose, ok := prop["package_parameter_name"]; ok { + atpp["PackageParameterName"] = purpose.(types.String).ValueString() + } + + if selectionMode, ok := prop["selection_mode"]; ok { + atpp["SelectionMode"] = selectionMode.(types.String).ValueString() + } + return atpp +} diff --git a/octopusdeploy_framework/schemas/step_template.go b/octopusdeploy_framework/schemas/step_template.go index 7daa17bb3..fab264a27 100644 --- a/octopusdeploy_framework/schemas/step_template.go +++ b/octopusdeploy_framework/schemas/step_template.go @@ -163,6 +163,7 @@ func GetStepTemplateParameterResourceSchema() rs.ListNestedAttribute { "label": rs.StringAttribute{ Description: "The label shown beside the parameter when presented in the deployment process. Example: `Server name`.", Optional: true, + Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, @@ -213,7 +214,7 @@ func GetStepTemplatePackageResourceSchema() rs.ListNestedAttribute { Optional: true, Computed: true, Validators: []validator.String{ - stringvalidator.RegexMatches(regexp.MustCompile("^(True|Fasle)$"), "Extract must be True or False"), + stringvalidator.RegexMatches(regexp.MustCompile("^(True|False)$"), "Extract must be True or False"), }, }, "package_parameter_name": rs.StringAttribute{