From cb9c42156a4759a0f3f9a5e9f41cc3761155b9bd Mon Sep 17 00:00:00 2001 From: Huy Nguyen <162080607+HuyPhanNguyen@users.noreply.github.com> Date: Mon, 5 Aug 2024 18:04:56 +1000 Subject: [PATCH 01/28] chore!: migrate project resource & datasource (#689) * Initial project resource * Fix old test * update schema * First test pass * implement flatten GitPersistenceSetting * Fix issue expand template * reverse test * Implement GitLibraryPersistenceSettings expand * Just check old provider * Add project data source * refactor project schema * fix datasource project build faile * more refactor * Fix issue helptext null * Fix datasource name * Handle skipMachineBehavior * Fix bugs * more bugs fix * Fix data source issue * fix GitCredentialTypeUsernamePassword test issue * Fix git setting again * Fix versioning strategy not return by API * fix bugs persistenceSettings versioningStrategy connectivityPolicy not return from API * fix incorrect test config * Cleanup * Get deployment settings from api server * Fix stage issue * refactor to use attribute builder * Update project resource schema * Update schema builder * Fix project datasource schema * Fix list test fail * Update tf configuration test script * Refactor * Remove SkipCI for tenant variable test * refactor * Fix comment * switch back base_path and default branch to optional * Fix other bug for ReleaseNotesTemplate --- docs/data-sources/projects.md | 146 ++-- docs/resources/project.md | 46 +- docs/resources/project_group.md | 4 +- octopusdeploy/data_source_projects.go | 52 -- octopusdeploy/provider.go | 2 - octopusdeploy/resource_channel_test.go | 97 +++ .../resource_deployment_process_test.go | 14 + octopusdeploy/resource_project.go | 156 ---- octopusdeploy/resource_tenant_test.go | 2 - octopusdeploy/schema_project.go | 705 ------------------ octopusdeploy_framework/datasource_project.go | 95 +++ octopusdeploy_framework/framework_provider.go | 2 + octopusdeploy_framework/resource_project.go | 220 ++++++ .../resource_project_expand.go | 339 +++++++++ .../resource_project_flatten.go | 427 +++++++++++ .../resource_project_model.go | 114 +++ .../resource_project_test.go | 129 +--- .../resource_tenant_common_variable_test.go | 81 +- .../resource_tenant_project_variable_test.go | 9 +- octopusdeploy_framework/schemas/project.go | 379 ++++++++++ .../util/datasource_attribute_builder.go | 182 +++++ .../util/resource_attribute_builder.go | 253 +++++++ octopusdeploy_framework/util/schema.go | 1 + octopusdeploy_framework/util/util.go | 7 + terraform/19b-projectspace/project.tf | 7 + terraform/39-projectgitusername/project.tf | 2 +- 26 files changed, 2327 insertions(+), 1144 deletions(-) delete mode 100644 octopusdeploy/data_source_projects.go delete mode 100644 octopusdeploy/resource_project.go delete mode 100644 octopusdeploy/schema_project.go create mode 100644 octopusdeploy_framework/datasource_project.go create mode 100644 octopusdeploy_framework/resource_project.go create mode 100644 octopusdeploy_framework/resource_project_expand.go create mode 100644 octopusdeploy_framework/resource_project_flatten.go create mode 100644 octopusdeploy_framework/resource_project_model.go rename {octopusdeploy => octopusdeploy_framework}/resource_project_test.go (83%) create mode 100644 octopusdeploy_framework/schemas/project.go create mode 100644 octopusdeploy_framework/util/datasource_attribute_builder.go create mode 100644 octopusdeploy_framework/util/resource_attribute_builder.go diff --git a/docs/data-sources/projects.md b/docs/data-sources/projects.md index 30f710030..bc050a7be 100644 --- a/docs/data-sources/projects.md +++ b/docs/data-sources/projects.md @@ -3,12 +3,12 @@ page_title: "octopusdeploy_projects Data Source - terraform-provider-octopusdeploy" subcategory: "" description: |- - Provides information about existing projects. + Provides information about existing Octopus Deploy projects. --- # octopusdeploy_projects (Data Source) -Provides information about existing projects. +Provides information about existing Octopus Deploy projects. ## Example Usage @@ -32,64 +32,74 @@ data "octopusdeploy_projects" "example" { - `cloned_from_project_id` (String) A filter to search for cloned resources by a project ID. - `ids` (List of String) A filter to search by a list of IDs. - `is_clone` (Boolean) A filter to search for cloned resources. -- `name` (String) A filter to search by name. -- `partial_name` (String) A filter to search by the partial match of a name. +- `name` (String) A filter to search by name +- `partial_name` (String) A filter to search by a partial name. - `skip` (Number) A filter to specify the number of items to skip in the response. -- `space_id` (String) A Space ID to filter by. Will revert what is specified on the provider if not set. +- `space_id` (String) A Space ID to filter by. Will revert what is specified on the provider if not set - `take` (Number) A filter to specify the number of items to take (or return) in the response. ### Read-Only - `id` (String) An auto-generated identifier that includes the timestamp when this data source was last modified. -- `projects` (Block List) A list of projects that match the filter(s). (see [below for nested schema](#nestedblock--projects)) +- `projects` (Attributes List) A list of projects that match the filter(s). (see [below for nested schema](#nestedatt--projects)) - + ### Nested Schema for `projects` Read-Only: - `allow_deployments_to_no_targets` (Boolean, Deprecated) - `auto_create_release` (Boolean) -- `auto_deploy_release_overrides` (List of String) +- `auto_deploy_release_overrides` (Attributes List) (see [below for nested schema](#nestedatt--projects--auto_deploy_release_overrides)) - `cloned_from_project_id` (String) -- `connectivity_policy` (List of Object) (see [below for nested schema](#nestedatt--projects--connectivity_policy)) +- `connectivity_policy` (Attributes List) (see [below for nested schema](#nestedatt--projects--connectivity_policy)) - `default_guided_failure_mode` (String) - `default_to_skip_if_already_installed` (Boolean) - `deployment_changes_template` (String) - `deployment_process_id` (String) -- `description` (String) The description of this project. +- `description` (String) The description of this project - `discrete_channel_release` (Boolean) Treats releases of different channels to the same environment as a separate deployment dimension -- `git_anonymous_persistence_settings` (List of Object) Provides Git-related persistence settings for a version-controlled project. (see [below for nested schema](#nestedatt--projects--git_anonymous_persistence_settings)) -- `git_library_persistence_settings` (List of Object) Provides Git-related persistence settings for a version-controlled project. (see [below for nested schema](#nestedatt--projects--git_library_persistence_settings)) -- `git_username_password_persistence_settings` (List of Object) Provides Git-related persistence settings for a version-controlled project. (see [below for nested schema](#nestedatt--projects--git_username_password_persistence_settings)) -- `id` (String) The unique ID for this resource. +- `git_anonymous_persistence_settings` (Attributes List) Git-related persistence settings for a version-controlled project using anonymous authentication. (see [below for nested schema](#nestedatt--projects--git_anonymous_persistence_settings)) +- `git_library_persistence_settings` (Attributes List) Git-related persistence settings for a version-controlled project using library authentication. (see [below for nested schema](#nestedatt--projects--git_library_persistence_settings)) +- `git_username_password_persistence_settings` (Attributes List) Git-related persistence settings for a version-controlled project using username_password authentication. (see [below for nested schema](#nestedatt--projects--git_username_password_persistence_settings)) +- `id` (String) - `included_library_variable_sets` (List of String) - `is_disabled` (Boolean) -- `is_discrete_channel_release` (Boolean) Treats releases of different channels to the same environment as a separate deployment dimension +- `is_discrete_channel_release` (Boolean) - `is_version_controlled` (Boolean) -- `jira_service_management_extension_settings` (List of Object) Provides extension settings for the Jira Service Management (JSM) integration for this project. (see [below for nested schema](#nestedatt--projects--jira_service_management_extension_settings)) -- `lifecycle_id` (String) The lifecycle ID associated with this project. +- `jira_service_management_extension_settings` (Attributes List) Extension settings for the Jira Service Management (JSM) integration. (see [below for nested schema](#nestedatt--projects--jira_service_management_extension_settings)) +- `lifecycle_id` (String) The lifecycle ID associated with this project - `name` (String) The name of the project in Octopus Deploy. This name must be unique. - `project_group_id` (String) The project group ID associated with this project. -- `release_creation_strategy` (List of Object) (see [below for nested schema](#nestedatt--projects--release_creation_strategy)) -- `release_notes_template` (String) -- `servicenow_extension_settings` (List of Object) Provides extension settings for the ServiceNow integration for this project. (see [below for nested schema](#nestedatt--projects--servicenow_extension_settings)) +- `release_creation_strategy` (Attributes List) The release creation strategy for the project. (see [below for nested schema](#nestedatt--projects--release_creation_strategy)) +- `release_notes_template` (String) The template to use for release notes. +- `servicenow_extension_settings` (Attributes List) Extension settings for the ServiceNow integration. (see [below for nested schema](#nestedatt--projects--servicenow_extension_settings)) - `slug` (String) A human-readable, unique identifier, used to identify a project. - `space_id` (String) The space ID associated with this project. -- `template` (List of Object) (see [below for nested schema](#nestedatt--projects--template)) -- `tenanted_deployment_participation` (String) The tenanted deployment mode of the resource. Valid account types are `Untenanted`, `TenantedOrUntenanted`, or `Tenanted`. -- `variable_set_id` (String) -- `versioning_strategy` (Set of Object) (see [below for nested schema](#nestedatt--projects--versioning_strategy)) +- `template` (Attributes List) Template parameters for the project. (see [below for nested schema](#nestedatt--projects--template)) +- `tenanted_deployment_participation` (String) The tenanted deployment mode of the project. +- `variable_set_id` (String) The ID of the variable set associated with this project. +- `versioning_strategy` (Attributes List) The versioning strategy for the project. (see [below for nested schema](#nestedatt--projects--versioning_strategy)) + + +### Nested Schema for `projects.auto_deploy_release_overrides` + +Read-Only: + +- `environment_id` (String) The environment ID for the auto deploy release override. +- `release_id` (String) The release ID for the auto deploy release override. +- `tenant_id` (String) The tenant ID for the auto deploy release override. + ### Nested Schema for `projects.connectivity_policy` Read-Only: -- `allow_deployments_to_no_targets` (Boolean) -- `exclude_unhealthy_targets` (Boolean) -- `skip_machine_behavior` (String) -- `target_roles` (List of String) +- `allow_deployments_to_no_targets` (Boolean) Allow deployments to be created when there are no targets. +- `exclude_unhealthy_targets` (Boolean) Exclude unhealthy targets from deployments. +- `skip_machine_behavior` (String) The behavior when a machine is skipped. +- `target_roles` (List of String) The target roles for the connectivity policy. @@ -97,10 +107,10 @@ Read-Only: Read-Only: -- `base_path` (String) -- `default_branch` (String) -- `protected_branches` (Set of String) -- `url` (String) +- `base_path` (String) The base path associated with these version control settings. +- `default_branch` (String) The default branch associated with these version control settings. +- `protected_branches` (Set of String) A list of protected branch patterns. +- `url` (String) The URL associated with these version control settings. @@ -108,11 +118,11 @@ Read-Only: Read-Only: -- `base_path` (String) -- `default_branch` (String) -- `git_credential_id` (String) -- `protected_branches` (Set of String) -- `url` (String) +- `base_path` (String) The base path associated with these version control settings. +- `default_branch` (String) The default branch associated with these version control settings. +- `git_credential_id` (String) The ID of the Git credential. +- `protected_branches` (Set of String) A list of protected branch patterns. +- `url` (String) The URL associated with these version control settings. @@ -120,12 +130,12 @@ Read-Only: Read-Only: -- `base_path` (String) -- `default_branch` (String) -- `password` (String) -- `protected_branches` (Set of String) -- `url` (String) -- `username` (String) +- `base_path` (String) The base path associated with these version control settings. +- `default_branch` (String) The default branch associated with these version control settings. +- `password` (String, Sensitive) The password for the Git credential. +- `protected_branches` (Set of String) A list of protected branch patterns. +- `url` (String) The URL associated with these version control settings. +- `username` (String) The username for the Git credential. @@ -133,9 +143,9 @@ Read-Only: Read-Only: -- `connection_id` (String) -- `is_enabled` (Boolean) -- `service_desk_project_name` (String) +- `connection_id` (String) The connection identifier for JSM. +- `is_enabled` (Boolean) Whether the JSM extension is enabled. +- `service_desk_project_name` (String) The JSM service desk project name. @@ -143,17 +153,17 @@ Read-Only: Read-Only: -- `channel_id` (String) -- `release_creation_package` (List of Object) (see [below for nested schema](#nestedobjatt--projects--release_creation_strategy--release_creation_package)) -- `release_creation_package_step_id` (String) +- `channel_id` (String) The ID of the channel to use for release creation. +- `release_creation_package` (Attributes List) Details of the package used for release creation. (see [below for nested schema](#nestedatt--projects--release_creation_strategy--release_creation_package)) +- `release_creation_package_step_id` (String) The ID of the step containing the package for release creation. - + ### Nested Schema for `projects.release_creation_strategy.release_creation_package` Read-Only: -- `deployment_action` (String) -- `package_reference` (String) +- `deployment_action` (String) The deployment action for the release creation package. +- `package_reference` (String) The package reference for the release creation package. @@ -162,10 +172,10 @@ Read-Only: Read-Only: -- `connection_id` (String) -- `is_enabled` (Boolean) -- `is_state_automatically_transitioned` (Boolean) -- `standard_change_template_name` (String) +- `connection_id` (String) The connection identifier for ServiceNow. +- `is_enabled` (Boolean) Whether the ServiceNow extension is enabled. +- `is_state_automatically_transitioned` (Boolean) Whether state is automatically transitioned in ServiceNow. +- `standard_change_template_name` (String) The name of the standard change template in ServiceNow. @@ -173,12 +183,12 @@ Read-Only: Read-Only: -- `default_value` (String) -- `display_settings` (Map of String) -- `help_text` (String) -- `id` (String) -- `label` (String) -- `name` (String) +- `default_value` (String) The default value for the parameter. +- `display_settings` (Map of String) The display settings for the parameter. +- `help_text` (String) The help text for the parameter. +- `id` (String) The ID of the template parameter. +- `label` (String) The label shown beside the parameter. +- `name` (String) The name of the variable set by the parameter. @@ -186,16 +196,16 @@ Read-Only: Read-Only: -- `donor_package` (List of Object) (see [below for nested schema](#nestedobjatt--projects--versioning_strategy--donor_package)) -- `donor_package_step_id` (String) -- `template` (String) +- `donor_package` (Attributes List) (see [below for nested schema](#nestedatt--projects--versioning_strategy--donor_package)) +- `donor_package_step_id` (String) The ID of the step containing the donor package. +- `template` (String) The template to use for version numbers. - + ### Nested Schema for `projects.versioning_strategy.donor_package` Read-Only: -- `deployment_action` (String) -- `package_reference` (String) +- `deployment_action` (String) The deployment action for the donor package. +- `package_reference` (String) The package reference for the donor package. diff --git a/docs/resources/project.md b/docs/resources/project.md index 3c2d7920f..6763fc2fd 100644 --- a/docs/resources/project.md +++ b/docs/resources/project.md @@ -67,44 +67,54 @@ resource "octopusdeploy_project" "example" { ### Required - `lifecycle_id` (String) The lifecycle ID associated with this project. -- `name` (String) The name of the project in Octopus Deploy. This name must be unique. +- `name` (String) The name of this resource. - `project_group_id` (String) The project group ID associated with this project. ### Optional - `allow_deployments_to_no_targets` (Boolean, Deprecated) - `auto_create_release` (Boolean) -- `auto_deploy_release_overrides` (List of String) -- `cloned_from_project_id` (String) -- `connectivity_policy` (Block List, Max: 1) (see [below for nested schema](#nestedblock--connectivity_policy)) +- `auto_deploy_release_overrides` (Block List) (see [below for nested schema](#nestedblock--auto_deploy_release_overrides)) +- `cloned_from_project_id` (String) The ID of the project this project was cloned from. +- `connectivity_policy` (Block List) (see [below for nested schema](#nestedblock--connectivity_policy)) - `default_guided_failure_mode` (String) - `default_to_skip_if_already_installed` (Boolean) - `deployment_changes_template` (String) - `description` (String) The description of this project. - `discrete_channel_release` (Boolean) Treats releases of different channels to the same environment as a separate deployment dimension -- `git_anonymous_persistence_settings` (Block List, Max: 1) Provides Git-related persistence settings for a version-controlled project. (see [below for nested schema](#nestedblock--git_anonymous_persistence_settings)) -- `git_library_persistence_settings` (Block List, Max: 1) Provides Git-related persistence settings for a version-controlled project. (see [below for nested schema](#nestedblock--git_library_persistence_settings)) -- `git_username_password_persistence_settings` (Block List, Max: 1) Provides Git-related persistence settings for a version-controlled project. (see [below for nested schema](#nestedblock--git_username_password_persistence_settings)) +- `git_anonymous_persistence_settings` (Block List) Provides Git-related persistence settings for a version-controlled project. (see [below for nested schema](#nestedblock--git_anonymous_persistence_settings)) +- `git_library_persistence_settings` (Block List) Provides Git-related persistence settings for a version-controlled project. (see [below for nested schema](#nestedblock--git_library_persistence_settings)) +- `git_username_password_persistence_settings` (Block List) Provides Git-related persistence settings for a version-controlled project. (see [below for nested schema](#nestedblock--git_username_password_persistence_settings)) - `id` (String) The unique ID for this resource. -- `included_library_variable_sets` (List of String) +- `included_library_variable_sets` (List of String) The list of included library variable set IDs. - `is_disabled` (Boolean) - `is_discrete_channel_release` (Boolean) Treats releases of different channels to the same environment as a separate deployment dimension - `is_version_controlled` (Boolean) -- `jira_service_management_extension_settings` (Block List, Max: 1) Provides extension settings for the Jira Service Management (JSM) integration for this project. (see [below for nested schema](#nestedblock--jira_service_management_extension_settings)) -- `release_creation_strategy` (Block List, Max: 1) (see [below for nested schema](#nestedblock--release_creation_strategy)) +- `jira_service_management_extension_settings` (Block List) Provides extension settings for the Jira Service Management (JSM) integration for this project. (see [below for nested schema](#nestedblock--jira_service_management_extension_settings)) +- `release_creation_strategy` (Block List) (see [below for nested schema](#nestedblock--release_creation_strategy)) - `release_notes_template` (String) -- `servicenow_extension_settings` (Block List, Max: 1) Provides extension settings for the ServiceNow integration for this project. (see [below for nested schema](#nestedblock--servicenow_extension_settings)) +- `servicenow_extension_settings` (Block List) Provides extension settings for the ServiceNow integration for this project. (see [below for nested schema](#nestedblock--servicenow_extension_settings)) - `slug` (String) A human-readable, unique identifier, used to identify a project. - `space_id` (String) The space ID associated with this project. - `template` (Block List) (see [below for nested schema](#nestedblock--template)) - `tenanted_deployment_participation` (String) The tenanted deployment mode of the resource. Valid account types are `Untenanted`, `TenantedOrUntenanted`, or `Tenanted`. -- `versioning_strategy` (Block Set) (see [below for nested schema](#nestedblock--versioning_strategy)) +- `versioning_strategy` (Block List) (see [below for nested schema](#nestedblock--versioning_strategy)) ### Read-Only - `deployment_process_id` (String) - `variable_set_id` (String) + +### Nested Schema for `auto_deploy_release_overrides` + +Optional: + +- `environment_id` (String) +- `release_id` (String) +- `tenant_id` (String) + + ### Nested Schema for `connectivity_policy` @@ -150,7 +160,7 @@ Optional: Required: -- `password` (String, Sensitive) The password for the Git credential. +- `password` (String, Sensitive) The password for the Git credential - `url` (String) The URL associated with these version control settings. - `username` (String) The username for the Git credential. @@ -177,7 +187,7 @@ Required: Optional: - `channel_id` (String) -- `release_creation_package` (Block List, Max: 1) (see [below for nested schema](#nestedblock--release_creation_strategy--release_creation_package)) +- `release_creation_package` (Block List) (see [below for nested schema](#nestedblock--release_creation_strategy--release_creation_package)) - `release_creation_package_step_id` (String) @@ -209,15 +219,15 @@ Optional: Required: -- `name` (String) The name of the variable set by the parameter. The name can contain letters, digits, dashes and periods. Example: `ServerName`. +- `name` (String) The name of the variable set by the parameter. The name can contain letters, digits, dashes and periods. Optional: - `default_value` (String) A default value for the parameter, if applicable. This can be a hard-coded value or a variable reference. - `display_settings` (Map of String) The display settings for the parameter. - `help_text` (String) The help presented alongside the parameter input. -- `id` (String) The unique ID for this resource. -- `label` (String) The label shown beside the parameter when presented in the deployment process. Example: `Server name`. +- `id` (String) The ID of the template parameter. +- `label` (String) The label shown beside the parameter when presented in the deployment process. @@ -225,7 +235,7 @@ Optional: Optional: -- `donor_package` (Block List, Max: 1) (see [below for nested schema](#nestedblock--versioning_strategy--donor_package)) +- `donor_package` (Block List) (see [below for nested schema](#nestedblock--versioning_strategy--donor_package)) - `donor_package_step_id` (String) - `template` (String) diff --git a/docs/resources/project_group.md b/docs/resources/project_group.md index 8bd9fc7d0..c0d250e03 100644 --- a/docs/resources/project_group.md +++ b/docs/resources/project_group.md @@ -3,12 +3,12 @@ page_title: "octopusdeploy_project_group Resource - terraform-provider-octopusdeploy" subcategory: "" description: |- - This resource manages project groups in Octopus Deploy. + --- # octopusdeploy_project_group (Resource) -This resource manages project groups in Octopus Deploy. + ## Example Usage diff --git a/octopusdeploy/data_source_projects.go b/octopusdeploy/data_source_projects.go deleted file mode 100644 index 0410efae8..000000000 --- a/octopusdeploy/data_source_projects.go +++ /dev/null @@ -1,52 +0,0 @@ -package octopusdeploy - -import ( - "context" - "time" - - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client" - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func dataSourceProjects() *schema.Resource { - return &schema.Resource{ - Description: "Provides information about existing projects.", - ReadContext: dataSourceProjectsRead, - Schema: getProjectDataSchema(), - } -} - -func dataSourceProjectsRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Info(ctx, "reading projects") - - query := projects.ProjectsQuery{ - ClonedFromProjectID: d.Get("cloned_from_project_id").(string), - IDs: expandArray(d.Get("ids").([]interface{})), - IsClone: d.Get("is_clone").(bool), - Name: d.Get("name").(string), - PartialName: d.Get("partial_name").(string), - Skip: d.Get("skip").(int), - Take: d.Get("take").(int), - } - - spaceID := d.Get("space_id").(string) - - client := m.(*client.Client) - existingProjects, err := projects.Get(client, spaceID, query) - if err != nil { - return diag.FromErr(err) - } - - flattenedProjects := []interface{}{} - for _, project := range existingProjects.Items { - flattenedProjects = append(flattenedProjects, flattenProject(ctx, d, project)) - } - - d.Set("projects", flattenedProjects) - d.SetId("Projects " + time.Now().UTC().String()) - - return nil -} diff --git a/octopusdeploy/provider.go b/octopusdeploy/provider.go index b97e48a48..cd51d9521 100644 --- a/octopusdeploy/provider.go +++ b/octopusdeploy/provider.go @@ -26,7 +26,6 @@ func Provider() *schema.Provider { "octopusdeploy_machine_policies": dataSourceMachinePolicies(), "octopusdeploy_offline_package_drop_deployment_targets": dataSourceOfflinePackageDropDeploymentTargets(), "octopusdeploy_polling_tentacle_deployment_targets": dataSourcePollingTentacleDeploymentTargets(), - "octopusdeploy_projects": dataSourceProjects(), "octopusdeploy_script_modules": dataSourceScriptModules(), "octopusdeploy_ssh_connection_deployment_targets": dataSourceSSHConnectionDeploymentTargets(), "octopusdeploy_tag_sets": dataSourceTagSets(), @@ -59,7 +58,6 @@ func Provider() *schema.Provider { "octopusdeploy_offline_package_drop_deployment_target": resourceOfflinePackageDropDeploymentTarget(), "octopusdeploy_polling_tentacle_deployment_target": resourcePollingTentacleDeploymentTarget(), "octopusdeploy_polling_subscription_id": resourcePollingSubscriptionId(), - "octopusdeploy_project": resourceProject(), "octopusdeploy_project_deployment_target_trigger": resourceProjectDeploymentTargetTrigger(), "octopusdeploy_external_feed_create_release_trigger": resourceExternalFeedCreateReleaseTrigger(), "octopusdeploy_project_scheduled_trigger": resourceProjectScheduledTrigger(), diff --git a/octopusdeploy/resource_channel_test.go b/octopusdeploy/resource_channel_test.go index ddaa59664..9d58ac81a 100644 --- a/octopusdeploy/resource_channel_test.go +++ b/octopusdeploy/resource_channel_test.go @@ -198,6 +198,58 @@ func testAccChannelBasic(localName string, lifecycleLocalName string, lifecycleN }`, localName, description, name, projectLocalName) } +func testAccProjectBasic(lifecycleLocalName string, lifecycleName string, projectGroupLocalName string, projectGroupName string, localName string, name string, description string) string { + projectGroup := internalTest.NewProjectGroupTestOptions() + projectGroup.LocalName = projectGroupLocalName + projectGroup.Resource.Name = projectGroupName + + return fmt.Sprintf(testAccLifecycle(lifecycleLocalName, lifecycleName)+"\n"+ + internalTest.ProjectGroupConfiguration(projectGroup)+"\n"+ + `resource "octopusdeploy_project" "%s" { + description = "%s" + lifecycle_id = octopusdeploy_lifecycle.%s.id + name = "%s" + project_group_id = octopusdeploy_project_group.%s.id + + template { + default_value = "default-value" + help_text = "help-test" + label = "label" + name = "2" + + display_settings = { + "Octopus.ControlType": "SingleLineText" + } + } + + template { + default_value = "default-value" + help_text = "help-test" + label = "label" + name = "1" + + display_settings = { + "Octopus.ControlType": "SingleLineText" + } + } + + // connectivity_policy { + // allow_deployments_to_no_targets = true + // skip_machine_behavior = "None" + // } + + // version_control_settings { + // default_branch = "foo" + // url = "https://example.com/" + // username = "bar" + // } + + // versioning_strategy { + // template = "alskdjaslkdj" + // } + }`, localName, description, lifecycleLocalName, name, projectGroupLocalName) +} + func testAccChannelWithOneRule(name, description, versionRange, actionName string) string { projectGroupName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) projectName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) @@ -435,3 +487,48 @@ func TestProjectChannelResource(t *testing.T) { t.Fatal("The environment lookup did not succeed. Lookup value was \"" + lookup + "\" while the resource value was \"" + resource.ID + "\".") } } + +type ProjectTestOptions struct { + AllowDeploymentsToNoTargets bool + LifecycleLocalName string + LocalName string + Name string + ProjectGroupLocalName string +} + +func NewProjectTestOptions(projectGroupLocalName string, lifecycleLocalName string) *ProjectTestOptions { + return &ProjectTestOptions{ + LifecycleLocalName: lifecycleLocalName, + LocalName: acctest.RandStringFromCharSet(20, acctest.CharSetAlpha), + Name: acctest.RandStringFromCharSet(20, acctest.CharSetAlpha), + ProjectGroupLocalName: projectGroupLocalName, + } +} + +func testAccProjectWithOptions(opt *ProjectTestOptions) string { + + return fmt.Sprintf(`resource "octopusdeploy_project" "%s" { + allow_deployments_to_no_targets = %v + lifecycle_id = octopusdeploy_lifecycle.%s.id + name = "%s" + project_group_id = octopusdeploy_project_group.%s.id + }`, opt.LocalName, opt.AllowDeploymentsToNoTargets, opt.LifecycleLocalName, opt.Name, opt.ProjectGroupLocalName) +} + +func testAccProjectWithTemplate(localName string, name string, lifecycleLocalName string, projectGroupLocalName string) string { + return fmt.Sprintf(`resource "octopusdeploy_project" "%s" { + lifecycle_id = octopusdeploy_lifecycle.%s.id + name = "%s" + project_group_id = octopusdeploy_project_group.%s.id + + template { + name = "project variable template name" + label = "project variable template label" + + display_settings = { + "Octopus.ControlType" = "Sensitive" + } + } + }`, localName, lifecycleLocalName, name, projectGroupLocalName) + +} diff --git a/octopusdeploy/resource_deployment_process_test.go b/octopusdeploy/resource_deployment_process_test.go index ec7d06cec..d800b81da 100644 --- a/octopusdeploy/resource_deployment_process_test.go +++ b/octopusdeploy/resource_deployment_process_test.go @@ -65,6 +65,20 @@ import ( // }`, options.LocalName, options.Project.LocalName, options.StepName, options.ActionType, options.ActionName, options.PackageName, options.PackageID) // } +func testAccProjectCheckDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "octopusdeploy_project" { + continue + } + + if project, err := octoClient.Projects.GetByID(rs.Primary.ID); err == nil { + return fmt.Errorf("project (%s) still exists", project.GetID()) + } + } + + return nil +} + func TestAccOctopusDeployDeploymentProcessBasic(t *testing.T) { localName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) resourceName := "octopusdeploy_deployment_process." + localName diff --git a/octopusdeploy/resource_project.go b/octopusdeploy/resource_project.go deleted file mode 100644 index a45eb4577..000000000 --- a/octopusdeploy/resource_project.go +++ /dev/null @@ -1,156 +0,0 @@ -package octopusdeploy - -import ( - "context" - "fmt" - - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client" - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects" - "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal/errors" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func resourceProject() *schema.Resource { - return &schema.Resource{ - CreateContext: resourceProjectCreate, - DeleteContext: resourceProjectDelete, - Description: "This resource manages projects in Octopus Deploy.", - Importer: getImporter(), - ReadContext: resourceProjectRead, - Schema: getProjectSchema(), - UpdateContext: resourceProjectUpdate, - } -} - -func resourceProjectCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - project := expandProject(ctx, d) - - // DANGER: the go provider is about to nil the persistence settings, to stop the API from exploding. Take a copy - // so we can make decisions. - persistenceSettings := project.PersistenceSettings - - tflog.Info(ctx, fmt.Sprintf("creating project (%s)", project.Name)) - - var spaceID string - if v, ok := d.GetOk("space_id"); ok { - spaceID = v.(string) - } - - client := m.(*client.Client) - createdProject, err := projects.Add(client, project) - if err != nil { - return diag.FromErr(err) - } - - if persistenceSettings != nil && persistenceSettings.Type() == projects.PersistenceSettingsTypeVersionControlled { - tflog.Info(ctx, "converting project to use VCS") - - vcsProject, err := projects.ConvertToVCS(client, createdProject, "converting project to use VCS", "", persistenceSettings.(projects.GitPersistenceSettings)) - if err != nil { - projects.DeleteByID(client, spaceID, createdProject.GetID()) - return diag.FromErr(err) - } - createdProject.PersistenceSettings = vcsProject.PersistenceSettings - } - - createdProject, err = projects.GetByID(client, spaceID, createdProject.GetID()) - if err != nil { - return diag.FromErr(err) - } - - if err := setProject(ctx, d, createdProject); err != nil { - return diag.FromErr(err) - } - - d.SetId(createdProject.GetID()) - - tflog.Info(ctx, fmt.Sprintf("project created (%s)", d.Id())) - return nil -} - -func resourceProjectDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Info(ctx, fmt.Sprintf("deleting project (%s)", d.Id())) - - var spaceID string - if v, ok := d.GetOk("space_id"); ok { - spaceID = v.(string) - } - - client := m.(*client.Client) - if err := projects.DeleteByID(client, spaceID, d.Id()); err != nil { - return diag.FromErr(err) - } - - tflog.Info(ctx, fmt.Sprintf("project deleted (%s)", d.Id())) - d.SetId("") - return nil -} - -func resourceProjectRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Info(ctx, fmt.Sprintf("reading project (%s)", d.Id())) - - var spaceID string - if v, ok := d.GetOk("space_id"); ok { - spaceID = v.(string) - } - - client := m.(*client.Client) - project, err := projects.GetByID(client, spaceID, d.Id()) - if err != nil { - return errors.ProcessApiError(ctx, d, err, "project") - } - - if err := setProject(ctx, d, project); err != nil { - return diag.FromErr(err) - } - - tflog.Info(ctx, fmt.Sprintf("project read (%s)", d.Id())) - return nil -} - -func resourceProjectUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Info(ctx, fmt.Sprintf("updating project (%s)", d.Id())) - - client := m.(*client.Client) - project := expandProject(ctx, d) - var updatedProject *projects.Project - var err error - - projectLinks, err := projects.GetByID(client, project.SpaceID, d.Id()) - if err != nil { - return diag.FromErr(err) - } - - if project.PersistenceSettings != nil && project.PersistenceSettings.Type() == projects.PersistenceSettingsTypeVersionControlled { - convertToVcsLink := projectLinks.Links["ConvertToVcs"] - - if len(convertToVcsLink) != 0 { - versionControlSettings := expandVersionControlSettingsForProjectConversion(ctx, d) - - tflog.Info(ctx, fmt.Sprintf("converting project to use VCS (%s)", d.Id())) - - project.Links["ConvertToVcs"] = convertToVcsLink - vcsProject, err := projects.ConvertToVCS(client, project, "converting project to use VCS", "", versionControlSettings) - if err != nil { - return diag.FromErr(err) - } - project.PersistenceSettings = vcsProject.PersistenceSettings - } - } - - project.Links = projectLinks.Links - - updatedProject, err = projects.Update(client, project) - if err != nil { - return diag.FromErr(err) - } - - if err := setProject(ctx, d, updatedProject); err != nil { - return diag.FromErr(err) - } - - tflog.Info(ctx, fmt.Sprintf("project updated (%s)", d.Id())) - return nil -} diff --git a/octopusdeploy/resource_tenant_test.go b/octopusdeploy/resource_tenant_test.go index ac2117870..22b03b365 100644 --- a/octopusdeploy/resource_tenant_test.go +++ b/octopusdeploy/resource_tenant_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tenants" - internalTest "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal/test" "github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework/octoclient" "github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework/test" @@ -16,7 +15,6 @@ import ( ) func TestAccTenantBasic(t *testing.T) { - internalTest.SkipCI(t, "project_environment have been refactor [deprecated] - will enable this test later after Ben fix") lifecycleLocalName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) lifecycleName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) projectGroupLocalName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) diff --git a/octopusdeploy/schema_project.go b/octopusdeploy/schema_project.go deleted file mode 100644 index 695f25f32..000000000 --- a/octopusdeploy/schema_project.go +++ /dev/null @@ -1,705 +0,0 @@ -package octopusdeploy - -import ( - "context" - "fmt" - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/actiontemplates" - "github.com/hashicorp/terraform-plugin-log/tflog" - - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/credentials" - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/extensions" - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects" - prj "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal/projects" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" -) - -func expandProject(ctx context.Context, d *schema.ResourceData) *projects.Project { - name := d.Get("name").(string) - lifecycleID := d.Get("lifecycle_id").(string) - projectGroupID := d.Get("project_group_id").(string) - - project := projects.NewProject(name, lifecycleID, projectGroupID) - project.ID = d.Id() - - if v, ok := d.GetOk("auto_create_release"); ok { - project.AutoCreateRelease = v.(bool) - } - - if v, ok := d.GetOk("auto_deploy_release_overrides"); ok { - project.AutoDeployReleaseOverrides = v.([]projects.AutoDeployReleaseOverride) - } - - if v, ok := d.GetOk("cloned_from_project_id"); ok { - project.ClonedFromProjectID = v.(string) - } - - if v, ok := d.GetOk("connectivity_policy"); ok { - project.ConnectivityPolicy = expandConnectivityPolicy(v.([]interface{})) - } - - if v, ok := d.GetOk("default_guided_failure_mode"); ok { - project.DefaultGuidedFailureMode = v.(string) - } - - if v, ok := d.GetOk("default_to_skip_if_already_installed"); ok { - project.DefaultToSkipIfAlreadyInstalled = v.(bool) - } - - if v, ok := d.GetOk("deployment_changes_template"); ok { - project.DeploymentChangesTemplate = v.(string) - } - - if v, ok := d.GetOk("deployment_process_id"); ok { - project.DeploymentProcessID = v.(string) - } - - if v, ok := d.GetOk("description"); ok { - project.Description = v.(string) - } - - tflog.Info(ctx, "expanding persistence settings") - - if v, ok := d.GetOk("git_library_persistence_settings"); ok { - project.PersistenceSettings = expandGitPersistenceSettings(ctx, v, expandLibraryGitCredential) - } - if v, ok := d.GetOk("git_username_password_persistence_settings"); ok { - project.PersistenceSettings = expandGitPersistenceSettings(ctx, v, expandUsernamePasswordGitCredential) - } - if v, ok := d.GetOk("git_anonymous_persistence_settings"); ok { - project.PersistenceSettings = expandGitPersistenceSettings(ctx, v, expandAnonymousGitCredential) - } - - if project.PersistenceSettings != nil { - tflog.Info(ctx, fmt.Sprintf("expanded persistence settings {%v}", project.PersistenceSettings)) - } - - if v, ok := d.GetOk("included_library_variable_sets"); ok { - project.IncludedLibraryVariableSets = getSliceFromTerraformTypeList(v) - } - - if v, ok := d.GetOk("is_disabled"); ok { - project.IsDisabled = v.(bool) - } - - if v, ok := d.GetOk("is_discrete_channel_release"); ok { - project.IsDiscreteChannelRelease = v.(bool) - } - - if v, ok := d.GetOk("is_version_controlled"); ok { - project.IsVersionControlled = v.(bool) - } - - if v, ok := d.GetOk("jira_service_management_extension_settings"); ok { - project.ExtensionSettings = append(project.ExtensionSettings, prj.ExpandJiraServiceManagementExtensionSettings(v)) - } - - if v, ok := d.GetOk("servicenow_extension_settings"); ok { - project.ExtensionSettings = append(project.ExtensionSettings, prj.ExpandServiceNowExtensionSettings(v)) - } - - if v, ok := d.GetOk("release_creation_strategy"); ok { - project.ReleaseCreationStrategy = expandReleaseCreationStrategy(v.([]interface{})) - } - - if v, ok := d.GetOk("release_notes_template"); ok { - project.ReleaseNotesTemplate = v.(string) - } - - if v, ok := d.GetOk("slug"); ok { - project.Slug = v.(string) - } - - if v, ok := d.GetOk("space_id"); ok { - project.SpaceID = v.(string) - } - - if v, ok := d.GetOk("template"); ok { - project.Templates = expandActionTemplateParameters(v.([]interface{})) - } - - if v, ok := d.GetOk("tenanted_deployment_participation"); ok { - project.TenantedDeploymentMode = core.TenantedDeploymentMode(v.(string)) - } - - if v, ok := d.GetOk("versioning_strategy"); ok { - project.VersioningStrategy = expandVersioningStrategy(v) - } - - return project -} - -func flattenProject(ctx context.Context, d *schema.ResourceData, project *projects.Project) map[string]interface{} { - if project == nil { - return nil - } - - projectMap := map[string]interface{}{ - "auto_create_release": project.AutoCreateRelease, - "auto_deploy_release_overrides": project.AutoDeployReleaseOverrides, - "cloned_from_project_id": project.ClonedFromProjectID, - "connectivity_policy": flattenConnectivityPolicy(project.ConnectivityPolicy), - "default_guided_failure_mode": project.DefaultGuidedFailureMode, - "default_to_skip_if_already_installed": project.DefaultToSkipIfAlreadyInstalled, - "deployment_changes_template": project.DeploymentChangesTemplate, - "deployment_process_id": project.DeploymentProcessID, - "description": project.Description, - "id": project.GetID(), - "included_library_variable_sets": project.IncludedLibraryVariableSets, - "is_disabled": project.IsDisabled, - "is_discrete_channel_release": project.IsDiscreteChannelRelease, - "is_version_controlled": project.IsVersionControlled, - "lifecycle_id": project.LifecycleID, - "name": project.Name, - "project_group_id": project.ProjectGroupID, - "release_creation_strategy": flattenReleaseCreationStrategy(project.ReleaseCreationStrategy), - "release_notes_template": project.ReleaseNotesTemplate, - "slug": project.Slug, - "space_id": project.SpaceID, - "template": flattenActionTemplateParameters(project.Templates), - "tenanted_deployment_participation": project.TenantedDeploymentMode, - "variable_set_id": project.VariableSetID, - "versioning_strategy": flattenVersioningStrategy(project.VersioningStrategy), - } - - if len(project.ExtensionSettings) != 0 { - for _, extensionSettings := range project.ExtensionSettings { - switch extensionSettings.ExtensionID() { - case extensions.JiraServiceManagementExtensionID: - if jiraServiceManagementExtensionSettings, ok := extensionSettings.(*projects.JiraServiceManagementExtensionSettings); ok { - projectMap["jira_service_management_extension_settings"] = prj.FlattenJiraServiceManagementExtensionSettings(jiraServiceManagementExtensionSettings) - } - case extensions.ServiceNowExtensionID: - if serviceNowExtensionSettings, ok := extensionSettings.(*projects.ServiceNowExtensionSettings); ok { - projectMap["servicenow_extension_settings"] = prj.FlattenServiceNowExtensionSettings(serviceNowExtensionSettings) - } - } - } - } - - if project.PersistenceSettings != nil { - if project.PersistenceSettings.Type() == projects.PersistenceSettingsTypeVersionControlled { - gitCredentialType := project.PersistenceSettings.(projects.GitPersistenceSettings).Credential().Type() - switch gitCredentialType { - case credentials.GitCredentialTypeReference: - projectMap["git_library_persistence_settings"] = flattenGitPersistenceSettings(ctx, project.PersistenceSettings) - case credentials.GitCredentialTypeUsernamePassword: - projectMap["git_username_password_persistence_settings"] = flattenGitPersistenceSettings(ctx, project.PersistenceSettings) - case credentials.GitCredentialTypeAnonymous: - projectMap["git_anonymous_persistence_settings"] = flattenGitPersistenceSettings(ctx, project.PersistenceSettings) - } - } - } - - return projectMap -} - -func getProjectDataSchema() map[string]*schema.Schema { - dataSchema := getProjectSchema() - setDataSchema(&dataSchema) - - return map[string]*schema.Schema{ - "cloned_from_project_id": getQueryClonedFromProjectID(), - "id": getDataSchemaID(), - "space_id": getQuerySpaceID(), - "ids": getQueryIDs(), - "is_clone": getQueryIsClone(), - "name": getQueryName(), - "partial_name": getQueryPartialName(), - "projects": { - Computed: true, - Description: "A list of projects that match the filter(s).", - Elem: &schema.Resource{Schema: dataSchema}, - Optional: true, - Type: schema.TypeList, - }, - "skip": getQuerySkip(), - "take": getQueryTake(), - } -} - -func getProjectSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "allow_deployments_to_no_targets": { - Deprecated: "This value is only valid for an associated connectivity policy and should not be specified here.", - Optional: true, - Type: schema.TypeBool, - }, - "auto_create_release": { - Computed: true, - Optional: true, - Type: schema.TypeBool, - }, - "auto_deploy_release_overrides": { - Computed: true, - Elem: &schema.Schema{Type: schema.TypeString}, - MaxItems: 1, - Optional: true, - Type: schema.TypeList, - }, - "cloned_from_project_id": { - Computed: true, - Optional: true, - Type: schema.TypeString, - }, - "connectivity_policy": { - Computed: true, - Elem: &schema.Resource{Schema: getConnectivityPolicySchema()}, - MaxItems: 1, - Optional: true, - Type: schema.TypeList, - }, - "default_guided_failure_mode": { - Computed: true, - Optional: true, - Type: schema.TypeString, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ - "EnvironmentDefault", - "Off", - "On", - }, false)), - }, - "default_to_skip_if_already_installed": { - Computed: true, - Optional: true, - Type: schema.TypeBool, - }, - "deployment_changes_template": { - Computed: true, - Optional: true, - Type: schema.TypeString, - }, - "deployment_process_id": { - Computed: true, - Type: schema.TypeString, - }, - "description": { - Computed: true, - ConflictsWith: []string{"deployment_process_id"}, - Description: "The description of this project.", - Optional: true, - Type: schema.TypeString, - }, - "discrete_channel_release": { - Description: "Treats releases of different channels to the same environment as a separate deployment dimension", - Optional: true, - Type: schema.TypeBool, - }, - "git_library_persistence_settings": { - ConflictsWith: []string{"git_username_password_persistence_settings", "git_anonymous_persistence_settings"}, - Description: "Provides Git-related persistence settings for a version-controlled project.", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "base_path": { - Default: ".octopus", - Description: "The base path associated with these version control settings.", - Optional: true, - Type: schema.TypeString, - }, - "default_branch": { - Default: "main", - Description: "The default branch associated with these version control settings.", - Optional: true, - Type: schema.TypeString, - }, - "git_credential_id": { - Required: true, - Type: schema.TypeString, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotEmpty), - }, - "protected_branches": { - Description: "A list of protected branch patterns.", - Elem: &schema.Schema{Type: schema.TypeString}, - Optional: true, - Type: schema.TypeSet, - }, - "url": { - Description: "The URL associated with these version control settings.", - Required: true, - Type: schema.TypeString, - ValidateDiagFunc: validation.ToDiagFunc(validation.IsURLWithHTTPorHTTPS), - }, - }, - }, - MaxItems: 1, - Optional: true, - Type: schema.TypeList, - }, - "git_username_password_persistence_settings": { - ConflictsWith: []string{"git_library_persistence_settings", "git_anonymous_persistence_settings"}, - Description: "Provides Git-related persistence settings for a version-controlled project.", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "base_path": { - Default: ".octopus", - Description: "The base path associated with these version control settings.", - Optional: true, - Type: schema.TypeString, - }, - "default_branch": { - Default: "main", - Description: "The default branch associated with these version control settings.", - Optional: true, - Type: schema.TypeString, - }, - "password": { - Description: "The password for the Git credential.", - Required: true, - Sensitive: true, - Type: schema.TypeString, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotEmpty), - }, - "protected_branches": { - Description: "A list of protected branch patterns.", - Elem: &schema.Schema{Type: schema.TypeString}, - Optional: true, - Type: schema.TypeSet, - }, - "url": { - Description: "The URL associated with these version control settings.", - Required: true, - Type: schema.TypeString, - ValidateDiagFunc: validation.ToDiagFunc(validation.IsURLWithHTTPorHTTPS), - }, - "username": { - Description: "The username for the Git credential.", - Required: true, - Type: schema.TypeString, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotWhiteSpace), - }, - }, - }, - MaxItems: 1, - Optional: true, - Type: schema.TypeList, - }, - "git_anonymous_persistence_settings": { - ConflictsWith: []string{"git_library_persistence_settings", "git_username_password_persistence_settings"}, - Description: "Provides Git-related persistence settings for a version-controlled project.", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "base_path": { - Default: ".octopus", - Description: "The base path associated with these version control settings.", - Optional: true, - Type: schema.TypeString, - }, - "default_branch": { - Default: "main", - Description: "The default branch associated with these version control settings.", - Optional: true, - Type: schema.TypeString, - }, - "protected_branches": { - Description: "A list of protected branch patterns.", - Elem: &schema.Schema{Type: schema.TypeString}, - Optional: true, - Type: schema.TypeSet, - }, - "url": { - Description: "The URL associated with these version control settings.", - Required: true, - Type: schema.TypeString, - ValidateDiagFunc: validation.ToDiagFunc(validation.IsURLWithHTTPorHTTPS), - }, - }, - }, - MaxItems: 1, - Optional: true, - Type: schema.TypeList, - }, - "id": getIDSchema(), - "included_library_variable_sets": { - Computed: true, - Elem: &schema.Schema{Type: schema.TypeString}, - Optional: true, - Type: schema.TypeList, - }, - "is_disabled": { - Computed: true, - Optional: true, - Type: schema.TypeBool, - }, - "is_discrete_channel_release": { - Computed: true, - Description: "Treats releases of different channels to the same environment as a separate deployment dimension", - Optional: true, - Type: schema.TypeBool, - }, - "is_version_controlled": { - Computed: true, - Optional: true, - Type: schema.TypeBool, - }, - "jira_service_management_extension_settings": { - Description: "Provides extension settings for the Jira Service Management (JSM) integration for this project.", - Elem: &schema.Resource{Schema: prj.GetJiraServiceManagementExtensionSettingsSchema()}, - MaxItems: 1, - Optional: true, - Type: schema.TypeList, - }, - "lifecycle_id": { - Description: "The lifecycle ID associated with this project.", - Required: true, - Type: schema.TypeString, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotWhiteSpace), - }, - "name": { - Description: "The name of the project in Octopus Deploy. This name must be unique.", - Required: true, - Type: schema.TypeString, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotWhiteSpace), - }, - "project_group_id": { - Description: "The project group ID associated with this project.", - Required: true, - Type: schema.TypeString, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotWhiteSpace), - }, - "release_creation_strategy": { - Computed: true, - Elem: &schema.Resource{Schema: getReleaseCreationStrategySchema()}, - MaxItems: 1, - Optional: true, - Type: schema.TypeList, - }, - "release_notes_template": { - Computed: true, - Optional: true, - Type: schema.TypeString, - }, - "servicenow_extension_settings": { - Description: "Provides extension settings for the ServiceNow integration for this project.", - Elem: &schema.Resource{Schema: prj.GetServiceNowExtensionSettingsSchema()}, - MaxItems: 1, - Optional: true, - Type: schema.TypeList, - }, - "slug": { - Computed: true, - Description: "A human-readable, unique identifier, used to identify a project.", - Optional: true, - Type: schema.TypeString, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotWhiteSpace), - }, - "space_id": { - Computed: true, - Description: "The space ID associated with this project.", - Optional: true, - Type: schema.TypeString, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotWhiteSpace), - }, - "template": { - Elem: &schema.Resource{Schema: getActionTemplateParameterSchema()}, - Optional: true, - Type: schema.TypeList, - }, - "tenanted_deployment_participation": getTenantedDeploymentSchema(), - "variable_set_id": { - Computed: true, - Type: schema.TypeString, - }, - "versioning_strategy": { - Computed: true, - Elem: &schema.Resource{Schema: getVersionStrategySchema()}, - Optional: true, - Type: schema.TypeSet, - }, - } -} - -func setProject(ctx context.Context, d *schema.ResourceData, project *projects.Project) error { - d.Set("auto_create_release", project.AutoCreateRelease) - - if err := d.Set("auto_deploy_release_overrides", project.AutoDeployReleaseOverrides); err != nil { - return fmt.Errorf("error setting auto_deploy_release_overrides: %s", err) - } - - d.Set("cloned_from_project_id", project.ClonedFromProjectID) - - if err := d.Set("connectivity_policy", flattenConnectivityPolicy(project.ConnectivityPolicy)); err != nil { - return fmt.Errorf("error setting connectivity_policy: %s", err) - } - - d.Set("default_guided_failure_mode", project.DefaultGuidedFailureMode) - d.Set("default_to_skip_if_already_installed", project.DefaultToSkipIfAlreadyInstalled) - d.Set("deployment_changes_template", project.DeploymentChangesTemplate) - d.Set("deployment_process_id", project.DeploymentProcessID) - d.Set("description", project.Description) - - if len(project.ExtensionSettings) != 0 { - if err := prj.SetExtensionSettings(d, project.ExtensionSettings); err != nil { - return fmt.Errorf("error setting extension settings: %s", err) - } - } - - if err := d.Set("included_library_variable_sets", project.IncludedLibraryVariableSets); err != nil { - return fmt.Errorf("error setting included_library_variable_sets: %s", err) - } - - d.Set("is_disabled", project.IsDisabled) - d.Set("is_discrete_channel_release", project.IsDiscreteChannelRelease) - d.Set("is_version_controlled", project.IsVersionControlled) - d.Set("lifecycle_id", project.LifecycleID) - d.Set("name", project.Name) - - if project.PersistenceSettings != nil { - tflog.Info(ctx, "reading Persistence Settings") - if project.PersistenceSettings.Type() == projects.PersistenceSettingsTypeVersionControlled { - credential := project.PersistenceSettings.(projects.GitPersistenceSettings).Credential() - tflog.Info(ctx, fmt.Sprintf("reading Git Persistence Settings - {%v}", credential)) - gitCredentialType := credential.Type() - tflog.Info(ctx, fmt.Sprintf("reading Git Persistence Settings - {%s}", gitCredentialType)) - - // if the current settings are u/p, we need to keep the password value from state and put it back - // This is different to how this would be dealt with elsewhere, because of the way we have to reshape - // the internal objects into the schema. - if v, ok := d.GetOk("git_username_password_persistence_settings"); ok { - settings := expandGitPersistenceSettings(ctx, v, expandUsernamePasswordGitCredential) - if project.PersistenceSettings.(projects.GitPersistenceSettings).Credential().Type() == credentials.GitCredentialTypeUsernamePassword { - credential := project.PersistenceSettings.(projects.GitPersistenceSettings).Credential().(*credentials.UsernamePassword) - credential.Password.NewValue = settings.Credential().(*credentials.UsernamePassword).Password.NewValue - } - } - - // Since you can switch to different types of settings, we nil out all of the existing things - // in state and then just write back what config now says. This is why we have to store the pwd above, - // in case we're staying on u/p and then we need to keep the value. - if err := d.Set("git_library_persistence_settings", nil); err != nil { - return fmt.Errorf("error setting git_library_persistence_settings: %s", err) - } - if err := d.Set("git_username_password_persistence_settings", nil); err != nil { - return fmt.Errorf("error setting git_library_persistence_settings: %s", err) - } - if err := d.Set("git_anonymous_persistence_settings", nil); err != nil { - return fmt.Errorf("error setting git_library_persistence_settings: %s", err) - } - - switch gitCredentialType { - case credentials.GitCredentialTypeReference: - if err := d.Set("git_library_persistence_settings", setGitPersistenceSettings(ctx, project.PersistenceSettings)); err != nil { - return fmt.Errorf("error setting git_library_persistence_settings: %s", err) - } - case credentials.GitCredentialTypeUsernamePassword: - if err := d.Set("git_username_password_persistence_settings", setGitPersistenceSettings(ctx, project.PersistenceSettings)); err != nil { - return fmt.Errorf("error setting git_username_password_persistence_settings: %s", err) - } - case credentials.GitCredentialTypeAnonymous: - if err := d.Set("git_anonymous_persistence_settings", setGitPersistenceSettings(ctx, project.PersistenceSettings)); err != nil { - return fmt.Errorf("error setting git_anonymous_persistence_settings: %s", err) - } - } - } - } else { - tflog.Info(ctx, "using Database Persistence Settings") - } - - d.Set("project_group_id", project.ProjectGroupID) - - if err := d.Set("release_creation_strategy", flattenReleaseCreationStrategy(project.ReleaseCreationStrategy)); err != nil { - return fmt.Errorf("error setting release_creation_strategy: %s", err) - } - - d.Set("release_notes_template", project.ReleaseNotesTemplate) - d.Set("slug", project.Slug) - d.Set("space_id", project.SpaceID) - - if err := d.Set("template", flattenActionTemplateParameters(project.Templates)); err != nil { - return fmt.Errorf("error setting templates: %s", err) - } - - d.Set("tenanted_deployment_participation", project.TenantedDeploymentMode) - d.Set("variable_set_id", project.VariableSetID) - - if err := d.Set("versioning_strategy", flattenVersioningStrategy(project.VersioningStrategy)); err != nil { - return fmt.Errorf("error setting versioning_strategy: %s", err) - } - - d.Set("id", project.GetID()) - - return nil -} - -// The Library Variable set migration has moved these methods to the action_template_parameter.go file in the framework. These are to be removed when migrating projects and the action_template_parameter.go versions used instead. -func expandActionTemplateParameters(actionTemplateParameters []interface{}) []actiontemplates.ActionTemplateParameter { - if len(actionTemplateParameters) == 0 { - return nil - } - - expandedActionTemplateParameters := []actiontemplates.ActionTemplateParameter{} - for _, actionTemplateParameter := range actionTemplateParameters { - actionTemplateParameterMap := actionTemplateParameter.(map[string]interface{}) - expandedActionTemplateParameters = append(expandedActionTemplateParameters, expandActionTemplateParameter(actionTemplateParameterMap)) - } - return expandedActionTemplateParameters -} - -func expandActionTemplateParameter(tfTemplate map[string]interface{}) actiontemplates.ActionTemplateParameter { - actionTemplateParameter := actiontemplates.NewActionTemplateParameter() - - propertyValue := core.NewPropertyValue(tfTemplate["default_value"].(string), false) - actionTemplateParameter.DefaultValue = &propertyValue - actionTemplateParameter.DisplaySettings = flattenDisplaySettings(tfTemplate["display_settings"].(map[string]interface{})) - actionTemplateParameter.HelpText = tfTemplate["help_text"].(string) - actionTemplateParameter.ID = tfTemplate["id"].(string) - actionTemplateParameter.Label = tfTemplate["label"].(string) - actionTemplateParameter.Name = tfTemplate["name"].(string) - - return *actionTemplateParameter -} - -func flattenDisplaySettings(displaySettings map[string]interface{}) map[string]string { - flattenedDisplaySettings := make(map[string]string, len(displaySettings)) - for key, displaySetting := range displaySettings { - flattenedDisplaySettings[key] = displaySetting.(string) - } - return flattenedDisplaySettings -} - -func flattenActionTemplateParameters(actionTemplateParameters []actiontemplates.ActionTemplateParameter) []interface{} { - flattenedActionTemplateParameters := make([]interface{}, 0) - for _, actionTemplateParameter := range actionTemplateParameters { - a := make(map[string]interface{}) - a["default_value"] = actionTemplateParameter.DefaultValue.Value - a["display_settings"] = actionTemplateParameter.DisplaySettings - a["help_text"] = actionTemplateParameter.HelpText - a["id"] = actionTemplateParameter.ID - a["label"] = actionTemplateParameter.Label - a["name"] = actionTemplateParameter.Name - flattenedActionTemplateParameters = append(flattenedActionTemplateParameters, a) - } - return flattenedActionTemplateParameters -} - -func getActionTemplateParameterSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "default_value": { - Description: "A default value for the parameter, if applicable. This can be a hard-coded value or a variable reference.", - Optional: true, - Type: schema.TypeString, - }, - "display_settings": { - Description: "The display settings for the parameter.", - Optional: true, - Type: schema.TypeMap, - }, - "help_text": { - Description: "The help presented alongside the parameter input.", - Optional: true, - Type: schema.TypeString, - }, - "id": getIDSchema(), - "label": { - Description: "The label shown beside the parameter when presented in the deployment process. Example: `Server name`.", - Optional: true, - Type: schema.TypeString, - }, - "name": { - Description: "The name of the variable set by the parameter. The name can contain letters, digits, dashes and periods. Example: `ServerName`.", - Required: true, - Type: schema.TypeString, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotEmpty), - }, - } -} diff --git a/octopusdeploy_framework/datasource_project.go b/octopusdeploy_framework/datasource_project.go new file mode 100644 index 000000000..2653294b7 --- /dev/null +++ b/octopusdeploy_framework/datasource_project.go @@ -0,0 +1,95 @@ +package octopusdeploy_framework + +import ( + "context" + "fmt" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/schemas" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/types" + "time" +) + +var _ datasource.DataSource = &projectsDataSource{} + +type projectsDataSource struct { + *Config +} + +type projectsDataSourceModel struct { + ID types.String `tfsdk:"id"` + SpaceID types.String `tfsdk:"space_id"` + ClonedFromProjectID types.String `tfsdk:"cloned_from_project_id"` + IDs types.List `tfsdk:"ids"` + IsClone types.Bool `tfsdk:"is_clone"` + Name types.String `tfsdk:"name"` + PartialName types.String `tfsdk:"partial_name"` + Skip types.Int64 `tfsdk:"skip"` + Take types.Int64 `tfsdk:"take"` + Projects []projectResourceModel `tfsdk:"projects"` +} + +func NewProjectsDataSource() datasource.DataSource { + return &projectsDataSource{} +} + +func (p *projectsDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = util.GetTypeName(schemas.ProjectDataSourceName) +} + +func (p *projectsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schemas.GetProjectDataSourceSchema() +} + +func (p *projectsDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + p.Config = DataSourceConfiguration(req, resp) +} + +func (p *projectsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data projectsDataSourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + query := projects.ProjectsQuery{ + ClonedFromProjectID: data.ClonedFromProjectID.ValueString(), + IsClone: data.IsClone.ValueBool(), + Name: data.Name.ValueString(), + PartialName: data.PartialName.ValueString(), + Skip: int(data.Skip.ValueInt64()), + Take: int(data.Take.ValueInt64()), + } + + if !data.IDs.IsNull() { + var ids []string + resp.Diagnostics.Append(data.IDs.ElementsAs(ctx, &ids, false)...) + if resp.Diagnostics.HasError() { + return + } + query.IDs = ids + } + + spaceID := data.SpaceID.ValueString() + + existingProjects, err := projects.Get(p.Client, spaceID, query) + if err != nil { + resp.Diagnostics.AddError("Unable to query projects", err.Error()) + return + } + + data.Projects = make([]projectResourceModel, 0, len(existingProjects.Items)) + for _, project := range existingProjects.Items { + flattenedProject, diags := flattenProject(ctx, project, &projectResourceModel{}) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + data.Projects = append(data.Projects, *flattenedProject) + } + + data.ID = types.StringValue(fmt.Sprintf("Projects-%s", time.Now().UTC().String())) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/octopusdeploy_framework/framework_provider.go b/octopusdeploy_framework/framework_provider.go index 19f7809ed..816b8f774 100644 --- a/octopusdeploy_framework/framework_provider.go +++ b/octopusdeploy_framework/framework_provider.go @@ -69,6 +69,7 @@ func (p *octopusDeployFrameworkProvider) DataSources(ctx context.Context) []func NewFeedsDataSource, NewLibraryVariableSetDataSource, NewVariablesDataSource, + NewProjectsDataSource, } } @@ -90,6 +91,7 @@ func (p *octopusDeployFrameworkProvider) Resources(ctx context.Context) []func() NewTenantCommonVariableResource, NewLibraryVariableSetFeedResource, NewVariableResource, + NewProjectResource, } } diff --git a/octopusdeploy_framework/resource_project.go b/octopusdeploy_framework/resource_project.go new file mode 100644 index 000000000..6b73a6d7a --- /dev/null +++ b/octopusdeploy_framework/resource_project.go @@ -0,0 +1,220 @@ +package octopusdeploy_framework + +import ( + "context" + "fmt" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects" + "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-sdk/v2/diag" +) + +var _ resource.Resource = &projectResource{} + +type projectResource struct { + *Config +} + +func NewProjectResource() resource.Resource { + return &projectResource{} +} + +func (r *projectResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = util.GetTypeName(schemas.ProjectResourceName) +} + +func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schemas.GetProjectResourceSchema() +} + +func (r *projectResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.Config = ResourceConfiguration(req, resp) +} + +func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan projectResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + project := expandProject(ctx, plan) + // PersistenceSettings.Password doesn't return from API so this is work around + persistenceSettings := project.PersistenceSettings + createdProject, err := projects.Add(r.Client, project) + if err != nil { + resp.Diagnostics.AddError("Error creating project", err.Error()) + return + } + + if persistenceSettings != nil && persistenceSettings.Type() == projects.PersistenceSettingsTypeVersionControlled { + _, err := projects.ConvertToVCS(r.Client, createdProject, "Converting project to use VCS", "", persistenceSettings.(projects.GitPersistenceSettings)) + if err != nil { + resp.Diagnostics.AddError("Error converting project to VCS", err.Error()) + _ = projects.DeleteByID(r.Client, plan.SpaceID.ValueString(), createdProject.GetID()) + return + } + } + + createdProject, err = projects.GetByID(r.Client, plan.SpaceID.ValueString(), createdProject.GetID()) + if persistenceSettings != nil { + createdProject.PersistenceSettings = persistenceSettings + } + + flattenedProject, diags := flattenProject(ctx, createdProject, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + deploymentDiags := r.updateStateWithDeploymentSettings(createdProject, flattenedProject, &plan) + if deploymentDiags.HasError() { + return + } + + diags = resp.State.Set(ctx, flattenedProject) + resp.Diagnostics.Append(diags...) +} + +func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state projectResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + stateProject := expandProject(ctx, state) + // PersistenceSettings.Password doesn't return from API so this is work around + persistenceSettings := stateProject.PersistenceSettings + + project, err := projects.GetByID(r.Client, state.SpaceID.ValueString(), state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Error reading project", err.Error()) + return + } + if persistenceSettings != nil { + project.PersistenceSettings = persistenceSettings + } + + flattenedProject, diags := flattenProject(ctx, project, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + diagFromUpdate := r.updateStateWithDeploymentSettings(project, flattenedProject, &state) + if diagFromUpdate.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, flattenedProject)...) +} + +func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan projectResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + existingProject, err := projects.GetByID(r.Client, plan.SpaceID.ValueString(), plan.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Error retrieving project", err.Error()) + return + } + + updatedProject := expandProject(ctx, plan) + updatedProject.ID = existingProject.ID + updatedProject.Links = existingProject.Links + // PersistenceSettings.Password doesn't return from API so this is work around + persistenceSettings := updatedProject.PersistenceSettings + + if updatedProject.PersistenceSettings != nil && updatedProject.PersistenceSettings.Type() == projects.PersistenceSettingsTypeVersionControlled { + if existingProject.PersistenceSettings == nil || existingProject.PersistenceSettings.Type() != projects.PersistenceSettingsTypeVersionControlled { + vcsProject, err := projects.ConvertToVCS(r.Client, existingProject, "Converting project to use VCS", "", updatedProject.PersistenceSettings.(projects.GitPersistenceSettings)) + if err != nil { + resp.Diagnostics.AddError("Error converting project to VCS", err.Error()) + return + } + updatedProject.PersistenceSettings = vcsProject.PersistenceSettings + } + } + + updatedProject, err = projects.Update(r.Client, updatedProject) + if err != nil { + resp.Diagnostics.AddError("Error updating project", err.Error()) + return + } + + if persistenceSettings != nil { + updatedProject.PersistenceSettings = persistenceSettings + } + + flattenedProject, diags := flattenProject(ctx, updatedProject, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + diagFromUpdate := r.updateStateWithDeploymentSettings(updatedProject, flattenedProject, &plan) + if diagFromUpdate.HasError() { + return + } + + diags = resp.State.Set(ctx, flattenedProject) + resp.Diagnostics.Append(diags...) +} + +func (r *projectResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state projectResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + err := projects.DeleteByID(r.Client, state.SpaceID.ValueString(), state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Error deleting project", err.Error()) + return + } + + resp.State.RemoveResource(ctx) +} + +func (r *projectResource) updateStateWithDeploymentSettings(project *projects.Project, newState *projectResourceModel, originalState *projectResourceModel) diag.Diagnostics { + var diags diag.Diagnostics + + var gitRef string + if project.IsVersionControlled { + if gitSettings, ok := project.PersistenceSettings.(projects.GitPersistenceSettings); ok { + gitRef = gitSettings.DefaultBranch() + } + if gitRef == "" { + gitRef = "main" + } + } + + deploymentSettings, err := r.Client.Deployments.GetDeploymentSettings(project, gitRef) + if err != nil { + return diag.FromErr(fmt.Errorf("error reading deployment settings: %w", err)) + } + + // Update the state with the deployment settings + if deploymentSettings.ConnectivityPolicy != nil && !originalState.ConnectivityPolicy.IsNull() { + newState.ConnectivityPolicy = flattenConnectivityPolicy(deploymentSettings.ConnectivityPolicy) + } + newState.DefaultGuidedFailureMode = types.StringValue(string(deploymentSettings.DefaultGuidedFailureMode)) + newState.DefaultToSkipIfAlreadyInstalled = types.BoolValue(deploymentSettings.DefaultToSkipIfAlreadyInstalled) + newState.DeploymentChangesTemplate = types.StringValue(deploymentSettings.DeploymentChangesTemplate) + newState.ReleaseNotesTemplate = types.StringValue(deploymentSettings.ReleaseNotesTemplate) + if deploymentSettings.VersioningStrategy != nil && !originalState.VersioningStrategy.IsNull() { + newState.VersioningStrategy = flattenVersioningStrategy(deploymentSettings.VersioningStrategy) + } + + return diags +} diff --git a/octopusdeploy_framework/resource_project_expand.go b/octopusdeploy_framework/resource_project_expand.go new file mode 100644 index 000000000..e0a3316da --- /dev/null +++ b/octopusdeploy_framework/resource_project_expand.go @@ -0,0 +1,339 @@ +package octopusdeploy_framework + +import ( + "context" + "fmt" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/actiontemplates" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/credentials" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/packages" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "net/url" +) + +func expandProject(ctx context.Context, model projectResourceModel) *projects.Project { + project := projects.NewProject( + model.Name.ValueString(), + model.LifecycleID.ValueString(), + model.ProjectGroupID.ValueString(), + ) + + project.ID = model.ID.ValueString() + project.SpaceID = model.SpaceID.ValueString() + project.Description = model.Description.ValueString() + project.IsDisabled = model.IsDisabled.ValueBool() + project.AutoCreateRelease = model.AutoCreateRelease.ValueBool() + project.DefaultGuidedFailureMode = model.DefaultGuidedFailureMode.ValueString() + project.DefaultToSkipIfAlreadyInstalled = model.DefaultToSkipIfAlreadyInstalled.ValueBool() + project.DeploymentChangesTemplate = model.DeploymentChangesTemplate.ValueString() + project.DeploymentProcessID = model.DeploymentProcessID.ValueString() + project.IsDiscreteChannelRelease = model.IsDiscreteChannelRelease.ValueBool() + project.IsVersionControlled = model.IsVersionControlled.ValueBool() + project.TenantedDeploymentMode = core.TenantedDeploymentMode(model.TenantedDeploymentParticipation.ValueString()) + project.ReleaseNotesTemplate = model.ReleaseNotesTemplate.ValueString() + project.Slug = model.Slug.ValueString() + project.ClonedFromProjectID = model.ClonedFromProjectID.ValueString() + + if !model.IncludedLibraryVariableSets.IsNull() { + var includedSets []string + model.IncludedLibraryVariableSets.ElementsAs(ctx, &includedSets, false) + project.IncludedLibraryVariableSets = includedSets + } + + if !model.ConnectivityPolicy.IsNull() { + project.ConnectivityPolicy = expandConnectivityPolicy(ctx, model.ConnectivityPolicy) + } + + if !model.GitLibraryPersistenceSettings.IsNull() { + var gitLibrarySettingsList []gitLibraryPersistenceSettingsModel + diags := model.GitLibraryPersistenceSettings.ElementsAs(ctx, &gitLibrarySettingsList, false) + if diags.HasError() { + fmt.Printf("Error converting Git library persistence settings: %v\n", diags) + } else { + fmt.Printf("Number of Git library persistence settings: %d\n", len(gitLibrarySettingsList)) + if len(gitLibrarySettingsList) > 0 { + project.PersistenceSettings = expandGitLibraryPersistenceSettings(ctx, gitLibrarySettingsList[0]) + project.IsVersionControlled = true + } + } + } else if !model.GitUsernamePasswordPersistenceSettings.IsNull() { + var gitUsernamePasswordSettingsList []gitUsernamePasswordPersistenceSettingsModel + diags := model.GitUsernamePasswordPersistenceSettings.ElementsAs(ctx, &gitUsernamePasswordSettingsList, false) + if diags.HasError() { + fmt.Printf("Error converting Git username/password persistence settings: %v\n", diags) + } else { + fmt.Printf("Number of Git username/password persistence settings: %d\n", len(gitUsernamePasswordSettingsList)) + if len(gitUsernamePasswordSettingsList) > 0 { + project.PersistenceSettings = expandGitUsernamePasswordPersistenceSettings(ctx, gitUsernamePasswordSettingsList[0]) + project.IsVersionControlled = true + } + } + } else if !model.GitAnonymousPersistenceSettings.IsNull() { + var gitAnonymousSettingsList []gitAnonymousPersistenceSettingsModel + diags := model.GitAnonymousPersistenceSettings.ElementsAs(ctx, &gitAnonymousSettingsList, false) + if diags.HasError() { + fmt.Printf("Error converting Git anonymous persistence settings: %v\n", diags) + } else { + fmt.Printf("Number of Git anonymous persistence settings: %d\n", len(gitAnonymousSettingsList)) + if len(gitAnonymousSettingsList) > 0 { + project.PersistenceSettings = expandGitAnonymousPersistenceSettings(ctx, gitAnonymousSettingsList[0]) + project.IsVersionControlled = true + } + } + } + + if !model.JiraServiceManagementExtensionSettings.IsNull() { + var settingsList []jiraServiceManagementExtensionSettingsModel + diags := model.JiraServiceManagementExtensionSettings.ElementsAs(ctx, &settingsList, false) + if !diags.HasError() && len(settingsList) > 0 { + settings := settingsList[0] + project.ExtensionSettings = append(project.ExtensionSettings, expandJiraServiceManagementExtensionSettings(settings)) + } + } + + if !model.ServiceNowExtensionSettings.IsNull() { + var settingsList []servicenowExtensionSettingsModel + diags := model.ServiceNowExtensionSettings.ElementsAs(ctx, &settingsList, false) + if !diags.HasError() && len(settingsList) > 0 { + settings := settingsList[0] + project.ExtensionSettings = append(project.ExtensionSettings, expandServiceNowExtensionSettings(settings)) + } + } + + if !model.VersioningStrategy.IsNull() { + project.VersioningStrategy = expandVersioningStrategy(ctx, model.VersioningStrategy) + } + + if !model.ReleaseCreationStrategy.IsNull() { + var strategy releaseCreationStrategyModel + model.ReleaseCreationStrategy.ElementsAs(ctx, &strategy, false) + project.ReleaseCreationStrategy = expandReleaseCreationStrategy(strategy) + } + + if !model.Template.IsNull() { + var templates []templateModel + diags := model.Template.ElementsAs(ctx, &templates, false) + if diags.HasError() { + fmt.Printf("Error converting templates: %v\n", diags) + } else { + fmt.Printf("Number of templates: %d\n", len(templates)) + project.Templates = expandTemplates(templates) + } + } else { + fmt.Println("Template is null") + project.Templates = []actiontemplates.ActionTemplateParameter{} + } + + if !model.AutoDeployReleaseOverrides.IsNull() { + var overrideModels []autoDeployReleaseOverrideModel + diags := model.AutoDeployReleaseOverrides.ElementsAs(ctx, &overrideModels, false) + if !diags.HasError() { + project.AutoDeployReleaseOverrides = expandAutoDeployReleaseOverrides(overrideModels) + } + } + + return project +} + +func expandGitLibraryPersistenceSettings(ctx context.Context, model gitLibraryPersistenceSettingsModel) projects.GitPersistenceSettings { + url, _ := url.Parse(model.URL.ValueString()) + var protectedBranches []string + model.ProtectedBranches.ElementsAs(ctx, &protectedBranches, false) + + return projects.NewGitPersistenceSettings( + model.BasePath.ValueString(), + &credentials.Reference{ + ID: model.GitCredentialID.ValueString(), + }, + model.DefaultBranch.ValueString(), + protectedBranches, + url, + ) +} + +func expandGitUsernamePasswordPersistenceSettings(ctx context.Context, model gitUsernamePasswordPersistenceSettingsModel) projects.GitPersistenceSettings { + url, _ := url.Parse(model.URL.ValueString()) + var protectedBranches []string + model.ProtectedBranches.ElementsAs(ctx, &protectedBranches, false) + + usernamePasswordCredential := credentials.NewUsernamePassword( + model.Username.ValueString(), + core.NewSensitiveValue(model.Password.ValueString()), + ) + + return projects.NewGitPersistenceSettings( + model.BasePath.ValueString(), + usernamePasswordCredential, + model.DefaultBranch.ValueString(), + protectedBranches, + url, + ) +} + +func expandGitAnonymousPersistenceSettings(ctx context.Context, model gitAnonymousPersistenceSettingsModel) projects.GitPersistenceSettings { + url, _ := url.Parse(model.URL.ValueString()) + var protectedBranches []string + model.ProtectedBranches.ElementsAs(ctx, &protectedBranches, false) + + return projects.NewGitPersistenceSettings( + model.BasePath.ValueString(), + &credentials.Anonymous{}, + model.DefaultBranch.ValueString(), + protectedBranches, + url, + ) +} + +func expandAutoDeployReleaseOverrides(models []autoDeployReleaseOverrideModel) []projects.AutoDeployReleaseOverride { + result := make([]projects.AutoDeployReleaseOverride, 0, len(models)) + + for _, model := range models { + override := projects.AutoDeployReleaseOverride{ + EnvironmentID: model.EnvironmentID.ValueString(), + } + + if !model.TenantID.IsNull() { + override.TenantID = model.TenantID.ValueString() + } + + result = append(result, override) + } + + return result +} + +func expandConnectivityPolicy(ctx context.Context, connectivityPolicyList types.List) *core.ConnectivityPolicy { + if connectivityPolicyList.IsNull() || connectivityPolicyList.IsUnknown() { + return nil + } + + var policyList []connectivityPolicyModel + diags := connectivityPolicyList.ElementsAs(ctx, &policyList, false) + if diags.HasError() { + return nil + } + + if len(policyList) == 0 { + return nil + } + policy := policyList[0] + + var targetRoles []string + if !policy.TargetRoles.IsNull() && !policy.TargetRoles.IsUnknown() { + policy.TargetRoles.ElementsAs(ctx, &targetRoles, false) + } + + skipMachineBehavior := core.SkipMachineBehavior(policy.SkipMachineBehavior.ValueString()) + + return &core.ConnectivityPolicy{ + AllowDeploymentsToNoTargets: policy.AllowDeploymentsToNoTargets.ValueBool(), + ExcludeUnhealthyTargets: policy.ExcludeUnhealthyTargets.ValueBool(), + SkipMachineBehavior: skipMachineBehavior, + TargetRoles: targetRoles, + } +} + +func expandJiraServiceManagementExtensionSettings(model jiraServiceManagementExtensionSettingsModel) *projects.JiraServiceManagementExtensionSettings { + return projects.NewJiraServiceManagementExtensionSettings( + model.ConnectionID.ValueString(), + model.IsEnabled.ValueBool(), + model.ServiceDeskProjectName.ValueString(), + ) +} + +func expandServiceNowExtensionSettings(model servicenowExtensionSettingsModel) *projects.ServiceNowExtensionSettings { + return projects.NewServiceNowExtensionSettings( + model.ConnectionID.ValueString(), + model.IsEnabled.ValueBool(), + model.StandardChangeTemplateName.ValueString(), + model.IsStateAutomaticallyTransitioned.ValueBool(), + ) +} + +func expandVersioningStrategy(ctx context.Context, versioningStrategyList types.List) *projects.VersioningStrategy { + if versioningStrategyList.IsNull() || versioningStrategyList.IsUnknown() { + return nil + } + + var strategyList []versioningStrategyModel + diags := versioningStrategyList.ElementsAs(ctx, &strategyList, false) + if diags.HasError() { + return nil + } + + if len(strategyList) == 0 { + return nil + } + strategy := strategyList[0] + + versioningStrategy := &projects.VersioningStrategy{ + Template: strategy.Template.ValueString(), + } + + if !strategy.DonorPackageStepID.IsNull() { + donorPackageStepID := strategy.DonorPackageStepID.ValueString() + versioningStrategy.DonorPackageStepID = &donorPackageStepID + } + + if !strategy.DonorPackage.IsNull() { + var donorPackageList []deploymentActionPackageModel + diags := strategy.DonorPackage.ElementsAs(ctx, &donorPackageList, false) + if !diags.HasError() && len(donorPackageList) > 0 { + donorPackage := donorPackageList[0] + versioningStrategy.DonorPackage = &packages.DeploymentActionPackage{ + DeploymentAction: donorPackage.DeploymentAction.ValueString(), + PackageReference: donorPackage.PackageReference.ValueString(), + } + } + } + return versioningStrategy +} + +func expandReleaseCreationStrategy(model releaseCreationStrategyModel) *projects.ReleaseCreationStrategy { + strategy := &projects.ReleaseCreationStrategy{ + ChannelID: model.ChannelID.ValueString(), + ReleaseCreationPackageStepID: model.ReleaseCreationPackageStepID.ValueString(), + } + if !model.ReleaseCreationPackage.IsNull() { + var releaseCreationPackage deploymentActionPackageModel + model.ReleaseCreationPackage.As(context.Background(), &releaseCreationPackage, basetypes.ObjectAsOptions{}) + strategy.ReleaseCreationPackage = expandDeploymentActionPackage(releaseCreationPackage) + } + return strategy +} + +func expandDeploymentActionPackage(model deploymentActionPackageModel) *packages.DeploymentActionPackage { + return &packages.DeploymentActionPackage{ + DeploymentAction: model.DeploymentAction.ValueString(), + PackageReference: model.PackageReference.ValueString(), + } +} +func expandTemplates(templates []templateModel) []actiontemplates.ActionTemplateParameter { + result := make([]actiontemplates.ActionTemplateParameter, len(templates)) + for i, template := range templates { + defaultValue := core.NewPropertyValue("", false) + if !template.DefaultValue.IsNull() { + defaultValue = core.NewPropertyValue(template.DefaultValue.ValueString(), false) + } + + displaySettings := make(map[string]string) + if !template.DisplaySettings.IsNull() && !template.DisplaySettings.IsUnknown() { + template.DisplaySettings.ElementsAs(context.Background(), &displaySettings, false) + } + + result[i] = actiontemplates.ActionTemplateParameter{ + DefaultValue: &defaultValue, + DisplaySettings: displaySettings, + HelpText: template.HelpText.ValueString(), + Label: template.Label.ValueString(), + Name: template.Name.ValueString(), + } + + if !template.ID.IsNull() { + result[i].Resource.ID = template.ID.ValueString() + } + } + return result +} diff --git a/octopusdeploy_framework/resource_project_flatten.go b/octopusdeploy_framework/resource_project_flatten.go new file mode 100644 index 000000000..39b4c3e80 --- /dev/null +++ b/octopusdeploy_framework/resource_project_flatten.go @@ -0,0 +1,427 @@ +package octopusdeploy_framework + +import ( + "context" + "fmt" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/actiontemplates" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/credentials" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/extensions" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/packages" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func flattenProject(ctx context.Context, project *projects.Project, state *projectResourceModel) (*projectResourceModel, diag.Diagnostics) { + if project == nil { + return nil, diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Error flattening project", + "The project is nil", + ), + } + } + + model := &projectResourceModel{ + ID: types.StringValue(project.GetID()), + SpaceID: types.StringValue(project.SpaceID), + Name: types.StringValue(project.Name), + Description: types.StringValue(project.Description), + LifecycleID: types.StringValue(project.LifecycleID), + ProjectGroupID: types.StringValue(project.ProjectGroupID), + IsDisabled: types.BoolValue(project.IsDisabled), + AutoCreateRelease: types.BoolValue(project.AutoCreateRelease), + DefaultGuidedFailureMode: types.StringValue(project.DefaultGuidedFailureMode), + DefaultToSkipIfAlreadyInstalled: types.BoolValue(project.DefaultToSkipIfAlreadyInstalled), + DeploymentChangesTemplate: types.StringValue(project.DeploymentChangesTemplate), + DeploymentProcessID: types.StringValue(project.DeploymentProcessID), + DiscreteChannelRelease: types.BoolValue(project.IsDiscreteChannelRelease), + IsDiscreteChannelRelease: types.BoolValue(project.IsDiscreteChannelRelease), + IsVersionControlled: types.BoolValue(project.IsVersionControlled), + TenantedDeploymentParticipation: types.StringValue(string(project.TenantedDeploymentMode)), + VariableSetID: types.StringValue(project.VariableSetID), + ReleaseNotesTemplate: util.StringOrNull(project.ReleaseNotesTemplate), + Slug: types.StringValue(project.Slug), + ClonedFromProjectID: util.StringOrNull(project.ClonedFromProjectID), + } + + model.IncludedLibraryVariableSets = util.FlattenStringList(project.IncludedLibraryVariableSets) + model.AutoDeployReleaseOverrides = flattenAutoDeployReleaseOverrides(project.AutoDeployReleaseOverrides) + + if state.ConnectivityPolicy.IsNull() { + model.ConnectivityPolicy = types.ListNull(types.ObjectType{AttrTypes: getConnectivityPolicyAttrTypes()}) + } else { + model.ConnectivityPolicy = flattenConnectivityPolicy(project.ConnectivityPolicy) + } + + if state.ReleaseCreationStrategy.IsNull() { + model.ReleaseCreationStrategy = types.ListNull(types.ObjectType{AttrTypes: getReleaseCreationStrategyAttrTypes()}) + } else { + model.ReleaseCreationStrategy = flattenReleaseCreationStrategy(project.ReleaseCreationStrategy) + } + + if state.VersioningStrategy.IsNull() { + model.VersioningStrategy = types.ListNull(types.ObjectType{AttrTypes: getVersioningStrategyAttrTypes()}) + } else { + model.VersioningStrategy = flattenVersioningStrategy(project.VersioningStrategy) + } + + model.Template = flattenTemplates(project.Templates) + + diags := processPersistenceSettings(ctx, project, model) + + if diags.HasError() { + return model, diags + } + + // Extension Settings + model.JiraServiceManagementExtensionSettings = flattenJiraServiceManagementExtensionSettings(nil) + model.ServiceNowExtensionSettings = flattenServiceNowExtensionSettings(nil) + + for _, extensionSetting := range project.ExtensionSettings { + switch extensionSetting.ExtensionID() { + case extensions.JiraServiceManagementExtensionID: + if jsmSettings, ok := extensionSetting.(*projects.JiraServiceManagementExtensionSettings); ok { + model.JiraServiceManagementExtensionSettings = flattenJiraServiceManagementExtensionSettings(jsmSettings) + } + case extensions.ServiceNowExtensionID: + if snowSettings, ok := extensionSetting.(*projects.ServiceNowExtensionSettings); ok { + model.ServiceNowExtensionSettings = flattenServiceNowExtensionSettings(snowSettings) + } + } + } + + return model, diags +} + +func processPersistenceSettings(ctx context.Context, project *projects.Project, model *projectResourceModel) diag.Diagnostics { + var diags diag.Diagnostics + if project.PersistenceSettings != nil { + if project.PersistenceSettings.Type() == projects.PersistenceSettingsTypeVersionControlled { + gitSettings := project.PersistenceSettings.(projects.GitPersistenceSettings) + gitCredentialType := gitSettings.Credential().Type() + model.IsVersionControlled = types.BoolValue(true) + switch gitCredentialType { + case credentials.GitCredentialTypeReference: + model.GitLibraryPersistenceSettings, diags = flattenGitPersistenceSettings(ctx, gitSettings) + model.GitUsernamePasswordPersistenceSettings = types.ListNull(types.ObjectType{AttrTypes: getGitUsernamePasswordPersistenceSettingsAttrTypes()}) + model.GitAnonymousPersistenceSettings = types.ListNull(types.ObjectType{AttrTypes: getGitAnonymousPersistenceSettingsAttrTypes()}) + case credentials.GitCredentialTypeUsernamePassword: + model.GitLibraryPersistenceSettings = types.ListNull(types.ObjectType{AttrTypes: getGitLibraryPersistenceSettingsAttrTypes()}) + model.GitUsernamePasswordPersistenceSettings, diags = flattenGitPersistenceSettings(ctx, gitSettings) + model.GitAnonymousPersistenceSettings = types.ListNull(types.ObjectType{AttrTypes: getGitAnonymousPersistenceSettingsAttrTypes()}) + case credentials.GitCredentialTypeAnonymous: + model.GitLibraryPersistenceSettings = types.ListNull(types.ObjectType{AttrTypes: getGitLibraryPersistenceSettingsAttrTypes()}) + model.GitUsernamePasswordPersistenceSettings = types.ListNull(types.ObjectType{AttrTypes: getGitUsernamePasswordPersistenceSettingsAttrTypes()}) + model.GitAnonymousPersistenceSettings, diags = flattenGitPersistenceSettings(ctx, gitSettings) + } + } else { + model.GitLibraryPersistenceSettings = types.ListNull(types.ObjectType{AttrTypes: getGitLibraryPersistenceSettingsAttrTypes()}) + model.GitUsernamePasswordPersistenceSettings = types.ListNull(types.ObjectType{AttrTypes: getGitUsernamePasswordPersistenceSettingsAttrTypes()}) + model.GitAnonymousPersistenceSettings = types.ListNull(types.ObjectType{AttrTypes: getGitAnonymousPersistenceSettingsAttrTypes()}) + } + } else { + model.GitLibraryPersistenceSettings = types.ListNull(types.ObjectType{AttrTypes: getGitLibraryPersistenceSettingsAttrTypes()}) + model.GitUsernamePasswordPersistenceSettings = types.ListNull(types.ObjectType{AttrTypes: getGitUsernamePasswordPersistenceSettingsAttrTypes()}) + model.GitAnonymousPersistenceSettings = types.ListNull(types.ObjectType{AttrTypes: getGitAnonymousPersistenceSettingsAttrTypes()}) + model.IsVersionControlled = types.BoolValue(false) + } + return diags +} + +func flattenConnectivityPolicy(policy *core.ConnectivityPolicy) types.List { + if policy == nil { + return types.ListNull(types.ObjectType{AttrTypes: getConnectivityPolicyAttrTypes()}) + } + + obj := types.ObjectValueMust(getConnectivityPolicyAttrTypes(), map[string]attr.Value{ + "allow_deployments_to_no_targets": types.BoolValue(policy.AllowDeploymentsToNoTargets), + "exclude_unhealthy_targets": types.BoolValue(policy.ExcludeUnhealthyTargets), + "skip_machine_behavior": types.StringValue(string(policy.SkipMachineBehavior)), + "target_roles": util.FlattenStringList(policy.TargetRoles), + }) + + return types.ListValueMust(types.ObjectType{AttrTypes: getConnectivityPolicyAttrTypes()}, []attr.Value{obj}) +} + +func flattenVersioningStrategy(strategy *projects.VersioningStrategy) types.List { + if strategy == nil { + return types.ListNull(types.ObjectType{AttrTypes: getVersioningStrategyAttrTypes()}) + } + obj := types.ObjectValueMust(getVersioningStrategyAttrTypes(), map[string]attr.Value{ + "donor_package": flattenDeploymentActionPackage(strategy.DonorPackage), + "donor_package_step_id": types.StringPointerValue(strategy.DonorPackageStepID), + "template": types.StringValue(strategy.Template), + }) + + return types.ListValueMust(types.ObjectType{AttrTypes: getVersioningStrategyAttrTypes()}, []attr.Value{obj}) +} + +func flattenGitPersistenceSettings(ctx context.Context, persistenceSettings projects.PersistenceSettings) (types.List, diag.Diagnostics) { + if persistenceSettings == nil || persistenceSettings.Type() == projects.PersistenceSettingsTypeDatabase { + return types.ListNull(types.ObjectType{AttrTypes: getGitAnonymousPersistenceSettingsAttrTypes()}), nil + } + + gitPersistenceSettings := persistenceSettings.(projects.GitPersistenceSettings) + + baseAttrValues := map[string]attr.Value{ + "base_path": types.StringValue(gitPersistenceSettings.BasePath()), + "default_branch": types.StringValue(gitPersistenceSettings.DefaultBranch()), + "url": types.StringValue(gitPersistenceSettings.URL().String()), + } + + protectedBranches, diags := types.SetValueFrom(ctx, types.StringType, gitPersistenceSettings.ProtectedBranchNamePatterns()) + if diags.HasError() { + return types.ListNull(types.ObjectType{}), diags + } + baseAttrValues["protected_branches"] = protectedBranches + + var attrTypes map[string]attr.Type + var attrValues map[string]attr.Value + + credential := gitPersistenceSettings.Credential() + switch credential.Type() { + case credentials.GitCredentialTypeReference: + attrTypes = getGitLibraryPersistenceSettingsAttrTypes() + attrValues = baseAttrValues + attrValues["git_credential_id"] = types.StringValue(credential.(*credentials.Reference).ID) + case credentials.GitCredentialTypeUsernamePassword: + attrTypes = getGitUsernamePasswordPersistenceSettingsAttrTypes() + attrValues = baseAttrValues + attrValues["username"] = types.StringValue(credential.(*credentials.UsernamePassword).Username) + attrValues["password"] = types.StringValue(*credential.(*credentials.UsernamePassword).Password.NewValue) + case credentials.GitCredentialTypeAnonymous: + attrTypes = getGitAnonymousPersistenceSettingsAttrTypes() + attrValues = baseAttrValues + default: + return types.ListNull(types.ObjectType{}), diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unsupported Git Credential Type", + fmt.Sprintf("Git credential type %v is not supported", credential.Type()), + ), + } + } + + objValue, diags := types.ObjectValue(attrTypes, attrValues) + if diags.HasError() { + return types.ListNull(types.ObjectType{AttrTypes: attrTypes}), diags + } + + return types.ListValueMust(types.ObjectType{AttrTypes: attrTypes}, []attr.Value{objValue}), nil +} + +func flattenJiraServiceManagementExtensionSettings(settings *projects.JiraServiceManagementExtensionSettings) types.List { + if settings == nil { + return types.ListValueMust(types.ObjectType{AttrTypes: getJSMExtensionSettingsAttrTypes()}, []attr.Value{}) + } + + obj := types.ObjectValueMust(getJSMExtensionSettingsAttrTypes(), map[string]attr.Value{ + "connection_id": types.StringValue(settings.ConnectionID()), + "is_enabled": types.BoolValue(settings.IsChangeControlled()), + "service_desk_project_name": types.StringValue(settings.ServiceDeskProjectName), + }) + + return types.ListValueMust(types.ObjectType{AttrTypes: getJSMExtensionSettingsAttrTypes()}, []attr.Value{obj}) +} + +func flattenServiceNowExtensionSettings(settings *projects.ServiceNowExtensionSettings) types.List { + if settings == nil { + return types.ListValueMust(types.ObjectType{AttrTypes: getServiceNowExtensionSettingsAttrTypes()}, []attr.Value{}) + } + + obj := types.ObjectValueMust(getServiceNowExtensionSettingsAttrTypes(), map[string]attr.Value{ + "connection_id": types.StringValue(settings.ConnectionID()), + "is_enabled": types.BoolValue(settings.IsChangeControlled()), + "is_state_automatically_transitioned": types.BoolValue(settings.IsStateAutomaticallyTransitioned), + "standard_change_template_name": util.StringOrNull(settings.StandardChangeTemplateName), + }) + + return types.ListValueMust(types.ObjectType{AttrTypes: getServiceNowExtensionSettingsAttrTypes()}, []attr.Value{obj}) +} + +func flattenTemplates(templates []actiontemplates.ActionTemplateParameter) types.List { + if len(templates) == 0 { + return types.ListNull(types.ObjectType{AttrTypes: getTemplateAttrTypes()}) + } + + templateList := make([]attr.Value, 0, len(templates)) + for _, template := range templates { + + obj := types.ObjectValueMust(getTemplateAttrTypes(), map[string]attr.Value{ + "id": types.StringValue(template.Resource.ID), + "name": types.StringValue(template.Name), + "label": util.StringOrNull(template.Label), + "help_text": util.StringOrNull(template.HelpText), + "default_value": util.StringOrNull(template.DefaultValue.Value), + "display_settings": types.MapValueMust( + types.StringType, + convertMapStringToMapAttrValue(template.DisplaySettings), + ), + }) + + templateList = append(templateList, obj) + } + + return types.ListValueMust(types.ObjectType{AttrTypes: getTemplateAttrTypes()}, templateList) +} + +func flattenAutoDeployReleaseOverrides(overrides []projects.AutoDeployReleaseOverride) types.List { + if len(overrides) == 0 { + return types.ListValueMust(types.ObjectType{AttrTypes: getAutoDeployReleaseOverrideAttrTypes()}, []attr.Value{}) + } + + overrideList := make([]attr.Value, 0, len(overrides)) + for _, override := range overrides { + obj := types.ObjectValueMust(getAutoDeployReleaseOverrideAttrTypes(), map[string]attr.Value{ + "environment_id": types.StringValue(override.EnvironmentID), + "release_id": types.StringValue(override.ReleaseID), + "tenant_id": types.StringValue(override.TenantID), + }) + overrideList = append(overrideList, obj) + } + + return types.ListValueMust(types.ObjectType{AttrTypes: getAutoDeployReleaseOverrideAttrTypes()}, overrideList) +} + +func getAutoDeployReleaseOverrideAttrTypes() map[string]attr.Type { + return map[string]attr.Type{ + "environment_id": types.StringType, + "release_id": types.StringType, + "tenant_id": types.StringType, + } +} + +func flattenReleaseCreationStrategy(strategy *projects.ReleaseCreationStrategy) types.List { + if strategy == nil { + return types.ListValueMust(types.ObjectType{AttrTypes: getReleaseCreationStrategyAttrTypes()}, []attr.Value{}) + } + + obj := types.ObjectValueMust(getReleaseCreationStrategyAttrTypes(), map[string]attr.Value{ + "channel_id": types.StringValue(strategy.ChannelID), + "release_creation_package_step_id": types.StringValue(strategy.ReleaseCreationPackageStepID), + "release_creation_package": flattenDeploymentActionPackage(strategy.ReleaseCreationPackage), + }) + + return types.ListValueMust(types.ObjectType{AttrTypes: getReleaseCreationStrategyAttrTypes()}, []attr.Value{obj}) +} + +func convertMapStringToMapAttrValue(m map[string]string) map[string]attr.Value { + result := make(map[string]attr.Value, len(m)) + for k, v := range m { + result[k] = types.StringValue(v) + } + return result +} + +func flattenDeploymentActionPackage(pkg *packages.DeploymentActionPackage) types.List { + if pkg == nil { + return types.ListNull(types.ObjectType{AttrTypes: getDonorPackageAttrTypes()}) + } + + obj := types.ObjectValueMust( + getDonorPackageAttrTypes(), + map[string]attr.Value{ + "deployment_action": types.StringValue(pkg.DeploymentAction), + "package_reference": types.StringValue(pkg.PackageReference), + }, + ) + + return types.ListValueMust(types.ObjectType{AttrTypes: getDonorPackageAttrTypes()}, []attr.Value{obj}) +} + +func getVersioningStrategyAttrTypes() map[string]attr.Type { + return map[string]attr.Type{ + "donor_package": types.ListType{ElemType: types.ObjectType{AttrTypes: getDonorPackageAttrTypes()}}, + "donor_package_step_id": types.StringType, + "template": types.StringType, + } +} + +func getDonorPackageAttrTypes() map[string]attr.Type { + return map[string]attr.Type{ + "deployment_action": types.StringType, + "package_reference": types.StringType, + } +} + +func getConnectivityPolicyAttrTypes() map[string]attr.Type { + return map[string]attr.Type{ + "allow_deployments_to_no_targets": types.BoolType, + "exclude_unhealthy_targets": types.BoolType, + "skip_machine_behavior": types.StringType, + "target_roles": types.ListType{ElemType: types.StringType}, + } +} + +func getReleaseCreationStrategyAttrTypes() map[string]attr.Type { + return map[string]attr.Type{ + "channel_id": types.StringType, + "release_creation_package_step_id": types.StringType, + "release_creation_package": types.ListType{ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "deployment_action": types.StringType, + "package_reference": types.StringType, + }, + }}, + } +} + +func getGitLibraryPersistenceSettingsAttrTypes() map[string]attr.Type { + return map[string]attr.Type{ + "git_credential_id": types.StringType, + "url": types.StringType, + "base_path": types.StringType, + "default_branch": types.StringType, + "protected_branches": types.SetType{ElemType: types.StringType}, + } +} + +func getGitAnonymousPersistenceSettingsAttrTypes() map[string]attr.Type { + return map[string]attr.Type{ + "url": types.StringType, + "base_path": types.StringType, + "default_branch": types.StringType, + "protected_branches": types.SetType{ElemType: types.StringType}, + } +} + +func getGitUsernamePasswordPersistenceSettingsAttrTypes() map[string]attr.Type { + return map[string]attr.Type{ + "url": types.StringType, + "base_path": types.StringType, + "default_branch": types.StringType, + "protected_branches": types.SetType{ElemType: types.StringType}, + "username": types.StringType, + "password": types.StringType, + } +} + +func getJSMExtensionSettingsAttrTypes() map[string]attr.Type { + return map[string]attr.Type{ + "connection_id": types.StringType, + "is_enabled": types.BoolType, + "service_desk_project_name": types.StringType, + } +} +func getTemplateAttrTypes() map[string]attr.Type { + return map[string]attr.Type{ + "id": types.StringType, + "name": types.StringType, + "label": types.StringType, + "help_text": types.StringType, + "default_value": types.StringType, + "display_settings": types.MapType{ElemType: types.StringType}, + } +} + +func getServiceNowExtensionSettingsAttrTypes() map[string]attr.Type { + return map[string]attr.Type{ + "connection_id": types.StringType, + "is_enabled": types.BoolType, + "is_state_automatically_transitioned": types.BoolType, + "standard_change_template_name": types.StringType, + } +} diff --git a/octopusdeploy_framework/resource_project_model.go b/octopusdeploy_framework/resource_project_model.go new file mode 100644 index 000000000..7e7386079 --- /dev/null +++ b/octopusdeploy_framework/resource_project_model.go @@ -0,0 +1,114 @@ +package octopusdeploy_framework + +import ( + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type projectResourceModel struct { + ID types.String `tfsdk:"id"` + SpaceID types.String `tfsdk:"space_id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + LifecycleID types.String `tfsdk:"lifecycle_id"` + ProjectGroupID types.String `tfsdk:"project_group_id"` + IsDisabled types.Bool `tfsdk:"is_disabled"` + AutoCreateRelease types.Bool `tfsdk:"auto_create_release"` + AllowDeploymentsToNoTargets types.Bool `tfsdk:"allow_deployments_to_no_targets"` + DefaultGuidedFailureMode types.String `tfsdk:"default_guided_failure_mode"` + DefaultToSkipIfAlreadyInstalled types.Bool `tfsdk:"default_to_skip_if_already_installed"` + DeploymentChangesTemplate types.String `tfsdk:"deployment_changes_template"` + DeploymentProcessID types.String `tfsdk:"deployment_process_id"` + DiscreteChannelRelease types.Bool `tfsdk:"discrete_channel_release"` + IsDiscreteChannelRelease types.Bool `tfsdk:"is_discrete_channel_release"` + IsVersionControlled types.Bool `tfsdk:"is_version_controlled"` + TenantedDeploymentParticipation types.String `tfsdk:"tenanted_deployment_participation"` + VariableSetID types.String `tfsdk:"variable_set_id"` + ReleaseNotesTemplate types.String `tfsdk:"release_notes_template"` + Slug types.String `tfsdk:"slug"` + ClonedFromProjectID types.String `tfsdk:"cloned_from_project_id"` + VersioningStrategy types.List `tfsdk:"versioning_strategy"` + ConnectivityPolicy types.List `tfsdk:"connectivity_policy"` + ReleaseCreationStrategy types.List `tfsdk:"release_creation_strategy"` + Template types.List `tfsdk:"template"` + GitAnonymousPersistenceSettings types.List `tfsdk:"git_anonymous_persistence_settings"` + GitLibraryPersistenceSettings types.List `tfsdk:"git_library_persistence_settings"` + GitUsernamePasswordPersistenceSettings types.List `tfsdk:"git_username_password_persistence_settings"` + JiraServiceManagementExtensionSettings types.List `tfsdk:"jira_service_management_extension_settings"` + ServiceNowExtensionSettings types.List `tfsdk:"servicenow_extension_settings"` + IncludedLibraryVariableSets types.List `tfsdk:"included_library_variable_sets"` + AutoDeployReleaseOverrides types.List `tfsdk:"auto_deploy_release_overrides"` +} + +type connectivityPolicyModel struct { + AllowDeploymentsToNoTargets types.Bool `tfsdk:"allow_deployments_to_no_targets"` + ExcludeUnhealthyTargets types.Bool `tfsdk:"exclude_unhealthy_targets"` + SkipMachineBehavior types.String `tfsdk:"skip_machine_behavior"` + TargetRoles types.List `tfsdk:"target_roles"` +} +type autoDeployReleaseOverrideModel struct { + EnvironmentID types.String `tfsdk:"environment_id"` + TenantID types.String `tfsdk:"tenant_id"` +} + +type jiraServiceManagementExtensionSettingsModel struct { + ConnectionID types.String `tfsdk:"connection_id"` + IsEnabled types.Bool `tfsdk:"is_enabled"` + ServiceDeskProjectName types.String `tfsdk:"service_desk_project_name"` +} + +type servicenowExtensionSettingsModel struct { + ConnectionID types.String `tfsdk:"connection_id"` + IsEnabled types.Bool `tfsdk:"is_enabled"` + IsStateAutomaticallyTransitioned types.Bool `tfsdk:"is_state_automatically_transitioned"` + StandardChangeTemplateName types.String `tfsdk:"standard_change_template_name"` +} + +type versioningStrategyModel struct { + DonorPackageStepID types.String `tfsdk:"donor_package_step_id"` + Template types.String `tfsdk:"template"` + DonorPackage types.List `tfsdk:"donor_package"` +} + +type releaseCreationStrategyModel struct { + ChannelID types.String `tfsdk:"channel_id"` + ReleaseCreationPackageStepID types.String `tfsdk:"release_creation_package_step_id"` + ReleaseCreationPackage types.Object `tfsdk:"release_creation_package"` +} + +type deploymentActionPackageModel struct { + DeploymentAction types.String `tfsdk:"deployment_action"` + PackageReference types.String `tfsdk:"package_reference"` +} + +type templateModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Label types.String `tfsdk:"label"` + HelpText types.String `tfsdk:"help_text"` + DefaultValue types.String `tfsdk:"default_value"` + DisplaySettings types.Map `tfsdk:"display_settings"` +} + +type gitLibraryPersistenceSettingsModel struct { + GitCredentialID types.String `tfsdk:"git_credential_id"` + URL types.String `tfsdk:"url"` + BasePath types.String `tfsdk:"base_path"` + DefaultBranch types.String `tfsdk:"default_branch"` + ProtectedBranches types.Set `tfsdk:"protected_branches"` +} + +type gitUsernamePasswordPersistenceSettingsModel struct { + URL types.String `tfsdk:"url"` + Username types.String `tfsdk:"username"` + Password types.String `tfsdk:"password"` + BasePath types.String `tfsdk:"base_path"` + DefaultBranch types.String `tfsdk:"default_branch"` + ProtectedBranches types.Set `tfsdk:"protected_branches"` +} + +type gitAnonymousPersistenceSettingsModel struct { + URL types.String `tfsdk:"url"` + BasePath types.String `tfsdk:"base_path"` + DefaultBranch types.String `tfsdk:"default_branch"` + ProtectedBranches types.Set `tfsdk:"protected_branches"` +} diff --git a/octopusdeploy/resource_project_test.go b/octopusdeploy_framework/resource_project_test.go similarity index 83% rename from octopusdeploy/resource_project_test.go rename to octopusdeploy_framework/resource_project_test.go index c8b1ff354..511e771e8 100644 --- a/octopusdeploy/resource_project_test.go +++ b/octopusdeploy_framework/resource_project_test.go @@ -1,20 +1,19 @@ -package octopusdeploy +package octopusdeploy_framework import ( "fmt" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/spaces" + internaltest "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal/test" "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" "os" "path/filepath" "sort" "testing" - - internaltest "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal/test" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) func TestAccProjectBasic(t *testing.T) { @@ -29,7 +28,7 @@ func TestAccProjectBasic(t *testing.T) { testAccProjectGroupCheckDestroy, testAccLifecycleCheckDestroy, ), - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { TestAccPreCheck(t) }, ProtoV6ProviderFactories: ProtoV6ProviderFactories(), Steps: []resource.TestStep{ { @@ -50,56 +49,33 @@ func TestAccProjectBasic(t *testing.T) { }) } -func testAccProject(localName string, name string, lifecycleLocalName string, projectGroupLocalName string) string { - return fmt.Sprintf(`resource "octopusdeploy_project" "%s" { - lifecycle_id = octopusdeploy_lifecycle.%s.id - name = "%s" - project_group_id = octopusdeploy_project_group.%s.id - }`, localName, lifecycleLocalName, name, projectGroupLocalName) -} - -type ProjectTestOptions struct { - AllowDeploymentsToNoTargets bool - LifecycleLocalName string - LocalName string - Name string - ProjectGroupLocalName string -} +func testAccProjectGroupCheckDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "octopusdeploy_project_group" { + continue + } -func NewProjectTestOptions(projectGroupLocalName string, lifecycleLocalName string) *ProjectTestOptions { - return &ProjectTestOptions{ - LifecycleLocalName: lifecycleLocalName, - LocalName: acctest.RandStringFromCharSet(20, acctest.CharSetAlpha), - Name: acctest.RandStringFromCharSet(20, acctest.CharSetAlpha), - ProjectGroupLocalName: projectGroupLocalName, + if projectGroup, err := octoClient.ProjectGroups.GetByID(rs.Primary.ID); err == nil { + return fmt.Errorf("project group (%s) still exists", projectGroup.GetID()) + } } -} -func testAccProjectWithOptions(opt *ProjectTestOptions) string { - - return fmt.Sprintf(`resource "octopusdeploy_project" "%s" { - allow_deployments_to_no_targets = %v - lifecycle_id = octopusdeploy_lifecycle.%s.id - name = "%s" - project_group_id = octopusdeploy_project_group.%s.id - }`, opt.LocalName, opt.AllowDeploymentsToNoTargets, opt.LifecycleLocalName, opt.Name, opt.ProjectGroupLocalName) + return nil } -func testAccProjectWithTemplate(localName string, name string, lifecycleLocalName string, projectGroupLocalName string) string { - return fmt.Sprintf(`resource "octopusdeploy_project" "%s" { - lifecycle_id = octopusdeploy_lifecycle.%s.id - name = "%s" - project_group_id = octopusdeploy_project_group.%s.id - - template { - name = "project variable template name" - label = "project variable template label" +func testProjectGroupExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } - display_settings = { - "Octopus.ControlType" = "Sensitive" - } + if _, err := octoClient.ProjectGroups.GetByID(rs.Primary.ID); err != nil { + return err } - }`, localName, lifecycleLocalName, name, projectGroupLocalName) + + return nil + } } func TestAccProjectWithUpdate(t *testing.T) { @@ -118,7 +94,7 @@ func TestAccProjectWithUpdate(t *testing.T) { testAccProjectCheckDestroy, testAccLifecycleCheckDestroy, ), - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { TestAccPreCheck(t) }, ProtoV6ProviderFactories: ProtoV6ProviderFactories(), Steps: []resource.TestStep{ { @@ -158,68 +134,31 @@ func testAccProjectBasic(lifecycleLocalName string, lifecycleName string, projec project_group_id = octopusdeploy_project_group.%s.id template { - default_value = "default-value" - help_text = "help-test" - label = "label" name = "2" - display_settings = { "Octopus.ControlType": "SingleLineText" } } template { - default_value = "default-value" - help_text = "help-test" - label = "label" name = "1" - display_settings = { "Octopus.ControlType": "SingleLineText" } } - // connectivity_policy { - // allow_deployments_to_no_targets = true - // skip_machine_behavior = "None" - // } + versioning_strategy { + template = "#{Octopus.Version.LastMajor}.#{Octopus.Version.LastMinor}.#{Octopus.Version.LastPatch}.#{Octopus.Version.NextRevision}" + } - // version_control_settings { - // default_branch = "foo" - // url = "https://example.com/" - // username = "bar" - // } + connectivity_policy { + allow_deployments_to_no_targets = true + skip_machine_behavior = "None" + } - // versioning_strategy { - // template = "alskdjaslkdj" - // } }`, localName, description, lifecycleLocalName, name, projectGroupLocalName) } -func testAccProjectCaC(spaceID string, lifecycleLocalName string, lifecycleName string, projectGroupLocalName string, projectGroupName string, localName string, name string, description string, basePath string, url string, password string, username string) string { - projectGroup := internaltest.NewProjectGroupTestOptions() - - return fmt.Sprintf(testAccLifecycle(lifecycleLocalName, lifecycleName)+"\n"+ - internaltest.ProjectGroupConfiguration(projectGroup)+"\n"+ - `resource "octopusdeploy_project" "%s" { - description = "%s" - lifecycle_id = octopusdeploy_lifecycle.%s.id - name = "%s" - project_group_id = octopusdeploy_project_group.%s.id - space_id = "%s" - - git_persistence_settings { - base_path = "%s" - url = "%s" - - credentials { - password = "%s" - username = "%s" - } - } - }`, localName, description, lifecycleLocalName, name, projectGroupLocalName, spaceID, basePath, url, password, username) -} - func testAccProjectCheckDestroy(s *terraform.State) error { for _, rs := range s.RootModule().Resources { if rs.Type != "octopusdeploy_project" { diff --git a/octopusdeploy_framework/resource_tenant_common_variable_test.go b/octopusdeploy_framework/resource_tenant_common_variable_test.go index 56a68c932..60ea33e05 100644 --- a/octopusdeploy_framework/resource_tenant_common_variable_test.go +++ b/octopusdeploy_framework/resource_tenant_common_variable_test.go @@ -15,7 +15,6 @@ import ( ) func TestAccTenantCommonVariableBasic(t *testing.T) { - internalTest.SkipCI(t, "project_environment have been refactor [deprecated] - will enable this test later after Ben fix") //SkipCI(t, "A managed resource \"octopusdeploy_project_group\" \"ewtxiwplhaenzmhpaqyx\" has\n not been declared in the root module.") lifecycleLocalName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) lifecycleName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) @@ -66,47 +65,51 @@ func testAccTenantCommonVariableBasic(lifecycleLocalName string, lifecycleName s sortOrder := acctest.RandIntRange(0, 10) useGuidedFailure := false projectGroup.LocalName = projectGroupLocalName + var tfConfig = fmt.Sprintf(testAccLifecycle(lifecycleLocalName, lifecycleName)+"\n"+ internalTest.ProjectGroupConfiguration(projectGroup)+"\n"+ testAccEnvironment(environmentLocalName, environmentName, description, allowDynamicInfrastructure, sortOrder, useGuidedFailure)+"\n"+` - resource "octopusdeploy_library_variable_set" "test-library-variable-set" { - name = "test" - - template { - default_value = "Default Value???" - help_text = "This is the help text" - label = "Test Label" - name = "Test Template" - - display_settings = { - "Octopus.ControlType" = "Sensitive" - } - } - } - - resource "octopusdeploy_project" "%s" { - included_library_variable_sets = [octopusdeploy_library_variable_set.test-library-variable-set.id] - lifecycle_id = octopusdeploy_lifecycle.%s.id - name = "%s" - project_group_id = octopusdeploy_project_group.%s.id - } - - resource "octopusdeploy_tenant" "%s" { - name = "%s" - } - - resource "octopusdeploy_tenant_project" "project_environment" { - tenant_id = octopusdeploy_tenant.%s.id - project_id = octopusdeploy_project.%s.id - environment_ids = [octopusdeploy_environment.%s.id] - } - - resource "octopusdeploy_tenant_common_variable" "%s" { - library_variable_set_id = octopusdeploy_library_variable_set.test-library-variable-set.id - template_id = octopusdeploy_library_variable_set.test-library-variable-set.template[0].id - tenant_id = octopusdeploy_tenant.%s.id - value = "%s" - }`, projectLocalName, lifecycleLocalName, projectName, projectGroupLocalName, tenantLocalName, tenantName, projectLocalName, environmentLocalName, localName, tenantLocalName, value) + resource "octopusdeploy_library_variable_set" "test-library-variable-set" { + name = "test" + + template { + default_value = "Default Value???" + help_text = "This is the help text" + label = "Test Label" + name = "Test Template" + + display_settings = { + "Octopus.ControlType" = "Sensitive" + } + } + } + + resource "octopusdeploy_project" "%[1]s" { + included_library_variable_sets = [octopusdeploy_library_variable_set.test-library-variable-set.id] + lifecycle_id = octopusdeploy_lifecycle.%[2]s.id + name = "%[3]s" + project_group_id = octopusdeploy_project_group.%[4]s.id + depends_on = [octopusdeploy_library_variable_set.test-library-variable-set] + } + + resource "octopusdeploy_tenant" "%[5]s" { + name = "%[6]s" + } + + resource "octopusdeploy_tenant_project" "project_environment" { + tenant_id = octopusdeploy_tenant.%[5]s.id + project_id = octopusdeploy_project.%[1]s.id + environment_ids = [octopusdeploy_environment.%[7]s.id] + depends_on = [octopusdeploy_project.%[1]s, octopusdeploy_tenant.%[5]s, octopusdeploy_environment.%[7]s] + } + + resource "octopusdeploy_tenant_common_variable" "%[8]s" { + library_variable_set_id = octopusdeploy_library_variable_set.test-library-variable-set.id + template_id = octopusdeploy_library_variable_set.test-library-variable-set.template[0].id + tenant_id = octopusdeploy_tenant.%[5]s.id + value = "%[9]s" + depends_on = [octopusdeploy_library_variable_set.test-library-variable-set, octopusdeploy_tenant_project.project_environment] + }`, projectLocalName, lifecycleLocalName, projectName, projectGroupLocalName, tenantLocalName, tenantName, environmentLocalName, localName, value) return tfConfig } diff --git a/octopusdeploy_framework/resource_tenant_project_variable_test.go b/octopusdeploy_framework/resource_tenant_project_variable_test.go index 29def6ad9..3c915e5c2 100644 --- a/octopusdeploy_framework/resource_tenant_project_variable_test.go +++ b/octopusdeploy_framework/resource_tenant_project_variable_test.go @@ -8,12 +8,9 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" - - internalTest "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal/test" ) func TestAccTenantProjectVariableBasic(t *testing.T) { - internalTest.SkipCI(t, "project_environment have been refactor [deprecated] - will enable this test later after Ben fix") lifecycleLocalName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) lifecycleName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) projectGroupLocalName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) @@ -124,7 +121,11 @@ func testTenantProjectVariable(localName string, environmentLocalName string, pr tenant_id = octopusdeploy_tenant.%s.id template_id = octopusdeploy_project.%s.template[0].id value = "%s" - }`, localName, environmentLocalName, projectLocalName, tenantLocalName, templateLocalName, value) + depends_on = [ + octopusdeploy_project.%s, + octopusdeploy_tenant_project.project_environment + ] + }`, localName, environmentLocalName, projectLocalName, tenantLocalName, templateLocalName, value, projectLocalName) } func testTenantProjectVariableExists(prefix string) resource.TestCheckFunc { diff --git a/octopusdeploy_framework/schemas/project.go b/octopusdeploy_framework/schemas/project.go new file mode 100644 index 000000000..fc4fee3b7 --- /dev/null +++ b/octopusdeploy_framework/schemas/project.go @@ -0,0 +1,379 @@ +package schemas + +import ( + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" + datasourceSchema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + resourceSchema "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +const ProjectResourceName = "project" +const ProjectDataSourceName = "projects" + +func GetProjectResourceSchema() resourceSchema.Schema { + return resourceSchema.Schema{ + Description: "This resource manages projects in Octopus Deploy.", + Attributes: map[string]resourceSchema.Attribute{ + "id": util.GetIdResourceSchema(), + "space_id": util.GetSpaceIdResourceSchema(ProjectResourceName), + "name": util.GetNameResourceSchema(true), + "description": util.GetDescriptionResourceSchema(ProjectResourceName), + "allow_deployments_to_no_targets": util.ResourceBool().Optional().Deprecated("This value is only valid for an associated connectivity policy and should not be specified here.").Build(), + "auto_create_release": util.ResourceBool().Optional().Computed().Build(), + "cloned_from_project_id": util.ResourceString().Optional().Description("The ID of the project this project was cloned from.").Build(), + "default_guided_failure_mode": util.ResourceString().Optional().Computed().Build(), + "default_to_skip_if_already_installed": util.ResourceBool().Optional().Computed().Build(), + "deployment_changes_template": util.ResourceString().Optional().Computed().Build(), + "discrete_channel_release": util.ResourceBool().Optional().Computed().Description("Treats releases of different channels to the same environment as a separate deployment dimension").Build(), + "is_disabled": util.ResourceBool().Optional().Computed().Build(), + "is_discrete_channel_release": util.ResourceBool().Optional().Computed().Description("Treats releases of different channels to the same environment as a separate deployment dimension").Build(), + "is_version_controlled": util.ResourceBool().Optional().Computed().Build(), + "lifecycle_id": util.ResourceString().Required().Description("The lifecycle ID associated with this project.").Build(), + "project_group_id": util.ResourceString().Required().Description("The project group ID associated with this project.").Build(), + "tenanted_deployment_participation": util.ResourceString().Optional().Computed().Description("The tenanted deployment mode of the resource. Valid account types are `Untenanted`, `TenantedOrUntenanted`, or `Tenanted`.").Build(), + "included_library_variable_sets": util.ResourceList(types.StringType).Optional().Computed().Description("The list of included library variable set IDs.").Build(), + "release_notes_template": util.ResourceString().Optional().Computed().Build(), + "slug": util.ResourceString().Optional().Computed().Description("A human-readable, unique identifier, used to identify a project.").Build(), + "deployment_process_id": util.ResourceString().Computed().Build(), + "variable_set_id": util.ResourceString().Computed().Build(), + }, + Blocks: map[string]resourceSchema.Block{ + // This is correct object that return from api for project object not a list string. + "auto_deploy_release_overrides": resourceSchema.ListNestedBlock{ + NestedObject: resourceSchema.NestedBlockObject{ + Attributes: map[string]resourceSchema.Attribute{ + "environment_id": util.ResourceString().Optional().Build(), + "release_id": util.ResourceString().Optional().Build(), + "tenant_id": util.ResourceString().Optional().Build(), + }, + }, + }, + "connectivity_policy": resourceSchema.ListNestedBlock{ + NestedObject: resourceSchema.NestedBlockObject{ + Attributes: map[string]resourceSchema.Attribute{ + "allow_deployments_to_no_targets": util.ResourceBool().Optional().Computed().Build(), + "exclude_unhealthy_targets": util.ResourceBool().Optional().Computed().Build(), + "skip_machine_behavior": util.ResourceString().Optional().Build(), + "target_roles": util.ResourceList(types.StringType).Optional().Computed().Build(), + }, + }, + }, + "git_anonymous_persistence_settings": resourceSchema.ListNestedBlock{ + NestedObject: resourceSchema.NestedBlockObject{ + Attributes: map[string]resourceSchema.Attribute{ + "url": util.ResourceString().Required().Description("The URL associated with these version control settings.").Build(), + "base_path": util.ResourceString().Optional().Description("The base path associated with these version control settings.").Build(), + "default_branch": util.ResourceString().Optional().Description("The default branch associated with these version control settings.").Build(), + "protected_branches": util.ResourceSet(types.StringType).Optional().Description("A list of protected branch patterns.").Build(), + }, + }, + Description: "Provides Git-related persistence settings for a version-controlled project.", + }, + "git_library_persistence_settings": resourceSchema.ListNestedBlock{ + NestedObject: resourceSchema.NestedBlockObject{ + Attributes: map[string]resourceSchema.Attribute{ + "git_credential_id": util.ResourceString().Required().Build(), + "url": util.ResourceString().Required().Description("The URL associated with these version control settings.").Build(), + "base_path": util.ResourceString().Optional().Description("The base path associated with these version control settings.").Build(), + "default_branch": util.ResourceString().Optional().Description("The default branch associated with these version control settings.").Build(), + "protected_branches": util.ResourceSet(types.StringType).Optional().Description("A list of protected branch patterns.").Build(), + }, + }, + Description: "Provides Git-related persistence settings for a version-controlled project.", + }, + "git_username_password_persistence_settings": resourceSchema.ListNestedBlock{ + NestedObject: resourceSchema.NestedBlockObject{ + Attributes: map[string]resourceSchema.Attribute{ + "url": util.ResourceString().Required().Description("The URL associated with these version control settings.").Build(), + "username": util.ResourceString().Required().Description("The username for the Git credential.").Build(), + "password": util.ResourceString().Sensitive().Required().Description("The password for the Git credential").Build(), //util.GetPasswordResourceSchema(false), + "base_path": util.ResourceString().Optional().Description("The base path associated with these version control settings.").Build(), + "default_branch": util.ResourceString().Optional().Description("The default branch associated with these version control settings.").Build(), + "protected_branches": util.ResourceSet(types.StringType).Optional().Description("A list of protected branch patterns.").Build(), + }, + }, + Description: "Provides Git-related persistence settings for a version-controlled project.", + }, + "jira_service_management_extension_settings": resourceSchema.ListNestedBlock{ + NestedObject: resourceSchema.NestedBlockObject{ + Attributes: map[string]resourceSchema.Attribute{ + "connection_id": util.ResourceString().Required().Description("The connection identifier associated with the extension settings.").Build(), + "is_enabled": util.ResourceBool().Required().Description("Specifies whether or not this extension is enabled for this project.").Build(), + "service_desk_project_name": util.ResourceString().Required().Description("The project name associated with this extension.").Build(), + }, + }, + Description: "Provides extension settings for the Jira Service Management (JSM) integration for this project.", + }, + "servicenow_extension_settings": resourceSchema.ListNestedBlock{ + NestedObject: resourceSchema.NestedBlockObject{ + Attributes: map[string]resourceSchema.Attribute{ + "connection_id": util.ResourceString().Required().Description("The connection identifier associated with the extension settings.").Build(), + "is_enabled": util.ResourceBool().Required().Description("Specifies whether or not this extension is enabled for this project.").Build(), + "is_state_automatically_transitioned": util.ResourceBool().Required().Description("Specifies whether or not this extension will automatically transition the state of a deployment for this project.").Build(), + "standard_change_template_name": util.ResourceString().Optional().Description("The name of the standard change template associated with this extension. If provided, deployments will create a standard change based on the provided template, otherwise a normal change will be created.").Build(), + }, + }, + Description: "Provides extension settings for the ServiceNow integration for this project.", + }, + "template": resourceSchema.ListNestedBlock{ + NestedObject: resourceSchema.NestedBlockObject{ + Attributes: map[string]resourceSchema.Attribute{ + "id": util.ResourceString().Optional().Computed().Description("The ID of the template parameter.").Build(), + "name": util.ResourceString().Required().Description("The name of the variable set by the parameter. The name can contain letters, digits, dashes and periods.").Build(), + "label": util.ResourceString().Optional().Description("The label shown beside the parameter when presented in the deployment process.").Build(), + "help_text": util.ResourceString().Optional().Description("The help presented alongside the parameter input.").Build(), + "default_value": util.ResourceString().Optional().Description("A default value for the parameter, if applicable. This can be a hard-coded value or a variable reference.").Build(), + "display_settings": resourceSchema.MapAttribute{ + Description: "The display settings for the parameter.", + ElementType: types.StringType, + Optional: true, + }, + }, + }, + }, + "versioning_strategy": resourceSchema.ListNestedBlock{ + NestedObject: resourceSchema.NestedBlockObject{ + Attributes: map[string]resourceSchema.Attribute{ + "donor_package_step_id": util.ResourceString().Optional().Build(), + "template": util.ResourceString().Optional().Build(), + }, + Blocks: map[string]resourceSchema.Block{ + "donor_package": resourceSchema.ListNestedBlock{ + NestedObject: resourceSchema.NestedBlockObject{ + Attributes: map[string]resourceSchema.Attribute{ + "deployment_action": util.ResourceString().Optional().Build(), + "package_reference": util.ResourceString().Optional().Build(), + }, + }, + }, + }, + }, + }, + "release_creation_strategy": resourceSchema.ListNestedBlock{ + NestedObject: resourceSchema.NestedBlockObject{ + Attributes: map[string]resourceSchema.Attribute{ + "channel_id": util.ResourceString().Optional().Build(), + "release_creation_package_step_id": util.ResourceString().Optional().Build(), + }, + Blocks: map[string]resourceSchema.Block{ + "release_creation_package": resourceSchema.ListNestedBlock{ + NestedObject: resourceSchema.NestedBlockObject{ + Attributes: map[string]resourceSchema.Attribute{ + "deployment_action": util.ResourceString().Optional().Build(), + "package_reference": util.ResourceString().Optional().Build(), + }, + }, + }, + }, + }, + }, + }, + } +} + +func GetProjectDataSourceSchema() datasourceSchema.Schema { + return datasourceSchema.Schema{ + Description: "Provides information about existing Octopus Deploy projects.", + Attributes: map[string]datasourceSchema.Attribute{ + "id": util.ResourceString().Computed().Description("An auto-generated identifier that includes the timestamp when this data source was last modified.").Build(), + "cloned_from_project_id": util.DataSourceString().Optional().Description("A filter to search for cloned resources by a project ID.").Build(), + "ids": util.GetQueryIDsDatasourceSchema(), + "is_clone": util.DataSourceBool().Optional().Description("A filter to search for cloned resources.").Build(), + "name": util.DataSourceString().Optional().Description("A filter to search by name").Build(), + "partial_name": util.GetQueryPartialNameDatasourceSchema(), + "skip": util.GetQuerySkipDatasourceSchema(), + "space_id": util.ResourceString().Optional().Description("A Space ID to filter by. Will revert what is specified on the provider if not set").Build(), + "take": util.GetQueryTakeDatasourceSchema(), + "projects": getProjectsDataSourceAttribute(), + }, + } +} + +func getProjectsDataSourceAttribute() datasourceSchema.ListNestedAttribute { + return datasourceSchema.ListNestedAttribute{ + Description: "A list of projects that match the filter(s).", + Computed: true, + NestedObject: datasourceSchema.NestedAttributeObject{ + Attributes: map[string]datasourceSchema.Attribute{ + "allow_deployments_to_no_targets": util.DataSourceBool().Computed().Deprecated("Allow deployments to be created when there are no targets.").Build(), + "auto_create_release": util.DataSourceBool().Computed().Build(), + "auto_deploy_release_overrides": getAutoDeployReleaseOverrides(), + "cloned_from_project_id": util.DataSourceString().Computed().Build(), + "default_guided_failure_mode": util.DataSourceString().Computed().Build(), + "default_to_skip_if_already_installed": util.DataSourceBool().Computed().Build(), + "deployment_changes_template": util.DataSourceString().Computed().Build(), + "deployment_process_id": util.DataSourceString().Computed().Build(), + "description": util.DataSourceString().Computed().Description("The description of this project").Build(), + "discrete_channel_release": util.DataSourceBool().Computed().Description("Treats releases of different channels to the same environment as a separate deployment dimension").Build(), + "id": util.DataSourceString().Computed().Build(), + "included_library_variable_sets": util.DataSourceList(types.StringType).Computed().Build(), + "is_disabled": util.DataSourceBool().Computed().Build(), + "is_discrete_channel_release": util.DataSourceBool().Computed().Build(), + "is_version_controlled": util.DataSourceBool().Computed().Build(), + "lifecycle_id": util.DataSourceString().Computed().Description("The lifecycle ID associated with this project").Build(), + "name": util.DataSourceString().Computed().Description("The name of the project in Octopus Deploy. This name must be unique.").Build(), + "project_group_id": util.DataSourceString().Computed().Description("The project group ID associated with this project.").Build(), + "release_notes_template": util.DataSourceString().Computed().Description("The template to use for release notes.").Build(), + "slug": util.DataSourceString().Computed().Description("A human-readable, unique identifier, used to identify a project.").Build(), + "space_id": util.DataSourceString().Computed().Description("The space ID associated with this project.").Build(), + "tenanted_deployment_participation": util.DataSourceString().Computed().Description("The tenanted deployment mode of the project.").Build(), + "variable_set_id": util.DataSourceString().Computed().Description("The ID of the variable set associated with this project.").Build(), + "connectivity_policy": getDataSourceConnectivityPolicyAttribute(), + "git_library_persistence_settings": getDataSourceGitPersistenceSettingsAttribute("library"), + "git_username_password_persistence_settings": getDataSourceGitPersistenceSettingsAttribute("username_password"), + "git_anonymous_persistence_settings": getDataSourceGitPersistenceSettingsAttribute("anonymous"), + "jira_service_management_extension_settings": getDataSourceJSMExtensionSettingsAttribute(), + "servicenow_extension_settings": getDataSourceServiceNowExtensionSettingsAttribute(), + "versioning_strategy": getDataSourceVersioningStrategyAttribute(), + "release_creation_strategy": getDataSourceReleaseCreationStrategyAttribute(), + "template": getDataSourceTemplateAttribute(), + }, + }, + } +} + +// This is correct object that return from api for project object not a list string. +func getAutoDeployReleaseOverrides() datasourceSchema.ListNestedAttribute { + return datasourceSchema.ListNestedAttribute{ + Computed: true, + NestedObject: datasourceSchema.NestedAttributeObject{ + Attributes: map[string]datasourceSchema.Attribute{ + "environment_id": util.DataSourceString().Computed().Description("The environment ID for the auto deploy release override.").Build(), + "release_id": util.DataSourceString().Computed().Description("The release ID for the auto deploy release override.").Build(), + "tenant_id": util.DataSourceString().Computed().Description("The tenant ID for the auto deploy release override.").Build(), + }, + }, + } +} + +func getDataSourceConnectivityPolicyAttribute() datasourceSchema.ListNestedAttribute { + return datasourceSchema.ListNestedAttribute{ + Computed: true, + NestedObject: datasourceSchema.NestedAttributeObject{ + Attributes: map[string]datasourceSchema.Attribute{ + "allow_deployments_to_no_targets": util.DataSourceBool().Computed().Description("Allow deployments to be created when there are no targets.").Build(), + "exclude_unhealthy_targets": util.DataSourceBool().Computed().Description("Exclude unhealthy targets from deployments.").Build(), + "skip_machine_behavior": util.DataSourceString().Computed().Description("The behavior when a machine is skipped.").Build(), + "target_roles": util.DataSourceList(types.StringType).Computed().Description("The target roles for the connectivity policy.").Build(), + }, + }, + } +} + +func getDataSourceGitPersistenceSettingsAttribute(settingType string) datasourceSchema.ListNestedAttribute { + attributes := map[string]datasourceSchema.Attribute{ + "base_path": util.DataSourceString().Computed().Description("The base path associated with these version control settings.").Build(), + "default_branch": util.DataSourceString().Computed().Description("The default branch associated with these version control settings.").Build(), + "protected_branches": util.DataSourceSet(types.StringType).Computed().Description("A list of protected branch patterns.").Build(), + "url": util.DataSourceString().Computed().Description("The URL associated with these version control settings.").Build(), + } + + switch settingType { + case "library": + attributes["git_credential_id"] = util.DataSourceString().Computed().Description("The ID of the Git credential.").Build() + case "username_password": + attributes["username"] = util.DataSourceString().Computed().Description("The username for the Git credential.").Build() + attributes["password"] = util.DataSourceString().Computed().Sensitive().Description("The password for the Git credential.").Build() + case "anonymous": + // No additional attributes for anonymous + } + + return datasourceSchema.ListNestedAttribute{ + Description: "Git-related persistence settings for a version-controlled project using " + settingType + " authentication.", + Computed: true, + NestedObject: datasourceSchema.NestedAttributeObject{ + Attributes: attributes, + }, + } +} + +func getDataSourceJSMExtensionSettingsAttribute() datasourceSchema.ListNestedAttribute { + return datasourceSchema.ListNestedAttribute{ + Description: "Extension settings for the Jira Service Management (JSM) integration.", + Computed: true, + NestedObject: datasourceSchema.NestedAttributeObject{ + Attributes: map[string]datasourceSchema.Attribute{ + "connection_id": util.DataSourceString().Computed().Description("The connection identifier for JSM.").Build(), + "is_enabled": util.DataSourceBool().Computed().Description("Whether the JSM extension is enabled.").Build(), + "service_desk_project_name": util.DataSourceString().Computed().Description("The JSM service desk project name.").Build(), + }, + }, + } +} + +func getDataSourceServiceNowExtensionSettingsAttribute() datasourceSchema.ListNestedAttribute { + return datasourceSchema.ListNestedAttribute{ + Description: "Extension settings for the ServiceNow integration.", + Computed: true, + NestedObject: datasourceSchema.NestedAttributeObject{ + Attributes: map[string]datasourceSchema.Attribute{ + "connection_id": util.DataSourceString().Computed().Description("The connection identifier for ServiceNow.").Build(), + "is_enabled": util.DataSourceBool().Computed().Description("Whether the ServiceNow extension is enabled.").Build(), + "is_state_automatically_transitioned": util.DataSourceBool().Computed().Description("Whether state is automatically transitioned in ServiceNow.").Build(), + "standard_change_template_name": util.DataSourceString().Computed().Description("The name of the standard change template in ServiceNow.").Build(), + }, + }, + } +} + +func getDataSourceVersioningStrategyAttribute() datasourceSchema.ListNestedAttribute { + return datasourceSchema.ListNestedAttribute{ + Description: "The versioning strategy for the project.", + Computed: true, + NestedObject: datasourceSchema.NestedAttributeObject{ + Attributes: map[string]datasourceSchema.Attribute{ + "donor_package_step_id": util.DataSourceString().Computed().Description("The ID of the step containing the donor package.").Build(), + "donor_package": datasourceSchema.ListNestedAttribute{ + Computed: true, + NestedObject: datasourceSchema.NestedAttributeObject{ + Attributes: map[string]datasourceSchema.Attribute{ + "deployment_action": util.DataSourceString().Computed().Description("The deployment action for the donor package.").Build(), + "package_reference": util.DataSourceString().Computed().Description("The package reference for the donor package.").Build(), + }, + }, + }, + "template": util.DataSourceString().Computed().Description("The template to use for version numbers.").Build(), + }, + }, + } +} + +func getDataSourceReleaseCreationStrategyAttribute() datasourceSchema.ListNestedAttribute { + return datasourceSchema.ListNestedAttribute{ + Description: "The release creation strategy for the project.", + Computed: true, + NestedObject: datasourceSchema.NestedAttributeObject{ + Attributes: map[string]datasourceSchema.Attribute{ + "channel_id": util.DataSourceString().Computed().Description("The ID of the channel to use for release creation.").Build(), + "release_creation_package": datasourceSchema.ListNestedAttribute{ + Description: "Details of the package used for release creation.", + Computed: true, + NestedObject: datasourceSchema.NestedAttributeObject{ + Attributes: map[string]datasourceSchema.Attribute{ + "deployment_action": util.DataSourceString().Computed().Description("The deployment action for the release creation package.").Build(), + "package_reference": util.DataSourceString().Computed().Description("The package reference for the release creation package.").Build(), + }, + }, + }, + "release_creation_package_step_id": util.DataSourceString().Computed().Description("The ID of the step containing the package for release creation.").Build(), + }, + }, + } +} + +func getDataSourceTemplateAttribute() datasourceSchema.ListNestedAttribute { + return datasourceSchema.ListNestedAttribute{ + Description: "Template parameters for the project.", + Computed: true, + NestedObject: datasourceSchema.NestedAttributeObject{ + Attributes: map[string]datasourceSchema.Attribute{ + "id": util.DataSourceString().Computed().Description("The ID of the template parameter.").Build(), + "name": util.DataSourceString().Computed().Description("The name of the variable set by the parameter.").Build(), + "label": util.DataSourceString().Computed().Description("The label shown beside the parameter.").Build(), + "help_text": util.DataSourceString().Computed().Description("The help text for the parameter.").Build(), + "default_value": util.DataSourceString().Computed().Description("The default value for the parameter.").Build(), + "display_settings": util.DataSourceMap(types.StringType).Computed().Description("The display settings for the parameter.").Build(), + }, + }, + } +} diff --git a/octopusdeploy_framework/util/datasource_attribute_builder.go b/octopusdeploy_framework/util/datasource_attribute_builder.go new file mode 100644 index 000000000..733ab13ed --- /dev/null +++ b/octopusdeploy_framework/util/datasource_attribute_builder.go @@ -0,0 +1,182 @@ +package util + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" +) + +type DataSourceAttributeBuilder[T any] struct { + attr T +} + +func NewDataSourceAttributeBuilder[T any]() *DataSourceAttributeBuilder[T] { + return &DataSourceAttributeBuilder[T]{} +} + +func (b *DataSourceAttributeBuilder[T]) Optional() *DataSourceAttributeBuilder[T] { + switch a := any(&b.attr).(type) { + case *schema.StringAttribute: + a.Optional = true + case *schema.BoolAttribute: + a.Optional = true + case *schema.Int64Attribute: + a.Optional = true + case *schema.Float64Attribute: + a.Optional = true + case *schema.ListAttribute: + a.Optional = true + case *schema.SetAttribute: + a.Optional = true + case *schema.MapAttribute: + a.Optional = true + } + return b +} + +func (b *DataSourceAttributeBuilder[T]) Deprecated(deprecationMessage string) *DataSourceAttributeBuilder[T] { + switch a := any(&b.attr).(type) { + case *schema.StringAttribute: + a.DeprecationMessage = deprecationMessage + case *schema.BoolAttribute: + a.DeprecationMessage = deprecationMessage + case *schema.Int64Attribute: + a.DeprecationMessage = deprecationMessage + case *schema.Float64Attribute: + a.DeprecationMessage = deprecationMessage + case *schema.NumberAttribute: + a.DeprecationMessage = deprecationMessage + case *schema.ListAttribute: + a.DeprecationMessage = deprecationMessage + case *schema.SetAttribute: + a.DeprecationMessage = deprecationMessage + case *schema.MapAttribute: + a.DeprecationMessage = deprecationMessage + case *schema.ObjectAttribute: + a.DeprecationMessage = deprecationMessage + } + return b +} + +func (b *DataSourceAttributeBuilder[T]) Required() *DataSourceAttributeBuilder[T] { + switch a := any(&b.attr).(type) { + case *schema.StringAttribute: + a.Required = true + case *schema.BoolAttribute: + a.Required = true + case *schema.Int64Attribute: + a.Required = true + case *schema.Float64Attribute: + a.Required = true + case *schema.ListAttribute: + a.Required = true + case *schema.SetAttribute: + a.Required = true + case *schema.MapAttribute: + a.Required = true + } + return b +} + +func (b *DataSourceAttributeBuilder[T]) Computed() *DataSourceAttributeBuilder[T] { + switch a := any(&b.attr).(type) { + case *schema.StringAttribute: + a.Computed = true + case *schema.BoolAttribute: + a.Computed = true + case *schema.Int64Attribute: + a.Computed = true + case *schema.Float64Attribute: + a.Computed = true + case *schema.ListAttribute: + a.Computed = true + case *schema.SetAttribute: + a.Computed = true + case *schema.MapAttribute: + a.Computed = true + } + return b +} + +func (b *DataSourceAttributeBuilder[T]) Description(desc string) *DataSourceAttributeBuilder[T] { + switch a := any(&b.attr).(type) { + case *schema.StringAttribute: + a.Description = desc + case *schema.BoolAttribute: + a.Description = desc + case *schema.Int64Attribute: + a.Description = desc + case *schema.Float64Attribute: + a.Description = desc + case *schema.ListAttribute: + a.Description = desc + case *schema.SetAttribute: + a.Description = desc + case *schema.MapAttribute: + a.Description = desc + } + return b +} + +func (b *DataSourceAttributeBuilder[T]) Sensitive() *DataSourceAttributeBuilder[T] { + switch a := any(&b.attr).(type) { + case *schema.StringAttribute: + a.Sensitive = true + case *schema.BoolAttribute: + a.Sensitive = true + case *schema.Int64Attribute: + a.Sensitive = true + case *schema.Float64Attribute: + a.Sensitive = true + case *schema.ListAttribute: + a.Sensitive = true + case *schema.SetAttribute: + a.Sensitive = true + case *schema.MapAttribute: + a.Sensitive = true + } + return b +} + +func (b *DataSourceAttributeBuilder[T]) ElementType(elementType attr.Type) *DataSourceAttributeBuilder[T] { + switch a := any(&b.attr).(type) { + case *schema.ListAttribute: + a.ElementType = elementType + case *schema.SetAttribute: + a.ElementType = elementType + case *schema.MapAttribute: + a.ElementType = elementType + } + return b +} + +func (b *DataSourceAttributeBuilder[T]) Build() T { + return b.attr +} + +func DataSourceString() *DataSourceAttributeBuilder[schema.StringAttribute] { + return NewDataSourceAttributeBuilder[schema.StringAttribute]() +} + +func DataSourceBool() *DataSourceAttributeBuilder[schema.BoolAttribute] { + return NewDataSourceAttributeBuilder[schema.BoolAttribute]() +} + +func DataSourceInt64() *DataSourceAttributeBuilder[schema.Int64Attribute] { + return NewDataSourceAttributeBuilder[schema.Int64Attribute]() +} + +func DataSourceFloat64() *DataSourceAttributeBuilder[schema.Float64Attribute] { + return NewDataSourceAttributeBuilder[schema.Float64Attribute]() +} + +func DataSourceList(elementType attr.Type) *DataSourceAttributeBuilder[schema.ListAttribute] { + return NewDataSourceAttributeBuilder[schema.ListAttribute]().ElementType(elementType) +} + +func DataSourceSet(elementType attr.Type) *DataSourceAttributeBuilder[schema.SetAttribute] { + return NewDataSourceAttributeBuilder[schema.SetAttribute]().ElementType(elementType) +} + +func DataSourceMap(elementType attr.Type) *DataSourceAttributeBuilder[schema.MapAttribute] { + return NewDataSourceAttributeBuilder[schema.MapAttribute]().ElementType(elementType) +} diff --git a/octopusdeploy_framework/util/resource_attribute_builder.go b/octopusdeploy_framework/util/resource_attribute_builder.go new file mode 100644 index 000000000..153cafbf7 --- /dev/null +++ b/octopusdeploy_framework/util/resource_attribute_builder.go @@ -0,0 +1,253 @@ +package util + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/float64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/setdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type AttributeBuilder[T any] struct { + attr T +} + +func NewAttributeBuilder[T any]() *AttributeBuilder[T] { + return &AttributeBuilder[T]{} +} + +func (b *AttributeBuilder[T]) Optional() *AttributeBuilder[T] { + switch a := any(&b.attr).(type) { + case *schema.StringAttribute: + a.Optional = true + case *schema.BoolAttribute: + a.Optional = true + case *schema.Int64Attribute: + a.Optional = true + case *schema.Float64Attribute: + a.Optional = true + case *schema.NumberAttribute: + a.Optional = true + case *schema.ListAttribute: + a.Optional = true + case *schema.SetAttribute: + a.Optional = true + case *schema.MapAttribute: + a.Optional = true + case *schema.ObjectAttribute: + a.Optional = true + } + return b +} + +func (b *AttributeBuilder[T]) Deprecated(deprecationMessage string) *AttributeBuilder[T] { + switch a := any(&b.attr).(type) { + case *schema.StringAttribute: + a.DeprecationMessage = deprecationMessage + case *schema.BoolAttribute: + a.DeprecationMessage = deprecationMessage + case *schema.Int64Attribute: + a.DeprecationMessage = deprecationMessage + case *schema.Float64Attribute: + a.DeprecationMessage = deprecationMessage + case *schema.NumberAttribute: + a.DeprecationMessage = deprecationMessage + case *schema.ListAttribute: + a.DeprecationMessage = deprecationMessage + case *schema.SetAttribute: + a.DeprecationMessage = deprecationMessage + case *schema.MapAttribute: + a.DeprecationMessage = deprecationMessage + case *schema.ObjectAttribute: + a.DeprecationMessage = deprecationMessage + } + return b +} + +func (b *AttributeBuilder[T]) Computed() *AttributeBuilder[T] { + switch a := any(&b.attr).(type) { + case *schema.StringAttribute: + a.Computed = true + case *schema.BoolAttribute: + a.Computed = true + case *schema.Int64Attribute: + a.Computed = true + case *schema.Float64Attribute: + a.Computed = true + case *schema.NumberAttribute: + a.Computed = true + case *schema.ListAttribute: + a.Computed = true + case *schema.SetAttribute: + a.Computed = true + case *schema.MapAttribute: + a.Computed = true + case *schema.ObjectAttribute: + a.Computed = true + } + return b +} + +func (b *AttributeBuilder[T]) Required() *AttributeBuilder[T] { + switch a := any(&b.attr).(type) { + case *schema.StringAttribute: + a.Required = true + case *schema.BoolAttribute: + a.Required = true + case *schema.Int64Attribute: + a.Required = true + case *schema.Float64Attribute: + a.Required = true + case *schema.NumberAttribute: + a.Required = true + case *schema.ListAttribute: + a.Required = true + case *schema.SetAttribute: + a.Required = true + case *schema.MapAttribute: + a.Required = true + case *schema.ObjectAttribute: + a.Required = true + } + return b +} + +func (b *AttributeBuilder[T]) Description(desc string) *AttributeBuilder[T] { + switch a := any(&b.attr).(type) { + case *schema.StringAttribute: + a.Description = desc + case *schema.BoolAttribute: + a.Description = desc + case *schema.Int64Attribute: + a.Description = desc + case *schema.Float64Attribute: + a.Description = desc + case *schema.NumberAttribute: + a.Description = desc + case *schema.ListAttribute: + a.Description = desc + case *schema.SetAttribute: + a.Description = desc + case *schema.MapAttribute: + a.Description = desc + case *schema.ObjectAttribute: + a.Description = desc + } + return b +} + +func (b *AttributeBuilder[T]) Default(defaultValue interface{}) *AttributeBuilder[T] { + switch a := any(&b.attr).(type) { + case *schema.StringAttribute: + if strDefault, ok := defaultValue.(string); ok { + a.Default = stringdefault.StaticString(strDefault) + } + case *schema.BoolAttribute: + if boolDefault, ok := defaultValue.(bool); ok { + a.Default = booldefault.StaticBool(boolDefault) + } + case *schema.Int64Attribute: + if intDefault, ok := defaultValue.(int64); ok { + a.Default = int64default.StaticInt64(intDefault) + } + case *schema.NumberAttribute: + case *schema.Float64Attribute: + if floatDefault, ok := defaultValue.(float64); ok { + a.Default = float64default.StaticFloat64(floatDefault) + } + case *schema.ListAttribute: + a.Default = listdefault.StaticValue(types.List{}) + case *schema.SetAttribute: + a.Default = setdefault.StaticValue(types.Set{}) + case *schema.MapAttribute: + a.Default = mapdefault.StaticValue(types.Map{}) + } + return b +} + +func (b *AttributeBuilder[T]) Sensitive() *AttributeBuilder[T] { + switch a := any(&b.attr).(type) { + case *schema.StringAttribute: + a.Sensitive = true + case *schema.BoolAttribute: + a.Sensitive = true + case *schema.Int64Attribute: + a.Sensitive = true + case *schema.Float64Attribute: + a.Sensitive = true + case *schema.NumberAttribute: + a.Sensitive = true + case *schema.ListAttribute: + a.Sensitive = true + case *schema.SetAttribute: + a.Sensitive = true + case *schema.MapAttribute: + a.Sensitive = true + case *schema.ObjectAttribute: + a.Sensitive = true + } + return b +} + +func (b *AttributeBuilder[T]) ElementType(elementType attr.Type) *AttributeBuilder[T] { + switch a := any(&b.attr).(type) { + case *schema.ListAttribute: + a.ElementType = elementType + case *schema.SetAttribute: + a.ElementType = elementType + case *schema.MapAttribute: + a.ElementType = elementType + } + return b +} + +func (b *AttributeBuilder[T]) AttributeTypes(attributeTypes map[string]attr.Type) *AttributeBuilder[T] { + if a, ok := any(&b.attr).(*schema.ObjectAttribute); ok { + a.AttributeTypes = attributeTypes + } + return b +} + +func (b *AttributeBuilder[T]) Build() T { + return b.attr +} + +func ResourceString() *AttributeBuilder[schema.StringAttribute] { + return NewAttributeBuilder[schema.StringAttribute]() +} + +func ResourceBool() *AttributeBuilder[schema.BoolAttribute] { + return NewAttributeBuilder[schema.BoolAttribute]() +} + +func ResourceInt64() *AttributeBuilder[schema.Int64Attribute] { + return NewAttributeBuilder[schema.Int64Attribute]() +} + +func ResourceFloat64() *AttributeBuilder[schema.Float64Attribute] { + return NewAttributeBuilder[schema.Float64Attribute]() +} + +func ResourceNumber() *AttributeBuilder[schema.NumberAttribute] { + return NewAttributeBuilder[schema.NumberAttribute]() +} + +func ResourceList(elementType attr.Type) *AttributeBuilder[schema.ListAttribute] { + return NewAttributeBuilder[schema.ListAttribute]().ElementType(elementType) +} + +func ResourceSet(elementType attr.Type) *AttributeBuilder[schema.SetAttribute] { + return NewAttributeBuilder[schema.SetAttribute]().ElementType(elementType) +} +func ResourceMap(elementType attr.Type) *AttributeBuilder[schema.MapAttribute] { + return NewAttributeBuilder[schema.MapAttribute]().ElementType(elementType) +} + +func ResourceObject(attributeTypes map[string]attr.Type) *AttributeBuilder[schema.ObjectAttribute] { + return NewAttributeBuilder[schema.ObjectAttribute]().AttributeTypes(attributeTypes) +} diff --git a/octopusdeploy_framework/util/schema.go b/octopusdeploy_framework/util/schema.go index a4ed08ee2..433892367 100644 --- a/octopusdeploy_framework/util/schema.go +++ b/octopusdeploy_framework/util/schema.go @@ -307,6 +307,7 @@ func GetPackageAcquisitionLocationOptionsResourceSchema() resourceSchema.Attribu }, } } + func GetFeedUriResourceSchema() resourceSchema.Attribute { return resourceSchema.StringAttribute{ Required: true, diff --git a/octopusdeploy_framework/util/util.go b/octopusdeploy_framework/util/util.go index 3aa77c459..0f167bf80 100644 --- a/octopusdeploy_framework/util/util.go +++ b/octopusdeploy_framework/util/util.go @@ -85,6 +85,13 @@ func ToValueSlice(slice []string) []attr.Value { return values } +func StringOrNull(s string) types.String { + if s == "" { + return types.StringNull() + } + return types.StringValue(s) +} + func Map[T, V any](items []T, fn func(T) V) []V { result := make([]V, len(items)) for i, t := range items { diff --git a/terraform/19b-projectspace/project.tf b/terraform/19b-projectspace/project.tf index f8104c064..92bb03da7 100644 --- a/terraform/19b-projectspace/project.tf +++ b/terraform/19b-projectspace/project.tf @@ -1,8 +1,10 @@ data "octopusdeploy_lifecycles" "lifecycle_default_lifecycle" { ids = null partial_name = "Default Lifecycle" + space_id = octopusdeploy_space.octopus_project_space_test.id skip = 0 take = 1 + depends_on = [octopusdeploy_space.octopus_project_space_test] } @@ -30,4 +32,9 @@ resource "octopusdeploy_project" "deploy_frontend_project" { exclude_unhealthy_targets = false skip_machine_behavior = "SkipUnavailableMachines" } + + depends_on = [ + octopusdeploy_space.octopus_project_space_test, + octopusdeploy_project_group.project_group_test, + ] } \ No newline at end of file diff --git a/terraform/39-projectgitusername/project.tf b/terraform/39-projectgitusername/project.tf index 44f9cb991..822ca1d1a 100644 --- a/terraform/39-projectgitusername/project.tf +++ b/terraform/39-projectgitusername/project.tf @@ -14,7 +14,7 @@ resource "octopusdeploy_project" "deploy_frontend_project" { discrete_channel_release = false is_disabled = false is_discrete_channel_release = false - is_version_controlled = false + is_version_controlled = true // use git_username_password_persistence_settings so this need to set to true lifecycle_id = data.octopusdeploy_lifecycles.lifecycle_default_lifecycle.lifecycles[0].id name = "Test" project_group_id = octopusdeploy_project_group.project_group_test.id From fdcf66c8ed14ec4394d8889c1b820caa89648fe8 Mon Sep 17 00:00:00 2001 From: Isaac Calligeros <101079287+IsaacCalligeros95@users.noreply.github.com> Date: Wed, 7 Aug 2024 14:34:53 +0930 Subject: [PATCH 02/28] Chore!: Migrate docker container registry (#702) * Migrate docker container registry * Tidy up * Rename APIVersion * Fix descriptions --- docs/data-sources/feeds.md | 35 ++-- docs/data-sources/library_variable_sets.md | 4 +- docs/resources/artifactory_generic_feed.md | 4 +- .../aws_elastic_container_registry.md | 6 +- docs/resources/docker_container_registry.md | 6 +- docs/resources/github_repository_feed.md | 4 +- docs/resources/helm_feed.md | 8 +- docs/resources/library_variable_set.md | 4 +- docs/resources/maven_feed.md | 4 +- docs/resources/nuget_feed.md | 6 +- octopusdeploy/provider.go | 1 - .../resource_docker_container_registry.go | 104 ---------- .../schema_docker_container_registry.go | 104 ---------- octopusdeploy_framework/framework_provider.go | 3 +- .../resource_artifactory_generic_feed.go | 3 +- ...resource_aws_elastic_container_registry.go | 3 +- .../resource_docker_container_registry.go | 183 ++++++++++++++++++ ...resource_docker_container_registry_test.go | 11 +- .../resource_github_repository_feed.go | 3 +- octopusdeploy_framework/resource_helm_feed.go | 3 +- .../resource_maven_feed.go | 3 +- .../resource_nuget_feed.go | 3 +- .../schemas/docker_container_registry_feed.go | 39 ++++ .../schemas/library_variable_set.go | 4 +- 24 files changed, 286 insertions(+), 262 deletions(-) delete mode 100644 octopusdeploy/resource_docker_container_registry.go delete mode 100644 octopusdeploy/schema_docker_container_registry.go create mode 100644 octopusdeploy_framework/resource_docker_container_registry.go rename {octopusdeploy => octopusdeploy_framework}/resource_docker_container_registry_test.go (94%) create mode 100644 octopusdeploy_framework/schemas/docker_container_registry_feed.go diff --git a/docs/data-sources/feeds.md b/docs/data-sources/feeds.md index 623ec5593..91fde3c96 100644 --- a/docs/data-sources/feeds.md +++ b/docs/data-sources/feeds.md @@ -27,37 +27,40 @@ data "octopusdeploy_feeds" "example" { ### Optional - `feed_type` (String) A filter to search by feed type. Valid feed types are `AwsElasticContainerRegistry`, `BuiltIn`, `Docker`, `GitHub`, `Helm`, `Maven`, `NuGet`, or `OctopusProject`. +- `feeds` (Block List) (see [below for nested schema](#nestedblock--feeds)) +- `id` (String) The unique ID for this resource. - `ids` (List of String) A filter to search by a list of IDs. -- `name` (String) A filter to search by name. -- `partial_name` (String) A filter to search by the partial match of a name. +- `name` (String) The name of this resource. +- `partial_name` (String) A filter to search by a partial name. - `skip` (Number) A filter to specify the number of items to skip in the response. -- `space_id` (String) The space ID associated with this resource. +- `space_id` (String) The space ID associated with this feeds. - `take` (Number) A filter to specify the number of items to take (or return) in the response. -### Read-Only - -- `feeds` (Block List) A list of feeds that match the filter(s). (see [below for nested schema](#nestedblock--feeds)) -- `id` (String) The ID of this resource. - ### Nested Schema for `feeds` -Read-Only: +Required: + +- `access_key` (String) The AWS access key to use when authenticating against Amazon Web Services. +- `feed_uri` (String) +- `name` (String) The name of this resource. + +Optional: -- `access_key` (String) - `api_version` (String) - `delete_unreleased_packages_after_days` (Number) - `download_attempts` (Number) The number of times a deployment should attempt to download a package from this feed before failing. - `download_retry_backoff_seconds` (Number) The number of seconds to apply as a linear back off between download attempts. -- `feed_type` (String) -- `feed_uri` (String) +- `feed_type` (String) A filter to search by feed type. Valid feed types are `AwsElasticContainerRegistry`, `BuiltIn`, `Docker`, `GitHub`, `Helm`, `Maven`, `NuGet`, or `OctopusProject`. - `id` (String) The unique ID for this resource. - `is_enhanced_mode` (Boolean) -- `name` (String) A short, memorable, unique name for this feed. Example: ACME Builds. - `package_acquisition_location_options` (List of String) - `password` (String, Sensitive) The password associated with this resource. -- `region` (String) - `registry_path` (String) - `secret_key` (String, Sensitive) -- `space_id` (String) The space ID associated with this resource. -- `username` (String, Sensitive) The username associated with this resource. \ No newline at end of file +- `space_id` (String) The space ID associated with this feeds. +- `username` (String, Sensitive) The username associated with this resource. + +Read-Only: + +- `region` (String) \ No newline at end of file diff --git a/docs/data-sources/library_variable_sets.md b/docs/data-sources/library_variable_sets.md index da636ab68..8b6eeb19f 100644 --- a/docs/data-sources/library_variable_sets.md +++ b/docs/data-sources/library_variable_sets.md @@ -3,12 +3,12 @@ page_title: "octopusdeploy_library_variable_sets Data Source - terraform-provider-octopusdeploy" subcategory: "" description: |- - + Provides information about existing library variable sets. --- # octopusdeploy_library_variable_sets (Data Source) - +Provides information about existing library variable sets. diff --git a/docs/resources/artifactory_generic_feed.md b/docs/resources/artifactory_generic_feed.md index 3d46c6eec..07ea676ce 100644 --- a/docs/resources/artifactory_generic_feed.md +++ b/docs/resources/artifactory_generic_feed.md @@ -29,7 +29,7 @@ resource "octopusdeploy_artifactory_generic_feed" "example" { ### Required - `feed_uri` (String) -- `name` (String) A short, memorable, unique name for this feed. Example: ACME Builds. +- `name` (String) The name of this resource. - `repository` (String) ### Optional @@ -38,7 +38,7 @@ resource "octopusdeploy_artifactory_generic_feed" "example" { - `layout_regex` (String) - `package_acquisition_location_options` (List of String) - `password` (String, Sensitive) The password associated with this resource. -- `space_id` (String) The space ID associated with this resource. +- `space_id` (String) The space ID associated with this helm feed. - `username` (String, Sensitive) The username associated with this resource. ## Import diff --git a/docs/resources/aws_elastic_container_registry.md b/docs/resources/aws_elastic_container_registry.md index 5549897ea..e0b4f802f 100644 --- a/docs/resources/aws_elastic_container_registry.md +++ b/docs/resources/aws_elastic_container_registry.md @@ -25,15 +25,15 @@ resource "octopusdeploy_aws_elastic_container_registry" "example" { ### Required - `access_key` (String) The AWS access key to use when authenticating against Amazon Web Services. -- `name` (String) A short, memorable, unique name for this feed. Example: ACME Builds. +- `name` (String) The name of this resource. - `region` (String) The AWS region where the registry resides. - `secret_key` (String, Sensitive) The AWS secret key to use when authenticating against Amazon Web Services. ### Optional -- `id` (String) The unique ID for this feed. +- `id` (String) The unique ID for this resource. - `package_acquisition_location_options` (List of String) -- `space_id` (String) The space ID associated with this feed. +- `space_id` (String) The space ID associated with this aws elastic container registry. ## Import diff --git a/docs/resources/docker_container_registry.md b/docs/resources/docker_container_registry.md index ebc6b68cb..8a59fe071 100644 --- a/docs/resources/docker_container_registry.md +++ b/docs/resources/docker_container_registry.md @@ -25,8 +25,8 @@ resource "octopusdeploy_docker_container_registry" "example" { ### Required -- `feed_uri` (String) The URL to a Docker repository. -- `name` (String) A short, memorable, unique name for this feed. Example: ACME Builds. +- `feed_uri` (String) +- `name` (String) The name of this resource. ### Optional @@ -35,7 +35,7 @@ resource "octopusdeploy_docker_container_registry" "example" { - `package_acquisition_location_options` (List of String) - `password` (String, Sensitive) The password associated with this resource. - `registry_path` (String) -- `space_id` (String) The space ID associated with this resource. +- `space_id` (String) The space ID associated with this docker container registry feed. - `username` (String, Sensitive) The username associated with this resource. ## Import diff --git a/docs/resources/github_repository_feed.md b/docs/resources/github_repository_feed.md index c743a47dd..37e9601c1 100644 --- a/docs/resources/github_repository_feed.md +++ b/docs/resources/github_repository_feed.md @@ -27,7 +27,7 @@ resource "octopusdeploy_github_repository_feed" "example" { ### Required - `feed_uri` (String) -- `name` (String) A short, memorable, unique name for this feed. Example: ACME Builds. +- `name` (String) The name of this resource. ### Optional @@ -36,7 +36,7 @@ resource "octopusdeploy_github_repository_feed" "example" { - `id` (String) The unique ID for this resource. - `package_acquisition_location_options` (List of String) - `password` (String, Sensitive) The password associated with this resource. -- `space_id` (String) The space ID associated with this resource. +- `space_id` (String) The space ID associated with this github repository feed. - `username` (String, Sensitive) The username associated with this resource. ## Import diff --git a/docs/resources/helm_feed.md b/docs/resources/helm_feed.md index 037bc5888..5eed44882 100644 --- a/docs/resources/helm_feed.md +++ b/docs/resources/helm_feed.md @@ -2,12 +2,12 @@ page_title: "octopusdeploy_helm_feed Resource - terraform-provider-octopusdeploy" subcategory: "Feeds" description: |- - This resource manages a Helm feed in Octopus Deploy. + This resource manages a Helm Feed in Octopus Deploy. --- # octopusdeploy_helm_feed (Resource) -This resource manages a Helm feed in Octopus Deploy. +This resource manages a Helm Feed in Octopus Deploy. ## Example Usage @@ -25,14 +25,14 @@ resource "octopusdeploy_helm_feed" "example" { ### Required - `feed_uri` (String) -- `name` (String) A short, memorable, unique name for this feed. Example: ACME Builds. +- `name` (String) The name of this resource. ### Optional - `id` (String) The unique ID for this resource. - `package_acquisition_location_options` (List of String) - `password` (String, Sensitive) The password associated with this resource. -- `space_id` (String) The space ID associated with this resource. +- `space_id` (String) The space ID associated with this helm feed. - `username` (String, Sensitive) The username associated with this resource. ## Import diff --git a/docs/resources/library_variable_set.md b/docs/resources/library_variable_set.md index b30fd3f1f..9bff4bf78 100644 --- a/docs/resources/library_variable_set.md +++ b/docs/resources/library_variable_set.md @@ -3,12 +3,12 @@ page_title: "octopusdeploy_library_variable_set Resource - terraform-provider-octopusdeploy" subcategory: "" description: |- - + This resource manages library variable sets in Octopus Deploy. --- # octopusdeploy_library_variable_set (Resource) - +This resource manages library variable sets in Octopus Deploy. diff --git a/docs/resources/maven_feed.md b/docs/resources/maven_feed.md index a589dc13f..1c01d16e8 100644 --- a/docs/resources/maven_feed.md +++ b/docs/resources/maven_feed.md @@ -27,7 +27,7 @@ resource "octopusdeploy_maven_feed" "example" { ### Required - `feed_uri` (String) -- `name` (String) A short, memorable, unique name for this feed. Example: ACME Builds. +- `name` (String) The name of this resource. ### Optional @@ -36,7 +36,7 @@ resource "octopusdeploy_maven_feed" "example" { - `id` (String) The unique ID for this resource. - `package_acquisition_location_options` (List of String) - `password` (String, Sensitive) The password associated with this resource. -- `space_id` (String) The space ID associated with this resource. +- `space_id` (String) The space ID associated with this maven feed. - `username` (String, Sensitive) The username associated with this resource. ## Import diff --git a/docs/resources/nuget_feed.md b/docs/resources/nuget_feed.md index 0444d55ba..b5758af66 100644 --- a/docs/resources/nuget_feed.md +++ b/docs/resources/nuget_feed.md @@ -27,8 +27,8 @@ resource "octopusdeploy_nuget_feed" "example" { ### Required -- `feed_uri` (String) The feed URI can be a URL or a folder path. -- `name` (String) A short, memorable, unique name for this feed. Example: ACME Builds. +- `feed_uri` (String) +- `name` (String) The name of this resource. ### Optional @@ -38,7 +38,7 @@ resource "octopusdeploy_nuget_feed" "example" { - `is_enhanced_mode` (Boolean) This will improve performance of the NuGet feed but may not be supported by some older feeds. Disable if the operation, Create Release does not return the latest version for a package. - `package_acquisition_location_options` (List of String) - `password` (String, Sensitive) The password associated with this resource. -- `space_id` (String) The space ID associated with this resource. +- `space_id` (String) The space ID associated with this nuget feed. - `username` (String, Sensitive) The username associated with this resource. ## Import diff --git a/octopusdeploy/provider.go b/octopusdeploy/provider.go index cd51d9521..07f503374 100644 --- a/octopusdeploy/provider.go +++ b/octopusdeploy/provider.go @@ -48,7 +48,6 @@ func Provider() *schema.Provider { "octopusdeploy_channel": resourceChannel(), "octopusdeploy_cloud_region_deployment_target": resourceCloudRegionDeploymentTarget(), "octopusdeploy_deployment_process": resourceDeploymentProcess(), - "octopusdeploy_docker_container_registry": resourceDockerContainerRegistry(), "octopusdeploy_dynamic_worker_pool": resourceDynamicWorkerPool(), "octopusdeploy_gcp_account": resourceGoogleCloudPlatformAccount(), "octopusdeploy_kubernetes_agent_deployment_target": resourceKubernetesAgentDeploymentTarget(), diff --git a/octopusdeploy/resource_docker_container_registry.go b/octopusdeploy/resource_docker_container_registry.go deleted file mode 100644 index bf5d1edd8..000000000 --- a/octopusdeploy/resource_docker_container_registry.go +++ /dev/null @@ -1,104 +0,0 @@ -package octopusdeploy - -import ( - "context" - "fmt" - - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client" - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/feeds" - "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal/errors" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func resourceDockerContainerRegistry() *schema.Resource { - return &schema.Resource{ - CreateContext: resourceDockerContainerRegistryCreate, - DeleteContext: resourceDockerContainerRegistryDelete, - Description: "This resource manages a Docker Container Registry in Octopus Deploy.", - Importer: getImporter(), - ReadContext: resourceDockerContainerRegistryRead, - Schema: getDockerContainerRegistrySchema(), - UpdateContext: resourceDockerContainerRegistryUpdate, - } -} - -func resourceDockerContainerRegistryCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - dockerContainerRegistry, err := expandDockerContainerRegistry(d) - if err != nil { - return diag.FromErr(err) - } - - tflog.Info(ctx, fmt.Sprintf("creating Docker container registry, %s", dockerContainerRegistry.GetName())) - - client := m.(*client.Client) - createdDockerContainerRegistry, err := feeds.Add(client, dockerContainerRegistry) - if err != nil { - return diag.FromErr(err) - } - - if err := setDockerContainerRegistry(ctx, d, createdDockerContainerRegistry.(*feeds.DockerContainerRegistry)); err != nil { - return diag.FromErr(err) - } - - d.SetId(createdDockerContainerRegistry.GetID()) - - tflog.Info(ctx, fmt.Sprintf("Docker container registry created (%s)", d.Id())) - return nil -} - -func resourceDockerContainerRegistryDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Info(ctx, fmt.Sprintf("deleting Docker container registry (%s)", d.Id())) - - client := m.(*client.Client) - err := client.Feeds.DeleteByID(d.Id()) - if err != nil { - return diag.FromErr(err) - } - - d.SetId("") - - tflog.Info(ctx, "Docker container registry deleted") - return nil -} - -func resourceDockerContainerRegistryRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Info(ctx, fmt.Sprintf("reading Docker container registry (%s)", d.Id())) - - client := m.(*client.Client) - feed, err := feeds.GetByID(client, d.Get("space_id").(string), d.Id()) - if err != nil { - return errors.ProcessApiError(ctx, d, err, "Docker container registry") - } - - dockerContainerRegistry := feed.(*feeds.DockerContainerRegistry) - if err := setDockerContainerRegistry(ctx, d, dockerContainerRegistry); err != nil { - return diag.FromErr(err) - } - - tflog.Info(ctx, fmt.Sprintf("Docker container registry read (%s)", dockerContainerRegistry.GetID())) - return nil -} - -func resourceDockerContainerRegistryUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - feed, err := expandDockerContainerRegistry(d) - if err != nil { - return diag.FromErr(err) - } - - tflog.Info(ctx, fmt.Sprintf("updating Docker container registry (%s)", feed.GetID())) - - client := m.(*client.Client) - updatedFeed, err := feeds.Update(client, feed) - if err != nil { - return diag.FromErr(err) - } - - if err := setDockerContainerRegistry(ctx, d, updatedFeed.(*feeds.DockerContainerRegistry)); err != nil { - return diag.FromErr(err) - } - - tflog.Info(ctx, fmt.Sprintf("Docker container registry updated (%s)", d.Id())) - return nil -} diff --git a/octopusdeploy/schema_docker_container_registry.go b/octopusdeploy/schema_docker_container_registry.go deleted file mode 100644 index a28f565cd..000000000 --- a/octopusdeploy/schema_docker_container_registry.go +++ /dev/null @@ -1,104 +0,0 @@ -package octopusdeploy - -import ( - "context" - "fmt" - - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/feeds" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" -) - -func expandDockerContainerRegistry(d *schema.ResourceData) (*feeds.DockerContainerRegistry, error) { - name := d.Get("name").(string) - - feed, err := feeds.NewDockerContainerRegistry(name) - if err != nil { - return nil, err - } - - feed.ID = d.Id() - - if v, ok := d.GetOk("api_version"); ok { - feed.APIVersion = v.(string) - } - - if v, ok := d.GetOk("feed_uri"); ok { - feed.FeedURI = v.(string) - } - - if v, ok := d.GetOk("registry_path"); ok { - feed.RegistryPath = v.(string) - } - - if v, ok := d.GetOk("space_id"); ok { - feed.SpaceID = v.(string) - } - - if v, ok := d.GetOk("package_acquisition_location_options"); ok { - feed.PackageAcquisitionLocationOptions = getSliceFromTerraformTypeList(v) - } - - if v, ok := d.GetOk("password"); ok { - feed.Password = core.NewSensitiveValue(v.(string)) - } - - if v, ok := d.GetOk("username"); ok { - feed.Username = v.(string) - } - - return feed, nil -} - -func getDockerContainerRegistrySchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "api_version": { - Optional: true, - Type: schema.TypeString, - }, - "feed_uri": { - Description: "The URL to a Docker repository.", - Required: true, - Type: schema.TypeString, - ValidateDiagFunc: validation.ToDiagFunc(validation.IsURLWithHTTPorHTTPS), - }, - "id": getIDSchema(), - "name": { - Description: "A short, memorable, unique name for this feed. Example: ACME Builds.", - Required: true, - Type: schema.TypeString, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotEmpty), - }, - "password": getPasswordSchema(false), - "package_acquisition_location_options": { - Computed: true, - Elem: &schema.Schema{Type: schema.TypeString}, - Optional: true, - Type: schema.TypeList, - }, - "registry_path": { - Optional: true, - Type: schema.TypeString, - }, - "space_id": getSpaceIDSchema(), - "username": getUsernameSchema(false), - } -} - -func setDockerContainerRegistry(ctx context.Context, d *schema.ResourceData, feed *feeds.DockerContainerRegistry) error { - d.Set("api_version", feed.APIVersion) - d.Set("feed_uri", feed.FeedURI) - d.Set("name", feed.Name) - d.Set("registry_path", feed.RegistryPath) - d.Set("space_id", feed.SpaceID) - d.Set("username", feed.Username) - - if err := d.Set("package_acquisition_location_options", feed.PackageAcquisitionLocationOptions); err != nil { - return fmt.Errorf("error setting package_acquisition_location_options: %s", err) - } - - d.SetId(feed.GetID()) - - return nil -} diff --git a/octopusdeploy_framework/framework_provider.go b/octopusdeploy_framework/framework_provider.go index 816b8f774..447f66237 100644 --- a/octopusdeploy_framework/framework_provider.go +++ b/octopusdeploy_framework/framework_provider.go @@ -2,9 +2,9 @@ package octopusdeploy_framework import ( "context" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" "os" - "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/schema" @@ -92,6 +92,7 @@ func (p *octopusDeployFrameworkProvider) Resources(ctx context.Context) []func() NewLibraryVariableSetFeedResource, NewVariableResource, NewProjectResource, + NewDockerContainerRegistryFeedResource, } } diff --git a/octopusdeploy_framework/resource_artifactory_generic_feed.go b/octopusdeploy_framework/resource_artifactory_generic_feed.go index e70900061..a1eb7ab58 100644 --- a/octopusdeploy_framework/resource_artifactory_generic_feed.go +++ b/octopusdeploy_framework/resource_artifactory_generic_feed.go @@ -29,7 +29,8 @@ func (r *artifactoryGenericFeedTypeResource) Metadata(ctx context.Context, req r func (r *artifactoryGenericFeedTypeResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ - Attributes: schemas.GetArtifactoryGenericFeedResourceSchema(), + Attributes: schemas.GetArtifactoryGenericFeedResourceSchema(), + Description: "This resource manages a Artifactory Generic feed in Octopus Deploy.", } } diff --git a/octopusdeploy_framework/resource_aws_elastic_container_registry.go b/octopusdeploy_framework/resource_aws_elastic_container_registry.go index 96f089a83..72cef3b51 100644 --- a/octopusdeploy_framework/resource_aws_elastic_container_registry.go +++ b/octopusdeploy_framework/resource_aws_elastic_container_registry.go @@ -28,7 +28,8 @@ func (r *awsElasticContainerRegistryFeedTypeResource) Metadata(ctx context.Conte func (r *awsElasticContainerRegistryFeedTypeResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ - Attributes: schemas.GetAwsElasticContainerRegistryFeedResourceSchema(), + Attributes: schemas.GetAwsElasticContainerRegistryFeedResourceSchema(), + Description: "This resource manages an AWS Elastic Container Registry in Octopus Deploy.", } } diff --git a/octopusdeploy_framework/resource_docker_container_registry.go b/octopusdeploy_framework/resource_docker_container_registry.go new file mode 100644 index 000000000..fa73ef902 --- /dev/null +++ b/octopusdeploy_framework/resource_docker_container_registry.go @@ -0,0 +1,183 @@ +package octopusdeploy_framework + +import ( + "context" + "fmt" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/feeds" + "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/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +type dockerContainerRegistryFeedTypeResource struct { + *Config +} + +func NewDockerContainerRegistryFeedResource() resource.Resource { + return &dockerContainerRegistryFeedTypeResource{} +} + +func (r *dockerContainerRegistryFeedTypeResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = util.GetTypeName("docker_container_registry") +} + +func (r *dockerContainerRegistryFeedTypeResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: schemas.GetDockerContainerRegistryFeedResourceSchema(), + Description: "This resource manages a Docker Container Registry in Octopus Deploy.", + } +} + +func (r *dockerContainerRegistryFeedTypeResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.Config = ResourceConfiguration(req, resp) +} + +func (r *dockerContainerRegistryFeedTypeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data *schemas.DockerContainerRegistryFeedTypeResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + dockerContainerRegistryFeed, err := createDockerContainerRegistryFeedResourceFromData(data) + if err != nil { + return + } + + tflog.Info(ctx, fmt.Sprintf("creating Docker Container Registry feed: %s", dockerContainerRegistryFeed.GetName())) + + client := r.Config.Client + createdFeed, err := feeds.Add(client, dockerContainerRegistryFeed) + if err != nil { + resp.Diagnostics.AddError("unable to create docker container registry feed", err.Error()) + return + } + + updateDataFromDockerContainerRegistryFeed(data, data.SpaceID.ValueString(), createdFeed.(*feeds.DockerContainerRegistry)) + + tflog.Info(ctx, fmt.Sprintf("Docker Container Registry feed created (%s)", data.ID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *dockerContainerRegistryFeedTypeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data *schemas.DockerContainerRegistryFeedTypeResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Info(ctx, fmt.Sprintf("reading Docker Container Registry feed (%s)", data.ID)) + + client := r.Config.Client + feed, err := feeds.GetByID(client, data.SpaceID.ValueString(), data.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("unable to load docker container registry feed", err.Error()) + return + } + + dockerContainerRegistry := feed.(*feeds.DockerContainerRegistry) + updateDataFromDockerContainerRegistryFeed(data, data.SpaceID.ValueString(), dockerContainerRegistry) + + tflog.Info(ctx, fmt.Sprintf("Docker Container Registry feed read (%s)", dockerContainerRegistry.GetID())) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *dockerContainerRegistryFeedTypeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data, state *schemas.DockerContainerRegistryFeedTypeResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, fmt.Sprintf("updating docker container registry feed '%s'", data.ID.ValueString())) + + feed, err := createDockerContainerRegistryFeedResourceFromData(data) + feed.ID = state.ID.ValueString() + if err != nil { + resp.Diagnostics.AddError("unable to load docker container registry feed", err.Error()) + return + } + + tflog.Info(ctx, fmt.Sprintf("updating Docker Container Registry feed (%s)", data.ID)) + + client := r.Config.Client + updatedFeed, err := feeds.Update(client, feed) + if err != nil { + resp.Diagnostics.AddError("unable to update docker container registry feed", err.Error()) + return + } + + updateDataFromDockerContainerRegistryFeed(data, state.SpaceID.ValueString(), updatedFeed.(*feeds.DockerContainerRegistry)) + + tflog.Info(ctx, fmt.Sprintf("Docker Container Registry feed updated (%s)", data.ID)) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *dockerContainerRegistryFeedTypeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data schemas.DockerContainerRegistryFeedTypeResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + if err := feeds.DeleteByID(r.Config.Client, data.SpaceID.ValueString(), data.ID.ValueString()); err != nil { + resp.Diagnostics.AddError("unable to delete docker container registry feed", err.Error()) + return + } +} + +func createDockerContainerRegistryFeedResourceFromData(data *schemas.DockerContainerRegistryFeedTypeResourceModel) (*feeds.DockerContainerRegistry, error) { + feed, err := feeds.NewDockerContainerRegistry(data.Name.ValueString()) + if err != nil { + return nil, err + } + + feed.ID = data.ID.ValueString() + feed.FeedURI = data.FeedUri.ValueString() + + var packageAcquisitionLocationOptions []string + for _, element := range data.PackageAcquisitionLocationOptions.Elements() { + packageAcquisitionLocationOptions = append(packageAcquisitionLocationOptions, element.(types.String).ValueString()) + } + + feed.PackageAcquisitionLocationOptions = packageAcquisitionLocationOptions + feed.Password = core.NewSensitiveValue(data.Password.ValueString()) + feed.SpaceID = data.SpaceID.ValueString() + feed.Username = data.Username.ValueString() + feed.APIVersion = data.APIVersion.ValueString() + feed.RegistryPath = data.RegistryPath.ValueString() + + return feed, nil +} + +func updateDataFromDockerContainerRegistryFeed(data *schemas.DockerContainerRegistryFeedTypeResourceModel, spaceId string, feed *feeds.DockerContainerRegistry) { + data.FeedUri = types.StringValue(feed.FeedURI) + data.Name = types.StringValue(feed.Name) + data.SpaceID = types.StringValue(spaceId) + if feed.APIVersion != "" { + data.APIVersion = types.StringValue(feed.APIVersion) + } + if feed.RegistryPath != "" { + data.RegistryPath = types.StringValue(feed.RegistryPath) + } + if feed.Username != "" { + data.Username = types.StringValue(feed.Username) + } + + packageAcquisitionLocationOptionsList := make([]attr.Value, len(feed.PackageAcquisitionLocationOptions)) + for i, option := range feed.PackageAcquisitionLocationOptions { + packageAcquisitionLocationOptionsList[i] = types.StringValue(option) + } + + var packageAcquisitionLocationOptionsListValue, _ = types.ListValue(types.StringType, packageAcquisitionLocationOptionsList) + data.PackageAcquisitionLocationOptions = packageAcquisitionLocationOptionsListValue + data.ID = types.StringValue(feed.ID) +} diff --git a/octopusdeploy/resource_docker_container_registry_test.go b/octopusdeploy_framework/resource_docker_container_registry_test.go similarity index 94% rename from octopusdeploy/resource_docker_container_registry_test.go rename to octopusdeploy_framework/resource_docker_container_registry_test.go index 377ced443..9e2c893fa 100644 --- a/octopusdeploy/resource_docker_container_registry_test.go +++ b/octopusdeploy_framework/resource_docker_container_registry_test.go @@ -1,16 +1,15 @@ -package octopusdeploy +package octopusdeploy_framework import ( "fmt" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/feeds" "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" "path/filepath" "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) func TestAccOctopusDeployDockerContainerRegistry(t *testing.T) { @@ -26,7 +25,7 @@ func TestAccOctopusDeployDockerContainerRegistry(t *testing.T) { resource.Test(t, resource.TestCase{ CheckDestroy: testDockerContainerRegistryCheckDestroy, - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { TestAccPreCheck(t) }, ProtoV6ProviderFactories: ProtoV6ProviderFactories(), Steps: []resource.TestStep{ { diff --git a/octopusdeploy_framework/resource_github_repository_feed.go b/octopusdeploy_framework/resource_github_repository_feed.go index 7a198daaa..30f7bdb28 100644 --- a/octopusdeploy_framework/resource_github_repository_feed.go +++ b/octopusdeploy_framework/resource_github_repository_feed.go @@ -28,7 +28,8 @@ func (r *githubRepositoryFeedTypeResource) Metadata(ctx context.Context, req res func (r *githubRepositoryFeedTypeResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ - Attributes: schemas.GetGitHubRepositoryFeedResourceSchema(), + Attributes: schemas.GetGitHubRepositoryFeedResourceSchema(), + Description: "This resource manages a GitHub repository feed in Octopus Deploy.", } } diff --git a/octopusdeploy_framework/resource_helm_feed.go b/octopusdeploy_framework/resource_helm_feed.go index 4fc1c9d3c..64ec52677 100644 --- a/octopusdeploy_framework/resource_helm_feed.go +++ b/octopusdeploy_framework/resource_helm_feed.go @@ -29,7 +29,8 @@ func (r *helmFeedTypeResource) Metadata(ctx context.Context, req resource.Metada func (r *helmFeedTypeResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ - Attributes: schemas.GetHelmFeedResourceSchema(), + Attributes: schemas.GetHelmFeedResourceSchema(), + Description: "This resource manages a Helm Feed in Octopus Deploy.", } } diff --git a/octopusdeploy_framework/resource_maven_feed.go b/octopusdeploy_framework/resource_maven_feed.go index c6f6ddd19..722081e81 100644 --- a/octopusdeploy_framework/resource_maven_feed.go +++ b/octopusdeploy_framework/resource_maven_feed.go @@ -28,7 +28,8 @@ func (r *mavenFeedTypeResource) Metadata(ctx context.Context, req resource.Metad func (r *mavenFeedTypeResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ - Attributes: schemas.GetMavenFeedResourceSchema(), + Attributes: schemas.GetMavenFeedResourceSchema(), + Description: "This resource manages a Maven feed in Octopus Deploy.", } } diff --git a/octopusdeploy_framework/resource_nuget_feed.go b/octopusdeploy_framework/resource_nuget_feed.go index 306e16f14..f053ea450 100644 --- a/octopusdeploy_framework/resource_nuget_feed.go +++ b/octopusdeploy_framework/resource_nuget_feed.go @@ -28,7 +28,8 @@ func (r *nugetFeedTypeResource) Metadata(ctx context.Context, req resource.Metad func (r *nugetFeedTypeResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ - Attributes: schemas.GetNugetFeedResourceSchema(), + Attributes: schemas.GetNugetFeedResourceSchema(), + Description: "This resource manages a Nuget feed in Octopus Deploy.", } } diff --git a/octopusdeploy_framework/schemas/docker_container_registry_feed.go b/octopusdeploy_framework/schemas/docker_container_registry_feed.go new file mode 100644 index 000000000..d7ff550c4 --- /dev/null +++ b/octopusdeploy_framework/schemas/docker_container_registry_feed.go @@ -0,0 +1,39 @@ +package schemas + +import ( + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" + resourceSchema "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +const dockerContainerRegistryFeedDescription = "docker container registry feed" + +func GetDockerContainerRegistryFeedResourceSchema() map[string]resourceSchema.Attribute { + return map[string]resourceSchema.Attribute{ + "api_version": resourceSchema.StringAttribute{ + Optional: true, + }, + "feed_uri": util.GetFeedUriResourceSchema(), + "id": util.GetIdResourceSchema(), + "name": util.GetNameResourceSchema(true), + "package_acquisition_location_options": util.GetPackageAcquisitionLocationOptionsResourceSchema(), + "password": util.GetPasswordResourceSchema(false), + "space_id": util.GetSpaceIdResourceSchema(dockerContainerRegistryFeedDescription), + "username": util.GetUsernameResourceSchema(false), + "registry_path": resourceSchema.StringAttribute{ + Optional: true, + }, + } +} + +type DockerContainerRegistryFeedTypeResourceModel struct { + APIVersion types.String `tfsdk:"api_version"` + FeedUri types.String `tfsdk:"feed_uri"` + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + PackageAcquisitionLocationOptions types.List `tfsdk:"package_acquisition_location_options"` + Password types.String `tfsdk:"password"` + SpaceID types.String `tfsdk:"space_id"` + Username types.String `tfsdk:"username"` + RegistryPath types.String `tfsdk:"registry_path"` +} diff --git a/octopusdeploy_framework/schemas/library_variable_set.go b/octopusdeploy_framework/schemas/library_variable_set.go index f3355768f..5fab75d02 100644 --- a/octopusdeploy_framework/schemas/library_variable_set.go +++ b/octopusdeploy_framework/schemas/library_variable_set.go @@ -22,7 +22,8 @@ type LibraryVariableSetResourceModel struct { func GetLibraryVariableSetDataSourceSchema() datasourceSchema.Schema { return datasourceSchema.Schema{ - Attributes: getLibraryVariableSetDataSchema(), + Attributes: getLibraryVariableSetDataSchema(), + Description: "Provides information about existing library variable sets.", Blocks: map[string]datasourceSchema.Block{ "library_variable_sets": datasourceSchema.ListNestedBlock{ Description: "A list of library variable sets that match the filter(s).", @@ -97,6 +98,7 @@ func GetLibraryVariableSetResourceSchema() resourceSchema.Schema { Computed: true, }, }, + Description: "This resource manages library variable sets in Octopus Deploy.", Blocks: map[string]resourceSchema.Block{ "template": resourceSchema.ListNestedBlock{ NestedObject: resourceSchema.NestedBlockObject{ From 7c81ce944a04db0272c2a4aab8adac7d3b56a1b1 Mon Sep 17 00:00:00 2001 From: Huy Nguyen <162080607+HuyPhanNguyen@users.noreply.github.com> Date: Wed, 7 Aug 2024 18:37:14 +1000 Subject: [PATCH 03/28] Fix bug datasource spaces (#710) * Fix query.Ids convert and add basic test * Update the test * Fix the util method instead * just ignore if cant convert to string * try to fix flaky test * second try * Add lifecycle datasource test * Last try to avoid race condition * Test why not 7 variables create * switch the order --- octopusdeploy/testing_container_test.go | 5 ++ .../datasource_lifecycle_test.go | 63 +++++++++++++++++ .../datasource_spaces_test.go | 58 ++++++++++++++++ .../resource_variable_test.go | 15 +++- octopusdeploy_framework/schemas/schema.go | 7 +- terraform/49-variables/variables.tf | 68 ++++++++++++++++--- 6 files changed, 203 insertions(+), 13 deletions(-) create mode 100644 octopusdeploy_framework/datasource_lifecycle_test.go create mode 100644 octopusdeploy_framework/datasource_spaces_test.go diff --git a/octopusdeploy/testing_container_test.go b/octopusdeploy/testing_container_test.go index 8c1bd6397..6fd5dfba7 100644 --- a/octopusdeploy/testing_container_test.go +++ b/octopusdeploy/testing_container_test.go @@ -57,6 +57,11 @@ func TestMain(m *testing.M) { log.Printf("Failed to create client: (%s)", err.Error()) panic(m) } + + octoContainer = &test.OctopusContainer{ + Container: nil, + URI: url, + } } code := m.Run() os.Exit(code) diff --git a/octopusdeploy_framework/datasource_lifecycle_test.go b/octopusdeploy_framework/datasource_lifecycle_test.go new file mode 100644 index 000000000..c7febe404 --- /dev/null +++ b/octopusdeploy_framework/datasource_lifecycle_test.go @@ -0,0 +1,63 @@ +package octopusdeploy_framework + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccDataSourceLifecycles(t *testing.T) { + spaceName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) + lifecycleName := "Default Lifecycle" + resourceName := "data.octopusdeploy_lifecycles.lifecycle_default_lifecycle" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { TestAccPreCheck(t) }, + ProtoV6ProviderFactories: ProtoV6ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: testAccDataSourceLifecyclesConfig(spaceName, lifecycleName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(resourceName, "space_id"), + resource.TestCheckResourceAttr(resourceName, "partial_name", lifecycleName), + resource.TestCheckResourceAttr(resourceName, "lifecycles.#", "1"), + resource.TestCheckResourceAttrSet(resourceName, "lifecycles.0.id"), + resource.TestCheckResourceAttr(resourceName, "lifecycles.0.name", lifecycleName), + testAccCheckOutputExists("octopus_space_id"), + testAccCheckOutputExists("octopus_lifecycle_id"), + ), + }, + }, + }) +} + +func testAccDataSourceLifecyclesConfig(spaceName, lifecycleName string) string { + return fmt.Sprintf(` +resource "octopusdeploy_space" "octopus_project_space_test" { + name = "%s" + is_default = false + is_task_queue_stopped = false + description = "Test space for lifecycles datasource" + space_managers_teams = ["teams-administrators"] +} + +data "octopusdeploy_lifecycles" "lifecycle_default_lifecycle" { + ids = null + partial_name = "%s" + space_id = octopusdeploy_space.octopus_project_space_test.id + skip = 0 + take = 1 + depends_on = [octopusdeploy_space.octopus_project_space_test] +} + +output "octopus_space_id" { + value = octopusdeploy_space.octopus_project_space_test.id +} + +output "octopus_lifecycle_id" { + value = data.octopusdeploy_lifecycles.lifecycle_default_lifecycle.lifecycles[0].id +} +`, spaceName, lifecycleName) +} diff --git a/octopusdeploy_framework/datasource_spaces_test.go b/octopusdeploy_framework/datasource_spaces_test.go new file mode 100644 index 000000000..dc48f1108 --- /dev/null +++ b/octopusdeploy_framework/datasource_spaces_test.go @@ -0,0 +1,58 @@ +package octopusdeploy_framework + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccDataSourceSpaces(t *testing.T) { + spaceID := "Spaces-1" + resourceName := "data.octopusdeploy_spaces.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { TestAccPreCheck(t) }, + ProtoV6ProviderFactories: ProtoV6ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: testAccDataSourceSpacesConfig(spaceID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "ids.#", "1"), + resource.TestCheckResourceAttr(resourceName, "ids.0", spaceID), + resource.TestCheckResourceAttr(resourceName, "skip", "0"), + resource.TestCheckResourceAttr(resourceName, "take", "1"), + resource.TestCheckResourceAttrSet(resourceName, "spaces.0.id"), + testAccCheckOutputExists("octopus_space_id"), + resource.TestCheckOutput("octopus_space_id", spaceID), + ), + }, + }, + }) +} + +func testAccDataSourceSpacesConfig(spaceID string) string { + tfConfig := fmt.Sprintf(` + data "octopusdeploy_spaces" "test" { + ids = ["%s"] + skip = 0 + take = 1 + } + + output "octopus_space_id" { + value = data.octopusdeploy_spaces.test.spaces[0].id + } + `, spaceID) + return tfConfig +} + +func testAccCheckOutputExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + _, ok := s.RootModule().Outputs[name] + if !ok { + return fmt.Errorf("output %s not found", name) + } + return nil + } +} diff --git a/octopusdeploy_framework/resource_variable_test.go b/octopusdeploy_framework/resource_variable_test.go index 055d796d4..d71cc6dd8 100644 --- a/octopusdeploy_framework/resource_variable_test.go +++ b/octopusdeploy_framework/resource_variable_test.go @@ -3,7 +3,9 @@ package octopusdeploy_framework import ( "fmt" "path/filepath" + "strings" "testing" + "time" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/variables" internalTest "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal/test" @@ -320,6 +322,9 @@ func TestVariableResource(t *testing.T) { // Assert client, err := octoclient.CreateClient(octoContainer.URI, newSpaceId, test.ApiKey) project, err := client.Projects.GetByName("Test") + + // Add a short delay before querying the API + time.Sleep(5 * time.Second) variableSet, err := client.Variables.GetAll(project.ID) if err != nil { @@ -327,7 +332,15 @@ func TestVariableResource(t *testing.T) { } if len(variableSet.Variables) != 7 { - t.Fatalf("Expected 7 variables to be created.") + var report strings.Builder + report.WriteString(fmt.Sprintf("Expected 7 variables, but found %d.\nReturned variables:\n", len(variableSet.Variables))) + + for _, v := range variableSet.Variables { + report.WriteString(fmt.Sprintf("- Name: %s\n Type: %s\n Value: %s\n Scope: %+v\n\n", + v.Name, v.Type, v.Value, v.Scope)) + } + + t.Fatalf(report.String()) } for _, variable := range variableSet.Variables { diff --git a/octopusdeploy_framework/schemas/schema.go b/octopusdeploy_framework/schemas/schema.go index 5c786144b..395aefc7f 100644 --- a/octopusdeploy_framework/schemas/schema.go +++ b/octopusdeploy_framework/schemas/schema.go @@ -191,7 +191,12 @@ func GetBooleanResourceAttribute(description string, defaultValue bool, isOption func GetIds(ids types.List) []string { var result = make([]string, 0, len(ids.Elements())) for _, id := range ids.Elements() { - result = append(result, id.String()) + strVal, ok := id.(types.String) + + if !ok || strVal.IsNull() || strVal.IsUnknown() { + continue + } + result = append(result, strVal.ValueString()) } return result } diff --git a/terraform/49-variables/variables.tf b/terraform/49-variables/variables.tf index 9ebdc692c..0e2f15da7 100644 --- a/terraform/49-variables/variables.tf +++ b/terraform/49-variables/variables.tf @@ -1,21 +1,35 @@ + + +resource "octopusdeploy_variable" "scoped_project_variable_action" { + depends_on = [ + octopusdeploy_project.test_project, + ] + owner_id = octopusdeploy_project.test_project.id + type = "String" + name = "ActionScopedVariable" + value = "unscoped variable" + scope { + actions = [octopusdeploy_deployment_process.test_deployment_process.step[0].run_script_action[0].id] + } +} + resource "octopusdeploy_variable" "unscoped_project_variable" { + depends_on = [ + octopusdeploy_project.test_project, + octopusdeploy_variable.scoped_project_variable_action, + ] owner_id = octopusdeploy_project.test_project.id type = "String" name = "UnscopedVariable" value = "UnscopedVariable" } -resource "octopusdeploy_variable" "scoped_project_variable_action" { - owner_id = octopusdeploy_project.test_project.id - type = "String" - name = "ActionScopedVariable" - value = "unscoped variable" - scope { - actions = [octopusdeploy_deployment_process.test_deployment_process.step[0].run_script_action[0].id] - } -} - resource "octopusdeploy_variable" "scoped_project_variable_channel" { + depends_on = [ + octopusdeploy_project.test_project, + octopusdeploy_variable.unscoped_project_variable, + octopusdeploy_variable.scoped_project_variable_action, + ] owner_id = octopusdeploy_project.test_project.id type = "String" name = "ChannelScopedVariable" @@ -26,6 +40,12 @@ resource "octopusdeploy_variable" "scoped_project_variable_channel" { } resource "octopusdeploy_variable" "scoped_project_variable_environment" { + depends_on = [ + octopusdeploy_project.test_project, + octopusdeploy_variable.unscoped_project_variable, + octopusdeploy_variable.scoped_project_variable_action, + octopusdeploy_variable.scoped_project_variable_channel, + ] owner_id = octopusdeploy_project.test_project.id type = "String" name = "EnvironmentScopedVariable" @@ -36,6 +56,14 @@ resource "octopusdeploy_variable" "scoped_project_variable_environment" { } resource "octopusdeploy_variable" "scoped_project_variable_machine" { + depends_on = [ + octopusdeploy_project.test_project, + octopusdeploy_variable.unscoped_project_variable, + octopusdeploy_variable.scoped_project_variable_action, + octopusdeploy_variable.scoped_project_variable_channel, + octopusdeploy_variable.scoped_project_variable_environment + ] + owner_id = octopusdeploy_project.test_project.id type = "String" name = "MachineScopedVariable" @@ -46,6 +74,14 @@ resource "octopusdeploy_variable" "scoped_project_variable_machine" { } resource "octopusdeploy_variable" "scoped_project_variable_process" { + depends_on = [ + octopusdeploy_project.test_project, + octopusdeploy_variable.unscoped_project_variable, + octopusdeploy_variable.scoped_project_variable_action, + octopusdeploy_variable.scoped_project_variable_channel, + octopusdeploy_variable.scoped_project_variable_environment, + octopusdeploy_variable.scoped_project_variable_machine, + ] owner_id = octopusdeploy_project.test_project.id type = "String" name = "ProcessScopedVariable" @@ -56,6 +92,16 @@ resource "octopusdeploy_variable" "scoped_project_variable_process" { } resource "octopusdeploy_variable" "scoped_project_variable_role" { + depends_on = [ + octopusdeploy_project.test_project, + octopusdeploy_variable.unscoped_project_variable, + octopusdeploy_variable.scoped_project_variable_action, + octopusdeploy_variable.scoped_project_variable_channel, + octopusdeploy_variable.scoped_project_variable_environment, + octopusdeploy_variable.scoped_project_variable_machine, + octopusdeploy_variable.scoped_project_variable_process, + ] + owner_id = octopusdeploy_project.test_project.id type = "String" name = "RoleScopedVariable" @@ -63,4 +109,4 @@ resource "octopusdeploy_variable" "scoped_project_variable_role" { scope { roles = ["role"] } -} +} \ No newline at end of file From 3a8a027a764cda034b8840bbba6df254e5f0d6b4 Mon Sep 17 00:00:00 2001 From: Isaac Calligeros <101079287+IsaacCalligeros95@users.noreply.github.com> Date: Thu, 8 Aug 2024 10:23:22 +0930 Subject: [PATCH 04/28] Isaac/mc schema fixes (#715) * propagate an error * The collection returned by data sources must not be optional * Add more optionals --------- Co-authored-by: Matthew Casperson --- octopusdeploy/data_source_tenants.go | 5 ++++- octopusdeploy/schema_account_resource.go | 1 + .../schema_azure_cloud_service_deployment_target.go | 2 +- .../schema_azure_service_fabric_cluster_deployment_target.go | 2 +- octopusdeploy/schema_azure_web_app_deployment_target.go | 2 +- octopusdeploy/schema_certificate.go | 2 +- octopusdeploy/schema_channel.go | 2 +- octopusdeploy/schema_cloud_region_deployment_target.go | 2 +- octopusdeploy/schema_deployment_target.go | 2 +- octopusdeploy/schema_kubernetes_agent_deployment_target.go | 2 +- octopusdeploy/schema_kubernetes_cluster_deployment_target.go | 2 +- octopusdeploy/schema_listening_tentacle_deployment_target.go | 2 +- octopusdeploy/schema_machine_policy.go | 2 +- .../schema_offline_package_drop_deployment_target.go | 2 +- octopusdeploy/schema_polling_tentacle_deployment_target.go | 2 +- octopusdeploy/schema_script_modules.go | 2 +- octopusdeploy/schema_ssh_connection_deployment_target.go | 2 +- octopusdeploy/schema_tag_set.go | 2 +- octopusdeploy/schema_team.go | 2 +- octopusdeploy/schema_tenant.go | 2 +- octopusdeploy/schema_user.go | 2 +- octopusdeploy/schema_user_role.go | 2 +- octopusdeploy/schema_worker_pool.go | 2 +- octopusdeploy_framework/schemas/gitCredential.go | 1 + octopusdeploy_framework/schemas/lifecycle.go | 1 + octopusdeploy_framework/schemas/project.go | 1 + 26 files changed, 29 insertions(+), 22 deletions(-) diff --git a/octopusdeploy/data_source_tenants.go b/octopusdeploy/data_source_tenants.go index fcbdceae8..4c9cfa569 100644 --- a/octopusdeploy/data_source_tenants.go +++ b/octopusdeploy/data_source_tenants.go @@ -44,7 +44,10 @@ func dataSourceTenantsRead(ctx context.Context, d *schema.ResourceData, meta int flattenedTenants = append(flattenedTenants, flattenTenant(tenant)) } - d.Set("tenants", flattenedTenants) + if err := d.Set("tenants", flattenedTenants); err != nil { + return diag.FromErr(err) + } + d.SetId("Tenants " + time.Now().UTC().String()) return nil diff --git a/octopusdeploy/schema_account_resource.go b/octopusdeploy/schema_account_resource.go index 5dbee9361..4a31f5017 100644 --- a/octopusdeploy/schema_account_resource.go +++ b/octopusdeploy/schema_account_resource.go @@ -50,6 +50,7 @@ func getAccountResourceDataSchema() map[string]*schema.Schema { Description: "A list of accounts that match the filter(s).", Elem: &schema.Resource{Schema: dataSchema}, Type: schema.TypeList, + Optional: false, }, "id": getDataSchemaID(), "space_id": getQuerySpaceID(), diff --git a/octopusdeploy/schema_azure_cloud_service_deployment_target.go b/octopusdeploy/schema_azure_cloud_service_deployment_target.go index 46488a934..45e00de20 100644 --- a/octopusdeploy/schema_azure_cloud_service_deployment_target.go +++ b/octopusdeploy/schema_azure_cloud_service_deployment_target.go @@ -70,7 +70,7 @@ func getAzureCloudServiceDeploymentTargetDataSchema() map[string]*schema.Schema Computed: true, Description: "A list of Azure cloud service deployment targets that match the filter(s).", Elem: &schema.Resource{Schema: dataSchema}, - Optional: true, + Optional: false, Type: schema.TypeList, } diff --git a/octopusdeploy/schema_azure_service_fabric_cluster_deployment_target.go b/octopusdeploy/schema_azure_service_fabric_cluster_deployment_target.go index 3f8af8905..639d08553 100644 --- a/octopusdeploy/schema_azure_service_fabric_cluster_deployment_target.go +++ b/octopusdeploy/schema_azure_service_fabric_cluster_deployment_target.go @@ -85,7 +85,7 @@ func getAzureServiceFabricClusterDeploymentTargetDataSchema() map[string]*schema Computed: true, Description: "A list of Azure service fabric cluster deployment targets that match the filter(s).", Elem: &schema.Resource{Schema: dataSchema}, - Optional: true, + Optional: false, Type: schema.TypeList, } diff --git a/octopusdeploy/schema_azure_web_app_deployment_target.go b/octopusdeploy/schema_azure_web_app_deployment_target.go index 8478378b6..f8b2cfe25 100644 --- a/octopusdeploy/schema_azure_web_app_deployment_target.go +++ b/octopusdeploy/schema_azure_web_app_deployment_target.go @@ -55,7 +55,7 @@ func getAzureWebAppDeploymentTargetDataSchema() map[string]*schema.Schema { Computed: true, Description: "A list of Azure web app deployment targets that match the filter(s).", Elem: &schema.Resource{Schema: dataSchema}, - Optional: true, + Optional: false, Type: schema.TypeList, } diff --git a/octopusdeploy/schema_certificate.go b/octopusdeploy/schema_certificate.go index 4252f9899..2d35164c9 100644 --- a/octopusdeploy/schema_certificate.go +++ b/octopusdeploy/schema_certificate.go @@ -170,7 +170,7 @@ func getCertificateDataSchema() map[string]*schema.Schema { Computed: true, Description: "A list of certificates that match the filter(s).", Elem: &schema.Resource{Schema: dataSchema}, - Optional: true, + Optional: false, Type: schema.TypeList, }, "first_result": getQueryFirstResult(), diff --git a/octopusdeploy/schema_channel.go b/octopusdeploy/schema_channel.go index 88aa0bda9..eed2eead6 100644 --- a/octopusdeploy/schema_channel.go +++ b/octopusdeploy/schema_channel.go @@ -73,7 +73,7 @@ func getChannelDataSchema() map[string]*schema.Schema { Computed: true, Description: "A channel that matches the specified filter(s).", Elem: &schema.Resource{Schema: dataSchema}, - Optional: true, + Optional: false, Type: schema.TypeList, }, "ids": getQueryIDs(), diff --git a/octopusdeploy/schema_cloud_region_deployment_target.go b/octopusdeploy/schema_cloud_region_deployment_target.go index bce3dd18e..8e2375e07 100644 --- a/octopusdeploy/schema_cloud_region_deployment_target.go +++ b/octopusdeploy/schema_cloud_region_deployment_target.go @@ -40,7 +40,7 @@ func getCloudRegionDeploymentTargetDataSchema() map[string]*schema.Schema { Computed: true, Description: "A list of cloud region deployment targets that match the filter(s).", Elem: &schema.Resource{Schema: dataSchema}, - Optional: true, + Optional: false, Type: schema.TypeList, } diff --git a/octopusdeploy/schema_deployment_target.go b/octopusdeploy/schema_deployment_target.go index 456265859..9649a7bec 100644 --- a/octopusdeploy/schema_deployment_target.go +++ b/octopusdeploy/schema_deployment_target.go @@ -90,7 +90,7 @@ func getDeploymentTargetDataSchema() map[string]*schema.Schema { Computed: true, Description: "A list of deployment targets that match the filter(s).", Elem: &schema.Resource{Schema: dataSchema}, - Optional: true, + Optional: false, Type: schema.TypeList, }, "environments": getQueryEnvironments(), diff --git a/octopusdeploy/schema_kubernetes_agent_deployment_target.go b/octopusdeploy/schema_kubernetes_agent_deployment_target.go index 822e7dce8..2323f311b 100644 --- a/octopusdeploy/schema_kubernetes_agent_deployment_target.go +++ b/octopusdeploy/schema_kubernetes_agent_deployment_target.go @@ -173,7 +173,7 @@ func getKubernetesAgentDeploymentTargetDataSchema() map[string]*schema.Schema { Computed: true, Description: "A list of kubernetes agent deployment targets that match the filter(s).", Elem: &schema.Resource{Schema: dataSchema}, - Optional: true, + Optional: false, Type: schema.TypeList, } diff --git a/octopusdeploy/schema_kubernetes_cluster_deployment_target.go b/octopusdeploy/schema_kubernetes_cluster_deployment_target.go index 6dcbd6a35..4704688ff 100644 --- a/octopusdeploy/schema_kubernetes_cluster_deployment_target.go +++ b/octopusdeploy/schema_kubernetes_cluster_deployment_target.go @@ -131,7 +131,7 @@ func getKubernetesClusterDeploymentTargetDataSchema() map[string]*schema.Schema Computed: true, Description: "A list of Kubernetes cluster deployment targets that match the filter(s).", Elem: &schema.Resource{Schema: dataSchema}, - Optional: true, + Optional: false, Type: schema.TypeList, } diff --git a/octopusdeploy/schema_listening_tentacle_deployment_target.go b/octopusdeploy/schema_listening_tentacle_deployment_target.go index 32d2b3481..a35cdb8af 100644 --- a/octopusdeploy/schema_listening_tentacle_deployment_target.go +++ b/octopusdeploy/schema_listening_tentacle_deployment_target.go @@ -57,7 +57,7 @@ func getListeningTentacleDeploymentTargetDataSchema() map[string]*schema.Schema Computed: true, Description: "A list of listening tentacle deployment targets that match the filter(s).", Elem: &schema.Resource{Schema: dataSchema}, - Optional: true, + Optional: false, Type: schema.TypeList, } diff --git a/octopusdeploy/schema_machine_policy.go b/octopusdeploy/schema_machine_policy.go index 537da34df..0d24b2f6c 100644 --- a/octopusdeploy/schema_machine_policy.go +++ b/octopusdeploy/schema_machine_policy.go @@ -112,7 +112,7 @@ func getMachinePolicyDataSchema() map[string]*schema.Schema { Computed: true, Description: "A list of machine policies that match the filter(s).", Elem: &schema.Resource{Schema: dataSchema}, - Optional: true, + Optional: false, Type: schema.TypeList, }, "partial_name": getQueryPartialName(), diff --git a/octopusdeploy/schema_offline_package_drop_deployment_target.go b/octopusdeploy/schema_offline_package_drop_deployment_target.go index f8b88f704..371a08b01 100644 --- a/octopusdeploy/schema_offline_package_drop_deployment_target.go +++ b/octopusdeploy/schema_offline_package_drop_deployment_target.go @@ -51,7 +51,7 @@ func getOfflinePackageDropDeploymentTargetDataSchema() map[string]*schema.Schema Computed: true, Description: "A list of offline package drop deployment targets that match the filter(s).", Elem: &schema.Resource{Schema: dataSchema}, - Optional: true, + Optional: false, Type: schema.TypeList, } diff --git a/octopusdeploy/schema_polling_tentacle_deployment_target.go b/octopusdeploy/schema_polling_tentacle_deployment_target.go index ab6472663..ee73b0cfb 100644 --- a/octopusdeploy/schema_polling_tentacle_deployment_target.go +++ b/octopusdeploy/schema_polling_tentacle_deployment_target.go @@ -51,7 +51,7 @@ func getPollingTentacleDeploymentTargetDataSchema() map[string]*schema.Schema { Computed: true, Description: "A list of polling tentacle deployment targets that match the filter(s).", Elem: &schema.Resource{Schema: dataSchema}, - Optional: true, + Optional: false, Type: schema.TypeList, } diff --git a/octopusdeploy/schema_script_modules.go b/octopusdeploy/schema_script_modules.go index c4a24d1f8..906d2ea54 100644 --- a/octopusdeploy/schema_script_modules.go +++ b/octopusdeploy/schema_script_modules.go @@ -86,7 +86,7 @@ func getScriptModuleDataSchema() map[string]*schema.Schema { Computed: true, Description: "A list of script modules that match the filter(s).", Elem: &schema.Resource{Schema: dataSchema}, - Optional: true, + Optional: false, Type: schema.TypeList, }, "partial_name": getQueryPartialName(), diff --git a/octopusdeploy/schema_ssh_connection_deployment_target.go b/octopusdeploy/schema_ssh_connection_deployment_target.go index 90934132f..df977c19d 100644 --- a/octopusdeploy/schema_ssh_connection_deployment_target.go +++ b/octopusdeploy/schema_ssh_connection_deployment_target.go @@ -57,7 +57,7 @@ func getSSHConnectionDeploymentTargetDataSchema() map[string]*schema.Schema { Computed: true, Description: "A list of SSH connection deployment targets that match the filter(s).", Elem: &schema.Resource{Schema: dataSchema}, - Optional: true, + Optional: false, Type: schema.TypeList, } diff --git a/octopusdeploy/schema_tag_set.go b/octopusdeploy/schema_tag_set.go index db5c31a23..953b5c40c 100644 --- a/octopusdeploy/schema_tag_set.go +++ b/octopusdeploy/schema_tag_set.go @@ -54,7 +54,7 @@ func getTagSetDataSchema() map[string]*schema.Schema { Computed: true, Description: "A list of tag sets that match the filter(s).", Elem: &schema.Resource{Schema: dataSchema}, - Optional: true, + Optional: false, Type: schema.TypeList, }, "take": getQueryTake(), diff --git a/octopusdeploy/schema_team.go b/octopusdeploy/schema_team.go index 3f782a1c9..d0656662e 100644 --- a/octopusdeploy/schema_team.go +++ b/octopusdeploy/schema_team.go @@ -85,7 +85,7 @@ func getTeamDataSchema() map[string]*schema.Schema { Computed: true, Description: "A list of teams that match the filter(s).", Elem: &schema.Resource{Schema: dataSchema}, - Optional: true, + Optional: false, Type: schema.TypeList, }, } diff --git a/octopusdeploy/schema_tenant.go b/octopusdeploy/schema_tenant.go index 6045f5981..260ba93f8 100644 --- a/octopusdeploy/schema_tenant.go +++ b/octopusdeploy/schema_tenant.go @@ -67,7 +67,7 @@ func getTenantDataSchema() map[string]*schema.Schema { Computed: true, Description: "A list of tenants that match the filter(s).", Elem: &schema.Resource{Schema: dataSchema}, - Optional: true, + Optional: false, Type: schema.TypeList, }, "take": getQueryTake(), diff --git a/octopusdeploy/schema_user.go b/octopusdeploy/schema_user.go index 5ef089c67..620c3db37 100644 --- a/octopusdeploy/schema_user.go +++ b/octopusdeploy/schema_user.go @@ -74,7 +74,7 @@ func getUserDataSchema() map[string]*schema.Schema { Computed: true, Description: "A list of users that match the filter(s).", Elem: &schema.Resource{Schema: dataSchema}, - Optional: true, + Optional: false, Type: schema.TypeList, }, } diff --git a/octopusdeploy/schema_user_role.go b/octopusdeploy/schema_user_role.go index 9258402e5..4487a9170 100644 --- a/octopusdeploy/schema_user_role.go +++ b/octopusdeploy/schema_user_role.go @@ -84,7 +84,7 @@ func getUserRoleDataSchema() map[string]*schema.Schema { Computed: true, Description: "A list of user roles that match the filter(s).", Elem: &schema.Resource{Schema: dataSchema}, - Optional: true, + Optional: false, Type: schema.TypeList, }, } diff --git a/octopusdeploy/schema_worker_pool.go b/octopusdeploy/schema_worker_pool.go index 9207124c2..036e2a04a 100644 --- a/octopusdeploy/schema_worker_pool.go +++ b/octopusdeploy/schema_worker_pool.go @@ -39,7 +39,7 @@ func getWorkerPoolDataSchema() map[string]*schema.Schema { Computed: true, Description: "A list of worker pools that match the filter(s).", Elem: &schema.Resource{Schema: dataSchema}, - Optional: true, + Optional: false, Type: schema.TypeList, }, } diff --git a/octopusdeploy_framework/schemas/gitCredential.go b/octopusdeploy_framework/schemas/gitCredential.go index 2a29f1b4d..594f927b1 100644 --- a/octopusdeploy_framework/schemas/gitCredential.go +++ b/octopusdeploy_framework/schemas/gitCredential.go @@ -54,6 +54,7 @@ func GetGitCredentialDataSourceSchema() map[string]datasourceSchema.Attribute { "take": util.GetQueryTakeDatasourceSchema(), "git_credentials": datasourceSchema.ListNestedAttribute{ Computed: true, + Optional: false, Description: "A list of Git Credentials that match the filter(s).", NestedObject: datasourceSchema.NestedAttributeObject{ Attributes: GetGitCredentialAttributes(), diff --git a/octopusdeploy_framework/schemas/lifecycle.go b/octopusdeploy_framework/schemas/lifecycle.go index 092d48ab6..04f7f493a 100644 --- a/octopusdeploy_framework/schemas/lifecycle.go +++ b/octopusdeploy_framework/schemas/lifecycle.go @@ -97,6 +97,7 @@ func GetDatasourceLifecycleSchema() datasourceSchema.Schema { "take": util.GetQueryTakeDatasourceSchema(), "lifecycles": datasourceSchema.ListNestedAttribute{ Computed: true, + Optional: false, NestedObject: datasourceSchema.NestedAttributeObject{ Attributes: map[string]datasourceSchema.Attribute{ "id": util.GetIdDatasourceSchema(), diff --git a/octopusdeploy_framework/schemas/project.go b/octopusdeploy_framework/schemas/project.go index fc4fee3b7..814710bda 100644 --- a/octopusdeploy_framework/schemas/project.go +++ b/octopusdeploy_framework/schemas/project.go @@ -193,6 +193,7 @@ func getProjectsDataSourceAttribute() datasourceSchema.ListNestedAttribute { return datasourceSchema.ListNestedAttribute{ Description: "A list of projects that match the filter(s).", Computed: true, + Optional: false, NestedObject: datasourceSchema.NestedAttributeObject{ Attributes: map[string]datasourceSchema.Attribute{ "allow_deployments_to_no_targets": util.DataSourceBool().Computed().Deprecated("Allow deployments to be created when there are no targets.").Build(), From 7fade0508fa4f3279c9ef91caa3e97bb6c781464 Mon Sep 17 00:00:00 2001 From: Huy Nguyen <162080607+HuyPhanNguyen@users.noreply.github.com> Date: Thu, 8 Aug 2024 14:25:25 +1000 Subject: [PATCH 05/28] Update schema and doc GitCredential, Lifecycles and TenantProjectVariable (#717) * Update lifecycles schema and doc * Fix lifecyle schema * Update git credential resource * Refactor tenant project variable * Make it consistent * Update datasource GitCredentialModel * fix test fail * Add username_password_account resource * Revert "Add username_password_account resource" This reverts commit 7a20ecb5c8698d7f341c2437d5f841aa2cf179ae. * Update doc example * small change on lifecycle example --- docs/data-sources/lifecycles.md | 70 ++++---- docs/resources/git_credential.md | 10 +- docs/resources/lifecycle.md | 38 ++--- docs/resources/tenant_project_variable.md | 18 +- .../datasource_git_credentials.go | 8 +- .../schemas/gitCredential.go | 95 +++++------ octopusdeploy_framework/schemas/lifecycle.go | 155 +++++++++--------- .../schemas/tenant_project_variable.go | 44 +++-- .../util/resource_attribute_builder.go | 80 +++++++++ 9 files changed, 307 insertions(+), 211 deletions(-) diff --git a/docs/data-sources/lifecycles.md b/docs/data-sources/lifecycles.md index 2d6ce478e..d7fb232f4 100644 --- a/docs/data-sources/lifecycles.md +++ b/docs/data-sources/lifecycles.md @@ -26,62 +26,62 @@ data "octopusdeploy_lifecycles" "example" { ### Optional -- `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. +- `ids` (List of String) A list of lifecycle IDs to filter by. +- `partial_name` (String) A partial name to filter lifecycles by. - `skip` (Number) A filter to specify the number of items to skip in the response. -- `space_id` (String) The space ID associated with this resource. +- `space_id` (String) The space ID associated with this lifecycle. - `take` (Number) A filter to specify the number of items to take (or return) in the response. ### Read-Only -- `id` (String) The ID of this resource. -- `lifecycles` (Block List) A list of lifecycles that match the filter(s). (see [below for nested schema](#nestedblock--lifecycles)) +- `id` (String) The ID of the lifecycle. +- `lifecycles` (Attributes List) (see [below for nested schema](#nestedatt--lifecycles)) - + ### Nested Schema for `lifecycles` Read-Only: -- `description` (String) The description of this lifecycle. -- `id` (String) The unique ID for this resource. -- `name` (String) The name of this resource. -- `phase` (List of Object) (see [below for nested schema](#nestedatt--lifecycles--phase)) -- `release_retention_policy` (List of Object) (see [below for nested schema](#nestedatt--lifecycles--release_retention_policy)) -- `space_id` (String) The space ID associated with this resource. -- `tentacle_retention_policy` (List of Object) (see [below for nested schema](#nestedatt--lifecycles--tentacle_retention_policy)) +- `description` (String) The description of the lifecycle. +- `id` (String) The ID of the lifecycle. +- `name` (String) The name of the lifecycle. +- `phase` (Attributes List) (see [below for nested schema](#nestedatt--lifecycles--phase)) +- `release_retention_policy` (Attributes List) (see [below for nested schema](#nestedatt--lifecycles--release_retention_policy)) +- `space_id` (String) The space ID associated with this lifecycle. +- `tentacle_retention_policy` (Attributes List) (see [below for nested schema](#nestedatt--lifecycles--tentacle_retention_policy)) ### Nested Schema for `lifecycles.phase` Read-Only: -- `automatic_deployment_targets` (List of String) -- `id` (String) -- `is_optional_phase` (Boolean) -- `minimum_environments_before_promotion` (Number) -- `name` (String) -- `optional_deployment_targets` (List of String) -- `release_retention_policy` (List of Object) (see [below for nested schema](#nestedobjatt--lifecycles--phase--release_retention_policy)) -- `tentacle_retention_policy` (List of Object) (see [below for nested schema](#nestedobjatt--lifecycles--phase--tentacle_retention_policy)) +- `automatic_deployment_targets` (List of String) The automatic deployment targets for this phase. +- `id` (String) The ID of the phase. +- `is_optional_phase` (Boolean) Whether this phase is optional. +- `minimum_environments_before_promotion` (Number) The minimum number of environments before promotion. +- `name` (String) The name of the phase. +- `optional_deployment_targets` (List of String) The optional deployment targets for this phase. +- `release_retention_policy` (Attributes List) (see [below for nested schema](#nestedatt--lifecycles--phase--release_retention_policy)) +- `tentacle_retention_policy` (Attributes List) (see [below for nested schema](#nestedatt--lifecycles--phase--tentacle_retention_policy)) - + ### Nested Schema for `lifecycles.phase.release_retention_policy` Read-Only: -- `quantity_to_keep` (Number) -- `should_keep_forever` (Boolean) -- `unit` (String) +- `quantity_to_keep` (Number) The quantity of releases to keep. +- `should_keep_forever` (Boolean) Whether releases should be kept forever. +- `unit` (String) The unit of time for the retention policy. - + ### Nested Schema for `lifecycles.phase.tentacle_retention_policy` Read-Only: -- `quantity_to_keep` (Number) -- `should_keep_forever` (Boolean) -- `unit` (String) +- `quantity_to_keep` (Number) The quantity of releases to keep. +- `should_keep_forever` (Boolean) Whether releases should be kept forever. +- `unit` (String) The unit of time for the retention policy. @@ -90,9 +90,9 @@ Read-Only: Read-Only: -- `quantity_to_keep` (Number) -- `should_keep_forever` (Boolean) -- `unit` (String) +- `quantity_to_keep` (Number) The quantity of releases to keep. +- `should_keep_forever` (Boolean) Whether releases should be kept forever. +- `unit` (String) The unit of time for the retention policy. @@ -100,8 +100,8 @@ Read-Only: Read-Only: -- `quantity_to_keep` (Number) -- `should_keep_forever` (Boolean) -- `unit` (String) +- `quantity_to_keep` (Number) The quantity of releases to keep. +- `should_keep_forever` (Boolean) Whether releases should be kept forever. +- `unit` (String) The unit of time for the retention policy. diff --git a/docs/resources/git_credential.md b/docs/resources/git_credential.md index e47402fa1..ad9aa4392 100644 --- a/docs/resources/git_credential.md +++ b/docs/resources/git_credential.md @@ -3,12 +3,12 @@ page_title: "octopusdeploy_git_credential Resource - terraform-provider-octopusdeploy" subcategory: "" description: |- - This resource manages Git credentials in Octopus Deploy. + Manages a Git credential in Octopus Deploy. --- # octopusdeploy_git_credential (Resource) -This resource manages Git credentials in Octopus Deploy. +Manages a Git credential in Octopus Deploy. @@ -17,15 +17,15 @@ This resource manages Git credentials in Octopus Deploy. ### Required -- `name` (String) The name of the Git credential. This name must be unique. +- `name` (String) The name of this Git Credential. - `password` (String, Sensitive) The password for the Git credential. - `username` (String) The username for the Git credential. ### Optional -- `description` (String) The description of this Git credential. +- `description` (String) The description of this Git Credential. - `id` (String) The unique ID for this resource. -- `space_id` (String) The space ID associated with this resource. +- `space_id` (String) The space ID associated with this Git Credential. - `type` (String) The Git credential authentication type. diff --git a/docs/resources/lifecycle.md b/docs/resources/lifecycle.md index a367cce62..8714ab06e 100644 --- a/docs/resources/lifecycle.md +++ b/docs/resources/lifecycle.md @@ -33,8 +33,8 @@ resource "octopusdeploy_lifecycle" "example" { name = "foo" release_retention_policy { - quantity_to_keep = 0 - should_keep_forever = true + quantity_to_keep = 0 + should_keep_forever = true // true only if quantity_to_keep = 0 unit = "Days" } @@ -64,10 +64,10 @@ resource "octopusdeploy_lifecycle" "example" { - `description` (String) The description of this lifecycle. - `id` (String) The unique ID for this resource. -- `phase` (Block List) (see [below for nested schema](#nestedblock--phase)) -- `release_retention_policy` (Block List, Max: 1) (see [below for nested schema](#nestedblock--release_retention_policy)) +- `phase` (Block List) Defines a phase in the lifecycle. (see [below for nested schema](#nestedblock--phase)) +- `release_retention_policy` (Block List) Defines the retention policy for releases or tentacles. (see [below for nested schema](#nestedblock--release_retention_policy)) - `space_id` (String) The space ID associated with this resource. -- `tentacle_retention_policy` (Block List, Max: 1) (see [below for nested schema](#nestedblock--tentacle_retention_policy)) +- `tentacle_retention_policy` (Block List) Defines the retention policy for releases or tentacles. (see [below for nested schema](#nestedblock--tentacle_retention_policy)) ### Nested Schema for `phase` @@ -83,17 +83,17 @@ Optional: - `is_optional_phase` (Boolean) If false a release must be deployed to this phase before it can be deployed to the next phase. - `minimum_environments_before_promotion` (Number) The number of units required before a release can enter the next phase. If 0, all environments are required. - `optional_deployment_targets` (List of String) Environment IDs in this phase that a release can be deployed to, but is not automatically deployed to -- `release_retention_policy` (Block List, Max: 1) (see [below for nested schema](#nestedblock--phase--release_retention_policy)) -- `tentacle_retention_policy` (Block List, Max: 1) (see [below for nested schema](#nestedblock--phase--tentacle_retention_policy)) +- `release_retention_policy` (Block List) Defines the retention policy for releases or tentacles. (see [below for nested schema](#nestedblock--phase--release_retention_policy)) +- `tentacle_retention_policy` (Block List) Defines the retention policy for releases or tentacles. (see [below for nested schema](#nestedblock--phase--tentacle_retention_policy)) ### Nested Schema for `phase.release_retention_policy` Optional: -- `quantity_to_keep` (Number) The number of days/releases to keep. The default value is `30`. If `0` then all are kept. -- `should_keep_forever` (Boolean) Indicates if items should never be deleted. The default value is `false`. -- `unit` (String) The unit of quantity to keep. Valid units are `Days` or `Items`. The default value is `Days`. +- `quantity_to_keep` (Number) The number of days/releases to keep. The default value is 30. If 0 then all are kept. +- `should_keep_forever` (Boolean) Indicates if items should never be deleted. The default value is false. +- `unit` (String) The unit of quantity to keep. Valid units are Days or Items. The default value is Days. @@ -101,9 +101,9 @@ Optional: Optional: -- `quantity_to_keep` (Number) The number of days/releases to keep. The default value is `30`. If `0` then all are kept. -- `should_keep_forever` (Boolean) Indicates if items should never be deleted. The default value is `false`. -- `unit` (String) The unit of quantity to keep. Valid units are `Days` or `Items`. The default value is `Days`. +- `quantity_to_keep` (Number) The number of days/releases to keep. The default value is 30. If 0 then all are kept. +- `should_keep_forever` (Boolean) Indicates if items should never be deleted. The default value is false. +- `unit` (String) The unit of quantity to keep. Valid units are Days or Items. The default value is Days. @@ -112,9 +112,9 @@ Optional: Optional: -- `quantity_to_keep` (Number) The number of days/releases to keep. The default value is `30`. If `0` then all are kept. -- `should_keep_forever` (Boolean) Indicates if items should never be deleted. The default value is `false`. -- `unit` (String) The unit of quantity to keep. Valid units are `Days` or `Items`. The default value is `Days`. +- `quantity_to_keep` (Number) The number of days/releases to keep. The default value is 30. If 0 then all are kept. +- `should_keep_forever` (Boolean) Indicates if items should never be deleted. The default value is false. +- `unit` (String) The unit of quantity to keep. Valid units are Days or Items. The default value is Days. @@ -122,9 +122,9 @@ Optional: Optional: -- `quantity_to_keep` (Number) The number of days/releases to keep. The default value is `30`. If `0` then all are kept. -- `should_keep_forever` (Boolean) Indicates if items should never be deleted. The default value is `false`. -- `unit` (String) The unit of quantity to keep. Valid units are `Days` or `Items`. The default value is `Days`. +- `quantity_to_keep` (Number) The number of days/releases to keep. The default value is 30. If 0 then all are kept. +- `should_keep_forever` (Boolean) Indicates if items should never be deleted. The default value is false. +- `unit` (String) The unit of quantity to keep. Valid units are Days or Items. The default value is Days. ## Import diff --git a/docs/resources/tenant_project_variable.md b/docs/resources/tenant_project_variable.md index c27b1afae..053b6739a 100644 --- a/docs/resources/tenant_project_variable.md +++ b/docs/resources/tenant_project_variable.md @@ -3,12 +3,12 @@ page_title: "octopusdeploy_tenant_project_variable Resource - terraform-provider-octopusdeploy" subcategory: "" description: |- - This resource manages tenant project variables in Octopus Deploy. + Manages a tenant project variable in Octopus Deploy. --- # octopusdeploy_tenant_project_variable (Resource) -This resource manages tenant project variables in Octopus Deploy. +Manages a tenant project variable in Octopus Deploy. @@ -17,18 +17,18 @@ This resource manages tenant project variables in Octopus Deploy. ### Required -- `environment_id` (String) -- `project_id` (String) -- `template_id` (String) -- `tenant_id` (String) +- `environment_id` (String) The ID of the environment. +- `project_id` (String) The ID of the project. +- `template_id` (String) The ID of the variable template. +- `tenant_id` (String) The ID of the tenant. ### Optional -- `space_id` (String) -- `value` (String, Sensitive) +- `space_id` (String) The space ID associated with this Tenant Project Variable. +- `value` (String, Sensitive) The value of the variable. ### Read-Only -- `id` (String) The ID of this resource. +- `id` (String) The unique ID for this resource. diff --git a/octopusdeploy_framework/datasource_git_credentials.go b/octopusdeploy_framework/datasource_git_credentials.go index 210c7d83b..5dc1592c3 100644 --- a/octopusdeploy_framework/datasource_git_credentials.go +++ b/octopusdeploy_framework/datasource_git_credentials.go @@ -8,7 +8,6 @@ import ( "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/types" "time" ) @@ -35,6 +34,7 @@ type GitCredentialModel struct { Description types.String `tfsdk:"description"` Type types.String `tfsdk:"type"` Username types.String `tfsdk:"username"` + Password types.String `tfsdk:"password"` } func NewGitCredentialsDataSource() datasource.DataSource { @@ -46,10 +46,7 @@ func (g *gitCredentialsDataSource) Metadata(_ context.Context, req datasource.Me } func (g *gitCredentialsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: "A list of Git Credentials that match the filter(s).", - Attributes: schemas.GetGitCredentialDataSourceSchema(), - } + resp.Schema = schemas.GetGitCredentialDataSourceSchema() } func (g *gitCredentialsDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { @@ -102,6 +99,7 @@ func GetGitCredentialAttrTypes() map[string]attr.Type { "description": types.StringType, "type": types.StringType, "username": types.StringType, + "password": types.StringType, } } diff --git a/octopusdeploy_framework/schemas/gitCredential.go b/octopusdeploy_framework/schemas/gitCredential.go index 594f927b1..c4b0b4d6f 100644 --- a/octopusdeploy_framework/schemas/gitCredential.go +++ b/octopusdeploy_framework/schemas/gitCredential.go @@ -5,59 +5,55 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" datasourceSchema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" resourceSchema "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) const ( - GitCredentialResourceDescription = "Git Credential" - GitCredentialResourceName = "git_credential" - GitCredentialDatasourceName = "git_credentials" + GitCredentialResourceName = "git_credential" + GitCredentialDatasourceName = "git_credentials" ) func GetGitCredentialResourceSchema() resourceSchema.Schema { return resourceSchema.Schema{ Description: "Manages a Git credential in Octopus Deploy.", Attributes: map[string]resourceSchema.Attribute{ - "id": util.GetIdResourceSchema(), - "space_id": util.GetSpaceIdResourceSchema(GitCredentialResourceDescription), - "name": util.GetNameResourceSchema(true), - "description": util.GetDescriptionResourceSchema(GitCredentialResourceDescription), - "type": resourceSchema.StringAttribute{ - Optional: true, - Description: "The Git credential authentication type.", - }, - "username": resourceSchema.StringAttribute{ - Required: true, - Description: "The username for the Git credential.", - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "password": resourceSchema.StringAttribute{ - Required: true, - Sensitive: true, - Description: "The password for the Git credential.", - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, + "id": util.ResourceString().Optional().Computed().Description("The unique ID for this resource.").Build(), + "space_id": util.ResourceString().Optional().Computed().Description("The space ID associated with this Git Credential.").Build(), + "name": util.ResourceString().Required().Description("The name of this Git Credential.").Build(), + "description": util.ResourceString().Optional().Description("The description of this Git Credential.").Build(), + "type": util.ResourceString(). + Optional(). + Description("The Git credential authentication type."). + Build(), + "username": util.ResourceString(). + Required(). + Description("The username for the Git credential."). + Validators(stringvalidator.LengthAtLeast(1)). + Build(), + "password": util.ResourceString(). + Required(). + Sensitive(). + Description("The password for the Git credential."). + Validators(stringvalidator.LengthAtLeast(1)). + Build(), }, } } -func GetGitCredentialDataSourceSchema() map[string]datasourceSchema.Attribute { - return map[string]datasourceSchema.Attribute{ - "id": util.GetIdDatasourceSchema(), - "space_id": util.GetSpaceIdDatasourceSchema(GitCredentialResourceDescription), - "name": util.GetQueryNameDatasourceSchema(), - "skip": util.GetQuerySkipDatasourceSchema(), - "take": util.GetQueryTakeDatasourceSchema(), - "git_credentials": datasourceSchema.ListNestedAttribute{ - Computed: true, - Optional: false, - Description: "A list of Git Credentials that match the filter(s).", - NestedObject: datasourceSchema.NestedAttributeObject{ - Attributes: GetGitCredentialAttributes(), +func GetGitCredentialDataSourceSchema() datasourceSchema.Schema { + return datasourceSchema.Schema{ + Description: "Use this data source to retrieve information about Git credentials in Octopus Deploy.", + Attributes: map[string]datasourceSchema.Attribute{ + "id": util.DataSourceString().Computed().Description("The unique ID for this resource.").Build(), + "space_id": util.DataSourceString().Optional().Description("The space ID associated with this Git Credential.").Build(), + "name": util.DataSourceString().Optional().Description("The name of the Git Credential to filter by.").Build(), + "skip": util.DataSourceInt64().Optional().Description("The number of records to skip.").Build(), + "take": util.DataSourceInt64().Optional().Description("The number of records to take.").Build(), + "git_credentials": datasourceSchema.ListNestedAttribute{ + Computed: true, + Description: "Provides information about existing GitCredentials.", + NestedObject: datasourceSchema.NestedAttributeObject{ + Attributes: GetGitCredentialAttributes(), + }, }, }, } @@ -65,17 +61,12 @@ func GetGitCredentialDataSourceSchema() map[string]datasourceSchema.Attribute { func GetGitCredentialAttributes() map[string]datasourceSchema.Attribute { return map[string]datasourceSchema.Attribute{ - "id": util.GetIdDatasourceSchema(), - "space_id": util.GetSpaceIdDatasourceSchema(GitCredentialResourceDescription), - "name": util.GetQueryNameDatasourceSchema(), - "description": util.GetDescriptionDatasourceSchema(GitCredentialResourceDescription), - "type": datasourceSchema.StringAttribute{ - Computed: true, - Description: "The Git credential authentication type.", - }, - "username": datasourceSchema.StringAttribute{ - Computed: true, - Description: "The username for the Git credential.", - }, + "id": util.DataSourceString().Computed().Description("The unique ID for this resource.").Build(), + "space_id": util.DataSourceString().Computed().Description("The space ID associated with this Git Credential.").Build(), + "name": util.DataSourceString().Computed().Description("The name of this Git Credential.").Build(), + "description": util.DataSourceString().Computed().Description("The description of this Git Credential.").Build(), + "type": util.DataSourceString().Computed().Description("The Git credential authentication type.").Build(), + "username": util.DataSourceString().Computed().Description("The username for the Git credential.").Build(), + "password": util.DataSourceString().Computed().Sensitive().Description("The password for the Git credential.").Build(), } } diff --git a/octopusdeploy_framework/schemas/lifecycle.go b/octopusdeploy_framework/schemas/lifecycle.go index 04f7f493a..ba28f35b9 100644 --- a/octopusdeploy_framework/schemas/lifecycle.go +++ b/octopusdeploy_framework/schemas/lifecycle.go @@ -7,16 +7,18 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/types" ) func GetResourceLifecycleSchema() resourceSchema.Schema { return resourceSchema.Schema{ + Description: "This resource manages lifecycles in Octopus Deploy.", Attributes: map[string]resourceSchema.Attribute{ - "id": util.GetIdResourceSchema(), - "space_id": util.GetSpaceIdResourceSchema("lifecycle"), - "name": util.GetNameResourceSchema(true), - "description": util.GetDescriptionResourceSchema("lifecycle"), + "id": util.ResourceString().Optional().Computed().Description("The unique ID for this resource.").PlanModifiers(stringplanmodifier.UseStateForUnknown()).Build(), + "space_id": util.ResourceString().Optional().Computed().Description("The space ID associated with this resource.").PlanModifiers(stringplanmodifier.UseStateForUnknown()).Build(), + "name": util.ResourceString().Required().Description("The name of this resource.").Build(), + "description": util.ResourceString().Optional().Computed().Default("").Description("The description of this lifecycle.").Build(), }, Blocks: map[string]resourceSchema.Block{ "phase": getResourcePhaseBlockSchema(), @@ -28,30 +30,29 @@ func GetResourceLifecycleSchema() resourceSchema.Schema { func getResourcePhaseBlockSchema() resourceSchema.ListNestedBlock { return resourceSchema.ListNestedBlock{ + Description: "Defines a phase in the lifecycle.", NestedObject: resourceSchema.NestedBlockObject{ Attributes: map[string]resourceSchema.Attribute{ - "id": util.GetIdResourceSchema(), - "name": util.GetNameResourceSchema(true), - "automatic_deployment_targets": resourceSchema.ListAttribute{ - ElementType: types.StringType, - Optional: true, - Computed: true, - }, - "optional_deployment_targets": resourceSchema.ListAttribute{ - ElementType: types.StringType, - Optional: true, - Computed: true, - }, - "minimum_environments_before_promotion": resourceSchema.Int64Attribute{ - Optional: true, - Computed: true, - Default: int64default.StaticInt64(0), - }, - "is_optional_phase": resourceSchema.BoolAttribute{ - Optional: true, - Computed: true, - Default: booldefault.StaticBool(false), - }, + "id": util.ResourceString().Optional().Computed().Description("The unique ID for this resource.").Build(), + "name": util.ResourceString().Required().Description("The name of this resource.").Build(), + "automatic_deployment_targets": util.ResourceList(types.StringType). + Optional().Computed(). + Description("Environment IDs in this phase that a release is automatically deployed to when it is eligible for this phase"). + Build(), + "optional_deployment_targets": util.ResourceList(types.StringType). + Optional().Computed(). + Description("Environment IDs in this phase that a release can be deployed to, but is not automatically deployed to"). + Build(), + "minimum_environments_before_promotion": util.ResourceInt64(). + Optional().Computed(). + Default(int64default.StaticInt64(0)). + Description("The number of units required before a release can enter the next phase. If 0, all environments are required."). + Build(), + "is_optional_phase": util.ResourceBool(). + Optional().Computed(). + Default(booldefault.StaticBool(false)). + Description("If false a release must be deployed to this phase before it can be deployed to the next phase."). + Build(), }, Blocks: map[string]resourceSchema.Block{ "release_retention_policy": getResourceRetentionPolicyBlockSchema(), @@ -63,83 +64,87 @@ func getResourcePhaseBlockSchema() resourceSchema.ListNestedBlock { func getResourceRetentionPolicyBlockSchema() resourceSchema.ListNestedBlock { return resourceSchema.ListNestedBlock{ + Description: "Defines the retention policy for releases or tentacles.", NestedObject: resourceSchema.NestedBlockObject{ Attributes: map[string]resourceSchema.Attribute{ - "quantity_to_keep": resourceSchema.Int64Attribute{ - Optional: true, - Computed: true, - Default: int64default.StaticInt64(30), - }, - "should_keep_forever": resourceSchema.BoolAttribute{ - Optional: true, - Computed: true, - Default: booldefault.StaticBool(false), - }, - "unit": resourceSchema.StringAttribute{ - Optional: true, - Computed: true, - Default: stringdefault.StaticString("Days"), - }, + "quantity_to_keep": util.ResourceInt64(). + Optional().Computed(). + Default(int64default.StaticInt64(30)). + Description("The number of days/releases to keep. The default value is 30. If 0 then all are kept."). + Build(), + "should_keep_forever": util.ResourceBool(). + Optional().Computed(). + Default(booldefault.StaticBool(false)). + Description("Indicates if items should never be deleted. The default value is false."). + Build(), + "unit": util.ResourceString(). + Optional().Computed(). + Default(stringdefault.StaticString("Days")). + Description("The unit of quantity to keep. Valid units are Days or Items. The default value is Days."). + Build(), }, }, } } func GetDatasourceLifecycleSchema() datasourceSchema.Schema { - description := "lifecycle" return datasourceSchema.Schema{ + Description: "Provides information about existing lifecycles.", Attributes: map[string]datasourceSchema.Attribute{ - "id": util.GetIdDatasourceSchema(), - "space_id": util.GetSpaceIdDatasourceSchema(description), - "ids": util.GetQueryIDsDatasourceSchema(), - "partial_name": util.GetQueryPartialNameDatasourceSchema(), - "skip": util.GetQuerySkipDatasourceSchema(), - "take": util.GetQueryTakeDatasourceSchema(), - "lifecycles": datasourceSchema.ListNestedAttribute{ - Computed: true, - Optional: false, - NestedObject: datasourceSchema.NestedAttributeObject{ - Attributes: map[string]datasourceSchema.Attribute{ - "id": util.GetIdDatasourceSchema(), - "space_id": util.GetSpaceIdDatasourceSchema(description), - "name": util.GetNameDatasourceSchema(true), - "description": util.GetDescriptionDatasourceSchema(description), - "phase": getDatasourcePhasesSchema(), - "release_retention_policy": getDatasourceRetentionPolicySchema(), - "tentacle_retention_policy": getDatasourceRetentionPolicySchema(), - }, - }, + "id": util.DataSourceString().Computed().Description("The ID of the lifecycle.").Build(), + "space_id": util.DataSourceString().Optional().Description("The space ID associated with this lifecycle.").Build(), + "ids": util.DataSourceList(types.StringType).Optional().Description("A list of lifecycle IDs to filter by.").Build(), + "partial_name": util.DataSourceString().Optional().Description("A partial name to filter lifecycles by.").Build(), + "skip": util.DataSourceInt64().Optional().Description("A filter to specify the number of items to skip in the response.").Build(), + "take": util.DataSourceInt64().Optional().Description("A filter to specify the number of items to take (or return) in the response.").Build(), + "lifecycles": getLifecyclesAttribute(), + }, + } +} + +func getLifecyclesAttribute() datasourceSchema.ListNestedAttribute { + return datasourceSchema.ListNestedAttribute{ + Computed: true, + NestedObject: datasourceSchema.NestedAttributeObject{ + Attributes: map[string]datasourceSchema.Attribute{ + "id": util.DataSourceString().Computed().Description("The ID of the lifecycle.").Build(), + "space_id": util.DataSourceString().Computed().Description("The space ID associated with this lifecycle.").Build(), + "name": util.DataSourceString().Computed().Description("The name of the lifecycle.").Build(), + "description": util.DataSourceString().Computed().Description("The description of the lifecycle.").Build(), + "phase": getPhasesAttribute(), + "release_retention_policy": getRetentionPolicyAttribute(), + "tentacle_retention_policy": getRetentionPolicyAttribute(), }, }, } } -func getDatasourcePhasesSchema() datasourceSchema.ListNestedAttribute { +func getPhasesAttribute() datasourceSchema.ListNestedAttribute { return datasourceSchema.ListNestedAttribute{ Computed: true, NestedObject: datasourceSchema.NestedAttributeObject{ Attributes: map[string]datasourceSchema.Attribute{ - "id": util.GetIdDatasourceSchema(), - "name": util.GetNameDatasourceSchema(true), - "automatic_deployment_targets": datasourceSchema.ListAttribute{ElementType: types.StringType, Computed: true}, - "optional_deployment_targets": datasourceSchema.ListAttribute{ElementType: types.StringType, Computed: true}, - "minimum_environments_before_promotion": datasourceSchema.Int64Attribute{Computed: true}, - "is_optional_phase": datasourceSchema.BoolAttribute{Computed: true}, - "release_retention_policy": getDatasourceRetentionPolicySchema(), - "tentacle_retention_policy": getDatasourceRetentionPolicySchema(), + "id": util.DataSourceString().Computed().Description("The ID of the phase.").Build(), + "name": util.DataSourceString().Computed().Description("The name of the phase.").Build(), + "automatic_deployment_targets": util.DataSourceList(types.StringType).Computed().Description("The automatic deployment targets for this phase.").Build(), + "optional_deployment_targets": util.DataSourceList(types.StringType).Computed().Description("The optional deployment targets for this phase.").Build(), + "minimum_environments_before_promotion": util.DataSourceInt64().Computed().Description("The minimum number of environments before promotion.").Build(), + "is_optional_phase": util.DataSourceBool().Computed().Description("Whether this phase is optional.").Build(), + "release_retention_policy": getRetentionPolicyAttribute(), + "tentacle_retention_policy": getRetentionPolicyAttribute(), }, }, } } -func getDatasourceRetentionPolicySchema() datasourceSchema.ListNestedAttribute { +func getRetentionPolicyAttribute() datasourceSchema.ListNestedAttribute { return datasourceSchema.ListNestedAttribute{ Computed: true, NestedObject: datasourceSchema.NestedAttributeObject{ Attributes: map[string]datasourceSchema.Attribute{ - "quantity_to_keep": datasourceSchema.Int64Attribute{Computed: true}, - "should_keep_forever": datasourceSchema.BoolAttribute{Computed: true}, - "unit": datasourceSchema.StringAttribute{Computed: true}, + "quantity_to_keep": util.DataSourceInt64().Computed().Description("The quantity of releases to keep.").Build(), + "should_keep_forever": util.DataSourceBool().Computed().Description("Whether releases should be kept forever.").Build(), + "unit": util.DataSourceString().Computed().Description("The unit of time for the retention policy.").Build(), }, }, } diff --git a/octopusdeploy_framework/schemas/tenant_project_variable.go b/octopusdeploy_framework/schemas/tenant_project_variable.go index 99553e7c6..4e681a99e 100644 --- a/octopusdeploy_framework/schemas/tenant_project_variable.go +++ b/octopusdeploy_framework/schemas/tenant_project_variable.go @@ -3,6 +3,7 @@ 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" ) const ( @@ -14,17 +15,38 @@ func GetTenantProjectVariableResourceSchema() schema.Schema { return schema.Schema{ Description: "Manages a tenant project variable in Octopus Deploy.", Attributes: map[string]schema.Attribute{ - "id": util.GetIdResourceSchema(), - "space_id": util.GetSpaceIdResourceSchema(TenantProjectVariableResourceDescription), - "tenant_id": util.GetRequiredStringResourceSchema("The ID of the tenant."), - "project_id": util.GetRequiredStringResourceSchema("The ID of the project."), - "environment_id": util.GetRequiredStringResourceSchema("The ID of the environment."), - "template_id": util.GetRequiredStringResourceSchema("The ID of the variable template."), - "value": schema.StringAttribute{ - Required: true, - Description: "The value of the variable.", - Sensitive: true, - }, + "id": util.ResourceString(). + Computed(). + Description("The unique ID for this resource."). + PlanModifiers(stringplanmodifier.UseStateForUnknown()). + Build(), + "space_id": util.ResourceString(). + Optional(). + Computed(). + Description("The space ID associated with this Tenant Project Variable."). + PlanModifiers(stringplanmodifier.UseStateForUnknown()). + Build(), + "tenant_id": util.ResourceString(). + Required(). + Description("The ID of the tenant."). + Build(), + "project_id": util.ResourceString(). + Required(). + Description("The ID of the project."). + Build(), + "environment_id": util.ResourceString(). + Required(). + Description("The ID of the environment."). + Build(), + "template_id": util.ResourceString(). + Required(). + Description("The ID of the variable template."). + Build(), + "value": util.ResourceString(). + Optional(). + Sensitive(). + Description("The value of the variable."). + Build(), }, } } diff --git a/octopusdeploy_framework/util/resource_attribute_builder.go b/octopusdeploy_framework/util/resource_attribute_builder.go index 153cafbf7..908f07929 100644 --- a/octopusdeploy_framework/util/resource_attribute_builder.go +++ b/octopusdeploy_framework/util/resource_attribute_builder.go @@ -8,8 +8,10 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" "github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/setdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -217,6 +219,84 @@ func (b *AttributeBuilder[T]) Build() T { return b.attr } +func (b *AttributeBuilder[T]) PlanModifiers(modifiers ...any) *AttributeBuilder[T] { + switch a := any(&b.attr).(type) { + case *schema.StringAttribute: + if stringModifiers, ok := convertToTypedSlice[planmodifier.String](modifiers); ok { + a.PlanModifiers = append(a.PlanModifiers, stringModifiers...) + } + case *schema.BoolAttribute: + if boolModifiers, ok := convertToTypedSlice[planmodifier.Bool](modifiers); ok { + a.PlanModifiers = append(a.PlanModifiers, boolModifiers...) + } + case *schema.Int64Attribute: + if int64Modifiers, ok := convertToTypedSlice[planmodifier.Int64](modifiers); ok { + a.PlanModifiers = append(a.PlanModifiers, int64Modifiers...) + } + case *schema.Float64Attribute: + if float64Modifiers, ok := convertToTypedSlice[planmodifier.Float64](modifiers); ok { + a.PlanModifiers = append(a.PlanModifiers, float64Modifiers...) + } + case *schema.ListAttribute: + if listModifiers, ok := convertToTypedSlice[planmodifier.List](modifiers); ok { + a.PlanModifiers = append(a.PlanModifiers, listModifiers...) + } + case *schema.SetAttribute: + if setModifiers, ok := convertToTypedSlice[planmodifier.Set](modifiers); ok { + a.PlanModifiers = append(a.PlanModifiers, setModifiers...) + } + case *schema.MapAttribute: + if mapModifiers, ok := convertToTypedSlice[planmodifier.Map](modifiers); ok { + a.PlanModifiers = append(a.PlanModifiers, mapModifiers...) + } + } + return b +} +func (b *AttributeBuilder[T]) Validators(validators ...any) *AttributeBuilder[T] { + switch a := any(&b.attr).(type) { + case *schema.StringAttribute: + if stringValidators, ok := convertToTypedSlice[validator.String](validators); ok { + a.Validators = append(a.Validators, stringValidators...) + } + case *schema.BoolAttribute: + if boolValidators, ok := convertToTypedSlice[validator.Bool](validators); ok { + a.Validators = append(a.Validators, boolValidators...) + } + case *schema.Int64Attribute: + if int64Validators, ok := convertToTypedSlice[validator.Int64](validators); ok { + a.Validators = append(a.Validators, int64Validators...) + } + case *schema.Float64Attribute: + if float64Validators, ok := convertToTypedSlice[validator.Float64](validators); ok { + a.Validators = append(a.Validators, float64Validators...) + } + case *schema.ListAttribute: + if listValidators, ok := convertToTypedSlice[validator.List](validators); ok { + a.Validators = append(a.Validators, listValidators...) + } + case *schema.SetAttribute: + if setValidators, ok := convertToTypedSlice[validator.Set](validators); ok { + a.Validators = append(a.Validators, setValidators...) + } + case *schema.MapAttribute: + if mapValidators, ok := convertToTypedSlice[validator.Map](validators); ok { + a.Validators = append(a.Validators, mapValidators...) + } + } + return b +} + +func convertToTypedSlice[T any](slice []any) ([]T, bool) { + typedSlice := make([]T, 0, len(slice)) + for _, item := range slice { + if typed, ok := item.(T); ok { + typedSlice = append(typedSlice, typed) + } else { + return nil, false + } + } + return typedSlice, true +} func ResourceString() *AttributeBuilder[schema.StringAttribute] { return NewAttributeBuilder[schema.StringAttribute]() } From b1a1cd2f7f0d7638e05cd8b54a1bb76f6d8ae6cb Mon Sep 17 00:00:00 2001 From: Henrik Andersson Date: Wed, 7 Aug 2024 22:38:06 -0700 Subject: [PATCH 06/28] chore: ensure resources deleted from Octopus are removed from state (#720) --- go.mod | 2 +- go.sum | 2 ++ internal/errors/error.go | 22 +++++++++++++++++++ .../resource_artifactory_generic_feed.go | 6 ++++- ...resource_aws_elastic_container_registry.go | 6 ++++- .../resource_docker_container_registry.go | 6 ++++- .../resource_environment.go | 6 ++++- .../resource_git_credential.go | 9 ++++++-- .../resource_github_repository_feed.go | 6 ++++- octopusdeploy_framework/resource_helm_feed.go | 6 ++++- .../resource_library_variable_set.go | 8 +++++-- octopusdeploy_framework/resource_lifecycle.go | 17 +++++++++----- .../resource_lifecycle_test.go | 7 +++--- .../resource_maven_feed.go | 6 ++++- .../resource_nuget_feed.go | 6 ++++- octopusdeploy_framework/resource_project.go | 6 ++++- .../resource_project_flatten.go | 4 +++- .../resource_project_group.go | 6 ++++- .../resource_project_model.go | 4 +++- octopusdeploy_framework/resource_space.go | 8 +++++-- .../resource_tenant_common_variable.go | 3 ++- .../resource_tenant_project.go | 3 ++- .../resource_tenant_project_variable.go | 6 +++-- octopusdeploy_framework/resource_variable.go | 5 ++++- .../schemas/artifactory_generic_feed.go | 3 ++- .../schemas/aws_elastic_container_registry.go | 3 ++- .../schemas/docker_container_registry_feed.go | 3 ++- .../schemas/environment.go | 3 ++- .../schemas/github_repository_feed.go | 3 ++- octopusdeploy_framework/schemas/helm_feed.go | 3 ++- .../schemas/library_variable_set.go | 3 ++- octopusdeploy_framework/schemas/maven_feed.go | 3 ++- octopusdeploy_framework/schemas/nuget_feed.go | 3 ++- .../schemas/project_group.go | 3 ++- octopusdeploy_framework/schemas/resource.go | 19 ++++++++++++++++ octopusdeploy_framework/schemas/space.go | 3 ++- octopusdeploy_framework/schemas/variable.go | 6 +++-- 37 files changed, 173 insertions(+), 45 deletions(-) create mode 100644 octopusdeploy_framework/schemas/resource.go diff --git a/go.mod b/go.mod index 2cad3a7e8..4cc35d04a 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/google/uuid v1.6.0 github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637 github.com/hashicorp/terraform-plugin-docs v0.13.0 - github.com/hashicorp/terraform-plugin-framework v1.9.0 + github.com/hashicorp/terraform-plugin-framework v1.11.0 github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 github.com/hashicorp/terraform-plugin-go v0.23.0 github.com/hashicorp/terraform-plugin-log v0.9.0 diff --git a/go.sum b/go.sum index fd5a915a5..c917536b0 100644 --- a/go.sum +++ b/go.sum @@ -166,6 +166,8 @@ github.com/hashicorp/terraform-plugin-docs v0.13.0 h1:6e+VIWsVGb6jYJewfzq2ok2smP github.com/hashicorp/terraform-plugin-docs v0.13.0/go.mod h1:W0oCmHAjIlTHBbvtppWHe8fLfZ2BznQbuv8+UD8OucQ= github.com/hashicorp/terraform-plugin-framework v1.9.0 h1:caLcDoxiRucNi2hk8+j3kJwkKfvHznubyFsJMWfZqKU= github.com/hashicorp/terraform-plugin-framework v1.9.0/go.mod h1:qBXLDn69kM97NNVi/MQ9qgd1uWWsVftGSnygYG1tImM= +github.com/hashicorp/terraform-plugin-framework v1.11.0 h1:M7+9zBArexHFXDx/pKTxjE6n/2UCXY6b8FIq9ZYhwfE= +github.com/hashicorp/terraform-plugin-framework v1.11.0/go.mod h1:qBXLDn69kM97NNVi/MQ9qgd1uWWsVftGSnygYG1tImM= github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 h1:HOjBuMbOEzl7snOdOoUfE2Jgeto6JOjLVQ39Ls2nksc= github.com/hashicorp/terraform-plugin-framework-validators v0.12.0/go.mod h1:jfHGE/gzjxYz6XoUwi/aYiiKrJDeutQNUtGQXkaHklg= github.com/hashicorp/terraform-plugin-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/125t+AAurhqphJ2Co= diff --git a/internal/errors/error.go b/internal/errors/error.go index 21c2d4f74..733b46675 100644 --- a/internal/errors/error.go +++ b/internal/errors/error.go @@ -6,6 +6,8 @@ import ( "net/http" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/schemas" + "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -29,3 +31,23 @@ func ProcessApiError(ctx context.Context, d *schema.ResourceData, err error, res return diag.FromErr(err) } + +func DeleteFromStateV2(ctx context.Context, resp *resource.ReadResponse, resource schemas.IResourceModel, resourceDescription string) error { + log.Printf("[INFO] %s (%s) not found; deleting from state", resourceDescription, resource.GetID()) + resp.State.RemoveResource(ctx) + return nil +} + +func ProcessApiErrorV2(ctx context.Context, resp *resource.ReadResponse, resource schemas.IResourceModel, err error, resourceDescription string) error { + if err == nil { + return nil + } + + if apiError, ok := err.(*core.APIError); ok { + if apiError.StatusCode == http.StatusNotFound { + return DeleteFromStateV2(ctx, resp, resource, resourceDescription) + } + } + + return nil +} diff --git a/octopusdeploy_framework/resource_artifactory_generic_feed.go b/octopusdeploy_framework/resource_artifactory_generic_feed.go index a1eb7ab58..b6c765d51 100644 --- a/octopusdeploy_framework/resource_artifactory_generic_feed.go +++ b/octopusdeploy_framework/resource_artifactory_generic_feed.go @@ -3,7 +3,9 @@ package octopusdeploy_framework import ( "context" "fmt" + "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/attr" @@ -77,7 +79,9 @@ func (r *artifactoryGenericFeedTypeResource) Read(ctx context.Context, req resou client := r.Config.Client feed, err := feeds.GetByID(client, data.SpaceID.ValueString(), data.ID.ValueString()) if err != nil { - resp.Diagnostics.AddError("unable to load artifactoryGeneric feed", err.Error()) + if err := errors.ProcessApiErrorV2(ctx, resp, data, err, "artifactory generic feed"); err != nil { + resp.Diagnostics.AddError("unable to load artifactoryGeneric feed", err.Error()) + } return } diff --git a/octopusdeploy_framework/resource_aws_elastic_container_registry.go b/octopusdeploy_framework/resource_aws_elastic_container_registry.go index 72cef3b51..daeb36f31 100644 --- a/octopusdeploy_framework/resource_aws_elastic_container_registry.go +++ b/octopusdeploy_framework/resource_aws_elastic_container_registry.go @@ -3,8 +3,10 @@ package octopusdeploy_framework import ( "context" "fmt" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/feeds" + "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/attr" @@ -78,7 +80,9 @@ func (r *awsElasticContainerRegistryFeedTypeResource) Read(ctx context.Context, client := r.Config.Client feed, err := feeds.GetByID(client, data.SpaceID.ValueString(), data.ID.ValueString()) if err != nil { - resp.Diagnostics.AddError("unable to load aws elastic container registry", err.Error()) + if err := errors.ProcessApiErrorV2(ctx, resp, data, err, "aws elastic container registry"); err != nil { + resp.Diagnostics.AddError("unable to load aws elastic container registry", err.Error()) + } return } diff --git a/octopusdeploy_framework/resource_docker_container_registry.go b/octopusdeploy_framework/resource_docker_container_registry.go index fa73ef902..0b246633e 100644 --- a/octopusdeploy_framework/resource_docker_container_registry.go +++ b/octopusdeploy_framework/resource_docker_container_registry.go @@ -3,8 +3,10 @@ package octopusdeploy_framework import ( "context" "fmt" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/feeds" + "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/attr" @@ -76,7 +78,9 @@ func (r *dockerContainerRegistryFeedTypeResource) Read(ctx context.Context, req client := r.Config.Client feed, err := feeds.GetByID(client, data.SpaceID.ValueString(), data.ID.ValueString()) if err != nil { - resp.Diagnostics.AddError("unable to load docker container registry feed", err.Error()) + if err := errors.ProcessApiErrorV2(ctx, resp, data, err, "docker container registry feed"); err != nil { + resp.Diagnostics.AddError("unable to load docker container registry feed", err.Error()) + } return } diff --git a/octopusdeploy_framework/resource_environment.go b/octopusdeploy_framework/resource_environment.go index a393279ad..281639f6d 100644 --- a/octopusdeploy_framework/resource_environment.go +++ b/octopusdeploy_framework/resource_environment.go @@ -5,6 +5,7 @@ import ( "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/environments" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/extensions" + "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" @@ -82,7 +83,10 @@ func (r *environmentTypeResource) Read(ctx context.Context, req resource.ReadReq environment, err := environments.GetByID(r.Config.Client, data.SpaceID.ValueString(), data.ID.ValueString()) if err != nil { - resp.Diagnostics.AddError("unable to load environment", err.Error()) + if err := errors.ProcessApiErrorV2(ctx, resp, data, err, "environment"); err != nil { + resp.Diagnostics.AddError("unable to load environment", err.Error()) + } + return } updateEnvironment(ctx, &data, environment) diff --git a/octopusdeploy_framework/resource_git_credential.go b/octopusdeploy_framework/resource_git_credential.go index 478b4f8b4..8a5baeec1 100644 --- a/octopusdeploy_framework/resource_git_credential.go +++ b/octopusdeploy_framework/resource_git_credential.go @@ -2,8 +2,10 @@ package octopusdeploy_framework import ( "context" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/credentials" + "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" @@ -18,13 +20,14 @@ type gitCredentialResource struct { } type gitCredentialResourceModel struct { - ID types.String `tfsdk:"id"` SpaceID types.String `tfsdk:"space_id"` Name types.String `tfsdk:"name"` Description types.String `tfsdk:"description"` Type types.String `tfsdk:"type"` Username types.String `tfsdk:"username"` Password types.String `tfsdk:"password"` + + schemas.ResourceModel } func NewGitCredentialResource() resource.Resource { @@ -92,7 +95,9 @@ func (g *gitCredentialResource) Read(ctx context.Context, req resource.ReadReque gitCredential, err := credentials.GetByID(g.Client, state.SpaceID.ValueString(), state.ID.ValueString()) if err != nil { - resp.Diagnostics.AddError("Error reading Git credential", err.Error()) + if err := errors.ProcessApiErrorV2(ctx, resp, state, err, "git credential"); err != nil { + resp.Diagnostics.AddError("Error reading Git credential", err.Error()) + } return } diff --git a/octopusdeploy_framework/resource_github_repository_feed.go b/octopusdeploy_framework/resource_github_repository_feed.go index 30f7bdb28..6b6aabb63 100644 --- a/octopusdeploy_framework/resource_github_repository_feed.go +++ b/octopusdeploy_framework/resource_github_repository_feed.go @@ -3,8 +3,10 @@ package octopusdeploy_framework import ( "context" "fmt" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/feeds" + "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/attr" @@ -78,7 +80,9 @@ func (r *githubRepositoryFeedTypeResource) Read(ctx context.Context, req resourc client := r.Config.Client feed, err := feeds.GetByID(client, data.SpaceID.ValueString(), data.ID.ValueString()) if err != nil { - resp.Diagnostics.AddError("unable to load github repository feed", err.Error()) + if err := errors.ProcessApiErrorV2(ctx, resp, data, err, "github repository feed"); err != nil { + resp.Diagnostics.AddError("unable to load github repository feed", err.Error()) + } return } diff --git a/octopusdeploy_framework/resource_helm_feed.go b/octopusdeploy_framework/resource_helm_feed.go index 64ec52677..dd7667eff 100644 --- a/octopusdeploy_framework/resource_helm_feed.go +++ b/octopusdeploy_framework/resource_helm_feed.go @@ -3,7 +3,9 @@ package octopusdeploy_framework import ( "context" "fmt" + "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/attr" @@ -77,7 +79,9 @@ func (r *helmFeedTypeResource) Read(ctx context.Context, req resource.ReadReques client := r.Config.Client feed, err := feeds.GetByID(client, data.SpaceID.ValueString(), data.ID.ValueString()) if err != nil { - resp.Diagnostics.AddError("unable to load helm feed", err.Error()) + if err := errors.ProcessApiErrorV2(ctx, resp, data, err, "helm feed"); err != nil { + resp.Diagnostics.AddError("unable to load helm feed", err.Error()) + } return } diff --git a/octopusdeploy_framework/resource_library_variable_set.go b/octopusdeploy_framework/resource_library_variable_set.go index 733b88664..cdf616aa0 100644 --- a/octopusdeploy_framework/resource_library_variable_set.go +++ b/octopusdeploy_framework/resource_library_variable_set.go @@ -3,12 +3,14 @@ package octopusdeploy_framework import ( "context" "fmt" + "log" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/libraryvariablesets" + "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-log/tflog" - "log" ) type libraryVariableSetFeedTypeResource struct { @@ -61,7 +63,9 @@ func (r *libraryVariableSetFeedTypeResource) Read(ctx context.Context, req resou libraryVariableSet, err := libraryvariablesets.GetByID(r.Config.Client, data.SpaceID.ValueString(), data.ID.ValueString()) if err != nil { - resp.Diagnostics.AddError("unable to load library variable set", err.Error()) + if err := errors.ProcessApiErrorV2(ctx, resp, data, err, "library variable set"); err != nil { + resp.Diagnostics.AddError("unable to load library variable set", err.Error()) + } return } diff --git a/octopusdeploy_framework/resource_lifecycle.go b/octopusdeploy_framework/resource_lifecycle.go index afdea2ef6..a4c863f8c 100644 --- a/octopusdeploy_framework/resource_lifecycle.go +++ b/octopusdeploy_framework/resource_lifecycle.go @@ -3,8 +3,11 @@ package octopusdeploy_framework import ( "context" "fmt" + "strings" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/lifecycles" + "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/attr" @@ -12,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" - "strings" ) type lifecycleTypeResource struct { @@ -23,13 +25,14 @@ var _ resource.Resource = &lifecycleTypeResource{} var _ resource.ResourceWithImportState = &lifecycleTypeResource{} type lifecycleTypeResourceModel struct { - ID types.String `tfsdk:"id"` SpaceID types.String `tfsdk:"space_id"` Name types.String `tfsdk:"name"` Description types.String `tfsdk:"description"` Phase types.List `tfsdk:"phase"` ReleaseRetentionPolicy types.List `tfsdk:"release_retention_policy"` TentacleRetentionPolicy types.List `tfsdk:"tentacle_retention_policy"` + + schemas.ResourceModel } func NewLifecycleResource() resource.Resource { @@ -88,7 +91,9 @@ func (r *lifecycleTypeResource) Read(ctx context.Context, req resource.ReadReque lifecycle, err := lifecycles.GetByID(r.Config.Client, data.SpaceID.ValueString(), data.ID.ValueString()) if err != nil { - resp.Diagnostics.AddError("unable to load lifecycle", err.Error()) + if err := errors.ProcessApiErrorV2(ctx, resp, data, err, "lifecycle"); err != nil { + resp.Diagnostics.AddError("unable to load lifecycle", err.Error()) + } return } data = flattenLifecycleResource(lifecycle) @@ -195,8 +200,7 @@ func setDefaultRetentionPolicies(data *lifecycleTypeResourceModel) { } func flattenLifecycleResource(lifecycle *lifecycles.Lifecycle) *lifecycleTypeResourceModel { - return &lifecycleTypeResourceModel{ - ID: types.StringValue(lifecycle.ID), + flattenedLifecycle := &lifecycleTypeResourceModel{ SpaceID: types.StringValue(lifecycle.SpaceID), Name: types.StringValue(lifecycle.Name), Description: types.StringValue(lifecycle.Description), @@ -204,6 +208,9 @@ func flattenLifecycleResource(lifecycle *lifecycles.Lifecycle) *lifecycleTypeRes ReleaseRetentionPolicy: flattenRetentionPeriod(lifecycle.ReleaseRetentionPolicy), TentacleRetentionPolicy: flattenRetentionPeriod(lifecycle.TentacleRetentionPolicy), } + flattenedLifecycle.ID = types.StringValue(lifecycle.GetID()) + + return flattenedLifecycle } func flattenPhases(phases []*lifecycles.Phase) types.List { diff --git a/octopusdeploy_framework/resource_lifecycle_test.go b/octopusdeploy_framework/resource_lifecycle_test.go index 639fc6a93..bbcc08030 100644 --- a/octopusdeploy_framework/resource_lifecycle_test.go +++ b/octopusdeploy_framework/resource_lifecycle_test.go @@ -2,6 +2,9 @@ package octopusdeploy_framework import ( "fmt" + "path/filepath" + "testing" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/lifecycles" @@ -13,8 +16,6 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/stretchr/testify/require" - "path/filepath" - "testing" ) func TestExpandLifecycleWithNil(t *testing.T) { @@ -31,7 +32,6 @@ func TestExpandLifecycle(t *testing.T) { tentacleRetention := core.NewRetentionPeriod(2, "Items", false) data := &lifecycleTypeResourceModel{ - ID: types.StringValue(Id), Description: types.StringValue(description), Name: types.StringValue(name), SpaceID: types.StringValue(spaceID), @@ -62,6 +62,7 @@ func TestExpandLifecycle(t *testing.T) { }, ), } + data.ID = types.StringValue(Id) lifecycle := expandLifecycle(data) diff --git a/octopusdeploy_framework/resource_maven_feed.go b/octopusdeploy_framework/resource_maven_feed.go index 722081e81..55346da60 100644 --- a/octopusdeploy_framework/resource_maven_feed.go +++ b/octopusdeploy_framework/resource_maven_feed.go @@ -3,8 +3,10 @@ package octopusdeploy_framework import ( "context" "fmt" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/feeds" + "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/attr" @@ -76,7 +78,9 @@ func (r *mavenFeedTypeResource) Read(ctx context.Context, req resource.ReadReque client := r.Config.Client feed, err := feeds.GetByID(client, data.SpaceID.ValueString(), data.ID.ValueString()) if err != nil { - resp.Diagnostics.AddError("unable to load maven feed", err.Error()) + if err := errors.ProcessApiErrorV2(ctx, resp, data, err, "maven feed"); err != nil { + resp.Diagnostics.AddError("unable to load maven feed", err.Error()) + } return } diff --git a/octopusdeploy_framework/resource_nuget_feed.go b/octopusdeploy_framework/resource_nuget_feed.go index f053ea450..bf0576248 100644 --- a/octopusdeploy_framework/resource_nuget_feed.go +++ b/octopusdeploy_framework/resource_nuget_feed.go @@ -3,8 +3,10 @@ package octopusdeploy_framework import ( "context" "fmt" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/feeds" + "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/attr" @@ -78,7 +80,9 @@ func (r *nugetFeedTypeResource) Read(ctx context.Context, req resource.ReadReque client := r.Config.Client feed, err := feeds.GetByID(client, data.SpaceID.ValueString(), data.ID.ValueString()) if err != nil { - resp.Diagnostics.AddError("unable to load nuget feed", err.Error()) + if err := errors.ProcessApiErrorV2(ctx, resp, data, err, "nuget feed"); err != nil { + resp.Diagnostics.AddError("unable to load nuget feed", err.Error()) + } return } diff --git a/octopusdeploy_framework/resource_project.go b/octopusdeploy_framework/resource_project.go index 6b73a6d7a..bcefbbf69 100644 --- a/octopusdeploy_framework/resource_project.go +++ b/octopusdeploy_framework/resource_project.go @@ -3,7 +3,9 @@ package octopusdeploy_framework import ( "context" "fmt" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects" + "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" @@ -92,7 +94,9 @@ func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, re project, err := projects.GetByID(r.Client, state.SpaceID.ValueString(), state.ID.ValueString()) if err != nil { - resp.Diagnostics.AddError("Error reading project", err.Error()) + if err := errors.ProcessApiErrorV2(ctx, resp, state, err, "lifecycle"); err != nil { + resp.Diagnostics.AddError("Error reading project", err.Error()) + } return } if persistenceSettings != nil { diff --git a/octopusdeploy_framework/resource_project_flatten.go b/octopusdeploy_framework/resource_project_flatten.go index 39b4c3e80..6e411b08d 100644 --- a/octopusdeploy_framework/resource_project_flatten.go +++ b/octopusdeploy_framework/resource_project_flatten.go @@ -3,6 +3,7 @@ package octopusdeploy_framework import ( "context" "fmt" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/actiontemplates" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/credentials" @@ -26,7 +27,6 @@ func flattenProject(ctx context.Context, project *projects.Project, state *proje } model := &projectResourceModel{ - ID: types.StringValue(project.GetID()), SpaceID: types.StringValue(project.SpaceID), Name: types.StringValue(project.Name), Description: types.StringValue(project.Description), @@ -48,6 +48,8 @@ func flattenProject(ctx context.Context, project *projects.Project, state *proje ClonedFromProjectID: util.StringOrNull(project.ClonedFromProjectID), } + model.ID = types.StringValue(project.GetID()) + model.IncludedLibraryVariableSets = util.FlattenStringList(project.IncludedLibraryVariableSets) model.AutoDeployReleaseOverrides = flattenAutoDeployReleaseOverrides(project.AutoDeployReleaseOverrides) diff --git a/octopusdeploy_framework/resource_project_group.go b/octopusdeploy_framework/resource_project_group.go index e78f49cef..8d7a628de 100644 --- a/octopusdeploy_framework/resource_project_group.go +++ b/octopusdeploy_framework/resource_project_group.go @@ -3,7 +3,9 @@ package octopusdeploy_framework import ( "context" "fmt" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projectgroups" + "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" @@ -67,7 +69,9 @@ func (r *projectGroupTypeResource) Read(ctx context.Context, req resource.ReadRe group, err := projectgroups.GetByID(r.Config.Client, data.SpaceID.ValueString(), data.ID.ValueString()) if err != nil { - resp.Diagnostics.AddError("unable to load project group", err.Error()) + if err := errors.ProcessApiErrorV2(ctx, resp, data, err, "project group"); err != nil { + resp.Diagnostics.AddError("unable to load project group", err.Error()) + } return } diff --git a/octopusdeploy_framework/resource_project_model.go b/octopusdeploy_framework/resource_project_model.go index 7e7386079..d58409893 100644 --- a/octopusdeploy_framework/resource_project_model.go +++ b/octopusdeploy_framework/resource_project_model.go @@ -1,11 +1,11 @@ package octopusdeploy_framework import ( + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/schemas" "github.com/hashicorp/terraform-plugin-framework/types" ) type projectResourceModel struct { - ID types.String `tfsdk:"id"` SpaceID types.String `tfsdk:"space_id"` Name types.String `tfsdk:"name"` Description types.String `tfsdk:"description"` @@ -37,6 +37,8 @@ type projectResourceModel struct { ServiceNowExtensionSettings types.List `tfsdk:"servicenow_extension_settings"` IncludedLibraryVariableSets types.List `tfsdk:"included_library_variable_sets"` AutoDeployReleaseOverrides types.List `tfsdk:"auto_deploy_release_overrides"` + + schemas.ResourceModel } type connectivityPolicyModel struct { diff --git a/octopusdeploy_framework/resource_space.go b/octopusdeploy_framework/resource_space.go index 01cf13cdb..42470551a 100644 --- a/octopusdeploy_framework/resource_space.go +++ b/octopusdeploy_framework/resource_space.go @@ -3,7 +3,10 @@ package octopusdeploy_framework import ( "context" "fmt" + "strings" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/spaces" + "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/path" @@ -11,7 +14,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" - "strings" ) const spaceManagersTeamIDPrefix = "teams-spacemanagers-" @@ -129,7 +131,9 @@ func (s *spaceResource) Read(ctx context.Context, req resource.ReadRequest, resp spaceResult, err := spaces.GetByID(s.Client, data.ID.ValueString()) if err != nil { - resp.Diagnostics.AddError("unable to query spaces", err.Error()) + if err := errors.ProcessApiErrorV2(ctx, resp, data, err, "space"); err != nil { + resp.Diagnostics.AddError("unable to query spaces", err.Error()) + } return } diff --git a/octopusdeploy_framework/resource_tenant_common_variable.go b/octopusdeploy_framework/resource_tenant_common_variable.go index 5ce0bd4a3..715f306bb 100644 --- a/octopusdeploy_framework/resource_tenant_common_variable.go +++ b/octopusdeploy_framework/resource_tenant_common_variable.go @@ -24,12 +24,13 @@ type tenantCommonVariableResource struct { } type tenantCommonVariableResourceModel struct { - ID types.String `tfsdk:"id"` SpaceID types.String `tfsdk:"space_id"` TenantID types.String `tfsdk:"tenant_id"` LibraryVariableSetID types.String `tfsdk:"library_variable_set_id"` TemplateID types.String `tfsdk:"template_id"` Value types.String `tfsdk:"value"` + + schemas.ResourceModel } func NewTenantCommonVariableResource() resource.Resource { diff --git a/octopusdeploy_framework/resource_tenant_project.go b/octopusdeploy_framework/resource_tenant_project.go index f860af3e3..37a6879c9 100644 --- a/octopusdeploy_framework/resource_tenant_project.go +++ b/octopusdeploy_framework/resource_tenant_project.go @@ -21,11 +21,12 @@ import ( ) type TenantProjectModel struct { - ID types.String `tfsdk:"id"` SpaceID types.String `tfsdk:"space_id"` TenantID types.String `tfsdk:"tenant_id"` ProjectID types.String `tfsdk:"project_id"` EnvironmentIDs types.List `tfsdk:"environment_ids"` + + schemas.ResourceModel } type tenantProjectResource struct { diff --git a/octopusdeploy_framework/resource_tenant_project_variable.go b/octopusdeploy_framework/resource_tenant_project_variable.go index 7c7fb8191..7718f658e 100644 --- a/octopusdeploy_framework/resource_tenant_project_variable.go +++ b/octopusdeploy_framework/resource_tenant_project_variable.go @@ -3,6 +3,8 @@ package octopusdeploy_framework import ( "context" "fmt" + "strings" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tenants" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/variables" @@ -12,7 +14,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" - "strings" ) var _ resource.Resource = &tenantProjectVariableResource{} @@ -23,13 +24,14 @@ type tenantProjectVariableResource struct { } type tenantProjectVariableResourceModel struct { - ID types.String `tfsdk:"id"` SpaceID types.String `tfsdk:"space_id"` TenantID types.String `tfsdk:"tenant_id"` ProjectID types.String `tfsdk:"project_id"` EnvironmentID types.String `tfsdk:"environment_id"` TemplateID types.String `tfsdk:"template_id"` Value types.String `tfsdk:"value"` + + schemas.ResourceModel } func NewTenantProjectVariableResource() resource.Resource { diff --git a/octopusdeploy_framework/resource_variable.go b/octopusdeploy_framework/resource_variable.go index 2b411f50c..78d48b5a2 100644 --- a/octopusdeploy_framework/resource_variable.go +++ b/octopusdeploy_framework/resource_variable.go @@ -7,6 +7,7 @@ import ( "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/variables" "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal" + "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/attr" @@ -125,7 +126,9 @@ func (r *variableTypeResource) Read(ctx context.Context, req resource.ReadReques variable, err := variables.GetByID(r.Config.Client, data.SpaceID.ValueString(), variableOwnerID.ValueString(), data.ID.ValueString()) if err != nil { - resp.Diagnostics.AddError("unable to load variable", err.Error()) + if err := errors.ProcessApiErrorV2(ctx, resp, data, err, schemas.VariableResourceDescription); err != nil { + resp.Diagnostics.AddError("unable to load variable", err.Error()) + } return } diff --git a/octopusdeploy_framework/schemas/artifactory_generic_feed.go b/octopusdeploy_framework/schemas/artifactory_generic_feed.go index 9ee7623f8..aecde79de 100644 --- a/octopusdeploy_framework/schemas/artifactory_generic_feed.go +++ b/octopusdeploy_framework/schemas/artifactory_generic_feed.go @@ -33,7 +33,6 @@ func GetArtifactoryGenericFeedResourceSchema() map[string]resourceSchema.Attribu type ArtifactoryGenericFeedTypeResourceModel struct { FeedUri types.String `tfsdk:"feed_uri"` - ID types.String `tfsdk:"id"` Name types.String `tfsdk:"name"` PackageAcquisitionLocationOptions types.List `tfsdk:"package_acquisition_location_options"` Password types.String `tfsdk:"password"` @@ -41,4 +40,6 @@ type ArtifactoryGenericFeedTypeResourceModel struct { Username types.String `tfsdk:"username"` Repository types.String `tfsdk:"repository"` LayoutRegex types.String `tfsdk:"layout_regex"` + + ResourceModel } diff --git a/octopusdeploy_framework/schemas/aws_elastic_container_registry.go b/octopusdeploy_framework/schemas/aws_elastic_container_registry.go index 92c3c847b..fbc321f9e 100644 --- a/octopusdeploy_framework/schemas/aws_elastic_container_registry.go +++ b/octopusdeploy_framework/schemas/aws_elastic_container_registry.go @@ -32,10 +32,11 @@ func GetAwsElasticContainerRegistryFeedResourceSchema() map[string]resourceSchem type AwsElasticContainerRegistryFeedTypeResourceModel struct { AccessKey types.String `tfsdk:"access_key"` - ID types.String `tfsdk:"id"` Name types.String `tfsdk:"name"` PackageAcquisitionLocationOptions types.List `tfsdk:"package_acquisition_location_options"` Region types.String `tfsdk:"region"` SecretKey types.String `tfsdk:"secret_key"` SpaceID types.String `tfsdk:"space_id"` + + ResourceModel } diff --git a/octopusdeploy_framework/schemas/docker_container_registry_feed.go b/octopusdeploy_framework/schemas/docker_container_registry_feed.go index d7ff550c4..346eee869 100644 --- a/octopusdeploy_framework/schemas/docker_container_registry_feed.go +++ b/octopusdeploy_framework/schemas/docker_container_registry_feed.go @@ -29,11 +29,12 @@ func GetDockerContainerRegistryFeedResourceSchema() map[string]resourceSchema.At type DockerContainerRegistryFeedTypeResourceModel struct { APIVersion types.String `tfsdk:"api_version"` FeedUri types.String `tfsdk:"feed_uri"` - ID types.String `tfsdk:"id"` Name types.String `tfsdk:"name"` PackageAcquisitionLocationOptions types.List `tfsdk:"package_acquisition_location_options"` Password types.String `tfsdk:"password"` SpaceID types.String `tfsdk:"space_id"` Username types.String `tfsdk:"username"` RegistryPath types.String `tfsdk:"registry_path"` + + ResourceModel } diff --git a/octopusdeploy_framework/schemas/environment.go b/octopusdeploy_framework/schemas/environment.go index af4a31e20..964532446 100644 --- a/octopusdeploy_framework/schemas/environment.go +++ b/octopusdeploy_framework/schemas/environment.go @@ -180,7 +180,6 @@ func MapServiceNowExtensionSettings(serviceNowExtensionSettings *environments.Se } type EnvironmentTypeResourceModel struct { - ID types.String `tfsdk:"id"` Slug types.String `tfsdk:"slug"` Name types.String `tfsdk:"name"` Description types.String `tfsdk:"description"` @@ -191,4 +190,6 @@ type EnvironmentTypeResourceModel struct { JiraExtensionSettings types.List `tfsdk:"jira_extension_settings"` JiraServiceManagementExtensionSettings types.List `tfsdk:"jira_service_management_extension_settings"` ServiceNowExtensionSettings types.List `tfsdk:"servicenow_extension_settings"` + + ResourceModel } diff --git a/octopusdeploy_framework/schemas/github_repository_feed.go b/octopusdeploy_framework/schemas/github_repository_feed.go index 12cda7b7e..71049f464 100644 --- a/octopusdeploy_framework/schemas/github_repository_feed.go +++ b/octopusdeploy_framework/schemas/github_repository_feed.go @@ -26,10 +26,11 @@ type GitHubRepositoryFeedTypeResourceModel struct { DownloadAttempts types.Int64 `tfsdk:"download_attempts"` DownloadRetryBackoffSeconds types.Int64 `tfsdk:"download_retry_backoff_seconds"` FeedUri types.String `tfsdk:"feed_uri"` - ID types.String `tfsdk:"id"` Name types.String `tfsdk:"name"` PackageAcquisitionLocationOptions types.List `tfsdk:"package_acquisition_location_options"` Password types.String `tfsdk:"password"` SpaceID types.String `tfsdk:"space_id"` Username types.String `tfsdk:"username"` + + ResourceModel } diff --git a/octopusdeploy_framework/schemas/helm_feed.go b/octopusdeploy_framework/schemas/helm_feed.go index 62b567b60..7e3072fee 100644 --- a/octopusdeploy_framework/schemas/helm_feed.go +++ b/octopusdeploy_framework/schemas/helm_feed.go @@ -22,10 +22,11 @@ func GetHelmFeedResourceSchema() map[string]resourceSchema.Attribute { type HelmFeedTypeResourceModel struct { FeedUri types.String `tfsdk:"feed_uri"` - ID types.String `tfsdk:"id"` Name types.String `tfsdk:"name"` PackageAcquisitionLocationOptions types.List `tfsdk:"package_acquisition_location_options"` Password types.String `tfsdk:"password"` SpaceID types.String `tfsdk:"space_id"` Username types.String `tfsdk:"username"` + + ResourceModel } diff --git a/octopusdeploy_framework/schemas/library_variable_set.go b/octopusdeploy_framework/schemas/library_variable_set.go index 5fab75d02..3ca65b627 100644 --- a/octopusdeploy_framework/schemas/library_variable_set.go +++ b/octopusdeploy_framework/schemas/library_variable_set.go @@ -12,12 +12,13 @@ import ( type LibraryVariableSetResourceModel struct { Description types.String `tfsdk:"description"` - ID types.String `tfsdk:"id"` Name types.String `tfsdk:"name"` SpaceID types.String `tfsdk:"space_id"` Template types.List `tfsdk:"template"` TemplateIds types.Map `tfsdk:"template_ids"` VariableSetId types.String `tfsdk:"variable_set_id"` + + ResourceModel } func GetLibraryVariableSetDataSourceSchema() datasourceSchema.Schema { diff --git a/octopusdeploy_framework/schemas/maven_feed.go b/octopusdeploy_framework/schemas/maven_feed.go index 096d7b4f4..9c01debc3 100644 --- a/octopusdeploy_framework/schemas/maven_feed.go +++ b/octopusdeploy_framework/schemas/maven_feed.go @@ -26,10 +26,11 @@ type MavenFeedTypeResourceModel struct { DownloadAttempts types.Int64 `tfsdk:"download_attempts"` DownloadRetryBackoffSeconds types.Int64 `tfsdk:"download_retry_backoff_seconds"` FeedUri types.String `tfsdk:"feed_uri"` - ID types.String `tfsdk:"id"` Name types.String `tfsdk:"name"` PackageAcquisitionLocationOptions types.List `tfsdk:"package_acquisition_location_options"` Password types.String `tfsdk:"password"` SpaceID types.String `tfsdk:"space_id"` Username types.String `tfsdk:"username"` + + ResourceModel } diff --git a/octopusdeploy_framework/schemas/nuget_feed.go b/octopusdeploy_framework/schemas/nuget_feed.go index a9d5567fb..a744dafb1 100644 --- a/octopusdeploy_framework/schemas/nuget_feed.go +++ b/octopusdeploy_framework/schemas/nuget_feed.go @@ -33,11 +33,12 @@ type NugetFeedTypeResourceModel struct { DownloadAttempts types.Int64 `tfsdk:"download_attempts"` DownloadRetryBackoffSeconds types.Int64 `tfsdk:"download_retry_backoff_seconds"` FeedUri types.String `tfsdk:"feed_uri"` - ID types.String `tfsdk:"id"` IsEnhancedMode types.Bool `tfsdk:"is_enhanced_mode"` Name types.String `tfsdk:"name"` PackageAcquisitionLocationOptions types.List `tfsdk:"package_acquisition_location_options"` Password types.String `tfsdk:"password"` SpaceID types.String `tfsdk:"space_id"` Username types.String `tfsdk:"username"` + + ResourceModel } diff --git a/octopusdeploy_framework/schemas/project_group.go b/octopusdeploy_framework/schemas/project_group.go index 1aaf65d18..bafc5ea42 100644 --- a/octopusdeploy_framework/schemas/project_group.go +++ b/octopusdeploy_framework/schemas/project_group.go @@ -43,9 +43,10 @@ func GetProjectGroupResourceSchema() map[string]resourceSchema.Attribute { } type ProjectGroupTypeResourceModel struct { - ID types.String `tfsdk:"id"` Name types.String `tfsdk:"name"` SpaceID types.String `tfsdk:"space_id"` Description types.String `tfsdk:"description"` RetentionPolicyID types.String `tfsdk:"retention_policy_id"` + + ResourceModel } diff --git a/octopusdeploy_framework/schemas/resource.go b/octopusdeploy_framework/schemas/resource.go new file mode 100644 index 000000000..b97eb5efc --- /dev/null +++ b/octopusdeploy_framework/schemas/resource.go @@ -0,0 +1,19 @@ +package schemas + +import ( + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type IResourceModel interface { + GetID() string +} + +type ResourceModel struct { + ID types.String `tfsdk:"id"` + + IResourceModel `tfsdk:"-"` // Ignore resource model interface in object conversion +} + +func (r ResourceModel) GetID() string { + return r.ID.ValueString() +} diff --git a/octopusdeploy_framework/schemas/space.go b/octopusdeploy_framework/schemas/space.go index c14354752..662a95c96 100644 --- a/octopusdeploy_framework/schemas/space.go +++ b/octopusdeploy_framework/schemas/space.go @@ -11,7 +11,6 @@ import ( const spaceDescription = "space" type SpaceModel struct { - ID types.String `tfsdk:"id"` Name types.String `tfsdk:"name"` Slug types.String `tfsdk:"slug"` Description types.String `tfsdk:"description"` @@ -19,6 +18,8 @@ type SpaceModel struct { SpaceManagersTeams types.Set `tfsdk:"space_managers_teams"` SpaceManagersTeamMembers types.Set `tfsdk:"space_managers_team_members"` IsTaskQueueStopped types.Bool `tfsdk:"is_task_queue_stopped"` + + ResourceModel } func GetSpaceResourceSchema() map[string]resourceSchema.Attribute { diff --git a/octopusdeploy_framework/schemas/variable.go b/octopusdeploy_framework/schemas/variable.go index b3195028b..5d9836e99 100644 --- a/octopusdeploy_framework/schemas/variable.go +++ b/octopusdeploy_framework/schemas/variable.go @@ -218,7 +218,6 @@ func GetVariableResourceSchema() resourceSchema.Schema { } type VariableTypeResourceModel struct { - ID types.String `tfsdk:"id"` Name types.String `tfsdk:"name"` Description types.String `tfsdk:"description"` OwnerID types.String `tfsdk:"owner_id"` @@ -234,6 +233,8 @@ type VariableTypeResourceModel struct { Prompt types.List `tfsdk:"prompt"` Scope types.List `tfsdk:"scope"` SpaceID types.String `tfsdk:"space_id"` + + ResourceModel } type VariablesDataSourceModel struct { @@ -242,11 +243,12 @@ type VariablesDataSourceModel struct { Scope types.List `tfsdk:"scope"` SpaceID types.String `tfsdk:"space_id"` Description types.String `tfsdk:"description"` - ID types.String `tfsdk:"id"` IsEditable types.Bool `tfsdk:"is_editable"` IsSensitive types.Bool `tfsdk:"is_sensitive"` Prompt types.List `tfsdk:"prompt"` SensitiveValue types.String `tfsdk:"sensitive_value"` Type types.String `tfsdk:"type"` Value types.String `tfsdk:"value"` + + ResourceModel } From 7ac25f7e821378b13a78088411332565f665a8d8 Mon Sep 17 00:00:00 2001 From: Isaac Calligeros <101079287+IsaacCalligeros95@users.noreply.github.com> Date: Fri, 9 Aug 2024 13:29:25 +0930 Subject: [PATCH 07/28] Chore!: Migrate tenant datasource (#699) * Migrate tenants datasource * Update previously migrated data source schemas to be read only --- .../azure_cloud_service_deployment_targets.md | 28 ++--- ...rvice_fabric_cluster_deployment_targets.md | 30 ++--- .../azure_web_app_deployment_targets.md | 28 ++--- docs/data-sources/certificates.md | 26 ++--- docs/data-sources/channels.md | 26 ++--- .../cloud_region_deployment_targets.md | 28 ++--- docs/data-sources/deployment_targets.md | 28 ++--- docs/data-sources/environments.md | 20 ++-- docs/data-sources/feeds.md | 23 ++-- docs/data-sources/git_credentials.md | 24 ++-- .../kubernetes_agent_deployment_targets.md | 44 +++---- .../kubernetes_cluster_deployment_targets.md | 56 ++++----- docs/data-sources/library_variable_sets.md | 14 +-- .../listening_tentacle_deployment_targets.md | 52 ++++----- docs/data-sources/machine_policies.md | 38 +++---- ...offline_package_drop_deployment_targets.md | 32 +++--- .../polling_tentacle_deployment_targets.md | 32 +++--- docs/data-sources/project_groups.md | 19 ++-- docs/data-sources/script_modules | 18 +-- docs/data-sources/script_modules.md | 18 +-- docs/data-sources/space.md | 2 +- docs/data-sources/spaces.md | 17 +-- .../ssh_connection_deployment_targets.md | 28 ++--- docs/data-sources/tag_sets.md | 14 +-- docs/data-sources/teams.md | 20 ++-- docs/data-sources/tenants.md | 10 +- docs/data-sources/user_roles.md | 10 +- docs/data-sources/users.md | 18 +-- docs/data-sources/worker_pools.md | 14 +-- docs/index.md | 5 +- docs/resources/azure_subscription_account.md | 4 +- docs/resources/environment.md | 29 +++-- docs/resources/lifecycle.md | 4 +- docs/resources/nuget_feed.md | 4 +- docs/resources/space.md | 4 +- docs/resources/tenant_common_variable.md | 19 ++-- docs/resources/tenant_project.md | 13 +-- go.mod | 2 +- go.sum | 4 +- octopusdeploy/data_source_tenants.go | 54 --------- octopusdeploy/provider.go | 2 - .../resource_deployment_process_test.go | 1 - octopusdeploy/testing_container_test.go | 2 +- .../datasource_environments.go | 4 +- .../datasource_project_groups.go | 4 +- octopusdeploy_framework/datasource_spaces.go | 2 +- octopusdeploy_framework/datasource_tenants.go | 80 +++++++++++++ .../datasource_tenants_test.go | 11 +- octopusdeploy_framework/framework_provider.go | 3 +- .../schemas/environment.go | 15 +-- octopusdeploy_framework/schemas/feed.go | 33 +++--- .../schemas/library_variable_set.go | 14 +-- .../schemas/project_group.go | 2 +- octopusdeploy_framework/schemas/schema.go | 58 ++++++++-- octopusdeploy_framework/schemas/space.go | 23 ++-- octopusdeploy_framework/schemas/tenant.go | 107 ++++++++++++++++++ .../testing_container_test.go | 2 +- octopusdeploy_framework/util/schema.go | 40 ++++--- 58 files changed, 708 insertions(+), 554 deletions(-) delete mode 100644 octopusdeploy/data_source_tenants.go create mode 100644 octopusdeploy_framework/datasource_tenants.go rename octopusdeploy/data_source_tenants_test.go => octopusdeploy_framework/datasource_tenants_test.go (82%) create mode 100644 octopusdeploy_framework/schemas/tenant.go diff --git a/docs/data-sources/azure_cloud_service_deployment_targets.md b/docs/data-sources/azure_cloud_service_deployment_targets.md index 5bd56afb8..c202843d6 100644 --- a/docs/data-sources/azure_cloud_service_deployment_targets.md +++ b/docs/data-sources/azure_cloud_service_deployment_targets.md @@ -45,10 +45,10 @@ data "octopusdeploy_azure_cloud_service_deployment_targets" "example" { ### Read-Only -- `azure_cloud_service_deployment_targets` (Block List) A list of Azure cloud service deployment targets that match the filter(s). (see [below for nested schema](#nestedblock--azure_cloud_service_deployment_targets)) +- `azure_cloud_service_deployment_targets` (List of Object) A list of Azure cloud service deployment targets that match the filter(s). (see [below for nested schema](#nestedatt--azure_cloud_service_deployment_targets)) - `id` (String) An auto-generated identifier that includes the timestamp when this data source was last modified. - + ### Nested Schema for `azure_cloud_service_deployment_targets` Read-Only: @@ -56,33 +56,33 @@ Read-Only: - `account_id` (String) - `cloud_service_name` (String) - `default_worker_pool_id` (String) -- `endpoint` (List of Object) (see [below for nested schema](#nestedatt--azure_cloud_service_deployment_targets--endpoint)) -- `environments` (List of String) A list of environment IDs associated with this resource. +- `endpoint` (List of Object) (see [below for nested schema](#nestedobjatt--azure_cloud_service_deployment_targets--endpoint)) +- `environments` (List of String) - `has_latest_calamari` (Boolean) -- `health_status` (String) Represents the health status of this deployment target. Valid health statuses are `HasWarnings`, `Healthy`, `Unavailable`, `Unhealthy`, or `Unknown`. -- `id` (String) The unique ID for this resource. +- `health_status` (String) +- `id` (String) - `is_disabled` (Boolean) - `is_in_process` (Boolean) - `machine_policy_id` (String) -- `name` (String) The name of this resource. +- `name` (String) - `operating_system` (String) - `roles` (List of String) - `shell_name` (String) - `shell_version` (String) - `slot` (String) -- `space_id` (String) The space ID associated with this resource. -- `status` (String) The status of this resource. Valid statuses are `CalamariNeedsUpgrade`, `Disabled`, `NeedsUpgrade`, `Offline`, `Online`, or `Unknown`. -- `status_summary` (String) A summary elaborating on the status of this resource. +- `space_id` (String) +- `status` (String) +- `status_summary` (String) - `storage_account_name` (String) - `swap_if_possible` (Boolean) -- `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. +- `tenant_tags` (List of String) +- `tenanted_deployment_participation` (String) +- `tenants` (List of String) - `thumbprint` (String) - `uri` (String) - `use_current_instance_count` (Boolean) - + ### Nested Schema for `azure_cloud_service_deployment_targets.endpoint` Read-Only: diff --git a/docs/data-sources/azure_service_fabric_cluster_deployment_targets.md b/docs/data-sources/azure_service_fabric_cluster_deployment_targets.md index f6aedf261..13a82ca92 100644 --- a/docs/data-sources/azure_service_fabric_cluster_deployment_targets.md +++ b/docs/data-sources/azure_service_fabric_cluster_deployment_targets.md @@ -45,47 +45,47 @@ data "octopusdeploy_azure_service_fabric_cluster_deployment_targets" "example" { ### Read-Only -- `azure_service_fabric_cluster_deployment_targets` (Block List) A list of Azure service fabric cluster deployment targets that match the filter(s). (see [below for nested schema](#nestedblock--azure_service_fabric_cluster_deployment_targets)) +- `azure_service_fabric_cluster_deployment_targets` (List of Object) A list of Azure service fabric cluster deployment targets that match the filter(s). (see [below for nested schema](#nestedatt--azure_service_fabric_cluster_deployment_targets)) - `id` (String) An auto-generated identifier that includes the timestamp when this data source was last modified. - + ### Nested Schema for `azure_service_fabric_cluster_deployment_targets` Read-Only: - `aad_client_credential_secret` (String) - `aad_credential_type` (String) -- `aad_user_credential_password` (String, Sensitive) +- `aad_user_credential_password` (String) - `aad_user_credential_username` (String) - `certificate_store_location` (String) - `certificate_store_name` (String) - `client_certificate_variable` (String) - `connection_endpoint` (String) -- `endpoint` (List of Object) (see [below for nested schema](#nestedatt--azure_service_fabric_cluster_deployment_targets--endpoint)) -- `environments` (List of String) A list of environment IDs associated with this resource. +- `endpoint` (List of Object) (see [below for nested schema](#nestedobjatt--azure_service_fabric_cluster_deployment_targets--endpoint)) +- `environments` (List of String) - `has_latest_calamari` (Boolean) -- `health_status` (String) Represents the health status of this deployment target. Valid health statuses are `HasWarnings`, `Healthy`, `Unavailable`, `Unhealthy`, or `Unknown`. -- `id` (String) The unique ID for this resource. +- `health_status` (String) +- `id` (String) - `is_disabled` (Boolean) - `is_in_process` (Boolean) - `machine_policy_id` (String) -- `name` (String) The name of this resource. +- `name` (String) - `operating_system` (String) - `roles` (List of String) - `security_mode` (String) - `server_certificate_thumbprint` (String) - `shell_name` (String) - `shell_version` (String) -- `space_id` (String) The space ID associated with this resource. -- `status` (String) The status of this resource. Valid statuses are `CalamariNeedsUpgrade`, `Disabled`, `NeedsUpgrade`, `Offline`, `Online`, or `Unknown`. -- `status_summary` (String) A summary elaborating on the status of 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. +- `space_id` (String) +- `status` (String) +- `status_summary` (String) +- `tenant_tags` (List of String) +- `tenanted_deployment_participation` (String) +- `tenants` (List of String) - `thumbprint` (String) - `uri` (String) - + ### Nested Schema for `azure_service_fabric_cluster_deployment_targets.endpoint` Read-Only: diff --git a/docs/data-sources/azure_web_app_deployment_targets.md b/docs/data-sources/azure_web_app_deployment_targets.md index 26a8b8a06..a5f00dd3d 100644 --- a/docs/data-sources/azure_web_app_deployment_targets.md +++ b/docs/data-sources/azure_web_app_deployment_targets.md @@ -45,41 +45,41 @@ data "octopusdeploy_azure_web_app_deployment_targets" "example" { ### Read-Only -- `azure_web_app_deployment_targets` (Block List) A list of Azure web app deployment targets that match the filter(s). (see [below for nested schema](#nestedblock--azure_web_app_deployment_targets)) +- `azure_web_app_deployment_targets` (List of Object) A list of Azure web app deployment targets that match the filter(s). (see [below for nested schema](#nestedatt--azure_web_app_deployment_targets)) - `id` (String) An auto-generated identifier that includes the timestamp when this data source was last modified. - + ### Nested Schema for `azure_web_app_deployment_targets` Read-Only: - `account_id` (String) -- `endpoint` (List of Object) (see [below for nested schema](#nestedatt--azure_web_app_deployment_targets--endpoint)) -- `environments` (List of String) A list of environment IDs associated with this resource. +- `endpoint` (List of Object) (see [below for nested schema](#nestedobjatt--azure_web_app_deployment_targets--endpoint)) +- `environments` (List of String) - `has_latest_calamari` (Boolean) -- `health_status` (String) Represents the health status of this deployment target. Valid health statuses are `HasWarnings`, `Healthy`, `Unavailable`, `Unhealthy`, or `Unknown`. -- `id` (String) The unique ID for this resource. +- `health_status` (String) +- `id` (String) - `is_disabled` (Boolean) - `is_in_process` (Boolean) - `machine_policy_id` (String) -- `name` (String) The name of this resource. +- `name` (String) - `operating_system` (String) - `resource_group_name` (String) - `roles` (List of String) - `shell_name` (String) - `shell_version` (String) -- `space_id` (String) The space ID associated with this resource. -- `status` (String) The status of this resource. Valid statuses are `CalamariNeedsUpgrade`, `Disabled`, `NeedsUpgrade`, `Offline`, `Online`, or `Unknown`. -- `status_summary` (String) A summary elaborating on the status of 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. +- `space_id` (String) +- `status` (String) +- `status_summary` (String) +- `tenant_tags` (List of String) +- `tenanted_deployment_participation` (String) +- `tenants` (List of String) - `thumbprint` (String) - `uri` (String) - `web_app_name` (String) - `web_app_slot_name` (String) - + ### Nested Schema for `azure_web_app_deployment_targets.endpoint` Read-Only: diff --git a/docs/data-sources/certificates.md b/docs/data-sources/certificates.md index 9ba533b85..a62462d0b 100644 --- a/docs/data-sources/certificates.md +++ b/docs/data-sources/certificates.md @@ -40,29 +40,29 @@ data "octopusdeploy_certificates" "example" { ### Read-Only -- `certificates` (Block List) A list of certificates that match the filter(s). (see [below for nested schema](#nestedblock--certificates)) +- `certificates` (List of Object) A list of certificates that match the filter(s). (see [below for nested schema](#nestedatt--certificates)) - `id` (String) An auto-generated identifier that includes the timestamp when this data source was last modified. - + ### Nested Schema for `certificates` Read-Only: - `archived` (String) -- `certificate_data` (String, Sensitive) The encoded data of the certificate. -- `certificate_data_format` (String) Specifies the archive file format used for storing cryptography objects in the certificate. Valid formats are `Der`, `Pem`, `Pkcs12`, or `Unknown`. -- `environments` (List of String) A list of environment IDs associated with this resource. -- `has_private_key` (Boolean) Indicates if the certificate has a private key. -- `id` (String) The unique ID for this resource. -- `is_expired` (Boolean) Indicates if the certificate has expired. +- `certificate_data` (String) +- `certificate_data_format` (String) +- `environments` (List of String) +- `has_private_key` (Boolean) +- `id` (String) +- `is_expired` (Boolean) - `issuer_common_name` (String) - `issuer_distinguished_name` (String) - `issuer_organization` (String) -- `name` (String) The name of this resource. +- `name` (String) - `not_after` (String) - `not_before` (String) - `notes` (String) -- `password` (String, Sensitive) The password associated with this resource. +- `password` (String) - `replaced_by` (String) - `self_signed` (Boolean) - `serial_number` (String) @@ -72,9 +72,9 @@ Read-Only: - `subject_common_name` (String) - `subject_distinguished_name` (String) - `subject_organization` (String) -- `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. +- `tenant_tags` (List of String) +- `tenanted_deployment_participation` (String) +- `tenants` (List of String) - `thumbprint` (String) - `version` (Number) diff --git a/docs/data-sources/channels.md b/docs/data-sources/channels.md index bf1546f4f..b19b11d00 100644 --- a/docs/data-sources/channels.md +++ b/docs/data-sources/channels.md @@ -34,25 +34,25 @@ data "octopusdeploy_channels" "example" { ### Read-Only -- `channels` (Block List) A channel that matches the specified filter(s). (see [below for nested schema](#nestedblock--channels)) +- `channels` (List of Object) A channel that matches the specified filter(s). (see [below for nested schema](#nestedatt--channels)) - `id` (String) The ID of this resource. - + ### Nested Schema for `channels` Read-Only: -- `description` (String) The description of this channel. -- `id` (String) The unique ID for this resource. -- `is_default` (Boolean) Indicates if this is the default channel for the associated project. -- `lifecycle_id` (String) The lifecycle ID associated with this channel. -- `name` (String) The name of this resource. -- `project_id` (String) The project ID associated with this channel. -- `rule` (List of Object) A list of rules associated with this channel. (see [below for nested schema](#nestedatt--channels--rule)) -- `space_id` (String) The space ID associated with this resource. -- `tenant_tags` (List of String) A list of tenant tags associated with this resource. - - +- `description` (String) +- `id` (String) +- `is_default` (Boolean) +- `lifecycle_id` (String) +- `name` (String) +- `project_id` (String) +- `rule` (List of Object) (see [below for nested schema](#nestedobjatt--channels--rule)) +- `space_id` (String) +- `tenant_tags` (List of String) + + ### Nested Schema for `channels.rule` Read-Only: diff --git a/docs/data-sources/cloud_region_deployment_targets.md b/docs/data-sources/cloud_region_deployment_targets.md index 79adef44b..8818118d5 100644 --- a/docs/data-sources/cloud_region_deployment_targets.md +++ b/docs/data-sources/cloud_region_deployment_targets.md @@ -46,38 +46,38 @@ data "octopusdeploy_cloud_region_deployment_targets" "example" { ### Read-Only -- `cloud_region_deployment_targets` (Block List) A list of cloud region deployment targets that match the filter(s). (see [below for nested schema](#nestedblock--cloud_region_deployment_targets)) +- `cloud_region_deployment_targets` (List of Object) A list of cloud region deployment targets that match the filter(s). (see [below for nested schema](#nestedatt--cloud_region_deployment_targets)) - `id` (String) An auto-generated identifier that includes the timestamp when this data source was last modified. - + ### Nested Schema for `cloud_region_deployment_targets` Read-Only: - `default_worker_pool_id` (String) -- `endpoint` (List of Object) (see [below for nested schema](#nestedatt--cloud_region_deployment_targets--endpoint)) -- `environments` (List of String) A list of environment IDs associated with this resource. +- `endpoint` (List of Object) (see [below for nested schema](#nestedobjatt--cloud_region_deployment_targets--endpoint)) +- `environments` (List of String) - `has_latest_calamari` (Boolean) -- `health_status` (String) Represents the health status of this deployment target. Valid health statuses are `HasWarnings`, `Healthy`, `Unavailable`, `Unhealthy`, or `Unknown`. -- `id` (String) The unique ID for this resource. +- `health_status` (String) +- `id` (String) - `is_disabled` (Boolean) - `is_in_process` (Boolean) - `machine_policy_id` (String) -- `name` (String) The name of this resource. +- `name` (String) - `operating_system` (String) - `roles` (List of String) - `shell_name` (String) - `shell_version` (String) -- `space_id` (String) The space ID associated with this resource. -- `status` (String) The status of this resource. Valid statuses are `CalamariNeedsUpgrade`, `Disabled`, `NeedsUpgrade`, `Offline`, `Online`, or `Unknown`. -- `status_summary` (String) A summary elaborating on the status of 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. +- `space_id` (String) +- `status` (String) +- `status_summary` (String) +- `tenant_tags` (List of String) +- `tenanted_deployment_participation` (String) +- `tenants` (List of String) - `thumbprint` (String) - `uri` (String) - + ### Nested Schema for `cloud_region_deployment_targets.endpoint` Read-Only: diff --git a/docs/data-sources/deployment_targets.md b/docs/data-sources/deployment_targets.md index 4677decd0..50a12ed38 100644 --- a/docs/data-sources/deployment_targets.md +++ b/docs/data-sources/deployment_targets.md @@ -36,37 +36,37 @@ Provides information about existing deployment targets. ### Read-Only -- `deployment_targets` (Block List) A list of deployment targets that match the filter(s). (see [below for nested schema](#nestedblock--deployment_targets)) +- `deployment_targets` (List of Object) A list of deployment targets that match the filter(s). (see [below for nested schema](#nestedatt--deployment_targets)) - `id` (String) The ID of this resource. - + ### Nested Schema for `deployment_targets` Read-Only: -- `endpoint` (List of Object) (see [below for nested schema](#nestedatt--deployment_targets--endpoint)) -- `environments` (List of String) A list of environment IDs associated with this resource. +- `endpoint` (List of Object) (see [below for nested schema](#nestedobjatt--deployment_targets--endpoint)) +- `environments` (List of String) - `has_latest_calamari` (Boolean) -- `health_status` (String) Represents the health status of this deployment target. Valid health statuses are `HasWarnings`, `Healthy`, `Unavailable`, `Unhealthy`, or `Unknown`. -- `id` (String) The unique ID for this resource. +- `health_status` (String) +- `id` (String) - `is_disabled` (Boolean) - `is_in_process` (Boolean) - `machine_policy_id` (String) -- `name` (String) The name of this resource. +- `name` (String) - `operating_system` (String) - `roles` (List of String) - `shell_name` (String) - `shell_version` (String) -- `space_id` (String) The space ID associated with this resource. -- `status` (String) The status of this resource. Valid statuses are `CalamariNeedsUpgrade`, `Disabled`, `NeedsUpgrade`, `Offline`, `Online`, or `Unknown`. -- `status_summary` (String) A summary elaborating on the status of 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. +- `space_id` (String) +- `status` (String) +- `status_summary` (String) +- `tenant_tags` (List of String) +- `tenanted_deployment_participation` (String) +- `tenants` (List of String) - `thumbprint` (String) - `uri` (String) - + ### Nested Schema for `deployment_targets.endpoint` Read-Only: diff --git a/docs/data-sources/environments.md b/docs/data-sources/environments.md index 69bc1ab23..d67c25219 100644 --- a/docs/data-sources/environments.md +++ b/docs/data-sources/environments.md @@ -28,16 +28,16 @@ data "octopusdeploy_environments" "example" { ### Optional - `ids` (List of String) A filter to search by a list of IDs. -- `name` (String) A filter to search by name. -- `partial_name` (String) A filter to search by the partial match of a name. +- `name` (String) A filter search by exact name +- `partial_name` (String) A filter to search by a partial name. - `skip` (Number) A filter to specify the number of items to skip in the response. -- `space_id` (String) The space ID associated with this resource. +- `space_id` (String) The space ID associated with this environment. - `take` (Number) A filter to specify the number of items to take (or return) in the response. ### Read-Only -- `environments` (Block List) A list of environments that match the filter(s). (see [below for nested schema](#nestedblock--environments)) -- `id` (String) An auto-generated identifier that includes the timestamp when this data source was last modified. +- `environments` (Block List) Provides information about existing environments. (see [below for nested schema](#nestedblock--environments)) +- `id` (String) The unique ID for this resource. ### Nested Schema for `environments` @@ -47,12 +47,12 @@ Read-Only: - `allow_dynamic_infrastructure` (Boolean) - `description` (String) The description of this environment. - `id` (String) The unique ID for this resource. -- `jira_extension_settings` (List of Object) Provides extension settings for the Jira integration for this environment. (see [below for nested schema](#nestedatt--environments--jira_extension_settings)) -- `jira_service_management_extension_settings` (List of Object) Provides extension settings for the Jira Service Management (JSM) integration for this environment. (see [below for nested schema](#nestedatt--environments--jira_service_management_extension_settings)) +- `jira_extension_settings` (Attributes List) Provides extension settings for the Jira integration for this environment. (see [below for nested schema](#nestedatt--environments--jira_extension_settings)) +- `jira_service_management_extension_settings` (Attributes List) Provides extension settings for the Jira Service Management (JSM) integration for this environment. (see [below for nested schema](#nestedatt--environments--jira_service_management_extension_settings)) - `name` (String) The name of this resource. -- `servicenow_extension_settings` (List of Object) Provides extension settings for the ServiceNow integration for this environment. (see [below for nested schema](#nestedatt--environments--servicenow_extension_settings)) -- `slug` (String) -- `sort_order` (Number) The order number to sort an environment. +- `servicenow_extension_settings` (Attributes List) Provides extension settings for the ServiceNow integration for this environment. (see [below for nested schema](#nestedatt--environments--servicenow_extension_settings)) +- `slug` (String) The unique slug of this environment +- `sort_order` (Number) The order number to sort an environment - `space_id` (String) The space ID associated with this environment. - `use_guided_failure` (Boolean) diff --git a/docs/data-sources/feeds.md b/docs/data-sources/feeds.md index 91fde3c96..7afdb726d 100644 --- a/docs/data-sources/feeds.md +++ b/docs/data-sources/feeds.md @@ -27,8 +27,6 @@ data "octopusdeploy_feeds" "example" { ### Optional - `feed_type` (String) A filter to search by feed type. Valid feed types are `AwsElasticContainerRegistry`, `BuiltIn`, `Docker`, `GitHub`, `Helm`, `Maven`, `NuGet`, or `OctopusProject`. -- `feeds` (Block List) (see [below for nested schema](#nestedblock--feeds)) -- `id` (String) The unique ID for this resource. - `ids` (List of String) A filter to search by a list of IDs. - `name` (String) The name of this resource. - `partial_name` (String) A filter to search by a partial name. @@ -36,31 +34,30 @@ data "octopusdeploy_feeds" "example" { - `space_id` (String) The space ID associated with this feeds. - `take` (Number) A filter to specify the number of items to take (or return) in the response. +### Read-Only + +- `feeds` (Block List) (see [below for nested schema](#nestedblock--feeds)) +- `id` (String) The unique ID for this resource. + ### Nested Schema for `feeds` -Required: +Read-Only: - `access_key` (String) The AWS access key to use when authenticating against Amazon Web Services. -- `feed_uri` (String) -- `name` (String) The name of this resource. - -Optional: - - `api_version` (String) - `delete_unreleased_packages_after_days` (Number) - `download_attempts` (Number) The number of times a deployment should attempt to download a package from this feed before failing. - `download_retry_backoff_seconds` (Number) The number of seconds to apply as a linear back off between download attempts. - `feed_type` (String) A filter to search by feed type. Valid feed types are `AwsElasticContainerRegistry`, `BuiltIn`, `Docker`, `GitHub`, `Helm`, `Maven`, `NuGet`, or `OctopusProject`. +- `feed_uri` (String) - `id` (String) The unique ID for this resource. - `is_enhanced_mode` (Boolean) +- `name` (String) The name of this resource. - `package_acquisition_location_options` (List of String) - `password` (String, Sensitive) The password associated with this resource. +- `region` (String) - `registry_path` (String) - `secret_key` (String, Sensitive) - `space_id` (String) The space ID associated with this feeds. -- `username` (String, Sensitive) The username associated with this resource. - -Read-Only: - -- `region` (String) \ No newline at end of file +- `username` (String, Sensitive) The username associated with this resource. \ No newline at end of file diff --git a/docs/data-sources/git_credentials.md b/docs/data-sources/git_credentials.md index d5e0820a4..e7a4a3c4d 100644 --- a/docs/data-sources/git_credentials.md +++ b/docs/data-sources/git_credentials.md @@ -3,12 +3,12 @@ page_title: "octopusdeploy_git_credentials Data Source - terraform-provider-octopusdeploy" subcategory: "" description: |- - Provides information about existing GitCredentials. + Use this data source to retrieve information about Git credentials in Octopus Deploy. --- # octopusdeploy_git_credentials (Data Source) -Provides information about existing GitCredentials. +Use this data source to retrieve information about Git credentials in Octopus Deploy. @@ -17,26 +17,26 @@ Provides information about existing GitCredentials. ### Optional -- `name` (String) A filter to search by name. -- `skip` (Number) A filter to specify the number of items to skip in the response. -- `space_id` (String) The space ID associated with this resource. -- `take` (Number) A filter to specify the number of items to take (or return) in the response. +- `name` (String) The name of the Git Credential to filter by. +- `skip` (Number) The number of records to skip. +- `space_id` (String) The space ID associated with this Git Credential. +- `take` (Number) The number of records to take. ### Read-Only -- `git_credentials` (Block List) A list of Git Credentials that match the filter(s). (see [below for nested schema](#nestedblock--git_credentials)) -- `id` (String) The ID of this resource. +- `git_credentials` (Attributes List) Provides information about existing GitCredentials. (see [below for nested schema](#nestedatt--git_credentials)) +- `id` (String) The unique ID for this resource. - + ### Nested Schema for `git_credentials` Read-Only: -- `description` (String) The description of this Git credential. +- `description` (String) The description of this Git Credential. - `id` (String) The unique ID for this resource. -- `name` (String) The name of the Git credential. This name must be unique. +- `name` (String) The name of this Git Credential. - `password` (String, Sensitive) The password for the Git credential. -- `space_id` (String) The space ID associated with this resource. +- `space_id` (String) The space ID associated with this Git Credential. - `type` (String) The Git credential authentication type. - `username` (String) The username for the Git credential. diff --git a/docs/data-sources/kubernetes_agent_deployment_targets.md b/docs/data-sources/kubernetes_agent_deployment_targets.md index 527adad4a..dd4c61884 100644 --- a/docs/data-sources/kubernetes_agent_deployment_targets.md +++ b/docs/data-sources/kubernetes_agent_deployment_targets.md @@ -55,32 +55,32 @@ data "octopusdeploy_kubernetes_agent_deployment_targets" "kubernetes_agent_deplo ### Read-Only - `id` (String) An auto-generated identifier that includes the timestamp when this data source was last modified. -- `kubernetes_agent_deployment_targets` (Block List) A list of kubernetes agent deployment targets that match the filter(s). (see [below for nested schema](#nestedblock--kubernetes_agent_deployment_targets)) +- `kubernetes_agent_deployment_targets` (List of Object) A list of kubernetes agent deployment targets that match the filter(s). (see [below for nested schema](#nestedatt--kubernetes_agent_deployment_targets)) - + ### Nested Schema for `kubernetes_agent_deployment_targets` Read-Only: -- `agent_helm_release_name` (String) Name of the Helm release that the agent belongs to. -- `agent_kubernetes_namespace` (String) Name of the Kubernetes namespace where the agent is installed. -- `agent_tentacle_version` (String) Current Tentacle version of the agent -- `agent_upgrade_status` (String) Current upgrade availability status of the agent. One of 'NoUpgrades', 'UpgradeAvailable', 'UpgradeSuggested', 'UpgradeRequired' -- `agent_version` (String) Current Helm chart version of the agent. -- `communication_mode` (String) The communication mode used by the Kubernetes agent to communicate with Octopus Server. Currently, the only supported value is 'Polling'. -- `default_namespace` (String) Optional default namespace that will be used when using Kubernetes deployment steps, can be overrides within step configurations. -- `environments` (List of String) A list of environment IDs this Kubernetes agent can deploy to. -- `id` (String) The unique ID for this resource. -- `is_disabled` (Boolean) Whether the Kubernetes agent is disabled. If the agent is disabled, it will not be included in any deployments. -- `machine_policy_id` (String) Optional ID of the machine policy that the Kubernetes agent will use. If not provided the default machine policy will be used. -- `name` (String) The name of this resource. -- `roles` (List of String) A list of target roles that are associated to this Kubernetes agent. -- `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. -- `thumbprint` (String) The thumbprint of the Kubernetes agent's certificate used by server to verify the identity of the agent. This is the same thumbprint that was used when installing the agent. -- `upgrade_locked` (Boolean) If enabled the Kubernetes agent will not automatically upgrade and will stay on the currently installed version, even if the associated machine policy is configured to automatically upgrade. -- `uri` (String) The URI of the Kubernetes agent's used by the server to queue messages. This is the same subscription uri that was used when installing the agent. +- `agent_helm_release_name` (String) +- `agent_kubernetes_namespace` (String) +- `agent_tentacle_version` (String) +- `agent_upgrade_status` (String) +- `agent_version` (String) +- `communication_mode` (String) +- `default_namespace` (String) +- `environments` (List of String) +- `id` (String) +- `is_disabled` (Boolean) +- `machine_policy_id` (String) +- `name` (String) +- `roles` (List of String) +- `space_id` (String) +- `tenant_tags` (List of String) +- `tenanted_deployment_participation` (String) +- `tenants` (List of String) +- `thumbprint` (String) +- `upgrade_locked` (Boolean) +- `uri` (String) diff --git a/docs/data-sources/kubernetes_cluster_deployment_targets.md b/docs/data-sources/kubernetes_cluster_deployment_targets.md index 4bf178018..7b7da7741 100644 --- a/docs/data-sources/kubernetes_cluster_deployment_targets.md +++ b/docs/data-sources/kubernetes_cluster_deployment_targets.md @@ -36,52 +36,52 @@ Provides information about existing Kubernetes cluster deployment targets. ### Read-Only - `id` (String) An auto-generated identifier that includes the timestamp when this data source was last modified. -- `kubernetes_cluster_deployment_targets` (Block List) A list of Kubernetes cluster deployment targets that match the filter(s). (see [below for nested schema](#nestedblock--kubernetes_cluster_deployment_targets)) +- `kubernetes_cluster_deployment_targets` (List of Object) A list of Kubernetes cluster deployment targets that match the filter(s). (see [below for nested schema](#nestedatt--kubernetes_cluster_deployment_targets)) - + ### Nested Schema for `kubernetes_cluster_deployment_targets` Read-Only: -- `authentication` (List of Object) (see [below for nested schema](#nestedatt--kubernetes_cluster_deployment_targets--authentication)) -- `aws_account_authentication` (List of Object) (see [below for nested schema](#nestedatt--kubernetes_cluster_deployment_targets--aws_account_authentication)) -- `azure_service_principal_authentication` (List of Object) (see [below for nested schema](#nestedatt--kubernetes_cluster_deployment_targets--azure_service_principal_authentication)) -- `certificate_authentication` (List of Object) (see [below for nested schema](#nestedatt--kubernetes_cluster_deployment_targets--certificate_authentication)) +- `authentication` (List of Object) (see [below for nested schema](#nestedobjatt--kubernetes_cluster_deployment_targets--authentication)) +- `aws_account_authentication` (List of Object) (see [below for nested schema](#nestedobjatt--kubernetes_cluster_deployment_targets--aws_account_authentication)) +- `azure_service_principal_authentication` (List of Object) (see [below for nested schema](#nestedobjatt--kubernetes_cluster_deployment_targets--azure_service_principal_authentication)) +- `certificate_authentication` (List of Object) (see [below for nested schema](#nestedobjatt--kubernetes_cluster_deployment_targets--certificate_authentication)) - `cluster_certificate` (String) - `cluster_certificate_path` (String) - `cluster_url` (String) -- `container` (List of Object) (see [below for nested schema](#nestedatt--kubernetes_cluster_deployment_targets--container)) +- `container` (List of Object) (see [below for nested schema](#nestedobjatt--kubernetes_cluster_deployment_targets--container)) - `container_options` (String) - `default_worker_pool_id` (String) -- `endpoint` (List of Object) (see [below for nested schema](#nestedatt--kubernetes_cluster_deployment_targets--endpoint)) -- `environments` (List of String) A list of environment IDs associated with this resource. -- `gcp_account_authentication` (List of Object) (see [below for nested schema](#nestedatt--kubernetes_cluster_deployment_targets--gcp_account_authentication)) +- `endpoint` (List of Object) (see [below for nested schema](#nestedobjatt--kubernetes_cluster_deployment_targets--endpoint)) +- `environments` (List of String) +- `gcp_account_authentication` (List of Object) (see [below for nested schema](#nestedobjatt--kubernetes_cluster_deployment_targets--gcp_account_authentication)) - `has_latest_calamari` (Boolean) -- `health_status` (String) Represents the health status of this deployment target. Valid health statuses are `HasWarnings`, `Healthy`, `Unavailable`, `Unhealthy`, or `Unknown`. -- `id` (String) The unique ID for this resource. +- `health_status` (String) +- `id` (String) - `is_disabled` (Boolean) - `is_in_process` (Boolean) - `machine_policy_id` (String) -- `name` (String) The name of this resource. +- `name` (String) - `namespace` (String) - `operating_system` (String) -- `pod_authentication` (List of Object) (see [below for nested schema](#nestedatt--kubernetes_cluster_deployment_targets--pod_authentication)) +- `pod_authentication` (List of Object) (see [below for nested schema](#nestedobjatt--kubernetes_cluster_deployment_targets--pod_authentication)) - `proxy_id` (String) - `roles` (List of String) - `running_in_container` (Boolean) - `shell_name` (String) - `shell_version` (String) - `skip_tls_verification` (Boolean) -- `space_id` (String) The space ID associated with this resource. -- `status` (String) The status of this resource. Valid statuses are `CalamariNeedsUpgrade`, `Disabled`, `NeedsUpgrade`, `Offline`, `Online`, or `Unknown`. -- `status_summary` (String) A summary elaborating on the status of 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. +- `space_id` (String) +- `status` (String) +- `status_summary` (String) +- `tenant_tags` (List of String) +- `tenanted_deployment_participation` (String) +- `tenants` (List of String) - `thumbprint` (String) - `uri` (String) - + ### Nested Schema for `kubernetes_cluster_deployment_targets.authentication` Read-Only: @@ -89,7 +89,7 @@ Read-Only: - `account_id` (String) - + ### Nested Schema for `kubernetes_cluster_deployment_targets.aws_account_authentication` Read-Only: @@ -104,7 +104,7 @@ Read-Only: - `use_instance_role` (Boolean) - + ### Nested Schema for `kubernetes_cluster_deployment_targets.azure_service_principal_authentication` Read-Only: @@ -114,7 +114,7 @@ Read-Only: - `cluster_resource_group` (String) - + ### Nested Schema for `kubernetes_cluster_deployment_targets.certificate_authentication` Read-Only: @@ -122,7 +122,7 @@ Read-Only: - `client_certificate` (String) - + ### Nested Schema for `kubernetes_cluster_deployment_targets.container` Read-Only: @@ -131,7 +131,7 @@ Read-Only: - `image` (String) - + ### Nested Schema for `kubernetes_cluster_deployment_targets.endpoint` Read-Only: @@ -235,7 +235,7 @@ Read-Only: - + ### Nested Schema for `kubernetes_cluster_deployment_targets.gcp_account_authentication` Read-Only: @@ -250,7 +250,7 @@ Read-Only: - `zone` (String) - + ### Nested Schema for `kubernetes_cluster_deployment_targets.pod_authentication` Read-Only: diff --git a/docs/data-sources/library_variable_sets.md b/docs/data-sources/library_variable_sets.md index 8b6eeb19f..9b10353eb 100644 --- a/docs/data-sources/library_variable_sets.md +++ b/docs/data-sources/library_variable_sets.md @@ -18,34 +18,34 @@ Provides information about existing library variable sets. ### Optional - `content_type` (String) A filter to search by content type. -- `id` (String) The unique ID for this resource. - `ids` (List of String) A filter to search by a list of IDs. -- `library_variable_sets` (Block List) A list of library variable sets that match the filter(s). (see [below for nested schema](#nestedblock--library_variable_sets)) - `partial_name` (String) A filter to search by a partial name. - `skip` (Number) A filter to specify the number of items to skip in the response. - `space_id` (String) The space ID associated with this library variable set. - `take` (Number) A filter to specify the number of items to take (or return) in the response. +### Read-Only + +- `id` (String) The unique ID for this resource. +- `library_variable_sets` (Block List) A list of library variable sets that match the filter(s). (see [below for nested schema](#nestedblock--library_variable_sets)) + ### Nested Schema for `library_variable_sets` -Optional: +Read-Only: - `description` (String) The description of this library variable set. - `id` (String) The unique ID for this resource. - `name` (String) The name of this resource. - `space_id` (String) The space ID associated with this library variable set. - `template` (List of Object) (see [below for nested schema](#nestedatt--library_variable_sets--template)) - -Read-Only: - - `template_ids` (Map of String) - `variable_set_id` (String) ### Nested Schema for `library_variable_sets.template` -Optional: +Read-Only: - `default_value` (String) - `display_settings` (Map of String) diff --git a/docs/data-sources/listening_tentacle_deployment_targets.md b/docs/data-sources/listening_tentacle_deployment_targets.md index e8f7ccb8f..c90c679f0 100644 --- a/docs/data-sources/listening_tentacle_deployment_targets.md +++ b/docs/data-sources/listening_tentacle_deployment_targets.md @@ -55,39 +55,39 @@ data "octopusdeploy_listening_tentacle_deployment_targets" "listening_tentacle_d ### Read-Only - `id` (String) An auto-generated identifier that includes the timestamp when this data source was last modified. -- `listening_tentacle_deployment_targets` (Block List) A list of listening tentacle deployment targets that match the filter(s). (see [below for nested schema](#nestedblock--listening_tentacle_deployment_targets)) +- `listening_tentacle_deployment_targets` (List of Object) A list of listening tentacle deployment targets that match the filter(s). (see [below for nested schema](#nestedatt--listening_tentacle_deployment_targets)) - + ### Nested Schema for `listening_tentacle_deployment_targets` Read-Only: - `certificate_signature_algorithm` (String) -- `environments` (List of String) A list of environment IDs associated with this listening tentacle. +- `environments` (List of String) - `has_latest_calamari` (Boolean) -- `health_status` (String) Represents the health status of this deployment target. Valid health statuses are `HasWarnings`, `Healthy`, `Unavailable`, `Unhealthy`, or `Unknown`. -- `id` (String) The unique ID for this resource. -- `is_disabled` (Boolean) Represents the disabled status of this deployment target. -- `is_in_process` (Boolean) Represents the in-process status of this deployment target. -- `machine_policy_id` (String) The machine policy ID that is associated with this deployment target. -- `name` (String) The name of this resource. -- `operating_system` (String) The operating system that is associated with this deployment target. -- `proxy_id` (String) The proxy ID that is associated with this deployment target. -- `roles` (List of String) A list of role IDs that are associated with this deployment target. -- `shell_name` (String) The shell name associated with this deployment target. -- `shell_version` (String) The shell version associated with this deployment target. -- `space_id` (String) The space ID associated with this resource. -- `status` (String) The status of this resource. Valid statuses are `CalamariNeedsUpgrade`, `Disabled`, `NeedsUpgrade`, `Offline`, `Online`, or `Unknown`. -- `status_summary` (String) A summary elaborating on the status of 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. -- `tentacle_url` (String) The tenant URL of this deployment target. -- `tentacle_version_details` (List of Object) (see [below for nested schema](#nestedatt--listening_tentacle_deployment_targets--tentacle_version_details)) -- `thumbprint` (String) The thumbprint of this deployment target. -- `uri` (String) The URI of this deployment target. - - +- `health_status` (String) +- `id` (String) +- `is_disabled` (Boolean) +- `is_in_process` (Boolean) +- `machine_policy_id` (String) +- `name` (String) +- `operating_system` (String) +- `proxy_id` (String) +- `roles` (List of String) +- `shell_name` (String) +- `shell_version` (String) +- `space_id` (String) +- `status` (String) +- `status_summary` (String) +- `tenant_tags` (List of String) +- `tenanted_deployment_participation` (String) +- `tenants` (List of String) +- `tentacle_url` (String) +- `tentacle_version_details` (List of Object) (see [below for nested schema](#nestedobjatt--listening_tentacle_deployment_targets--tentacle_version_details)) +- `thumbprint` (String) +- `uri` (String) + + ### Nested Schema for `listening_tentacle_deployment_targets.tentacle_version_details` Read-Only: diff --git a/docs/data-sources/machine_policies.md b/docs/data-sources/machine_policies.md index 68e70b324..614a9a02e 100644 --- a/docs/data-sources/machine_policies.md +++ b/docs/data-sources/machine_policies.md @@ -26,29 +26,29 @@ Provides information about existing machine policies. ### Read-Only - `id` (String) The ID of this resource. -- `machine_policies` (Block List) A list of machine policies that match the filter(s). (see [below for nested schema](#nestedblock--machine_policies)) +- `machine_policies` (List of Object) A list of machine policies that match the filter(s). (see [below for nested schema](#nestedatt--machine_policies)) - + ### Nested Schema for `machine_policies` Read-Only: -- `connection_connect_timeout` (Number) In nanoseconds. Minimum value: 10000000000 (10 seconds). +- `connection_connect_timeout` (Number) - `connection_retry_count_limit` (Number) -- `connection_retry_sleep_interval` (Number) In nanoseconds. -- `connection_retry_time_limit` (Number) In nanoseconds. -- `description` (String) The description of this machine policy. -- `id` (String) The unique ID for this resource. +- `connection_retry_sleep_interval` (Number) +- `connection_retry_time_limit` (Number) +- `description` (String) +- `id` (String) - `is_default` (Boolean) -- `machine_cleanup_policy` (Set of Object) (see [below for nested schema](#nestedatt--machine_policies--machine_cleanup_policy)) -- `machine_connectivity_policy` (Set of Object) (see [below for nested schema](#nestedatt--machine_policies--machine_connectivity_policy)) -- `machine_health_check_policy` (Set of Object) (see [below for nested schema](#nestedatt--machine_policies--machine_health_check_policy)) -- `machine_update_policy` (Set of Object) (see [below for nested schema](#nestedatt--machine_policies--machine_update_policy)) -- `name` (String) The name of this resource. -- `polling_request_queue_timeout` (Number) In nanoseconds. -- `space_id` (String) The space ID associated with this resource. - - +- `machine_cleanup_policy` (Set of Object) (see [below for nested schema](#nestedobjatt--machine_policies--machine_cleanup_policy)) +- `machine_connectivity_policy` (Set of Object) (see [below for nested schema](#nestedobjatt--machine_policies--machine_connectivity_policy)) +- `machine_health_check_policy` (Set of Object) (see [below for nested schema](#nestedobjatt--machine_policies--machine_health_check_policy)) +- `machine_update_policy` (Set of Object) (see [below for nested schema](#nestedobjatt--machine_policies--machine_update_policy)) +- `name` (String) +- `polling_request_queue_timeout` (Number) +- `space_id` (String) + + ### Nested Schema for `machine_policies.machine_cleanup_policy` Read-Only: @@ -57,7 +57,7 @@ Read-Only: - `delete_machines_elapsed_timespan` (Number) - + ### Nested Schema for `machine_policies.machine_connectivity_policy` Read-Only: @@ -65,7 +65,7 @@ Read-Only: - `machine_connectivity_behavior` (String) - + ### Nested Schema for `machine_policies.machine_health_check_policy` Read-Only: @@ -96,7 +96,7 @@ Read-Only: - + ### Nested Schema for `machine_policies.machine_update_policy` Read-Only: diff --git a/docs/data-sources/offline_package_drop_deployment_targets.md b/docs/data-sources/offline_package_drop_deployment_targets.md index fc01cb4ad..193982d1e 100644 --- a/docs/data-sources/offline_package_drop_deployment_targets.md +++ b/docs/data-sources/offline_package_drop_deployment_targets.md @@ -36,39 +36,39 @@ Provides information about existing offline package drop deployment targets. ### Read-Only - `id` (String) An auto-generated identifier that includes the timestamp when this data source was last modified. -- `offline_package_drop_deployment_targets` (Block List) A list of offline package drop deployment targets that match the filter(s). (see [below for nested schema](#nestedblock--offline_package_drop_deployment_targets)) +- `offline_package_drop_deployment_targets` (List of Object) A list of offline package drop deployment targets that match the filter(s). (see [below for nested schema](#nestedatt--offline_package_drop_deployment_targets)) - + ### Nested Schema for `offline_package_drop_deployment_targets` Read-Only: - `applications_directory` (String) -- `destination` (List of Object) (see [below for nested schema](#nestedatt--offline_package_drop_deployment_targets--destination)) -- `endpoint` (List of Object) (see [below for nested schema](#nestedatt--offline_package_drop_deployment_targets--endpoint)) -- `environments` (List of String) A list of environment IDs associated with this resource. +- `destination` (List of Object) (see [below for nested schema](#nestedobjatt--offline_package_drop_deployment_targets--destination)) +- `endpoint` (List of Object) (see [below for nested schema](#nestedobjatt--offline_package_drop_deployment_targets--endpoint)) +- `environments` (List of String) - `has_latest_calamari` (Boolean) -- `health_status` (String) Represents the health status of this deployment target. Valid health statuses are `HasWarnings`, `Healthy`, `Unavailable`, `Unhealthy`, or `Unknown`. -- `id` (String) The unique ID for this resource. +- `health_status` (String) +- `id` (String) - `is_disabled` (Boolean) - `is_in_process` (Boolean) - `machine_policy_id` (String) -- `name` (String) The name of this resource. +- `name` (String) - `operating_system` (String) - `roles` (List of String) - `shell_name` (String) - `shell_version` (String) -- `space_id` (String) The space ID associated with this resource. -- `status` (String) The status of this resource. Valid statuses are `CalamariNeedsUpgrade`, `Disabled`, `NeedsUpgrade`, `Offline`, `Online`, or `Unknown`. -- `status_summary` (String) A summary elaborating on the status of 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. +- `space_id` (String) +- `status` (String) +- `status_summary` (String) +- `tenant_tags` (List of String) +- `tenanted_deployment_participation` (String) +- `tenants` (List of String) - `thumbprint` (String) - `uri` (String) - `working_directory` (String) - + ### Nested Schema for `offline_package_drop_deployment_targets.destination` Read-Only: @@ -77,7 +77,7 @@ Read-Only: - `drop_folder_path` (String) - + ### Nested Schema for `offline_package_drop_deployment_targets.endpoint` Read-Only: diff --git a/docs/data-sources/polling_tentacle_deployment_targets.md b/docs/data-sources/polling_tentacle_deployment_targets.md index 5124fddfc..b28cdfddf 100644 --- a/docs/data-sources/polling_tentacle_deployment_targets.md +++ b/docs/data-sources/polling_tentacle_deployment_targets.md @@ -36,39 +36,39 @@ Provides information about existing polling tentacle deployment targets. ### Read-Only - `id` (String) An auto-generated identifier that includes the timestamp when this data source was last modified. -- `polling_tentacle_deployment_targets` (Block List) A list of polling tentacle deployment targets that match the filter(s). (see [below for nested schema](#nestedblock--polling_tentacle_deployment_targets)) +- `polling_tentacle_deployment_targets` (List of Object) A list of polling tentacle deployment targets that match the filter(s). (see [below for nested schema](#nestedatt--polling_tentacle_deployment_targets)) - + ### Nested Schema for `polling_tentacle_deployment_targets` Read-Only: - `certificate_signature_algorithm` (String) -- `endpoint` (List of Object) (see [below for nested schema](#nestedatt--polling_tentacle_deployment_targets--endpoint)) -- `environments` (List of String) A list of environment IDs associated with this resource. +- `endpoint` (List of Object) (see [below for nested schema](#nestedobjatt--polling_tentacle_deployment_targets--endpoint)) +- `environments` (List of String) - `has_latest_calamari` (Boolean) -- `health_status` (String) Represents the health status of this deployment target. Valid health statuses are `HasWarnings`, `Healthy`, `Unavailable`, `Unhealthy`, or `Unknown`. -- `id` (String) The unique ID for this resource. +- `health_status` (String) +- `id` (String) - `is_disabled` (Boolean) - `is_in_process` (Boolean) - `machine_policy_id` (String) -- `name` (String) The name of this resource. +- `name` (String) - `operating_system` (String) - `roles` (List of String) - `shell_name` (String) - `shell_version` (String) -- `space_id` (String) The space ID associated with this resource. -- `status` (String) The status of this resource. Valid statuses are `CalamariNeedsUpgrade`, `Disabled`, `NeedsUpgrade`, `Offline`, `Online`, or `Unknown`. -- `status_summary` (String) A summary elaborating on the status of 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. +- `space_id` (String) +- `status` (String) +- `status_summary` (String) +- `tenant_tags` (List of String) +- `tenanted_deployment_participation` (String) +- `tenants` (List of String) - `tentacle_url` (String) -- `tentacle_version_details` (List of Object) (see [below for nested schema](#nestedatt--polling_tentacle_deployment_targets--tentacle_version_details)) +- `tentacle_version_details` (List of Object) (see [below for nested schema](#nestedobjatt--polling_tentacle_deployment_targets--tentacle_version_details)) - `thumbprint` (String) - `uri` (String) - + ### Nested Schema for `polling_tentacle_deployment_targets.endpoint` Read-Only: @@ -172,7 +172,7 @@ Read-Only: - + ### Nested Schema for `polling_tentacle_deployment_targets.tentacle_version_details` Read-Only: diff --git a/docs/data-sources/project_groups.md b/docs/data-sources/project_groups.md index ed88f743a..ab1d0f4eb 100644 --- a/docs/data-sources/project_groups.md +++ b/docs/data-sources/project_groups.md @@ -3,12 +3,12 @@ page_title: "octopusdeploy_project_groups Data Source - terraform-provider-octopusdeploy" subcategory: "" description: |- - Provides information about existing project groups. + --- # octopusdeploy_project_groups (Data Source) -Provides information about existing project groups. + ## Example Usage @@ -27,25 +27,28 @@ data "octopusdeploy_project_groups" "example" { ### Optional - `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. +- `partial_name` (String) A filter to search by a partial name. +- `project_groups` (Block List) A list of project groups that match the filter(s). (see [below for nested schema](#nestedblock--project_groups)) - `skip` (Number) A filter to specify the number of items to skip in the response. -- `space_id` (String) A Space ID to filter by. Will revert what is specified on the provider if not set. +- `space_id` (String) The space ID associated with this project group. - `take` (Number) A filter to specify the number of items to take (or return) in the response. ### Read-Only -- `id` (String) An auto-generated identifier that includes the timestamp when this data source was last modified. -- `project_groups` (Block List) A list of project groups that match the filter(s). (see [below for nested schema](#nestedblock--project_groups)) +- `id` (String) The unique ID for this resource. ### Nested Schema for `project_groups` -Read-Only: +Optional: - `description` (String) The description of this project group. - `id` (String) The unique ID for this resource. -- `name` (String) The name of this resource. - `retention_policy_id` (String) The ID of the retention policy associated with this project group. - `space_id` (String) The space ID associated with this project group. +Read-Only: + +- `name` (String) The name of this resource. + diff --git a/docs/data-sources/script_modules b/docs/data-sources/script_modules index 6625186eb..6a12509e7 100644 --- a/docs/data-sources/script_modules +++ b/docs/data-sources/script_modules @@ -34,21 +34,21 @@ data "octopusdeploy_script_modules" "example" { ### Read-Only - `id` (String) An auto-generated identifier that includes the timestamp when this data source was last modified. -- `script_modules` (Block List) A list of script modules that match the filter(s). (see [below for nested schema](#nestedblock--script_modules)) +- `script_modules` (List of Object) A list of script modules that match the filter(s). (see [below for nested schema](#nestedatt--script_modules)) - + ### Nested Schema for `script_modules` Read-Only: -- `description` (String) The description of this script module. -- `id` (String) The unique ID for this resource. -- `name` (String) The name of this resource. -- `script` (Set of Object) The script associated with this script module. (see [below for nested schema](#nestedatt--script_modules--script)) -- `space_id` (String) The space ID associated with this resource. -- `variable_set_id` (String) The variable set ID for this script module. +- `description` (String) +- `id` (String) +- `name` (String) +- `script` (Set of Object) (see [below for nested schema](#nestedobjatt--script_modules--script)) +- `space_id` (String) +- `variable_set_id` (String) - + ### Nested Schema for `script_modules.script` Read-Only: diff --git a/docs/data-sources/script_modules.md b/docs/data-sources/script_modules.md index 283f804cc..35d7e16a8 100644 --- a/docs/data-sources/script_modules.md +++ b/docs/data-sources/script_modules.md @@ -35,21 +35,21 @@ data "octopusdeploy_script_modules" "example" { ### Read-Only - `id` (String) An auto-generated identifier that includes the timestamp when this data source was last modified. -- `script_modules` (Block List) A list of script modules that match the filter(s). (see [below for nested schema](#nestedblock--script_modules)) +- `script_modules` (List of Object) A list of script modules that match the filter(s). (see [below for nested schema](#nestedatt--script_modules)) - + ### Nested Schema for `script_modules` Read-Only: -- `description` (String) The description of this script module. -- `id` (String) The unique ID for this resource. -- `name` (String) The name of this resource. -- `script` (Set of Object) The script associated with this script module. (see [below for nested schema](#nestedatt--script_modules--script)) -- `space_id` (String) The space ID associated with this resource. -- `variable_set_id` (String) The variable set ID for this script module. +- `description` (String) +- `id` (String) +- `name` (String) +- `script` (Set of Object) (see [below for nested schema](#nestedobjatt--script_modules--script)) +- `space_id` (String) +- `variable_set_id` (String) - + ### Nested Schema for `script_modules.script` Read-Only: diff --git a/docs/data-sources/space.md b/docs/data-sources/space.md index 36bcc4c81..2ec59a7f5 100644 --- a/docs/data-sources/space.md +++ b/docs/data-sources/space.md @@ -25,7 +25,7 @@ Provides information about an existing space. - `id` (String) The unique ID for this resource. - `is_default` (Boolean) Specifies if this space is the default space in Octopus. - `is_task_queue_stopped` (Boolean) Specifies the status of the task queue for this space. -- `slug` (String) The unique slug of this space. +- `slug` (String) The unique slug of this space - `space_managers_team_members` (Set of String) A list of user IDs designated to be managers of this space. - `space_managers_teams` (Set of String) A list of team IDs designated to be managers of this space. diff --git a/docs/data-sources/spaces.md b/docs/data-sources/spaces.md index 36485b0a1..ff5ae2193 100644 --- a/docs/data-sources/spaces.md +++ b/docs/data-sources/spaces.md @@ -3,12 +3,12 @@ page_title: "octopusdeploy_spaces Data Source - terraform-provider-octopusdeploy" subcategory: "" description: |- - Provides information about existing spaces. + --- # octopusdeploy_spaces (Data Source) -Provides information about existing spaces. + ## Example Usage @@ -27,26 +27,29 @@ data "octopusdeploy_spaces" "spaces" { ### Optional - `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. +- `partial_name` (String) A filter to search by a partial name. - `skip` (Number) A filter to specify the number of items to skip in the response. +- `spaces` (Block List) Provides information about existing spaces. (see [below for nested schema](#nestedblock--spaces)) - `take` (Number) A filter to specify the number of items to take (or return) in the response. ### Read-Only -- `id` (String) An auto-generated identifier that includes the timestamp when this data source was last modified. -- `spaces` (Block List) A list of spaces that match the filter(s). (see [below for nested schema](#nestedblock--spaces)) +- `id` (String) The unique ID for this resource. ### Nested Schema for `spaces` +Required: + +- `name` (String) The name of this resource, no more than 20 characters long + Read-Only: - `description` (String) The description of this space. - `id` (String) The unique ID for this resource. - `is_default` (Boolean) Specifies if this space is the default space in Octopus. - `is_task_queue_stopped` (Boolean) Specifies the status of the task queue for this space. -- `name` (String) The name of this resource, no more than 20 characters long -- `slug` (String) The unique slug of this space. +- `slug` (String) The unique slug of this space - `space_managers_team_members` (Set of String) A list of user IDs designated to be managers of this space. - `space_managers_teams` (Set of String) A list of team IDs designated to be managers of this space. diff --git a/docs/data-sources/ssh_connection_deployment_targets.md b/docs/data-sources/ssh_connection_deployment_targets.md index 43e054adb..4c0d32f1c 100644 --- a/docs/data-sources/ssh_connection_deployment_targets.md +++ b/docs/data-sources/ssh_connection_deployment_targets.md @@ -36,42 +36,42 @@ Provides information about existing SSH connection deployment targets. ### Read-Only - `id` (String) An auto-generated identifier that includes the timestamp when this data source was last modified. -- `ssh_connection_deployment_targets` (Block List) A list of SSH connection deployment targets that match the filter(s). (see [below for nested schema](#nestedblock--ssh_connection_deployment_targets)) +- `ssh_connection_deployment_targets` (List of Object) A list of SSH connection deployment targets that match the filter(s). (see [below for nested schema](#nestedatt--ssh_connection_deployment_targets)) - + ### Nested Schema for `ssh_connection_deployment_targets` Read-Only: - `account_id` (String) - `dot_net_core_platform` (String) -- `endpoint` (List of Object) (see [below for nested schema](#nestedatt--ssh_connection_deployment_targets--endpoint)) -- `environments` (List of String) A list of environment IDs associated with this resource. +- `endpoint` (List of Object) (see [below for nested schema](#nestedobjatt--ssh_connection_deployment_targets--endpoint)) +- `environments` (List of String) - `fingerprint` (String) - `has_latest_calamari` (Boolean) -- `health_status` (String) Represents the health status of this deployment target. Valid health statuses are `HasWarnings`, `Healthy`, `Unavailable`, `Unhealthy`, or `Unknown`. +- `health_status` (String) - `host` (String) -- `id` (String) The unique ID for this resource. +- `id` (String) - `is_disabled` (Boolean) - `is_in_process` (Boolean) - `machine_policy_id` (String) -- `name` (String) The name of this resource. +- `name` (String) - `operating_system` (String) - `port` (Number) - `proxy_id` (String) - `roles` (List of String) - `shell_name` (String) - `shell_version` (String) -- `space_id` (String) The space ID associated with this resource. -- `status` (String) The status of this resource. Valid statuses are `CalamariNeedsUpgrade`, `Disabled`, `NeedsUpgrade`, `Offline`, `Online`, or `Unknown`. -- `status_summary` (String) A summary elaborating on the status of 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. +- `space_id` (String) +- `status` (String) +- `status_summary` (String) +- `tenant_tags` (List of String) +- `tenanted_deployment_participation` (String) +- `tenants` (List of String) - `thumbprint` (String) - `uri` (String) - + ### Nested Schema for `ssh_connection_deployment_targets.endpoint` Read-Only: diff --git a/docs/data-sources/tag_sets.md b/docs/data-sources/tag_sets.md index fb8fffe78..56b41b61b 100644 --- a/docs/data-sources/tag_sets.md +++ b/docs/data-sources/tag_sets.md @@ -26,17 +26,17 @@ Provides information about existing tag sets. ### Read-Only - `id` (String) The ID of this resource. -- `tag_sets` (Block List) A list of tag sets that match the filter(s). (see [below for nested schema](#nestedblock--tag_sets)) +- `tag_sets` (List of Object) A list of tag sets that match the filter(s). (see [below for nested schema](#nestedatt--tag_sets)) - + ### Nested Schema for `tag_sets` Read-Only: -- `description` (String) The description of this tag set. -- `id` (String) The unique ID for this resource. -- `name` (String) The name of this resource. -- `sort_order` (Number) The sort order associated with this resource. -- `space_id` (String) The space ID associated with this resource. +- `description` (String) +- `id` (String) +- `name` (String) +- `sort_order` (Number) +- `space_id` (String) diff --git a/docs/data-sources/teams.md b/docs/data-sources/teams.md index ba635c2e0..1075d7295 100644 --- a/docs/data-sources/teams.md +++ b/docs/data-sources/teams.md @@ -27,9 +27,9 @@ Provides information about existing users. ### Read-Only - `id` (String) An auto-generated identifier that includes the timestamp when this data source was last modified. -- `teams` (Block List) A list of teams that match the filter(s). (see [below for nested schema](#nestedblock--teams)) +- `teams` (List of Object) A list of teams that match the filter(s). (see [below for nested schema](#nestedatt--teams)) - + ### Nested Schema for `teams` Read-Only: @@ -38,14 +38,14 @@ Read-Only: - `can_be_renamed` (Boolean) - `can_change_members` (Boolean) - `can_change_roles` (Boolean) -- `description` (String) The user-friendly description of this team. -- `external_security_group` (List of Object) (see [below for nested schema](#nestedatt--teams--external_security_group)) -- `id` (String) The unique ID for this resource. -- `name` (String) The name of this team. -- `space_id` (String) The space associated with this team. -- `users` (Set of String) A list of user IDs designated to be members of this team. - - +- `description` (String) +- `external_security_group` (List of Object) (see [below for nested schema](#nestedobjatt--teams--external_security_group)) +- `id` (String) +- `name` (String) +- `space_id` (String) +- `users` (Set of String) + + ### Nested Schema for `teams.external_security_group` Read-Only: diff --git a/docs/data-sources/tenants.md b/docs/data-sources/tenants.md index 28d093d17..7b6486318 100644 --- a/docs/data-sources/tenants.md +++ b/docs/data-sources/tenants.md @@ -21,16 +21,16 @@ Provides information about existing tenants. - `ids` (List of String) A filter to search by a list of IDs. - `is_clone` (Boolean) A filter to search for cloned resources. - `name` (String) A filter to search by name. -- `partial_name` (String) A filter to search by the partial match of a name. +- `partial_name` (String) A filter to search by a partial name. - `project_id` (String) A filter to search by a project ID. - `skip` (Number) A filter to specify the number of items to skip in the response. -- `space_id` (String) A Space ID to filter by. Will revert what is specified on the provider if not set. +- `space_id` (String) The space ID associated with this tenants. - `tags` (List of String) A filter to search by a list of tags. - `take` (Number) A filter to specify the number of items to take (or return) in the response. ### Read-Only -- `id` (String) An auto-generated identifier that includes the timestamp when this data source was last modified. +- `id` (String) The unique ID for this resource. - `tenants` (Block List) A list of tenants that match the filter(s). (see [below for nested schema](#nestedblock--tenants)) @@ -39,10 +39,10 @@ Provides information about existing tenants. Read-Only: - `cloned_from_tenant_id` (String) The ID of the tenant from which this tenant was cloned. -- `description` (String) The description of this tenant. +- `description` (String) The description of this tenants. - `id` (String) The unique ID for this resource. - `name` (String) The name of this resource. -- `space_id` (String) The space ID associated with this resource. +- `space_id` (String) The space ID associated with this tenant. - `tenant_tags` (List of String) A list of tenant tags associated with this resource. diff --git a/docs/data-sources/user_roles.md b/docs/data-sources/user_roles.md index 19010c518..8c6bc4544 100644 --- a/docs/data-sources/user_roles.md +++ b/docs/data-sources/user_roles.md @@ -35,19 +35,19 @@ data "octopusdeploy_user_roles" "example" { ### Read-Only - `id` (String) An auto-generated identifier that includes the timestamp when this data source was last modified. -- `user_roles` (Block List) A list of user roles that match the filter(s). (see [below for nested schema](#nestedblock--user_roles)) +- `user_roles` (List of Object) A list of user roles that match the filter(s). (see [below for nested schema](#nestedatt--user_roles)) - + ### Nested Schema for `user_roles` Read-Only: - `can_be_deleted` (Boolean) -- `description` (String) The description of this user role. +- `description` (String) - `granted_space_permissions` (List of String) - `granted_system_permissions` (List of String) -- `id` (String) The unique ID for this resource. -- `name` (String) The name of this resource. +- `id` (String) +- `name` (String) - `space_permission_descriptions` (List of String) - `supported_restrictions` (List of String) - `system_permission_descriptions` (List of String) diff --git a/docs/data-sources/users.md b/docs/data-sources/users.md index dd1a19994..ca9f963dd 100644 --- a/docs/data-sources/users.md +++ b/docs/data-sources/users.md @@ -34,25 +34,25 @@ data "octopusdeploy_users" "example" { ### Read-Only - `id` (String) An auto-generated identifier that includes the timestamp when this data source was last modified. -- `users` (Block List) A list of users that match the filter(s). (see [below for nested schema](#nestedblock--users)) +- `users` (List of Object) A list of users that match the filter(s). (see [below for nested schema](#nestedatt--users)) - + ### Nested Schema for `users` Read-Only: - `can_password_be_edited` (Boolean) -- `display_name` (String) The display name of this resource. -- `email_address` (String) The email address of this resource. -- `id` (String) The unique ID for this resource. -- `identity` (Set of Object) (see [below for nested schema](#nestedatt--users--identity)) +- `display_name` (String) +- `email_address` (String) +- `id` (String) +- `identity` (Set of Object) (see [below for nested schema](#nestedobjatt--users--identity)) - `is_active` (Boolean) - `is_requestor` (Boolean) - `is_service` (Boolean) -- `password` (String, Sensitive) The password associated with this resource. -- `username` (String, Sensitive) The username associated with this resource. +- `password` (String) +- `username` (String) - + ### Nested Schema for `users.identity` Read-Only: diff --git a/docs/data-sources/worker_pools.md b/docs/data-sources/worker_pools.md index 7c6636313..95c0cdf35 100644 --- a/docs/data-sources/worker_pools.md +++ b/docs/data-sources/worker_pools.md @@ -27,20 +27,20 @@ Provides information about existing worker pools. ### Read-Only - `id` (String) The ID of this resource. -- `worker_pools` (Block List) A list of worker pools that match the filter(s). (see [below for nested schema](#nestedblock--worker_pools)) +- `worker_pools` (List of Object) A list of worker pools that match the filter(s). (see [below for nested schema](#nestedatt--worker_pools)) - + ### Nested Schema for `worker_pools` Read-Only: - `can_add_workers` (Boolean) -- `description` (String) The description of this worker pool. -- `id` (String) The unique ID for this resource. +- `description` (String) +- `id` (String) - `is_default` (Boolean) -- `name` (String) The name of this resource. -- `sort_order` (Number) The order number to sort a dynamic worker pool. -- `space_id` (String) The space ID associated with this resource. +- `name` (String) +- `sort_order` (Number) +- `space_id` (String) - `worker_pool_type` (String) - `worker_type` (String) diff --git a/docs/index.md b/docs/index.md index 9787f53dc..e0fadfb5f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -79,11 +79,8 @@ resource "octopusdeploy_environment" "Env3" { ## Schema -### Required +### Optional - `address` (String) The endpoint of the Octopus REST API - `api_key` (String) The API key to use with the Octopus REST API - -### Optional - - `space_id` (String) The space ID to target \ No newline at end of file diff --git a/docs/resources/azure_subscription_account.md b/docs/resources/azure_subscription_account.md index a7025e91a..7f11a8822 100644 --- a/docs/resources/azure_subscription_account.md +++ b/docs/resources/azure_subscription_account.md @@ -22,9 +22,7 @@ resource "octopusdeploy_azure_subscription_account" "example" { ### Required -- `management_endpoint` (String) - `name` (String) The name of this resource. -- `storage_endpoint_suffix` (String) The storage endpoint suffix associated with this Azure subscription account. - `subscription_id` (String) The subscription ID of this resource. ### Optional @@ -34,7 +32,9 @@ resource "octopusdeploy_azure_subscription_account" "example" { - `certificate_thumbprint` (String, Sensitive) - `description` (String) The description of this Azure subscription account. - `environments` (List of String) A list of environment IDs associated with this resource. +- `management_endpoint` (String) - `space_id` (String) The space ID associated with this resource. +- `storage_endpoint_suffix` (String) The storage endpoint suffix associated with this Azure subscription account. - `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. diff --git a/docs/resources/environment.md b/docs/resources/environment.md index e8a5c7438..c9f9c5d1e 100644 --- a/docs/resources/environment.md +++ b/docs/resources/environment.md @@ -3,12 +3,12 @@ page_title: "octopusdeploy_environment Resource - terraform-provider-octopusdeploy" subcategory: "" description: |- - This resource manages environments in Octopus Deploy. + --- # octopusdeploy_environment (Resource) -This resource manages environments in Octopus Deploy. + ## Example Usage @@ -45,39 +45,36 @@ resource "octopusdeploy_environment" "example" { - `allow_dynamic_infrastructure` (Boolean) - `description` (String) The description of this environment. - `id` (String) The unique ID for this resource. -- `jira_extension_settings` (Block List, Max: 1) Provides extension settings for the Jira integration for this environment. (see [below for nested schema](#nestedblock--jira_extension_settings)) -- `jira_service_management_extension_settings` (Block List, Max: 1) Provides extension settings for the Jira Service Management (JSM) integration for this environment. (see [below for nested schema](#nestedblock--jira_service_management_extension_settings)) -- `servicenow_extension_settings` (Block List, Max: 1) Provides extension settings for the ServiceNow integration for this environment. (see [below for nested schema](#nestedblock--servicenow_extension_settings)) -- `sort_order` (Number) The order number to sort an environment. +- `jira_extension_settings` (Block List) Provides extension settings for the Jira integration for this environment. (see [below for nested schema](#nestedblock--jira_extension_settings)) +- `jira_service_management_extension_settings` (Block List) Provides extension settings for the Jira Service Management (JSM) integration for this environment. (see [below for nested schema](#nestedblock--jira_service_management_extension_settings)) +- `servicenow_extension_settings` (Block List) Provides extension settings for the ServiceNow integration for this environment. (see [below for nested schema](#nestedblock--servicenow_extension_settings)) +- `slug` (String) The unique slug of this environment +- `sort_order` (Number) The order number to sort an environment - `space_id` (String) The space ID associated with this environment. - `use_guided_failure` (Boolean) -### Read-Only - -- `slug` (String) - ### Nested Schema for `jira_extension_settings` -Required: +Optional: -- `environment_type` (String) The Jira environment type of this Octopus deployment environment. Valid values are `"development"`, `"production"`, `"staging"`, `"testing"`, or `"unmapped"`. +- `environment_type` (String) ### Nested Schema for `jira_service_management_extension_settings` -Required: +Optional: -- `is_enabled` (Boolean) Specifies whether or not this extension is enabled for this project. +- `is_enabled` (Boolean) ### Nested Schema for `servicenow_extension_settings` -Required: +Optional: -- `is_enabled` (Boolean) Specifies whether or not this extension is enabled for this project. +- `is_enabled` (Boolean) ## Import diff --git a/docs/resources/lifecycle.md b/docs/resources/lifecycle.md index 8714ab06e..e5178b91e 100644 --- a/docs/resources/lifecycle.md +++ b/docs/resources/lifecycle.md @@ -33,8 +33,8 @@ resource "octopusdeploy_lifecycle" "example" { name = "foo" release_retention_policy { - quantity_to_keep = 0 - should_keep_forever = true // true only if quantity_to_keep = 0 + quantity_to_keep = 1 + should_keep_forever = true unit = "Days" } diff --git a/docs/resources/nuget_feed.md b/docs/resources/nuget_feed.md index b5758af66..4a736c70a 100644 --- a/docs/resources/nuget_feed.md +++ b/docs/resources/nuget_feed.md @@ -2,12 +2,12 @@ page_title: "octopusdeploy_nuget_feed Resource - terraform-provider-octopusdeploy" subcategory: "Feeds" description: |- - This resource manages a NuGet feed in Octopus Deploy. + This resource manages a Nuget feed in Octopus Deploy. --- # octopusdeploy_nuget_feed (Resource) -This resource manages a NuGet feed in Octopus Deploy. +This resource manages a Nuget feed in Octopus Deploy. ## Example Usage diff --git a/docs/resources/space.md b/docs/resources/space.md index 6ab45052e..f2b0f3084 100644 --- a/docs/resources/space.md +++ b/docs/resources/space.md @@ -28,7 +28,7 @@ resource "octopusdeploy_space" "example" { ### Required -- `name` (String) The name of this resource, no more than 20 characters long +- `name` (String) The name of this resource. ### Optional @@ -36,7 +36,7 @@ resource "octopusdeploy_space" "example" { - `id` (String) The unique ID for this resource. - `is_default` (Boolean) Specifies if this space is the default space in Octopus. - `is_task_queue_stopped` (Boolean) Specifies the status of the task queue for this space. -- `slug` (String) The unique slug of this space. +- `slug` (String) The unique slug of this space - `space_managers_team_members` (Set of String) A list of user IDs designated to be managers of this space. - `space_managers_teams` (Set of String) A list of team IDs designated to be managers of this space. diff --git a/docs/resources/tenant_common_variable.md b/docs/resources/tenant_common_variable.md index 4c1c54054..c306e3c59 100644 --- a/docs/resources/tenant_common_variable.md +++ b/docs/resources/tenant_common_variable.md @@ -3,12 +3,12 @@ page_title: "octopusdeploy_tenant_common_variable Resource - terraform-provider-octopusdeploy" subcategory: "" description: |- - This resource manages tenant common variables in Octopus Deploy. + Manages a tenant common variable in Octopus Deploy. --- # octopusdeploy_tenant_common_variable (Resource) -This resource manages tenant common variables in Octopus Deploy. +Manages a tenant common variable in Octopus Deploy. @@ -17,17 +17,14 @@ This resource manages tenant common variables in Octopus Deploy. ### Required -- `library_variable_set_id` (String) -- `template_id` (String) -- `tenant_id` (String) +- `library_variable_set_id` (String) The ID of the library variable set. +- `template_id` (String) The ID of the variable template. +- `tenant_id` (String) The ID of the tenant. ### Optional -- `space_id` (String) -- `value` (String, Sensitive) - -### Read-Only - -- `id` (String) The ID of this resource. +- `id` (String) The unique ID for this resource. +- `space_id` (String) The space ID associated with this Tenant Common Variable. +- `value` (String, Sensitive) The value of the variable. diff --git a/docs/resources/tenant_project.md b/docs/resources/tenant_project.md index f537e8fcc..2558f847b 100644 --- a/docs/resources/tenant_project.md +++ b/docs/resources/tenant_project.md @@ -3,12 +3,12 @@ page_title: "octopusdeploy_tenant_project Resource - terraform-provider-octopusdeploy" subcategory: "" description: |- - This resource represents the connection between tenants and projects. + --- # octopusdeploy_tenant_project (Resource) -This resource represents the connection between tenants and projects. + @@ -22,11 +22,8 @@ This resource represents the connection between tenants and projects. ### Optional -- `environment_ids` (List of String) The environment ID associated with this tenant. -- `space_id` (String) The space ID associated with this resource. - -### Read-Only - -- `id` (String) The ID of this resource. +- `environment_ids` (List of String) The environment IDs associated with this tenant. +- `id` (String) The unique ID for this resource. +- `space_id` (String) The space ID associated with this project tenant. diff --git a/go.mod b/go.mod index 4cc35d04a..f3ed22597 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.21 require ( github.com/OctopusDeploy/go-octopusdeploy/v2 v2.49.1 - github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework v0.0.0-20240725054341-2848f54d101e + 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 github.com/hashicorp/terraform-plugin-docs v0.13.0 diff --git a/go.sum b/go.sum index c917536b0..c0bdd145e 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,8 @@ github.com/OctopusDeploy/go-octodiff v1.0.0 h1:U+ORg6azniwwYo+O44giOw6TiD5USk8S4 github.com/OctopusDeploy/go-octodiff v1.0.0/go.mod h1:Mze0+EkOWTgTmi8++fyUc6r0aLZT7qD9gX+31t8MmIU= github.com/OctopusDeploy/go-octopusdeploy/v2 v2.49.1 h1:lzC3tcnfvC07Ilqn5J51qhOnW79kGD6nTIxCBOdmAe8= github.com/OctopusDeploy/go-octopusdeploy/v2 v2.49.1/go.mod h1:ggvOXzMnq+w0pLg6C9zdjz6YBaHfO3B3tqmmB7JQdaw= -github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework v0.0.0-20240725054341-2848f54d101e h1:FIvWa8wNg8IBG5uVhqkKvcBhaxx4TgN7T8/5Ed4VQUE= -github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework v0.0.0-20240725054341-2848f54d101e/go.mod h1:Oq9KbiRNDBB5jFmrwnrgLX0urIqR/1ptY18TzkqXm7M= +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= github.com/ProtonMail/go-crypto v1.1.0-alpha.2/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= diff --git a/octopusdeploy/data_source_tenants.go b/octopusdeploy/data_source_tenants.go deleted file mode 100644 index 4c9cfa569..000000000 --- a/octopusdeploy/data_source_tenants.go +++ /dev/null @@ -1,54 +0,0 @@ -package octopusdeploy - -import ( - "context" - "time" - - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client" - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tenants" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func dataSourceTenants() *schema.Resource { - return &schema.Resource{ - Description: "Provides information about existing tenants.", - ReadContext: dataSourceTenantsRead, - Schema: getTenantDataSchema(), - } -} - -func dataSourceTenantsRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - query := tenants.TenantsQuery{ - ClonedFromTenantID: d.Get("cloned_from_tenant_id").(string), - IDs: expandArray(d.Get("ids").([]interface{})), - IsClone: d.Get("is_clone").(bool), - Name: d.Get("name").(string), - PartialName: d.Get("partial_name").(string), - ProjectID: d.Get("project_id").(string), - Skip: d.Get("skip").(int), - Tags: expandArray(d.Get("tags").([]interface{})), - Take: d.Get("take").(int), - } - - spaceID := d.Get("space_id").(string) - - client := meta.(*client.Client) - existingTenants, err := tenants.Get(client, spaceID, query) - if err != nil { - return diag.FromErr(err) - } - - flattenedTenants := []interface{}{} - for _, tenant := range existingTenants.Items { - flattenedTenants = append(flattenedTenants, flattenTenant(tenant)) - } - - if err := d.Set("tenants", flattenedTenants); err != nil { - return diag.FromErr(err) - } - - d.SetId("Tenants " + time.Now().UTC().String()) - - return nil -} diff --git a/octopusdeploy/provider.go b/octopusdeploy/provider.go index 07f503374..6de5657b1 100644 --- a/octopusdeploy/provider.go +++ b/octopusdeploy/provider.go @@ -2,7 +2,6 @@ package octopusdeploy import ( "context" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -30,7 +29,6 @@ func Provider() *schema.Provider { "octopusdeploy_ssh_connection_deployment_targets": dataSourceSSHConnectionDeploymentTargets(), "octopusdeploy_tag_sets": dataSourceTagSets(), "octopusdeploy_teams": dataSourceTeams(), - "octopusdeploy_tenants": dataSourceTenants(), "octopusdeploy_users": dataSourceUsers(), "octopusdeploy_user_roles": dataSourceUserRoles(), "octopusdeploy_worker_pools": dataSourceWorkerPools(), diff --git a/octopusdeploy/resource_deployment_process_test.go b/octopusdeploy/resource_deployment_process_test.go index d800b81da..10bdefef0 100644 --- a/octopusdeploy/resource_deployment_process_test.go +++ b/octopusdeploy/resource_deployment_process_test.go @@ -436,7 +436,6 @@ func testAccDeploymentProcessCheckDestroy(s *terraform.State) error { func TestDeploymentProcessWithGitDependency(t *testing.T) { testFramework := test.OctopusContainerTest{} - newSpaceId, err := testFramework.Act(t, octoContainer, "../terraform", "51-deploymentprocesswithgitdependency", []string{}) if err != nil { diff --git a/octopusdeploy/testing_container_test.go b/octopusdeploy/testing_container_test.go index 6fd5dfba7..1f37ad0a3 100644 --- a/octopusdeploy/testing_container_test.go +++ b/octopusdeploy/testing_container_test.go @@ -27,7 +27,7 @@ func TestMain(m *testing.M) { if *createSharedContainer { testFramework := test.OctopusContainerTest{} - octoContainer, octoClient, sqlServerContainer, network, err = testFramework.ArrangeContainer(m) + octoContainer, octoClient, sqlServerContainer, network, err = testFramework.ArrangeContainer() if err != nil { log.Printf("Failed to arrange containers: (%s)", err.Error()) } diff --git a/octopusdeploy_framework/datasource_environments.go b/octopusdeploy_framework/datasource_environments.go index b788107d5..06b52a527 100644 --- a/octopusdeploy_framework/datasource_environments.go +++ b/octopusdeploy_framework/datasource_environments.go @@ -45,14 +45,14 @@ func (*environmentDataSource) Schema(_ context.Context, req datasource.SchemaReq Attributes: map[string]schema.Attribute{ //request "ids": util.GetQueryIDsDatasourceSchema(), - "space_id": util.GetSpaceIdDatasourceSchema(schemas.EnvironmentResourceDescription), + "space_id": schemas.GetSpaceIdDatasourceSchema(schemas.EnvironmentResourceDescription, false), "name": util.GetQueryNameDatasourceSchema(), "partial_name": util.GetQueryPartialNameDatasourceSchema(), "skip": util.GetQuerySkipDatasourceSchema(), "take": util.GetQueryTakeDatasourceSchema(), //response - "id": util.GetIdDatasourceSchema(), + "id": schemas.GetIdDatasourceSchema(true), }, Blocks: map[string]schema.Block{ "environments": schema.ListNestedBlock{ diff --git a/octopusdeploy_framework/datasource_project_groups.go b/octopusdeploy_framework/datasource_project_groups.go index 01713abbc..bca82cd2a 100644 --- a/octopusdeploy_framework/datasource_project_groups.go +++ b/octopusdeploy_framework/datasource_project_groups.go @@ -50,14 +50,14 @@ func (p *projectGroupsDataSource) Schema(_ context.Context, _ datasource.SchemaR resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ // request - "space_id": util.GetSpaceIdDatasourceSchema(description), + "space_id": schemas.GetSpaceIdDatasourceSchema(description, false), "ids": util.GetQueryIDsDatasourceSchema(), "partial_name": util.GetQueryPartialNameDatasourceSchema(), "skip": util.GetQuerySkipDatasourceSchema(), "take": util.GetQueryTakeDatasourceSchema(), // response - "id": util.GetIdDatasourceSchema(), + "id": schemas.GetIdDatasourceSchema(true), }, Blocks: map[string]schema.Block{ "project_groups": schema.ListNestedBlock{ diff --git a/octopusdeploy_framework/datasource_spaces.go b/octopusdeploy_framework/datasource_spaces.go index c7ec6414c..00ba2352c 100644 --- a/octopusdeploy_framework/datasource_spaces.go +++ b/octopusdeploy_framework/datasource_spaces.go @@ -42,7 +42,7 @@ func (*spacesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, r "take": schemas.GetQueryTakeDatasourceSchema(), // response - "id": schemas.GetIdDatasourceSchema(), + "id": schemas.GetIdDatasourceSchema(true), }, Blocks: map[string]schema.Block{ "spaces": schema.ListNestedBlock{ diff --git a/octopusdeploy_framework/datasource_tenants.go b/octopusdeploy_framework/datasource_tenants.go new file mode 100644 index 000000000..bf8b2867a --- /dev/null +++ b/octopusdeploy_framework/datasource_tenants.go @@ -0,0 +1,80 @@ +package octopusdeploy_framework + +import ( + "context" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tenants" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/schemas" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" + "github.com/hashicorp/terraform-plugin-framework/datasource" + datasourceSchema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "time" +) + +type tenantsDataSource struct { + *Config +} + +func NewTenantsDataSource() datasource.DataSource { + return &tenantsDataSource{} +} + +func (*tenantsDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = util.GetTypeName("tenants") +} + +func (e *tenantsDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + e.Config = DataSourceConfiguration(req, resp) +} + +func (*tenantsDataSource) Schema(_ context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = datasourceSchema.Schema{ + Description: "Provides information about existing tenants.", + Attributes: schemas.GetTenantsDataSourceSchema(), + Blocks: map[string]datasourceSchema.Block{ + "tenants": datasourceSchema.ListNestedBlock{ + Description: "A list of tenants that match the filter(s).", + NestedObject: datasourceSchema.NestedBlockObject{ + Attributes: schemas.GetTenantDataSourceSchema(), + }, + }, + }, + } +} + +func (b *tenantsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var err error + var data schemas.TenantsModel + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + query := tenants.TenantsQuery{ + ClonedFromTenantID: data.ClonedFromTenantId.ValueString(), + IDs: util.ExpandStringList(data.IDs), + IsClone: data.IsClone.ValueBool(), + Name: data.Name.ValueString(), + PartialName: data.PartialName.ValueString(), + ProjectID: data.ProjectId.ValueString(), + Skip: int(data.Skip.ValueInt64()), + Tags: util.ExpandStringList(data.Tags), + Take: int(data.Take.ValueInt64()), + } + + existingTenants, err := tenants.Get(b.Client, data.SpaceID.ValueString(), query) + if err != nil { + resp.Diagnostics.AddError("unable to load tenants", err.Error()) + return + } + + flattenedTenants := []interface{}{} + for _, tenant := range existingTenants.Items { + flattenedTenants = append(flattenedTenants, schemas.FlattenTenant(tenant)) + } + + data.ID = types.StringValue("Tenants " + time.Now().UTC().String()) + data.Tenants, _ = types.ListValueFrom(ctx, types.ObjectType{AttrTypes: schemas.TenantObjectType()}, flattenedTenants) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/octopusdeploy/data_source_tenants_test.go b/octopusdeploy_framework/datasource_tenants_test.go similarity index 82% rename from octopusdeploy/data_source_tenants_test.go rename to octopusdeploy_framework/datasource_tenants_test.go index b7baa57c8..95907b087 100644 --- a/octopusdeploy/data_source_tenants_test.go +++ b/octopusdeploy_framework/datasource_tenants_test.go @@ -1,13 +1,12 @@ -package octopusdeploy +package octopusdeploy_framework import ( "fmt" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" "strconv" "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) func TestAccDataSourceTenants(t *testing.T) { @@ -17,7 +16,7 @@ func TestAccDataSourceTenants(t *testing.T) { take := acctest.RandIntRange(0, 100) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { TestAccPreCheck(t) }, ProtoV6ProviderFactories: ProtoV6ProviderFactories(), Steps: []resource.TestStep{ { diff --git a/octopusdeploy_framework/framework_provider.go b/octopusdeploy_framework/framework_provider.go index 447f66237..95bf6c0a1 100644 --- a/octopusdeploy_framework/framework_provider.go +++ b/octopusdeploy_framework/framework_provider.go @@ -2,9 +2,9 @@ package octopusdeploy_framework import ( "context" - "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" "os" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/schema" @@ -70,6 +70,7 @@ func (p *octopusDeployFrameworkProvider) DataSources(ctx context.Context) []func NewLibraryVariableSetDataSource, NewVariablesDataSource, NewProjectsDataSource, + NewTenantsDataSource, } } diff --git a/octopusdeploy_framework/schemas/environment.go b/octopusdeploy_framework/schemas/environment.go index 964532446..6ab91f49d 100644 --- a/octopusdeploy_framework/schemas/environment.go +++ b/octopusdeploy_framework/schemas/environment.go @@ -27,21 +27,20 @@ const ( func GetEnvironmentDatasourceSchema() map[string]datasourceSchema.Attribute { return map[string]datasourceSchema.Attribute{ - "id": util.GetIdDatasourceSchema(), - "slug": util.GetSlugDatasourceSchema(EnvironmentResourceDescription), - "name": util.GetNameDatasourceWithMaxLengthSchema(true, 50), + "id": GetIdDatasourceSchema(true), + "slug": util.GetSlugDatasourceSchema(EnvironmentResourceDescription, true), + "name": GetReadonlyNameDatasourceSchema(), "description": util.GetDescriptionDatasourceSchema(EnvironmentResourceDescription), EnvironmentSortOrder: util.GetSortOrderDataSourceSchema(EnvironmentResourceDescription), EnvironmentAllowDynamicInfrastructure: datasourceSchema.BoolAttribute{ - Optional: true, + Computed: true, }, EnvironmentUseGuidedFailure: datasourceSchema.BoolAttribute{ - Optional: true, + Computed: true, }, - "space_id": util.GetSpaceIdDatasourceSchema(EnvironmentResourceDescription), + "space_id": GetSpaceIdDatasourceSchema(EnvironmentResourceDescription, true), EnvironmentJiraExtensionSettings: datasourceSchema.ListNestedAttribute{ Description: "Provides extension settings for the Jira integration for this environment.", - Optional: true, Computed: true, NestedObject: datasourceSchema.NestedAttributeObject{ Attributes: map[string]datasourceSchema.Attribute{ @@ -62,7 +61,6 @@ func GetEnvironmentDatasourceSchema() map[string]datasourceSchema.Attribute { }, EnvironmentJiraServiceManagementExtensionSettings: datasourceSchema.ListNestedAttribute{ Description: "Provides extension settings for the Jira Service Management (JSM) integration for this environment.", - Optional: true, Computed: true, NestedObject: datasourceSchema.NestedAttributeObject{ Attributes: map[string]datasourceSchema.Attribute{ @@ -72,7 +70,6 @@ func GetEnvironmentDatasourceSchema() map[string]datasourceSchema.Attribute { }, EnvironmentServiceNowExtensionSettings: datasourceSchema.ListNestedAttribute{ Description: "Provides extension settings for the ServiceNow integration for this environment.", - Optional: true, Computed: true, NestedObject: datasourceSchema.NestedAttributeObject{ Attributes: map[string]datasourceSchema.Attribute{ diff --git a/octopusdeploy_framework/schemas/feed.go b/octopusdeploy_framework/schemas/feed.go index 60a084e64..61b01ae23 100644 --- a/octopusdeploy_framework/schemas/feed.go +++ b/octopusdeploy_framework/schemas/feed.go @@ -78,10 +78,10 @@ func GetFeedsDataSourceSchema() map[string]datasourceSchema.Attribute { "partial_name": util.GetQueryPartialNameDatasourceSchema(), "skip": util.GetQuerySkipDatasourceSchema(), "take": util.GetQueryTakeDatasourceSchema(), - "space_id": util.GetSpaceIdDatasourceSchema("feeds"), + "space_id": GetSpaceIdDatasourceSchema("feeds", false), // response - "id": util.GetIdDatasourceSchema(), + "id": GetIdDatasourceSchema(true), } } @@ -89,7 +89,7 @@ func GetFeedDataSourceSchema() map[string]datasourceSchema.Attribute { return map[string]datasourceSchema.Attribute{ "feed_type": datasourceSchema.StringAttribute{ Description: "A filter to search by feed type. Valid feed types are `AwsElasticContainerRegistry`, `BuiltIn`, `Docker`, `GitHub`, `Helm`, `Maven`, `NuGet`, or `OctopusProject`.", - Optional: true, + Computed: true, Validators: []validator.String{ stringvalidator.OneOf( "AwsElasticContainerRegistry", @@ -103,17 +103,17 @@ func GetFeedDataSourceSchema() map[string]datasourceSchema.Attribute { }, }, "feed_uri": datasourceSchema.StringAttribute{ - Required: true, + Computed: true, }, - "id": util.GetIdDatasourceSchema(), + "id": GetIdDatasourceSchema(true), "is_enhanced_mode": datasourceSchema.BoolAttribute{ - Optional: true, + Computed: true, }, - "name": util.GetNameDatasourceSchema(true), + "name": GetReadonlyNameDatasourceSchema(), "password": datasourceSchema.StringAttribute{ Description: "The password associated with this resource.", Sensitive: true, - Optional: true, + Computed: true, Validators: []validator.String{ stringvalidator.LengthAtLeast(1), }, @@ -121,45 +121,42 @@ func GetFeedDataSourceSchema() map[string]datasourceSchema.Attribute { "package_acquisition_location_options": datasourceSchema.ListAttribute{ Computed: true, ElementType: types.StringType, - Optional: true, }, "region": datasourceSchema.StringAttribute{ Computed: true, }, "registry_path": datasourceSchema.StringAttribute{ - Optional: true, + Computed: true, }, "secret_key": datasourceSchema.StringAttribute{ - Optional: true, + Computed: true, Sensitive: true, }, - "space_id": util.GetSpaceIdDatasourceSchema("feeds"), + "space_id": GetSpaceIdDatasourceSchema("feeds", true), "username": datasourceSchema.StringAttribute{ Description: "The username associated with this resource.", Sensitive: true, - Optional: true, + Computed: true, Validators: []validator.String{ stringvalidator.LengthAtLeast(1), }, }, "delete_unreleased_packages_after_days": datasourceSchema.Int64Attribute{ - Optional: true, + Computed: true, }, "access_key": datasourceSchema.StringAttribute{ - Required: true, + Computed: true, Description: "The AWS access key to use when authenticating against Amazon Web Services.", }, "api_version": datasourceSchema.StringAttribute{ - Optional: true, + Computed: true, }, "download_attempts": datasourceSchema.Int64Attribute{ Description: "The number of times a deployment should attempt to download a package from this feed before failing.", - Optional: true, Computed: true, }, "download_retry_backoff_seconds": datasourceSchema.Int64Attribute{ Description: "The number of seconds to apply as a linear back off between download attempts.", - Optional: true, Computed: true, }, } diff --git a/octopusdeploy_framework/schemas/library_variable_set.go b/octopusdeploy_framework/schemas/library_variable_set.go index 3ca65b627..76ba84d14 100644 --- a/octopusdeploy_framework/schemas/library_variable_set.go +++ b/octopusdeploy_framework/schemas/library_variable_set.go @@ -42,8 +42,8 @@ func getLibraryVariableSetDataSchema() map[string]datasourceSchema.Attribute { Description: "A filter to search by content type.", Optional: true, }, - "id": util.GetIdDatasourceSchema(), - "space_id": util.GetSpaceIdDatasourceSchema("library variable set"), + "id": GetIdDatasourceSchema(true), + "space_id": GetSpaceIdDatasourceSchema("library variable set", false), "ids": util.GetQueryIDsDatasourceSchema(), "partial_name": util.GetQueryPartialNameDatasourceSchema(), "skip": util.GetQuerySkipDatasourceSchema(), @@ -53,16 +53,16 @@ func getLibraryVariableSetDataSchema() map[string]datasourceSchema.Attribute { func GetLibraryVariableSetObjectDatasourceSchema() map[string]datasourceSchema.Attribute { return map[string]datasourceSchema.Attribute{ - "description": GetDescriptionDatasourceSchema("library variable set"), - "id": GetIdDatasourceSchema(), - "name": GetNameDatasourceSchema(false), - "space_id": GetSpaceIdDatasourceSchema("library variable set"), + "description": GetReadonlyDescriptionDatasourceSchema("library variable set"), + "id": GetIdDatasourceSchema(true), + "name": GetReadonlyNameDatasourceSchema(), + "space_id": GetSpaceIdDatasourceSchema("library variable set", true), "template_ids": datasourceSchema.MapAttribute{ ElementType: types.StringType, Computed: true, }, "template": datasourceSchema.ListAttribute{ - Optional: true, + Computed: true, ElementType: types.ObjectType{AttrTypes: TemplateObjectType()}, }, "variable_set_id": datasourceSchema.StringAttribute{ diff --git a/octopusdeploy_framework/schemas/project_group.go b/octopusdeploy_framework/schemas/project_group.go index bafc5ea42..27b42acfd 100644 --- a/octopusdeploy_framework/schemas/project_group.go +++ b/octopusdeploy_framework/schemas/project_group.go @@ -15,7 +15,7 @@ func GetProjectGroupDatasourceSchema() map[string]datasourceSchema.Attribute { return map[string]datasourceSchema.Attribute{ "id": util.GetIdResourceSchema(), "space_id": util.GetSpaceIdResourceSchema(projectGroupDescription), - "name": util.GetNameResourceSchema(true), + "name": GetReadonlyNameDatasourceSchema(), "retention_policy_id": datasourceSchema.StringAttribute{ Computed: true, Optional: true, diff --git a/octopusdeploy_framework/schemas/schema.go b/octopusdeploy_framework/schemas/schema.go index 395aefc7f..496dbc913 100644 --- a/octopusdeploy_framework/schemas/schema.go +++ b/octopusdeploy_framework/schemas/schema.go @@ -5,7 +5,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" - //"github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -56,20 +55,41 @@ func GetQueryTakeDatasourceSchema() datasourceSchema.Attribute { } } -func GetIdDatasourceSchema() datasourceSchema.Attribute { +func GetReadonlyNameDatasourceSchema() datasourceSchema.Attribute { return datasourceSchema.StringAttribute{ - Description: "The unique ID for this resource.", + Description: "The name of this resource.", Computed: true, - Optional: true, } } -func GetSpaceIdDatasourceSchema(resourceDescription string) datasourceSchema.Attribute { - return datasourceSchema.StringAttribute{ +func GetIdDatasourceSchema(isReadOnly bool) datasourceSchema.Attribute { + s := datasourceSchema.StringAttribute{ + Description: "The unique ID for this resource.", + } + + if isReadOnly { + s.Computed = true + } else { + s.Computed = true + s.Optional = true + } + + return s +} + +func GetSpaceIdDatasourceSchema(resourceDescription string, isReadOnly bool) datasourceSchema.Attribute { + s := datasourceSchema.StringAttribute{ Description: "The space ID associated with this " + resourceDescription + ".", - Computed: true, - Optional: true, } + + if isReadOnly { + s.Computed = true + } else { + s.Computed = true + s.Optional = true + } + + return s } func GetNameDatasourceWithMaxLengthSchema(isRequired bool, maxLength int) datasourceSchema.Attribute { @@ -113,6 +133,13 @@ func GetDescriptionDatasourceSchema(resourceDescription string) datasourceSchema } } +func GetReadonlyDescriptionDatasourceSchema(resourceDescription string) datasourceSchema.Attribute { + return datasourceSchema.StringAttribute{ + Description: "The description of this " + resourceDescription + ".", + Computed: true, + } +} + func GetIdResourceSchema() resourceSchema.Attribute { return resourceSchema.StringAttribute{ Description: "The unique ID for this resource.", @@ -155,12 +182,19 @@ func GetDescriptionResourceSchema(resourceDescription string) resourceSchema.Att } } -func GetSlugDatasourceSchema(resourceDescription string) datasourceSchema.Attribute { - return datasourceSchema.StringAttribute{ +func GetSlugDatasourceSchema(resourceDescription string, isReadOnly bool) datasourceSchema.Attribute { + s := datasourceSchema.StringAttribute{ Description: fmt.Sprintf("The unique slug of this %s", resourceDescription), - Optional: true, - Computed: true, } + + if isReadOnly { + s.Computed = true + } else { + s.Optional = true + s.Computed = true + } + + return s } func GetSlugResourceSchema(resourceDescription string) resourceSchema.Attribute { diff --git a/octopusdeploy_framework/schemas/space.go b/octopusdeploy_framework/schemas/space.go index 662a95c96..0c1159553 100644 --- a/octopusdeploy_framework/schemas/space.go +++ b/octopusdeploy_framework/schemas/space.go @@ -1,10 +1,13 @@ package schemas import ( + "fmt" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -57,29 +60,33 @@ func GetSpaceResourceSchema() map[string]resourceSchema.Attribute { func GetSpaceDatasourceSchema() map[string]datasourceSchema.Attribute { return map[string]datasourceSchema.Attribute{ - "id": GetIdDatasourceSchema(), - "description": GetDescriptionDatasourceSchema(spaceDescription), - "name": GetNameDatasourceWithMaxLengthSchema(true, 20), - "slug": GetSlugDatasourceSchema(spaceDescription), + "id": GetIdDatasourceSchema(true), + "description": GetReadonlyDescriptionDatasourceSchema(spaceDescription), + "name": datasourceSchema.StringAttribute{ + Description: fmt.Sprintf("The name of this resource, no more than %d characters long", 20), + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 20), + }, + Required: true, + }, + "slug": GetSlugDatasourceSchema(spaceDescription, true), "space_managers_teams": datasourceSchema.SetAttribute{ ElementType: types.StringType, Description: "A list of team IDs designated to be managers of this space.", - Optional: true, Computed: true, }, "space_managers_team_members": datasourceSchema.SetAttribute{ ElementType: types.StringType, Description: "A list of user IDs designated to be managers of this space.", - Optional: true, Computed: true, }, "is_task_queue_stopped": datasourceSchema.BoolAttribute{ Description: "Specifies the status of the task queue for this space.", - Optional: true, + Computed: true, }, "is_default": datasourceSchema.BoolAttribute{ Description: "Specifies if this space is the default space in Octopus.", - Optional: true, + Computed: true, }, } } diff --git a/octopusdeploy_framework/schemas/tenant.go b/octopusdeploy_framework/schemas/tenant.go new file mode 100644 index 000000000..41d55b7d1 --- /dev/null +++ b/octopusdeploy_framework/schemas/tenant.go @@ -0,0 +1,107 @@ +package schemas + +import ( + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tenants" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" + "github.com/hashicorp/terraform-plugin-framework/attr" + datasourceSchema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type TenantModel struct { + ClonedFromTenantId types.String `tfsdk:"cloned_from_tenant_id"` + Description types.String `tfsdk:"description"` + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + SpaceID types.String `tfsdk:"space_id"` + TenantTags types.List `tfsdk:"tenant_tags"` +} + +type TenantsModel struct { + ClonedFromTenantId types.String `tfsdk:"cloned_from_tenant_id"` + ID types.String `tfsdk:"id"` + IDs types.List `tfsdk:"ids"` + IsClone types.Bool `tfsdk:"is_clone"` + Name types.String `tfsdk:"name"` + PartialName types.String `tfsdk:"partial_name"` + ProjectId types.String `tfsdk:"project_id"` + Skip types.Int64 `tfsdk:"skip"` + Tags types.List `tfsdk:"tags"` + SpaceID types.String `tfsdk:"space_id"` + Tenants types.List `tfsdk:"tenants"` + Take types.Int64 `tfsdk:"take"` +} + +func TenantObjectType() map[string]attr.Type { + return map[string]attr.Type{ + "cloned_from_tenant_id": types.StringType, + "description": types.StringType, + "id": types.StringType, + "name": types.StringType, + "space_id": types.StringType, + "tenant_tags": types.ListType{ElemType: types.StringType}, + } +} + +func FlattenTenant(tenant *tenants.Tenant) attr.Value { + tenantTags := make([]attr.Value, len(tenant.TenantTags)) + for i, value := range tenant.TenantTags { + tenantTags[i] = types.StringValue(value) + } + var tenantTagsList, _ = types.ListValue(types.StringType, tenantTags) + + return types.ObjectValueMust(TenantObjectType(), map[string]attr.Value{ + "cloned_from_tenant_id": types.StringValue(tenant.ClonedFromTenantID), + "description": types.StringValue(tenant.Description), + "id": types.StringValue(tenant.GetID()), + "name": types.StringValue(tenant.Name), + "space_id": types.StringValue(tenant.SpaceID), + "tenant_tags": tenantTagsList, + }) +} + +func GetTenantsDataSourceSchema() map[string]datasourceSchema.Attribute { + return map[string]datasourceSchema.Attribute{ + "cloned_from_tenant_id": datasourceSchema.StringAttribute{ + Description: "A filter to search for a cloned tenant by its ID.", + Optional: true, + }, + "id": GetIdDatasourceSchema(true), + "ids": util.GetQueryIDsDatasourceSchema(), + "is_clone": datasourceSchema.BoolAttribute{ + Description: "A filter to search for cloned resources.", + Optional: true, + }, + "name": datasourceSchema.StringAttribute{ + Description: "A filter to search by name.", + Optional: true, + }, + "partial_name": util.GetQueryPartialNameDatasourceSchema(), + "project_id": datasourceSchema.StringAttribute{ + Description: "A filter to search by a project ID.", + Optional: true, + }, + "skip": util.GetQuerySkipDatasourceSchema(), + "tags": util.GetQueryDatasourceTags(), + "space_id": GetSpaceIdDatasourceSchema("tenants", false), + "take": util.GetQueryTakeDatasourceSchema(), + } +} + +func GetTenantDataSourceSchema() map[string]datasourceSchema.Attribute { + return map[string]datasourceSchema.Attribute{ + "cloned_from_tenant_id": datasourceSchema.StringAttribute{ + Description: "The ID of the tenant from which this tenant was cloned.", + Computed: true, + }, + "description": util.GetDescriptionDatasourceSchema("tenants"), + "id": GetIdDatasourceSchema(true), + "name": GetReadonlyNameDatasourceSchema(), + "space_id": GetSpaceIdDatasourceSchema("tenant", true), + "tenant_tags": datasourceSchema.ListAttribute{ + Computed: true, + Description: "A list of tenant tags associated with this resource.", + ElementType: types.StringType, + }, + } +} diff --git a/octopusdeploy_framework/testing_container_test.go b/octopusdeploy_framework/testing_container_test.go index a876440fd..49319d02e 100644 --- a/octopusdeploy_framework/testing_container_test.go +++ b/octopusdeploy_framework/testing_container_test.go @@ -28,7 +28,7 @@ func TestMain(m *testing.M) { if *createSharedContainer { testFramework := test.OctopusContainerTest{} - octoContainer, octoClient, sqlServerContainer, network, err = testFramework.ArrangeContainer(m) + octoContainer, octoClient, sqlServerContainer, network, err = testFramework.ArrangeContainer() if err != nil { log.Printf("Failed to arrange containers: (%s)", err.Error()) } diff --git a/octopusdeploy_framework/util/schema.go b/octopusdeploy_framework/util/schema.go index 433892367..d8108b803 100644 --- a/octopusdeploy_framework/util/schema.go +++ b/octopusdeploy_framework/util/schema.go @@ -60,22 +60,6 @@ func GetQueryTakeDatasourceSchema() datasourceSchema.Attribute { } } -func GetIdDatasourceSchema() datasourceSchema.Attribute { - return datasourceSchema.StringAttribute{ - Description: "The unique ID for this resource.", - Computed: true, - Optional: true, - } -} - -func GetSpaceIdDatasourceSchema(resourceDescription string) datasourceSchema.Attribute { - return datasourceSchema.StringAttribute{ - Description: "The space ID associated with this " + resourceDescription + ".", - Computed: true, - Optional: true, - } -} - func GetNameDatasourceWithMaxLengthSchema(isRequired bool, maxLength int) datasourceSchema.Attribute { s := datasourceSchema.StringAttribute{ Description: fmt.Sprintf("The name of this resource, no more than %d characters long", maxLength), @@ -117,6 +101,14 @@ func GetDescriptionDatasourceSchema(resourceDescription string) datasourceSchema } } +func GetQueryDatasourceTags() datasourceSchema.Attribute { + return datasourceSchema.ListAttribute{ + Description: "A filter to search by a list of tags.", + ElementType: types.StringType, + Optional: true, + } +} + func GetIdResourceSchema() resourceSchema.Attribute { return resourceSchema.StringAttribute{ Description: "The unique ID for this resource.", @@ -165,12 +157,19 @@ func GetDescriptionResourceSchema(resourceDescription string) resourceSchema.Att } } -func GetSlugDatasourceSchema(resourceDescription string) resourceSchema.Attribute { - return resourceSchema.StringAttribute{ +func GetSlugDatasourceSchema(resourceDescription string, isReadOnly bool) datasourceSchema.Attribute { + s := datasourceSchema.StringAttribute{ Description: fmt.Sprintf("The unique slug of this %s", resourceDescription), - Optional: true, - Computed: true, } + + if isReadOnly { + s.Computed = true + } else { + s.Optional = true + s.Computed = true + } + + return s } func GetSlugResourceSchema(resourceDescription string) resourceSchema.Attribute { @@ -184,7 +183,6 @@ func GetSlugResourceSchema(resourceDescription string) resourceSchema.Attribute func GetSortOrderDataSourceSchema(resourceDescription string) resourceSchema.Attribute { return resourceSchema.Int64Attribute{ Description: fmt.Sprintf("The order number to sort an %s", resourceDescription), - Optional: true, Computed: true, } } From e5533e062c96133ee8e1c084bf7a77cea5cea18e Mon Sep 17 00:00:00 2001 From: Henrik Andersson Date: Fri, 9 Aug 2024 00:55:57 -0700 Subject: [PATCH 08/28] chore: update environment resource and datasource docs (#708) --- docs/resources/environment.md | 17 ++++--- .../schemas/environment.go | 51 ++++++++++++++----- octopusdeploy_framework/util/schema.go | 3 +- 3 files changed, 49 insertions(+), 22 deletions(-) diff --git a/docs/resources/environment.md b/docs/resources/environment.md index c9f9c5d1e..22b847d57 100644 --- a/docs/resources/environment.md +++ b/docs/resources/environment.md @@ -3,12 +3,12 @@ page_title: "octopusdeploy_environment Resource - terraform-provider-octopusdeploy" subcategory: "" description: |- - + This resource manages environments in Octopus Deploy. --- # octopusdeploy_environment (Resource) - +This resource manages environments in Octopus Deploy. ## Example Usage @@ -48,17 +48,20 @@ resource "octopusdeploy_environment" "example" { - `jira_extension_settings` (Block List) Provides extension settings for the Jira integration for this environment. (see [below for nested schema](#nestedblock--jira_extension_settings)) - `jira_service_management_extension_settings` (Block List) Provides extension settings for the Jira Service Management (JSM) integration for this environment. (see [below for nested schema](#nestedblock--jira_service_management_extension_settings)) - `servicenow_extension_settings` (Block List) Provides extension settings for the ServiceNow integration for this environment. (see [below for nested schema](#nestedblock--servicenow_extension_settings)) -- `slug` (String) The unique slug of this environment -- `sort_order` (Number) The order number to sort an environment +- `sort_order` (Number) The order number to sort an environment. - `space_id` (String) The space ID associated with this environment. - `use_guided_failure` (Boolean) +### Read-Only + +- `slug` (String) The unique slug of this environment + ### Nested Schema for `jira_extension_settings` Optional: -- `environment_type` (String) +- `environment_type` (String) The Jira environment type of this Octopus deployment environment. Valid values are `"development"`, `"production"`, `"staging"`, `"testing"`, `"unmapped"`. @@ -66,7 +69,7 @@ Optional: Optional: -- `is_enabled` (Boolean) +- `is_enabled` (Boolean) Specifies whether or not this extension is enabled for this project. @@ -74,7 +77,7 @@ Optional: Optional: -- `is_enabled` (Boolean) +- `is_enabled` (Boolean) Specifies whether or not this extension is enabled for this project. ## Import diff --git a/octopusdeploy_framework/schemas/environment.go b/octopusdeploy_framework/schemas/environment.go index 6ab91f49d..ed57bba65 100644 --- a/octopusdeploy_framework/schemas/environment.go +++ b/octopusdeploy_framework/schemas/environment.go @@ -1,6 +1,9 @@ package schemas import ( + "fmt" + "strings" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/environments" "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -25,6 +28,28 @@ const ( EnvironmentServiceNowExtensionSettingsIsEnabled = "is_enabled" ) +var jiraEnvironmentTypeNames = struct { + Development string + Production string + Testing string + Staging string + Unmapped string +}{ + Development: "development", + Production: "production", + Testing: "testing", + Staging: "staging", + Unmapped: "unmapped", +} + +var jiraEnvironmentTypes = []string{ + jiraEnvironmentTypeNames.Development, + jiraEnvironmentTypeNames.Production, + jiraEnvironmentTypeNames.Staging, + jiraEnvironmentTypeNames.Testing, + jiraEnvironmentTypeNames.Unmapped, +} + func GetEnvironmentDatasourceSchema() map[string]datasourceSchema.Attribute { return map[string]datasourceSchema.Attribute{ "id": GetIdDatasourceSchema(true), @@ -48,11 +73,7 @@ func GetEnvironmentDatasourceSchema() map[string]datasourceSchema.Attribute { Computed: true, Validators: []validator.String{ stringvalidator.OneOfCaseInsensitive( - "development", - "production", - "testing", - "staging", - "unmapped", + jiraEnvironmentTypes..., ), }, }, @@ -82,6 +103,7 @@ func GetEnvironmentDatasourceSchema() map[string]datasourceSchema.Attribute { func GetEnvironmentResourceSchema() resourceSchema.Schema { return resourceSchema.Schema{ + Description: util.GetResourceSchemaDescription(EnvironmentResourceDescription), Attributes: map[string]resourceSchema.Attribute{ "id": util.GetIdResourceSchema(), "slug": util.GetSlugResourceSchema(EnvironmentResourceDescription), @@ -106,14 +128,11 @@ func GetEnvironmentResourceSchema() resourceSchema.Schema { NestedObject: resourceSchema.NestedBlockObject{ Attributes: map[string]resourceSchema.Attribute{ "environment_type": resourceSchema.StringAttribute{ - Optional: true, + Description: fmt.Sprintf("The Jira environment type of this Octopus deployment environment. Valid values are %s.", strings.Join(util.Map(jiraEnvironmentTypes, func(item string) string { return fmt.Sprintf("`\"%s\"`", item) }), ", ")), + Optional: true, Validators: []validator.String{ stringvalidator.OneOfCaseInsensitive( - "development", - "production", - "staging", - "testing", - "unmapped", + jiraEnvironmentTypes..., ), }, }, @@ -124,7 +143,10 @@ func GetEnvironmentResourceSchema() resourceSchema.Schema { Description: "Provides extension settings for the Jira Service Management (JSM) integration for this environment.", NestedObject: resourceSchema.NestedBlockObject{ Attributes: map[string]resourceSchema.Attribute{ - "is_enabled": resourceSchema.BoolAttribute{Optional: true}, + "is_enabled": resourceSchema.BoolAttribute{ + Description: "Specifies whether or not this extension is enabled for this project.", + Optional: true, + }, }, }, }, @@ -132,7 +154,10 @@ func GetEnvironmentResourceSchema() resourceSchema.Schema { Description: "Provides extension settings for the ServiceNow integration for this environment.", NestedObject: resourceSchema.NestedBlockObject{ Attributes: map[string]resourceSchema.Attribute{ - "is_enabled": resourceSchema.BoolAttribute{Optional: true}, + "is_enabled": resourceSchema.BoolAttribute{ + Description: "Specifies whether or not this extension is enabled for this project.", + Optional: true, + }, }, }, }, diff --git a/octopusdeploy_framework/util/schema.go b/octopusdeploy_framework/util/schema.go index d8108b803..26856a9da 100644 --- a/octopusdeploy_framework/util/schema.go +++ b/octopusdeploy_framework/util/schema.go @@ -175,7 +175,6 @@ func GetSlugDatasourceSchema(resourceDescription string, isReadOnly bool) dataso func GetSlugResourceSchema(resourceDescription string) resourceSchema.Attribute { return resourceSchema.StringAttribute{ Description: fmt.Sprintf("The unique slug of this %s", resourceDescription), - Optional: true, Computed: true, } } @@ -189,7 +188,7 @@ func GetSortOrderDataSourceSchema(resourceDescription string) resourceSchema.Att func GetSortOrderResourceSchema(resourceDescription string) resourceSchema.Attribute { return resourceSchema.Int64Attribute{ - Description: fmt.Sprintf("The order number to sort an %s", resourceDescription), + Description: fmt.Sprintf("The order number to sort an %s.", resourceDescription), Optional: true, Computed: true, } From 04b191762f5773a3b62386be37391d58dc51021b Mon Sep 17 00:00:00 2001 From: Isaac Calligeros <101079287+IsaacCalligeros95@users.noreply.github.com> Date: Mon, 12 Aug 2024 10:35:34 +0930 Subject: [PATCH 09/28] Chore!: Require updated docs (#697) * Adds a GHA to require no docs differences between generated and checked in --- .github/workflows/docs.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/docs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..2e2328067 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,19 @@ +name: Validate Docs +on: + push: + branches: + - '**' + workflow_dispatch: +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-go@v3 + with: + go-version: '1.22' + - run: go generate main.go + - name: Check for file changes + run: git diff --exit-code From c595415a729f483b16006da13967325bdc5fc900 Mon Sep 17 00:00:00 2001 From: Henrik Andersson Date: Sun, 11 Aug 2024 20:38:43 -0700 Subject: [PATCH 10/28] chore!: migrate runbook resource to tf framework (#709) --- docs/resources/runbook.md | 10 +- octopusdeploy/provider.go | 1 - octopusdeploy/resource_runbook.go | 104 ------- octopusdeploy/resource_runbook_process.go | 3 +- octopusdeploy/schema_runbook.go | 176 ----------- .../schema_runbook_retention_period.go | 50 --- octopusdeploy_framework/framework_provider.go | 1 + octopusdeploy_framework/resource_project.go | 2 +- octopusdeploy_framework/resource_runbook.go | 179 +++++++++++ .../resource_runbook_migration_test.go | 121 ++++++++ .../resource_runbook_test.go | 7 +- .../schemas/connectivity_policy.go | 142 +++++++++ octopusdeploy_framework/schemas/runbook.go | 288 ++++++++++++++++++ .../schemas/runbook_retention_period.go | 99 ++++++ octopusdeploy_framework/util/logging.go | 25 ++ 15 files changed, 867 insertions(+), 341 deletions(-) delete mode 100644 octopusdeploy/resource_runbook.go delete mode 100644 octopusdeploy/schema_runbook.go delete mode 100644 octopusdeploy/schema_runbook_retention_period.go create mode 100644 octopusdeploy_framework/resource_runbook.go create mode 100644 octopusdeploy_framework/resource_runbook_migration_test.go rename octopusdeploy/resource_runbook_process_test.go => octopusdeploy_framework/resource_runbook_test.go (99%) create mode 100644 octopusdeploy_framework/schemas/connectivity_policy.go create mode 100644 octopusdeploy_framework/schemas/runbook.go create mode 100644 octopusdeploy_framework/schemas/runbook_retention_period.go diff --git a/docs/resources/runbook.md b/docs/resources/runbook.md index b540fd6e7..09b12e7b3 100644 --- a/docs/resources/runbook.md +++ b/docs/resources/runbook.md @@ -22,16 +22,16 @@ This resource manages runbooks in Octopus Deploy. ### Optional -- `connectivity_policy` (Block List, Max: 1) (see [below for nested schema](#nestedblock--connectivity_policy)) +- `connectivity_policy` (Block List) (see [below for nested schema](#nestedblock--connectivity_policy)) - `default_guided_failure_mode` (String) Sets the runbook guided failure mode. - `description` (String) The description of this runbook. - `environment_scope` (String) Determines how the runbook is scoped to environments. - `environments` (List of String) When environment_scope is set to "Specified", this is the list of environments the runbook can be run against. -- `force_package_download` (Boolean) Whether to force packages to be re-downloaded or not +- `force_package_download` (Boolean) Whether to force packages to be re-downloaded or not. - `id` (String) The unique ID for this resource. -- `multi_tenancy_mode` (String) The tenanted deployment mode of the resource. Valid account types are `Untenanted`, `TenantedOrUntenanted`, or `Tenanted`. -- `retention_policy` (Block List, Max: 1) Sets the runbook retention policy (see [below for nested schema](#nestedblock--retention_policy)) -- `space_id` (String) The space ID associated with this resource. +- `multi_tenancy_mode` (String) The tenanted deployment mode of the runbook. Valid modes are `Untenanted`, `TenantedOrUntenanted`, `Tenanted` +- `retention_policy` (Block List) Sets the runbook retention policy. (see [below for nested schema](#nestedblock--retention_policy)) +- `space_id` (String) The space ID associated with this runbook. ### Read-Only diff --git a/octopusdeploy/provider.go b/octopusdeploy/provider.go index 6de5657b1..794853fff 100644 --- a/octopusdeploy/provider.go +++ b/octopusdeploy/provider.go @@ -58,7 +58,6 @@ func Provider() *schema.Provider { "octopusdeploy_project_deployment_target_trigger": resourceProjectDeploymentTargetTrigger(), "octopusdeploy_external_feed_create_release_trigger": resourceExternalFeedCreateReleaseTrigger(), "octopusdeploy_project_scheduled_trigger": resourceProjectScheduledTrigger(), - "octopusdeploy_runbook": resourceRunbook(), "octopusdeploy_runbook_process": resourceRunbookProcess(), "octopusdeploy_scoped_user_role": resourceScopedUserRole(), "octopusdeploy_script_module": resourceScriptModule(), diff --git a/octopusdeploy/resource_runbook.go b/octopusdeploy/resource_runbook.go deleted file mode 100644 index 7497a1a04..000000000 --- a/octopusdeploy/resource_runbook.go +++ /dev/null @@ -1,104 +0,0 @@ -package octopusdeploy - -import ( - "context" - "fmt" - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/runbooks" - - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client" - "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal/errors" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func resourceRunbook() *schema.Resource { - return &schema.Resource{ - CreateContext: resourceRunbookCreate, - DeleteContext: resourceRunbookDelete, - Description: "This resource manages runbooks in Octopus Deploy.", - Importer: getImporter(), - ReadContext: resourceRunbookRead, - Schema: getRunbookSchema(), - UpdateContext: resourceRunbookUpdate, - } -} - -func resourceRunbookCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - runbook := expandRunbook(ctx, d) - - tflog.Info(ctx, fmt.Sprintf("creating runbook (%s)", runbook.Name)) - - client := m.(*client.Client) - createdRunbook, err := runbooks.Add(client, runbook) - if err != nil { - return diag.FromErr(err) - } - - if err := setRunbook(ctx, d, createdRunbook); err != nil { - return diag.FromErr(err) - } - - d.SetId(createdRunbook.GetID()) - - tflog.Info(ctx, fmt.Sprintf("runbook created (%s)", d.Id())) - return nil -} - -func resourceRunbookDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Info(ctx, fmt.Sprintf("deleting runbook (%s)", d.Id())) - - client := m.(*client.Client) - if err := runbooks.DeleteByID(client, d.Get("space_id").(string), d.Id()); err != nil { - return diag.FromErr(err) - } - - tflog.Info(ctx, fmt.Sprintf("runbook deleted (%s)", d.Id())) - d.SetId("") - return nil -} - -func resourceRunbookRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Info(ctx, fmt.Sprintf("reading runbook (%s)", d.Id())) - - client := m.(*client.Client) - runbook, err := runbooks.GetByID(client, d.Get("space_id").(string), d.Id()) - if err != nil { - return errors.ProcessApiError(ctx, d, err, "runbook") - } - - if err := setRunbook(ctx, d, runbook); err != nil { - return diag.FromErr(err) - } - - tflog.Info(ctx, fmt.Sprintf("runbook read (%s)", d.Id())) - return nil -} - -func resourceRunbookUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Info(ctx, fmt.Sprintf("updating runbook (%s)", d.Id())) - - client := m.(*client.Client) - runbook := expandRunbook(ctx, d) - var updatedRunbook *runbooks.Runbook - var err error - - runbookLinks, err := runbooks.GetByID(client, d.Get("space_id").(string), d.Id()) - if err != nil { - return diag.FromErr(err) - } - - runbook.Links = runbookLinks.Links - - updatedRunbook, err = runbooks.Update(client, runbook) - if err != nil { - return diag.FromErr(err) - } - - if err := setRunbook(ctx, d, updatedRunbook); err != nil { - return diag.FromErr(err) - } - - tflog.Info(ctx, fmt.Sprintf("runbook updated (%s)", d.Id())) - return nil -} diff --git a/octopusdeploy/resource_runbook_process.go b/octopusdeploy/resource_runbook_process.go index 5ff0fb3ac..8019bd4b5 100644 --- a/octopusdeploy/resource_runbook_process.go +++ b/octopusdeploy/resource_runbook_process.go @@ -105,7 +105,8 @@ func resourceRunbookProcessDelete(ctx context.Context, d *schema.ResourceData, m } runbookProcess := &runbookprocess.RunbookProcess{ - Version: current.Version, + ProjectID: current.ProjectID, + Version: current.Version, } runbookProcess.Links = current.Links runbookProcess.ID = d.Id() diff --git a/octopusdeploy/schema_runbook.go b/octopusdeploy/schema_runbook.go deleted file mode 100644 index 47ffebba4..000000000 --- a/octopusdeploy/schema_runbook.go +++ /dev/null @@ -1,176 +0,0 @@ -package octopusdeploy - -import ( - "context" - "fmt" - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/runbooks" - - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" -) - -func expandRunbook(ctx context.Context, d *schema.ResourceData) *runbooks.Runbook { - name := d.Get("name").(string) - projectId := d.Get("project_id").(string) - - runbook := runbooks.NewRunbook(name, projectId) - runbook.ID = d.Id() - - if v, ok := d.GetOk("description"); ok { - runbook.Description = v.(string) - } - - if v, ok := d.GetOk("runbook_process_id"); ok { - runbook.RunbookProcessID = v.(string) - } - - if v, ok := d.GetOk("published_runbook_snapshot_id"); ok { - runbook.PublishedRunbookSnapshotID = v.(string) - } - - if v, ok := d.GetOk("space_id"); ok { - runbook.SpaceID = v.(string) - } - - if v, ok := d.GetOk("multi_tenancy_mode"); ok { - runbook.MultiTenancyMode = core.TenantedDeploymentMode(v.(string)) - } - - if v, ok := d.GetOk("connectivity_policy"); ok { - runbook.ConnectivityPolicy = expandConnectivityPolicy(v.([]interface{})) - } - - if v, ok := d.GetOk("environment_scope"); ok { - runbook.EnvironmentScope = v.(string) - } - - if v, ok := d.GetOk("environments"); ok { - runbook.Environments = getSliceFromTerraformTypeList(v) - } - - if v, ok := d.GetOk("default_guided_failure_mode"); ok { - runbook.DefaultGuidedFailureMode = v.(string) - } - - if v, ok := d.GetOk("retention_policy"); ok { - runbook.RunRetentionPolicy = expandRunbookRetentionPolicy(v.([]interface{})) - } - - if v, ok := d.GetOk("force_package_download"); ok { - runbook.ForcePackageDownload = v.(bool) - } - - return runbook -} - -func getRunbookSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "id": getIDSchema(), - "name": { - Description: "The name of the runbook in Octopus Deploy. This name must be unique.", - Required: true, - Type: schema.TypeString, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotWhiteSpace), - }, - "description": { - Computed: true, - Description: "The description of this runbook.", - Optional: true, - Type: schema.TypeString, - }, - "project_id": { - Description: "The project that this runbook belongs to.", - Required: true, - Type: schema.TypeString, - }, - "runbook_process_id": { - Description: "The runbook process ID.", - Computed: true, - Type: schema.TypeString, - }, - "published_runbook_snapshot_id": { - Description: "The published snapshot ID.", - Computed: true, - Type: schema.TypeString, - }, - "space_id": getSpaceIDSchema(), - "multi_tenancy_mode": getTenantedDeploymentSchema(), - "connectivity_policy": { - Computed: true, - Elem: &schema.Resource{Schema: getConnectivityPolicySchema()}, - MaxItems: 1, - Optional: true, - Type: schema.TypeList, - }, - "environment_scope": { - Description: "Determines how the runbook is scoped to environments.", - Computed: true, - Optional: true, - Type: schema.TypeString, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ - "All", - "Specified", - "FromProjectLifecycles", - }, false)), - }, - "environments": { - Description: "When environment_scope is set to \"Specified\", this is the list of environments the runbook can be run against.", - Computed: true, - Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, - Type: schema.TypeList, - }, - "default_guided_failure_mode": { - Description: "Sets the runbook guided failure mode.", - Computed: true, - Optional: true, - Type: schema.TypeString, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ - "EnvironmentDefault", - "Off", - "On", - }, false)), - }, - "retention_policy": { - Description: "Sets the runbook retention policy", - Computed: true, - DefaultFunc: func() (interface{}, error) { - return flattenRunbookRetentionPeriod(&runbooks.RunbookRetentionPeriod{ - QuantityToKeep: 100, - ShouldKeepForever: false, - }), nil - }, - Elem: &schema.Resource{Schema: getRunbookRetentionPeriodSchema()}, - MaxItems: 1, - Optional: true, - Type: schema.TypeList, - }, - "force_package_download": { - Description: "Whether to force packages to be re-downloaded or not", - Computed: true, - Optional: true, - Type: schema.TypeBool, - }, - } -} - -func setRunbook(ctx context.Context, d *schema.ResourceData, runbook *runbooks.Runbook) error { - d.Set("id", runbook.GetID()) - d.Set("name", runbook.Name) - d.Set("project_id", runbook.ProjectID) - d.Set("description", runbook.Description) - d.Set("runbook_process_id", runbook.RunbookProcessID) - d.Set("published_runbook_snapshot_id", runbook.PublishedRunbookSnapshotID) - d.Set("space_id", runbook.SpaceID) - d.Set("multi_tenancy_mode", runbook.MultiTenancyMode) - if err := d.Set("connectivity_policy", flattenConnectivityPolicy(runbook.ConnectivityPolicy)); err != nil { - return fmt.Errorf("error setting connectivity_policy: %s", err) - } - d.Set("environment_scope", runbook.EnvironmentScope) - d.Set("environments", runbook.Environments) - d.Set("default_guided_failure_mode", runbook.DefaultGuidedFailureMode) - d.Set("force_package_download", runbook.ForcePackageDownload) - - return nil -} diff --git a/octopusdeploy/schema_runbook_retention_period.go b/octopusdeploy/schema_runbook_retention_period.go deleted file mode 100644 index 516cada68..000000000 --- a/octopusdeploy/schema_runbook_retention_period.go +++ /dev/null @@ -1,50 +0,0 @@ -package octopusdeploy - -import ( - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/runbooks" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" -) - -func expandRunbookRetentionPolicy(flattenedRetentionPeriod interface{}) *runbooks.RunbookRetentionPeriod { - if flattenedRetentionPeriod == nil { - return nil - } - - retentionPeriodProperties := flattenedRetentionPeriod.([]interface{}) - if len(retentionPeriodProperties) == 1 { - retentionPeriodMap := retentionPeriodProperties[0].(map[string]interface{}) - return &runbooks.RunbookRetentionPeriod{ - QuantityToKeep: int32(retentionPeriodMap["quantity_to_keep"].(int)), - ShouldKeepForever: retentionPeriodMap["should_keep_forever"].(bool), - } - } - - return nil -} - -func flattenRunbookRetentionPeriod(r *runbooks.RunbookRetentionPeriod) []interface{} { - retentionPeriod := make(map[string]interface{}) - retentionPeriod["quantity_to_keep"] = int(r.QuantityToKeep) - retentionPeriod["should_keep_forever"] = r.ShouldKeepForever - return []interface{}{retentionPeriod} -} - -func getRunbookRetentionPeriodSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "quantity_to_keep": { - Default: 100, - Description: "How many runs to keep per environment.", - Optional: true, - Type: schema.TypeInt, - ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(0)), - }, - "should_keep_forever": { - Default: false, - Description: "Indicates if items should never be deleted. The default value is `false`.", - Optional: true, - Type: schema.TypeBool, - ConflictsWith: []string{"retention_policy.quantity_to_keep"}, - }, - } -} diff --git a/octopusdeploy_framework/framework_provider.go b/octopusdeploy_framework/framework_provider.go index 95bf6c0a1..fc1128666 100644 --- a/octopusdeploy_framework/framework_provider.go +++ b/octopusdeploy_framework/framework_provider.go @@ -94,6 +94,7 @@ func (p *octopusDeployFrameworkProvider) Resources(ctx context.Context) []func() NewVariableResource, NewProjectResource, NewDockerContainerRegistryFeedResource, + NewRunbookResource, } } diff --git a/octopusdeploy_framework/resource_project.go b/octopusdeploy_framework/resource_project.go index bcefbbf69..7a8ece169 100644 --- a/octopusdeploy_framework/resource_project.go +++ b/octopusdeploy_framework/resource_project.go @@ -94,7 +94,7 @@ func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, re project, err := projects.GetByID(r.Client, state.SpaceID.ValueString(), state.ID.ValueString()) if err != nil { - if err := errors.ProcessApiErrorV2(ctx, resp, state, err, "lifecycle"); err != nil { + if err := errors.ProcessApiErrorV2(ctx, resp, state, err, "project"); err != nil { resp.Diagnostics.AddError("Error reading project", err.Error()) } return diff --git a/octopusdeploy_framework/resource_runbook.go b/octopusdeploy_framework/resource_runbook.go new file mode 100644 index 000000000..066f32b13 --- /dev/null +++ b/octopusdeploy_framework/resource_runbook.go @@ -0,0 +1,179 @@ +package octopusdeploy_framework + +import ( + "context" + "fmt" + + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/runbooks" + "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/path" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +var _ resource.ResourceWithImportState = &runbookTypeResource{} + +type runbookTypeResource struct { + *Config +} + +func NewRunbookResource() resource.Resource { + return &runbookTypeResource{} +} + +func (*runbookTypeResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = util.GetTypeName(schemas.RunbookResourceDescription) +} + +func (*runbookTypeResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schemas.GetRunbookResourceSchema() +} + +func (r *runbookTypeResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.Config = ResourceConfiguration(req, resp) +} + +func (r *runbookTypeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan schemas.RunbookTypeResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + name := plan.Name.ValueString() + projectId := plan.ProjectID.ValueString() + + runbook := runbooks.NewRunbook(name, projectId) + if !plan.ID.IsNull() { + runbook.ID = plan.ID.ValueString() + } + + runbook.Description = plan.Description.ValueString() + runbook.RunbookProcessID = plan.RunbookProcessID.ValueString() + runbook.PublishedRunbookSnapshotID = plan.PublishedRunbookSnapshotID.ValueString() + runbook.SpaceID = plan.SpaceID.ValueString() + if !plan.MultiTenancyMode.IsNull() { + runbook.MultiTenancyMode = core.TenantedDeploymentMode(plan.MultiTenancyMode.ValueString()) + } + runbook.ConnectivityPolicy = schemas.MapToConnectivityPolicy(plan.ConnectivityPolicy) + runbook.EnvironmentScope = plan.EnvironmentScope.ValueString() + runbook.Environments = util.ExpandStringList(plan.Environments) + runbook.DefaultGuidedFailureMode = plan.DefaultGuidedFailureMode.ValueString() + runbook.RunRetentionPolicy = schemas.MapToRunbookRetentionPeriod(plan.RunRetentionPolicy) + runbook.ForcePackageDownload = plan.ForcePackageDownload.ValueBool() + + util.Create(ctx, schemas.RunbookResourceDescription, plan) + + createdRunbook, err := runbooks.Add(r.Config.Client, runbook) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("failed to create runbook (%s)", runbook.Name), err.Error()) + return + } + + resp.Diagnostics.Append(plan.RefreshFromApiResponse(ctx, createdRunbook)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + + util.Created(ctx, schemas.RunbookResourceDescription, createdRunbook) +} + +func (r *runbookTypeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state schemas.RunbookTypeResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + util.Reading(ctx, schemas.RunbookResourceDescription, state) + + runbook, err := runbooks.GetByID(r.Config.Client, state.SpaceID.ValueString(), state.ID.ValueString()) + if err != nil { + if err := errors.ProcessApiErrorV2(ctx, resp, state, err, schemas.RunbookResourceDescription); err != nil { + resp.Diagnostics.AddError("failed to load runbook", err.Error()) + } + return + } + + resp.Diagnostics.Append(state.RefreshFromApiResponse(ctx, runbook)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) + + util.Read(ctx, schemas.RunbookResourceDescription, runbook) +} + +func (r *runbookTypeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state schemas.RunbookTypeResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + util.Update(ctx, schemas.RunbookResourceDescription, plan) + + runbook, err := runbooks.GetByID(r.Config.Client, state.SpaceID.ValueString(), state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("unable to load runbook", err.Error()) + return + } + + updatedRunbook := runbooks.NewRunbook(plan.Name.ValueString(), plan.ProjectID.ValueString()) + updatedRunbook.ID = runbook.GetID() + updatedRunbook.SpaceID = runbook.SpaceID + updatedRunbook.Description = plan.Description.ValueString() + updatedRunbook.RunbookProcessID = plan.RunbookProcessID.ValueString() + updatedRunbook.PublishedRunbookSnapshotID = plan.PublishedRunbookSnapshotID.ValueString() + if !plan.MultiTenancyMode.IsNull() { + updatedRunbook.MultiTenancyMode = core.TenantedDeploymentMode(plan.MultiTenancyMode.ValueString()) + } + updatedRunbook.ConnectivityPolicy = schemas.MapToConnectivityPolicy(plan.ConnectivityPolicy) + updatedRunbook.EnvironmentScope = plan.EnvironmentScope.ValueString() + updatedRunbook.Environments = util.ExpandStringList(plan.Environments) + updatedRunbook.DefaultGuidedFailureMode = plan.DefaultGuidedFailureMode.ValueString() + updatedRunbook.RunRetentionPolicy = schemas.MapToRunbookRetentionPeriod(plan.RunRetentionPolicy) + updatedRunbook.ForcePackageDownload = plan.ForcePackageDownload.ValueBool() + + updatedRunbook, err = runbooks.Update(r.Config.Client, updatedRunbook) + if err != nil { + resp.Diagnostics.AddError("failed to update runbook", err.Error()) + } + + resp.Diagnostics.Append(plan.RefreshFromApiResponse(ctx, updatedRunbook)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + + util.Updated(ctx, schemas.RunbookResourceDescription, updatedRunbook) +} + +func (*runbookTypeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *runbookTypeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state schemas.RunbookTypeResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + util.Delete(ctx, schemas.RunbookResourceDescription, state) + + if err := runbooks.DeleteByID(r.Config.Client, state.SpaceID.ValueString(), state.ID.ValueString()); err != nil { + resp.Diagnostics.AddError("failed to delete runbook", err.Error()) + return + } + + util.Deleted(ctx, schemas.RunbookResourceDescription, state) + resp.State.RemoveResource(ctx) +} diff --git a/octopusdeploy_framework/resource_runbook_migration_test.go b/octopusdeploy_framework/resource_runbook_migration_test.go new file mode 100644 index 000000000..3eb66928a --- /dev/null +++ b/octopusdeploy_framework/resource_runbook_migration_test.go @@ -0,0 +1,121 @@ +package octopusdeploy_framework + +import ( + "fmt" + "os" + "testing" + + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/runbooks" + internaltest "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal/test" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/stretchr/testify/assert" +) + +func TestRunbookResource_UpgradeFromSDK_ToPluginFramework(t *testing.T) { + internaltest.SkipCI(t, "'octopusdeploy_runbook.runbook1' - expected NoOp, got action(s): [update]") + os.Setenv("TF_CLI_CONFIG_FILE=", "") + + name := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) + resource.Test(t, resource.TestCase{ + CheckDestroy: testRunbookDestroyed, + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "octopusdeploy": { + VersionConstraint: "0.22.0", + Source: "OctopusDeployLabs/octopusdeploy", + }, + }, + Config: runbookConfig(name), + }, + { + ProtoV6ProviderFactories: ProtoV6ProviderFactories(), + Config: runbookConfig(name), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("octopusdeploy_runbook.runbook1", "NoOp"), + }, + }, + }, + { + ProtoV6ProviderFactories: ProtoV6ProviderFactories(), + Config: updatedRunbookConfig(name), + Check: resource.ComposeTestCheckFunc( + testRunbookUpdated(t, name), + ), + }, + }, + }) +} + +func runbookConfig(name string) string { + return fmt.Sprintf(` + resource "octopusdeploy_project" "project1" { + name = "project %[1]s" + lifecycle_id = "Lifecycles-1" + project_group_id = "ProjectGroups-1" + } + resource "octopusdeploy_runbook" "runbook1" { + project_id = octopusdeploy_project.project1.id + name = "runbook %[1]s" + } + `, name) +} + +func updatedRunbookConfig(name string) string { + return fmt.Sprintf(` + resource "octopusdeploy_project" "project1" { + name = "project %[1]s" + lifecycle_id = "Lifecycles-1" + project_group_id = "ProjectGroups-1" + } + resource "octopusdeploy_runbook" "runbook1" { + project_id = octopusdeploy_project.project1.id + name = "runbook %[1]s" + description = "description %[1]s" + connectivity_policy { + allow_deployments_to_no_targets = true + exclude_unhealthy_targets = true + } + retention_policy { + quantity_to_keep = 10 + } + } + `, name) +} + +func testRunbookDestroyed(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "octopusdeploy_runbook" { + runbook, err := runbooks.GetByID(octoClient, octoClient.GetSpaceID(), rs.Primary.ID) + if err == nil && runbook != nil { + return fmt.Errorf("runbook (%s) still exists", rs.Primary.ID) + } + } + } + + return nil +} + +func testRunbookUpdated(t *testing.T, name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + projectId := s.RootModule().Resources["octopusdeploy_project.project1"].Primary.ID + runbookId := s.RootModule().Resources["octopusdeploy_runbook.runbook1"].Primary.ID + runbook, err := runbooks.GetByID(octoClient, octoClient.GetSpaceID(), runbookId) + if err != nil { + return fmt.Errorf("failed to retrieve runbook by ID: %s", err) + } + + assert.NotEmpty(t, runbook.ID, "Runbook ID did not match expected value") + assert.Equal(t, runbook.ProjectID, projectId) + assert.Equal(t, runbook.Description, fmt.Sprintf("description %s", name)) + assert.True(t, runbook.ConnectivityPolicy.AllowDeploymentsToNoTargets, "allow_deployments_to_no_targets should be true") + assert.True(t, runbook.ConnectivityPolicy.ExcludeUnhealthyTargets, "exclude_unhealthy_targets should be true") + assert.Equal(t, runbook.RunRetentionPolicy.QuantityToKeep, 10) + + return nil + } +} diff --git a/octopusdeploy/resource_runbook_process_test.go b/octopusdeploy_framework/resource_runbook_test.go similarity index 99% rename from octopusdeploy/resource_runbook_process_test.go rename to octopusdeploy_framework/resource_runbook_test.go index 0f1de80d0..992b38238 100644 --- a/octopusdeploy/resource_runbook_process_test.go +++ b/octopusdeploy_framework/resource_runbook_test.go @@ -1,11 +1,12 @@ -package octopusdeploy +package octopusdeploy_framework import ( "fmt" - "github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework/octoclient" - "github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework/test" "strings" "testing" + + "github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework/octoclient" + "github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework/test" ) // TestRunbookResource verifies that a runbook can be reimported with the correct settings diff --git a/octopusdeploy_framework/schemas/connectivity_policy.go b/octopusdeploy_framework/schemas/connectivity_policy.go new file mode 100644 index 000000000..508718f3b --- /dev/null +++ b/octopusdeploy_framework/schemas/connectivity_policy.go @@ -0,0 +1,142 @@ +package schemas + +import ( + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + resourceSchema "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var runbookConnectivityPolicySchemeAttributeNames = struct { + AllowDeploymentsToNoTargets string + ExcludeUnhealthyTargets string + SkipMachineBehavior string + TargetRoles string +}{ + AllowDeploymentsToNoTargets: "allow_deployments_to_no_targets", + ExcludeUnhealthyTargets: "exclude_unhealthy_targets", + SkipMachineBehavior: "skip_machine_behavior", + TargetRoles: "target_roles", +} + +var skipMachineBehaviorNames = struct { + SkipUnavailableMachines string + None string +}{ + SkipUnavailableMachines: "SkipUnavailableMachines", + None: "None", +} + +var skipMachineBehaviors = []string{ + skipMachineBehaviorNames.SkipUnavailableMachines, + skipMachineBehaviorNames.None, +} + +func GetConnectivityPolicyObjectType() map[string]attr.Type { + return map[string]attr.Type{ + runbookConnectivityPolicySchemeAttributeNames.AllowDeploymentsToNoTargets: types.BoolType, + runbookConnectivityPolicySchemeAttributeNames.ExcludeUnhealthyTargets: types.BoolType, + runbookConnectivityPolicySchemeAttributeNames.SkipMachineBehavior: types.StringType, + runbookConnectivityPolicySchemeAttributeNames.TargetRoles: types.ListType{ElemType: types.StringType}, + } +} + +func getConnectivityPolicySchema() map[string]resourceSchema.Attribute { + return map[string]resourceSchema.Attribute{ + runbookConnectivityPolicySchemeAttributeNames.AllowDeploymentsToNoTargets: resourceSchema.BoolAttribute{ + Computed: true, + Optional: true, + Default: booldefault.StaticBool(true), + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, + }, + runbookConnectivityPolicySchemeAttributeNames.ExcludeUnhealthyTargets: resourceSchema.BoolAttribute{ + Computed: true, + Optional: true, + Default: booldefault.StaticBool(false), + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, + }, + runbookConnectivityPolicySchemeAttributeNames.SkipMachineBehavior: resourceSchema.StringAttribute{ + Computed: true, + Optional: true, + Default: stringdefault.StaticString(skipMachineBehaviorNames.None), + Validators: []validator.String{ + stringvalidator.OneOf( + skipMachineBehaviors..., + ), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + runbookConnectivityPolicySchemeAttributeNames.TargetRoles: resourceSchema.ListAttribute{ + Computed: true, + Optional: true, + ElementType: types.StringType, + PlanModifiers: []planmodifier.List{ + listplanmodifier.UseStateForUnknown(), + }, + }, + } +} + +func GetDefaultConnectivityPolicy() *core.ConnectivityPolicy { + return &core.ConnectivityPolicy{ + AllowDeploymentsToNoTargets: true, + ExcludeUnhealthyTargets: false, + SkipMachineBehavior: core.SkipMachineBehaviorNone, + TargetRoles: []string{}, + } +} + +func MapFromConnectivityPolicy(connectivityPolicy *core.ConnectivityPolicy) attr.Value { + if connectivityPolicy == nil { + return MapFromConnectivityPolicy(GetDefaultConnectivityPolicy()) + } + + attrs := map[string]attr.Value{ + runbookConnectivityPolicySchemeAttributeNames.AllowDeploymentsToNoTargets: types.BoolValue(connectivityPolicy.AllowDeploymentsToNoTargets), + runbookConnectivityPolicySchemeAttributeNames.ExcludeUnhealthyTargets: types.BoolValue(connectivityPolicy.ExcludeUnhealthyTargets), + runbookConnectivityPolicySchemeAttributeNames.SkipMachineBehavior: types.StringValue(string(connectivityPolicy.SkipMachineBehavior)), + runbookConnectivityPolicySchemeAttributeNames.TargetRoles: util.FlattenStringList(connectivityPolicy.TargetRoles), + } + + return types.ObjectValueMust(GetConnectivityPolicyObjectType(), attrs) +} + +func MapToConnectivityPolicy(flattenedConnectivityPolicy types.List) *core.ConnectivityPolicy { + if flattenedConnectivityPolicy.IsNull() || len(flattenedConnectivityPolicy.Elements()) == 0 { + return GetDefaultConnectivityPolicy() + } + + obj := flattenedConnectivityPolicy.Elements()[0].(types.Object) + attrs := obj.Attributes() + + var connectivityPolicy core.ConnectivityPolicy + if allowDeploymentsToNoTargets, ok := attrs[runbookConnectivityPolicySchemeAttributeNames.AllowDeploymentsToNoTargets].(types.Bool); ok && !allowDeploymentsToNoTargets.IsNull() { + connectivityPolicy.AllowDeploymentsToNoTargets = allowDeploymentsToNoTargets.ValueBool() + } + if excludeUnhealthyTargets, ok := attrs[runbookConnectivityPolicySchemeAttributeNames.ExcludeUnhealthyTargets].(types.Bool); ok && !excludeUnhealthyTargets.IsNull() { + connectivityPolicy.ExcludeUnhealthyTargets = excludeUnhealthyTargets.ValueBool() + } + if skipMachineBehavior, ok := attrs[runbookConnectivityPolicySchemeAttributeNames.SkipMachineBehavior].(types.String); ok && !skipMachineBehavior.IsNull() { + connectivityPolicy.SkipMachineBehavior = core.SkipMachineBehavior(skipMachineBehavior.ValueString()) + } + if targetRoles, ok := attrs[runbookConnectivityPolicySchemeAttributeNames.TargetRoles].(types.List); ok && !targetRoles.IsNull() { + connectivityPolicy.TargetRoles = util.ExpandStringList(targetRoles) + } + + return &connectivityPolicy +} diff --git a/octopusdeploy_framework/schemas/runbook.go b/octopusdeploy_framework/schemas/runbook.go new file mode 100644 index 000000000..2bc493d94 --- /dev/null +++ b/octopusdeploy_framework/schemas/runbook.go @@ -0,0 +1,288 @@ +package schemas + +import ( + "context" + "fmt" + "regexp" + "strings" + + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/runbooks" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + 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/listplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +const RunbookResourceDescription = "runbook" + +var RunbookSchemaAttributeNames = struct { + ID string + Name string + Description string + ProjectID string + RunbookProcessID string + PublishedRunbookSnapshotID string + SpaceID string + MultiTenancyMode string + ConnectivityPolicy string + EnvironmentScope string + Environments string + DefaultGuidedFailureMode string + RetentionPolicy string + ForcePackageDownload string +}{ + ID: "id", + Name: "name", + Description: "description", + ProjectID: "project_id", + RunbookProcessID: "runbook_process_id", + PublishedRunbookSnapshotID: "published_runbook_snapshot_id", + SpaceID: "space_id", + MultiTenancyMode: "multi_tenancy_mode", + ConnectivityPolicy: "connectivity_policy", + EnvironmentScope: "environment_scope", + Environments: "environments", + DefaultGuidedFailureMode: "default_guided_failure_mode", + RetentionPolicy: "retention_policy", + ForcePackageDownload: "force_package_download", +} + +var tenantedDeploymentModeNames = struct { + Untenanted string + TenantedOrUntenanted string + Tenanted string +}{ + Untenanted: "Untenanted", + TenantedOrUntenanted: "TenantedOrUntenanted", + Tenanted: "Tenanted", +} + +var tenantedDeploymentModes = []string{ + tenantedDeploymentModeNames.Untenanted, + tenantedDeploymentModeNames.TenantedOrUntenanted, + tenantedDeploymentModeNames.Tenanted, +} + +var environmentScopeNames = struct { + All string + Specified string + FromProjectLifecycles string +}{ + All: "All", + Specified: "Specified", + FromProjectLifecycles: "FromProjectLifecycles", +} + +var environmentScopeTypes = []string{ + environmentScopeNames.All, + environmentScopeNames.Specified, + environmentScopeNames.FromProjectLifecycles, +} + +var defaultGuidedFailureModeNames = struct { + EnvironmentDefault string + Off string + On string +}{ + EnvironmentDefault: "EnvironmentDefault", + Off: "Off", + On: "On", +} + +var defaultGuidedFailureModes = []string{ + defaultGuidedFailureModeNames.EnvironmentDefault, + defaultGuidedFailureModeNames.Off, + defaultGuidedFailureModeNames.On, +} + +type RunbookTypeResourceModel struct { + Name types.String `tfsdk:"name"` + ProjectID types.String `tfsdk:"project_id"` + Description types.String `tfsdk:"description"` + RunbookProcessID types.String `tfsdk:"runbook_process_id"` + PublishedRunbookSnapshotID types.String `tfsdk:"published_runbook_snapshot_id"` + SpaceID types.String `tfsdk:"space_id"` + MultiTenancyMode types.String `tfsdk:"multi_tenancy_mode"` + ConnectivityPolicy types.List `tfsdk:"connectivity_policy"` + EnvironmentScope types.String `tfsdk:"environment_scope"` + Environments types.List `tfsdk:"environments"` + DefaultGuidedFailureMode types.String `tfsdk:"default_guided_failure_mode"` + RunRetentionPolicy types.List `tfsdk:"retention_policy"` + ForcePackageDownload types.Bool `tfsdk:"force_package_download"` + + ResourceModel +} + +type RunbookRetentionPeriodModel struct { + QuantityToKeep types.String `tfsdk:"quantity_to_keep"` + ShouldKeepForever types.Bool `tfsdk:"should_keep_forever"` +} + +type RunbookConnectivityPolicyModel struct { + AllowDeploymentsToNoTargets types.Bool `tfsdk:"allow_deployments_to_no_targets"` + ExcludeUnhealthyTargets types.Bool `tfsdk:"exclude_unhealthy_targets"` + SkipMachineBehavior types.String `tfsdk:"skip_machine_behaviour"` + TargetRoles types.List `tfsdk:"target_roles"` +} + +func GetRunbookResourceSchema() resourceSchema.Schema { + return resourceSchema.Schema{ + Description: util.GetResourceSchemaDescription(RunbookResourceDescription), + Attributes: map[string]resourceSchema.Attribute{ + RunbookSchemaAttributeNames.ID: util.GetIdResourceSchema(), + RunbookSchemaAttributeNames.Name: resourceSchema.StringAttribute{ + Description: "The name of the runbook in Octopus Deploy. This name must be unique.", + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile(`\S+`), + "expected value to not be an empty string or whitespace", + ), + }, + }, + RunbookSchemaAttributeNames.Description: util.GetDescriptionResourceSchema(RunbookResourceDescription), + RunbookSchemaAttributeNames.ProjectID: resourceSchema.StringAttribute{ + Description: "The project that this runbook belongs to.", + Required: true, + }, + RunbookSchemaAttributeNames.RunbookProcessID: resourceSchema.StringAttribute{ + Description: "The runbook process ID.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + RunbookSchemaAttributeNames.PublishedRunbookSnapshotID: resourceSchema.StringAttribute{ + Description: "The published snapshot ID.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + RunbookSchemaAttributeNames.SpaceID: util.GetSpaceIdResourceSchema(RunbookResourceDescription), + RunbookSchemaAttributeNames.MultiTenancyMode: resourceSchema.StringAttribute{ + Description: fmt.Sprintf("The tenanted deployment mode of the runbook. Valid modes are %s", strings.Join(util.Map(tenantedDeploymentModes, func(item string) string { return fmt.Sprintf("`%s`", item) }), ", ")), + Computed: true, + Optional: true, + Validators: []validator.String{ + stringvalidator.OneOf(tenantedDeploymentModes...), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + RunbookSchemaAttributeNames.EnvironmentScope: resourceSchema.StringAttribute{ + Description: "Determines how the runbook is scoped to environments.", + Computed: true, + Optional: true, + Validators: []validator.String{ + stringvalidator.OneOf(environmentScopeTypes...), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + RunbookSchemaAttributeNames.Environments: resourceSchema.ListAttribute{ + Description: fmt.Sprintf("When %s is set to \"%s\", this is the list of environments the runbook can be run against.", RunbookSchemaAttributeNames.EnvironmentScope, environmentScopeNames.Specified), + Optional: true, + Computed: true, + ElementType: types.StringType, + PlanModifiers: []planmodifier.List{ + listplanmodifier.UseStateForUnknown(), + }, + }, + RunbookSchemaAttributeNames.DefaultGuidedFailureMode: resourceSchema.StringAttribute{ + Description: "Sets the runbook guided failure mode.", + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.OneOf(defaultGuidedFailureModes...), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + RunbookSchemaAttributeNames.ForcePackageDownload: resourceSchema.BoolAttribute{ + Description: "Whether to force packages to be re-downloaded or not.", + Computed: true, + Optional: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, + }, + }, + Blocks: map[string]resourceSchema.Block{ + RunbookSchemaAttributeNames.ConnectivityPolicy: resourceSchema.ListNestedBlock{ + NestedObject: resourceSchema.NestedBlockObject{ + Attributes: getConnectivityPolicySchema(), + }, + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + }, + RunbookSchemaAttributeNames.RetentionPolicy: resourceSchema.ListNestedBlock{ + Description: "Sets the runbook retention policy.", + NestedObject: resourceSchema.NestedBlockObject{ + Attributes: getRunbookRetentionPeriodSchema(), + }, + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + }, + }, + } +} + +func (data *RunbookTypeResourceModel) RefreshFromApiResponse(ctx context.Context, runbook *runbooks.Runbook) diag.Diagnostics { + var diags diag.Diagnostics + + if runbook == nil { + return diags + } + + data.ID = types.StringValue(runbook.ID) + data.Name = types.StringValue(runbook.Name) + data.ProjectID = types.StringValue(runbook.ProjectID) + data.Description = types.StringValue(runbook.Description) + data.RunbookProcessID = types.StringValue(runbook.RunbookProcessID) + data.PublishedRunbookSnapshotID = types.StringValue(runbook.PublishedRunbookSnapshotID) + data.SpaceID = types.StringValue(runbook.SpaceID) + data.MultiTenancyMode = types.StringValue(string(runbook.MultiTenancyMode)) + data.EnvironmentScope = types.StringValue(runbook.EnvironmentScope) + data.Environments = util.FlattenStringList(runbook.Environments) + data.DefaultGuidedFailureMode = types.StringValue(runbook.DefaultGuidedFailureMode) + data.ForcePackageDownload = types.BoolValue(runbook.ForcePackageDownload) + if !data.ConnectivityPolicy.IsNull() { + result, d := types.ListValueFrom( + ctx, + types.ObjectType{AttrTypes: GetConnectivityPolicyObjectType()}, + []attr.Value{MapFromConnectivityPolicy(runbook.ConnectivityPolicy)}, + ) + diags.Append(d...) + data.ConnectivityPolicy = result + } /*else { + data.ConnectivityPolicy = types.ListValueMust( + types.ObjectType{AttrTypes: GetConnectivityPolicyObjectType()}, + []attr.Value{MapFromConnectivityPolicy(GetDefaultConnectivityPolicy())}, + ) + }*/ + if !data.RunRetentionPolicy.IsNull() { + result, d := types.ListValueFrom( + ctx, + types.ObjectType{AttrTypes: GetRunbookRetentionPeriodObjectType()}, + []attr.Value{MapFromRunbookRetentionPeriod(runbook.RunRetentionPolicy)}, + ) + diags.Append(d...) + data.RunRetentionPolicy = result + } + + return diags +} diff --git a/octopusdeploy_framework/schemas/runbook_retention_period.go b/octopusdeploy_framework/schemas/runbook_retention_period.go new file mode 100644 index 000000000..d8bbdc8b6 --- /dev/null +++ b/octopusdeploy_framework/schemas/runbook_retention_period.go @@ -0,0 +1,99 @@ +package schemas + +import ( + "fmt" + + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/runbooks" + "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + resourceSchema "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var runbookRetentionPeriodSchemeAttributeNames = struct { + QuantityToKeep string + ShouldKeepForever string +}{ + QuantityToKeep: "quantity_to_keep", + ShouldKeepForever: "should_keep_forever", +} + +func GetRunbookRetentionPeriodObjectType() map[string]attr.Type { + return map[string]attr.Type{ + runbookRetentionPeriodSchemeAttributeNames.QuantityToKeep: types.Int64Type, + runbookRetentionPeriodSchemeAttributeNames.ShouldKeepForever: types.BoolType, + } +} + +func getRunbookRetentionPeriodSchema() map[string]resourceSchema.Attribute { + return map[string]resourceSchema.Attribute{ + runbookRetentionPeriodSchemeAttributeNames.QuantityToKeep: resourceSchema.Int64Attribute{ + Description: "How many runs to keep per environment.", + Computed: true, + Optional: true, + Validators: []validator.Int64{ + int64validator.AtLeast(0), + }, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, + }, + runbookRetentionPeriodSchemeAttributeNames.ShouldKeepForever: resourceSchema.BoolAttribute{ + Description: "Indicates if items should never be deleted. The default value is `false`.", + Computed: true, + Optional: true, + Default: booldefault.StaticBool(false), + Validators: []validator.Bool{ + boolvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName(runbookRetentionPeriodSchemeAttributeNames.QuantityToKeep)), + }, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, + }, + } +} + +func GetDefaultRunbookRetentionPeriod() *runbooks.RunbookRetentionPeriod { + return &runbooks.RunbookRetentionPeriod{ + QuantityToKeep: 100, + ShouldKeepForever: false, + } +} + +func MapFromRunbookRetentionPeriod(retentionPeriod *runbooks.RunbookRetentionPeriod) attr.Value { + if retentionPeriod == nil { + return MapFromRunbookRetentionPeriod(GetDefaultRunbookRetentionPeriod()) + } + + attrs := map[string]attr.Value{ + runbookRetentionPeriodSchemeAttributeNames.QuantityToKeep: types.Int64Value(int64(retentionPeriod.QuantityToKeep)), + runbookRetentionPeriodSchemeAttributeNames.ShouldKeepForever: types.BoolValue(retentionPeriod.ShouldKeepForever), + } + + return types.ObjectValueMust(GetRunbookRetentionPeriodObjectType(), attrs) +} + +func MapToRunbookRetentionPeriod(flattenedRunbookRetentionPeriod types.List) *runbooks.RunbookRetentionPeriod { + if flattenedRunbookRetentionPeriod.IsNull() || len(flattenedRunbookRetentionPeriod.Elements()) == 0 { + return GetDefaultRunbookRetentionPeriod() + } + obj := flattenedRunbookRetentionPeriod.Elements()[0].(types.Object) + attrs := obj.Attributes() + + var runbookRetentionPeriod runbooks.RunbookRetentionPeriod + if quantityToKeep, ok := attrs[runbookRetentionPeriodSchemeAttributeNames.QuantityToKeep].(types.Int64); ok && !quantityToKeep.IsNull() { + runbookRetentionPeriod.QuantityToKeep = int32(quantityToKeep.ValueInt64()) + } + if shouldKeepForever, ok := attrs[runbookRetentionPeriodSchemeAttributeNames.ShouldKeepForever].(types.Bool); ok && !shouldKeepForever.IsNull() { + runbookRetentionPeriod.ShouldKeepForever = shouldKeepForever.ValueBool() + } + fmt.Printf("runbook retention period: %#v", runbookRetentionPeriod) + return &runbookRetentionPeriod +} diff --git a/octopusdeploy_framework/util/logging.go b/octopusdeploy_framework/util/logging.go index 139d7932d..66b3ca311 100644 --- a/octopusdeploy_framework/util/logging.go +++ b/octopusdeploy_framework/util/logging.go @@ -3,6 +3,7 @@ package util import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-log/tflog" ) @@ -13,3 +14,27 @@ func Create(ctx context.Context, resource string, v ...any) { func Created(ctx context.Context, resource string, v ...any) { tflog.Info(ctx, fmt.Sprintf("created %s: %#v", resource, v)) } + +func Delete(ctx context.Context, resource string, v ...any) { + tflog.Info(ctx, fmt.Sprintf("deleting %s: %#v", resource, v)) +} + +func Deleted(ctx context.Context, resource string, v ...any) { + tflog.Info(ctx, fmt.Sprintf("deleted %s: %#v", resource, v)) +} + +func Reading(ctx context.Context, resource string, v ...any) { + tflog.Info(ctx, fmt.Sprintf("reading %s: %#v", resource, v)) +} + +func Read(ctx context.Context, resource string, v ...any) { + tflog.Info(ctx, fmt.Sprintf("read %s: %#v", resource, v)) +} + +func Update(ctx context.Context, resource string, v ...any) { + tflog.Info(ctx, fmt.Sprintf("updating %s: %#v", resource, v)) +} + +func Updated(ctx context.Context, resource string, v ...any) { + tflog.Info(ctx, fmt.Sprintf("updated %s: %#v", resource, v)) +} From be6e0ecbc40204f589722263e49e681a6984f160 Mon Sep 17 00:00:00 2001 From: Isaac Calligeros <101079287+IsaacCalligeros95@users.noreply.github.com> Date: Mon, 12 Aug 2024 13:27:53 +0930 Subject: [PATCH 11/28] Chore!: migrate tenant resource and datasource (#707) * Migrate tenants resource --- docs/resources/tenant.md | 2 +- octopusdeploy/provider.go | 1 - octopusdeploy/resource_tenant.go | 108 ----------- octopusdeploy/schema_tenant.go | 104 ----------- octopusdeploy/schema_tenant_test.go | 40 ---- octopusdeploy_framework/framework_provider.go | 1 + octopusdeploy_framework/resource_tenant.go | 174 ++++++++++++++++++ .../resource_tenant_migration_test.go | 109 +++++++++++ .../resource_tenant_test.go | 11 +- octopusdeploy_framework/schemas/tenant.go | 31 +++- 10 files changed, 320 insertions(+), 261 deletions(-) delete mode 100644 octopusdeploy/resource_tenant.go delete mode 100644 octopusdeploy/schema_tenant.go delete mode 100644 octopusdeploy/schema_tenant_test.go create mode 100644 octopusdeploy_framework/resource_tenant.go create mode 100644 octopusdeploy_framework/resource_tenant_migration_test.go rename {octopusdeploy => octopusdeploy_framework}/resource_tenant_test.go (95%) diff --git a/docs/resources/tenant.md b/docs/resources/tenant.md index ba3ce6aa6..16851becb 100644 --- a/docs/resources/tenant.md +++ b/docs/resources/tenant.md @@ -21,7 +21,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. - `id` (String) The unique ID for this resource. -- `space_id` (String) The space ID associated with this resource. +- `space_id` (String) The space ID associated with this tenant. - `tenant_tags` (List of String) A list of tenant tags associated with this resource. ~> **NOTE property `project_environment` deprecated:** The `project_environment` property has been replaced by the `octopusdeploy_tenant_project` resource to allow more advanced provisioning scenarioes. \ No newline at end of file diff --git a/octopusdeploy/provider.go b/octopusdeploy/provider.go index 794853fff..ebdaf0b94 100644 --- a/octopusdeploy/provider.go +++ b/octopusdeploy/provider.go @@ -67,7 +67,6 @@ func Provider() *schema.Provider { "octopusdeploy_tag": resourceTag(), "octopusdeploy_tag_set": resourceTagSet(), "octopusdeploy_team": resourceTeam(), - "octopusdeploy_tenant": resourceTenant(), "octopusdeploy_tentacle_certificate": resourceTentacleCertificate(), "octopusdeploy_token_account": resourceTokenAccount(), "octopusdeploy_user": resourceUser(), diff --git a/octopusdeploy/resource_tenant.go b/octopusdeploy/resource_tenant.go deleted file mode 100644 index 899cf6f48..000000000 --- a/octopusdeploy/resource_tenant.go +++ /dev/null @@ -1,108 +0,0 @@ -package octopusdeploy - -import ( - "context" - "log" - - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client" - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tenants" - "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal" - "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 resourceTenant() *schema.Resource { - return &schema.Resource{ - CreateContext: resourceTenantCreate, - DeleteContext: resourceTenantDelete, - Description: "This resource manages tenants in Octopus Deploy.", - Importer: getImporter(), - ReadContext: resourceTenantRead, - Schema: getTenantSchema(), - UpdateContext: resourceTenantUpdate, - } -} - -func resourceTenantCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - internal.Mutex.Lock() - defer internal.Mutex.Unlock() - - tenant := expandTenant(d) - - log.Printf("[INFO] creating tenant: %#v", tenant) - - client := m.(*client.Client) - createdTenant, err := tenants.Add(client, tenant) - if err != nil { - return diag.FromErr(err) - } - - if err := setTenant(ctx, d, createdTenant); err != nil { - return diag.FromErr(err) - } - - d.SetId(createdTenant.GetID()) - - log.Printf("[INFO] tenant created (%s)", d.Id()) - return nil -} - -func resourceTenantDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - internal.Mutex.Lock() - defer internal.Mutex.Unlock() - - log.Printf("[INFO] deleting tenant (%s)", d.Id()) - - client := m.(*client.Client) - if err := tenants.DeleteByID(client, d.Get("space_id").(string), d.Id()); err != nil { - return diag.FromErr(err) - } - - log.Printf("[INFO] tenant deleted (%s)", d.Id()) - d.SetId("") - return nil -} - -func resourceTenantRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - log.Printf("[INFO] reading tenant (%s)", d.Id()) - - client := m.(*client.Client) - tenant, err := tenants.GetByID(client, d.Get("space_id").(string), d.Id()) - if err != nil { - return errors.ProcessApiError(ctx, d, err, "tenant") - } - - if err := setTenant(ctx, d, tenant); err != nil { - return diag.FromErr(err) - } - - log.Printf("[INFO] tenant read (%s)", d.Id()) - return nil -} - -func resourceTenantUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - internal.Mutex.Lock() - defer internal.Mutex.Unlock() - - log.Printf("[INFO] updating tenant (%s)", d.Id()) - - client := m.(*client.Client) - tenantFromApi, err := tenants.GetByID(client, d.Get("space_id").(string), d.Id()) - - tenant := expandTenant(d) - - // the project environments are not managed here, so we need to maintain the collection when updating - tenant.ProjectEnvironments = tenantFromApi.ProjectEnvironments - updatedTenant, err := tenants.Update(client, tenant) - if err != nil { - return diag.FromErr(err) - } - - if err := setTenant(ctx, d, updatedTenant); err != nil { - return diag.FromErr(err) - } - - log.Printf("[INFO] tenant updated (%s)", d.Id()) - return nil -} diff --git a/octopusdeploy/schema_tenant.go b/octopusdeploy/schema_tenant.go deleted file mode 100644 index 260ba93f8..000000000 --- a/octopusdeploy/schema_tenant.go +++ /dev/null @@ -1,104 +0,0 @@ -package octopusdeploy - -import ( - "context" - "fmt" - - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tenants" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func expandTenant(d *schema.ResourceData) *tenants.Tenant { - name := d.Get("name").(string) - - tenant := tenants.NewTenant(name) - tenant.ID = d.Id() - - if v, ok := d.GetOk("cloned_from_tenant_id"); ok { - tenant.ClonedFromTenantID = v.(string) - } - - if v, ok := d.GetOk("description"); ok { - tenant.Description = v.(string) - } - - if v, ok := d.GetOk("space_id"); ok { - tenant.SpaceID = v.(string) - } - - if v, ok := d.GetOk("tenant_tags"); ok { - tenant.TenantTags = getSliceFromTerraformTypeList(v) - } - - return tenant -} - -func flattenTenant(tenant *tenants.Tenant) map[string]interface{} { - if tenant == nil { - return nil - } - - return map[string]interface{}{ - "cloned_from_tenant_id": tenant.ClonedFromTenantID, - "description": tenant.Description, - "id": tenant.GetID(), - "name": tenant.Name, - "space_id": tenant.SpaceID, - "tenant_tags": tenant.TenantTags, - } -} - -func getTenantDataSchema() map[string]*schema.Schema { - dataSchema := getTenantSchema() - setDataSchema(&dataSchema) - - return map[string]*schema.Schema{ - "cloned_from_tenant_id": getQueryClonedFromTenantID(), - "id": getDataSchemaID(), - "ids": getQueryIDs(), - "is_clone": getQueryIsClone(), - "name": getQueryName(), - "partial_name": getQueryPartialName(), - "project_id": getQueryProjectID(), - "skip": getQuerySkip(), - "tags": getQueryTags(), - "space_id": getQuerySpaceID(), - "tenants": { - Computed: true, - Description: "A list of tenants that match the filter(s).", - Elem: &schema.Resource{Schema: dataSchema}, - Optional: false, - Type: schema.TypeList, - }, - "take": getQueryTake(), - } -} - -func getTenantSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "cloned_from_tenant_id": { - Description: "The ID of the tenant from which this tenant was cloned.", - Optional: true, - Type: schema.TypeString, - }, - "description": getDescriptionSchema("tenant"), - "id": getIDSchema(), - "name": getNameSchema(true), - "space_id": getSpaceIDSchema(), - "tenant_tags": getTenantTagsSchema(), - } -} - -func setTenant(ctx context.Context, d *schema.ResourceData, tenant *tenants.Tenant) error { - d.Set("cloned_from_tenant_id", tenant.ClonedFromTenantID) - d.Set("description", tenant.Description) - d.Set("id", tenant.GetID()) - d.Set("name", tenant.Name) - d.Set("space_id", tenant.SpaceID) - - if err := d.Set("tenant_tags", tenant.TenantTags); err != nil { - return fmt.Errorf("error setting tenant_tags: %s", err) - } - - return nil -} diff --git a/octopusdeploy/schema_tenant_test.go b/octopusdeploy/schema_tenant_test.go deleted file mode 100644 index 0a1d00d18..000000000 --- a/octopusdeploy/schema_tenant_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package octopusdeploy - -import ( - "reflect" - "testing" - - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tenants" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" - "github.com/stretchr/testify/require" -) - -func TestFlattenTenant(t *testing.T) { - clonedFromTenantID := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) - description := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) - id := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) - name := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) - projectEnvironments := map[string][]string{} - spaceID := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) - tenantTags := []string{acctest.RandStringFromCharSet(20, acctest.CharSetAlpha), acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)} - - expectedExpanded := tenants.NewTenant(name) - expectedExpanded.ClonedFromTenantID = clonedFromTenantID - expectedExpanded.Description = description - expectedExpanded.ID = id - expectedExpanded.ProjectEnvironments = projectEnvironments - expectedExpanded.SpaceID = spaceID - expectedExpanded.TenantTags = tenantTags - - expectedFlattened := map[string]interface{}{ - "cloned_from_tenant_id": clonedFromTenantID, - "description": description, - "id": id, - "name": name, - "space_id": spaceID, - "tenant_tags": tenantTags, - } - - actualFlattened := flattenTenant(expectedExpanded) - require.True(t, reflect.DeepEqual(expectedFlattened, actualFlattened)) -} diff --git a/octopusdeploy_framework/framework_provider.go b/octopusdeploy_framework/framework_provider.go index fc1128666..68686d521 100644 --- a/octopusdeploy_framework/framework_provider.go +++ b/octopusdeploy_framework/framework_provider.go @@ -95,6 +95,7 @@ func (p *octopusDeployFrameworkProvider) Resources(ctx context.Context) []func() NewProjectResource, NewDockerContainerRegistryFeedResource, NewRunbookResource, + NewTenantResource, } } diff --git a/octopusdeploy_framework/resource_tenant.go b/octopusdeploy_framework/resource_tenant.go new file mode 100644 index 000000000..3fcde55b3 --- /dev/null +++ b/octopusdeploy_framework/resource_tenant.go @@ -0,0 +1,174 @@ +package octopusdeploy_framework + +import ( + "context" + "fmt" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tenants" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal" + "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/attr" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "sort" +) + +type tenantTypeResource struct { + *Config +} + +func NewTenantResource() resource.Resource { + return &tenantTypeResource{} +} + +func (r *tenantTypeResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = util.GetTypeName("tenant") +} + +func (r *tenantTypeResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: schemas.GetTenantResourceSchema(), + Description: "This resource manages tenants in Octopus Deploy.", + } +} + +func (r *tenantTypeResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.Config = ResourceConfiguration(req, resp) +} + +func (r *tenantTypeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + internal.Mutex.Lock() + defer internal.Mutex.Unlock() + + var data *schemas.TenantModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + tenant, err := mapStateToTenant(data) + if err != nil { + return + } + + tflog.Info(ctx, fmt.Sprintf("creating Tenant: %s", tenant.Name)) + + createdTenant, err := tenants.Add(r.Config.Client, tenant) + if err != nil { + resp.Diagnostics.AddError("unable to create tenant", err.Error()) + return + } + + mapTenantToState(data, createdTenant) + + tflog.Info(ctx, fmt.Sprintf("Tenant created (%s)", data.ID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *tenantTypeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data *schemas.TenantModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Info(ctx, fmt.Sprintf("reading Tenant (%s)", data.ID)) + + client := r.Config.Client + tenant, err := tenants.GetByID(client, data.SpaceID.ValueString(), data.ID.ValueString()) + if err != nil { + if err := errors.ProcessApiErrorV2(ctx, resp, data, err, "tenant"); err != nil { + resp.Diagnostics.AddError("unable to load tenant", err.Error()) + } + return + } + + mapTenantToState(data, tenant) + + tflog.Info(ctx, fmt.Sprintf("Tenant read (%s)", tenant.GetID())) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *tenantTypeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + internal.Mutex.Lock() + defer internal.Mutex.Unlock() + + var data, state *schemas.TenantModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, fmt.Sprintf("updating tenant '%s'", data.ID.ValueString())) + + tenantFromApi, err := tenants.GetByID(r.Config.Client, data.SpaceID.ValueString(), data.ID.ValueString()) + + tenant, err := mapStateToTenant(data) + tenant.ID = state.ID.ValueString() + if err != nil { + resp.Diagnostics.AddError("unable to map to tenant", err.Error()) + return + } + + tflog.Info(ctx, fmt.Sprintf("updating Tenant (%s)", data.ID)) + + tenant.ProjectEnvironments = tenantFromApi.ProjectEnvironments + updatedTenant, err := tenants.Update(r.Config.Client, tenant) + if err != nil { + resp.Diagnostics.AddError("unable to update tenant", err.Error()) + return + } + + mapTenantToState(data, updatedTenant) + + tflog.Info(ctx, fmt.Sprintf("Tenant updated (%s)", data.ID)) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *tenantTypeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + internal.Mutex.Lock() + defer internal.Mutex.Unlock() + + var data schemas.TenantModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + if err := tenants.DeleteByID(r.Config.Client, data.SpaceID.ValueString(), data.ID.ValueString()); err != nil { + resp.Diagnostics.AddError("unable to delete tenant", err.Error()) + return + } +} + +func mapStateToTenant(data *schemas.TenantModel) (*tenants.Tenant, error) { + tenant := tenants.NewTenant(data.Name.ValueString()) + tenant.ID = data.ID.ValueString() + tenant.ClonedFromTenantID = data.ClonedFromTenantId.ValueString() + tenant.Description = data.Description.ValueString() + tenant.SpaceID = data.SpaceID.ValueString() + if len(data.TenantTags.Elements()) > 0 { + tenant.TenantTags = util.ExpandStringList(data.TenantTags) + } else { + tenant.TenantTags = []string{} + } + sort.Strings(tenant.TenantTags) + + return tenant, nil +} + +func mapTenantToState(data *schemas.TenantModel, tenant *tenants.Tenant) { + data.ID = types.StringValue(tenant.ID) + data.ClonedFromTenantId = types.StringValue(tenant.ClonedFromTenantID) + data.Description = types.StringValue(tenant.Description) + data.SpaceID = types.StringValue(tenant.SpaceID) + data.Name = types.StringValue(tenant.Name) + sort.Strings(tenant.TenantTags) + data.TenantTags = util.Ternary(tenant.TenantTags != nil && len(tenant.TenantTags) > 0, util.FlattenStringList(tenant.TenantTags), types.ListValueMust(types.StringType, make([]attr.Value, 0))) +} diff --git a/octopusdeploy_framework/resource_tenant_migration_test.go b/octopusdeploy_framework/resource_tenant_migration_test.go new file mode 100644 index 000000000..0ca7404e7 --- /dev/null +++ b/octopusdeploy_framework/resource_tenant_migration_test.go @@ -0,0 +1,109 @@ +package octopusdeploy_framework + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/stretchr/testify/assert" + "os" + "sort" + "testing" +) + +func TestTenantResource_UpgradeFromSDK_ToPluginFramework(t *testing.T) { + // override the path to check for terraformrc file and test against the real 0.21.1 version + os.Setenv("TF_CLI_CONFIG_FILE=", "") + + name := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) + resource.Test(t, resource.TestCase{ + CheckDestroy: testTenantProjectDestroyed, + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "octopusdeploy": { + VersionConstraint: "0.22.0", + Source: "OctopusDeployLabs/octopusdeploy", + }, + }, + Config: tenantConfig(), + }, + { + ProtoV6ProviderFactories: ProtoV6ProviderFactories(), + Config: tenantConfig(), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, + }, + { + ProtoV6ProviderFactories: ProtoV6ProviderFactories(), + Config: updatedTenantResourceConfig(), + Check: resource.ComposeTestCheckFunc( + testTenantResourceUpdated(t, name), + ), + }, + }, + }) +} + +func tenantConfig() string { + return fmt.Sprintf(` + resource "octopusdeploy_tenant" "tenant1" { + name = "tenant test" + }`) +} + +func updatedTenantResourceConfig() string { + return fmt.Sprintf(` +resource "octopusdeploy_tag_set" "tagset_tag1" { + name = "tag1" + description = "Test tagset" + sort_order = 0 +} + +resource "octopusdeploy_tag" "tag_a" { + name = "a" + color = "#333333" + description = "tag a" + sort_order = 2 + tag_set_id = octopusdeploy_tag_set.tagset_tag1.id +} + +resource "octopusdeploy_tag" "tag_b" { + name = "b" + color = "#333333" + description = "tag b" + sort_order = 3 + tag_set_id = octopusdeploy_tag_set.tagset_tag1.id +} + +resource "octopusdeploy_tenant" "tenant1" { + name = "Updated tenant" + description = "Updated description" + tenant_tags = ["tag1/a", "tag1/b"] + depends_on = [octopusdeploy_tag.tag_a, octopusdeploy_tag.tag_b] +}`) +} + +func testTenantResourceUpdated(t *testing.T, name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + tenantId := s.RootModule().Resources["octopusdeploy_tenant.tenant1"].Primary.ID + tenant, err := octoClient.Tenants.GetByID(tenantId) + if err != nil { + return fmt.Errorf("failed to retrieve tenant by ID: %s", err) + } + sort.Strings(tenant.TenantTags) + + assert.NotEmpty(t, "Tenant ID did not match expected value", tenant.ID) + assert.Equal(t, fmt.Sprintf("Updated description"), tenant.Description) + assert.Equal(t, "", tenant.ClonedFromTenantID) + assert.Equal(t, "Updated tenant", tenant.Name) + assert.Equal(t, "Spaces-1", tenant.SpaceID) + assert.Equal(t, []string{"tag1/a", "tag1/b"}, tenant.TenantTags) + + return nil + } +} diff --git a/octopusdeploy/resource_tenant_test.go b/octopusdeploy_framework/resource_tenant_test.go similarity index 95% rename from octopusdeploy/resource_tenant_test.go rename to octopusdeploy_framework/resource_tenant_test.go index 22b03b365..cd5a28b72 100644 --- a/octopusdeploy/resource_tenant_test.go +++ b/octopusdeploy_framework/resource_tenant_test.go @@ -1,17 +1,16 @@ -package octopusdeploy +package octopusdeploy_framework import ( "fmt" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" "path/filepath" "testing" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tenants" "github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework/octoclient" "github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework/test" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) func TestAccTenantBasic(t *testing.T) { @@ -34,7 +33,7 @@ func TestAccTenantBasic(t *testing.T) { resource.Test(t, resource.TestCase{ CheckDestroy: testAccTenantCheckDestroy, - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { TestAccPreCheck(t) }, ProtoV6ProviderFactories: ProtoV6ProviderFactories(), Steps: []resource.TestStep{ { diff --git a/octopusdeploy_framework/schemas/tenant.go b/octopusdeploy_framework/schemas/tenant.go index 41d55b7d1..497540621 100644 --- a/octopusdeploy_framework/schemas/tenant.go +++ b/octopusdeploy_framework/schemas/tenant.go @@ -5,16 +5,21 @@ import ( "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" "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/listplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" "github.com/hashicorp/terraform-plugin-framework/types" ) type TenantModel struct { ClonedFromTenantId types.String `tfsdk:"cloned_from_tenant_id"` Description types.String `tfsdk:"description"` - ID types.String `tfsdk:"id"` Name types.String `tfsdk:"name"` SpaceID types.String `tfsdk:"space_id"` TenantTags types.List `tfsdk:"tenant_tags"` + + ResourceModel } type TenantsModel struct { @@ -105,3 +110,27 @@ func GetTenantDataSourceSchema() map[string]datasourceSchema.Attribute { }, } } + +func GetTenantResourceSchema() map[string]resourceSchema.Attribute { + return map[string]resourceSchema.Attribute{ + "cloned_from_tenant_id": resourceSchema.StringAttribute{ + Description: "The ID of the tenant from which this tenant was cloned.", + Optional: true, + Computed: true, + Default: stringdefault.StaticString(""), + }, + "description": util.GetDescriptionResourceSchema("tenant"), + "id": util.GetIdResourceSchema(), + "name": util.GetNameResourceSchema(true), + "space_id": util.GetSpaceIdResourceSchema("tenant"), + "tenant_tags": resourceSchema.ListAttribute{ + Description: "A list of tenant tags associated with this resource.", + ElementType: types.StringType, + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.List{ + listplanmodifier.UseStateForUnknown(), + }, + }, + } +} From d10c46797e4914498e46dff5ab571661a83cf8dd Mon Sep 17 00:00:00 2001 From: Huy Nguyen <162080607+HuyPhanNguyen@users.noreply.github.com> Date: Mon, 12 Aug 2024 15:00:47 +1000 Subject: [PATCH 12/28] chore!: Migrate username_password_account resource (#721) * Add username_password_account resource * Fix test fail * Add import and import test * Update octopusdeploy_framework/schemas/username_password_account.go Co-authored-by: Henrik Andersson * Update follow new way to handle a missing resource in the Read function * fix the document * fix issue list set to state as null * tidy --------- Co-authored-by: Henrik Andersson --- docs/resources/username_password_account.md | 2 +- octopusdeploy/provider.go | 1 - ...bernetes_cluster_deployment_target_test.go | 7 + .../resource_username_password_account.go | 95 -------- .../schema_username_password_account.go | 90 -------- octopusdeploy_framework/framework_provider.go | 1 + .../resource_username_password_account.go | 210 ++++++++++++++++++ ...rname_password_account_integration_test.go | 2 +- ...resource_username_password_account_test.go | 87 ++++++-- .../schemas/username_password_account.go | 40 ++++ 10 files changed, 333 insertions(+), 202 deletions(-) delete mode 100644 octopusdeploy/resource_username_password_account.go delete mode 100644 octopusdeploy/schema_username_password_account.go create mode 100644 octopusdeploy_framework/resource_username_password_account.go rename {octopusdeploy => octopusdeploy_framework}/resource_username_password_account_integration_test.go (98%) rename {octopusdeploy => octopusdeploy_framework}/resource_username_password_account_test.go (56%) create mode 100644 octopusdeploy_framework/schemas/username_password_account.go diff --git a/docs/resources/username_password_account.md b/docs/resources/username_password_account.md index cd949fa53..f00bd5ad6 100644 --- a/docs/resources/username_password_account.md +++ b/docs/resources/username_password_account.md @@ -23,7 +23,7 @@ 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 diff --git a/octopusdeploy/provider.go b/octopusdeploy/provider.go index ebdaf0b94..ed362eb12 100644 --- a/octopusdeploy/provider.go +++ b/octopusdeploy/provider.go @@ -71,7 +71,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 68686d521..0e1ac1453 100644 --- a/octopusdeploy_framework/framework_provider.go +++ b/octopusdeploy_framework/framework_provider.go @@ -94,6 +94,7 @@ func (p *octopusDeployFrameworkProvider) Resources(ctx context.Context) []func() NewVariableResource, NewProjectResource, NewDockerContainerRegistryFeedResource, + NewUsernamePasswordAccountResource, NewRunbookResource, NewTenantResource, } diff --git a/octopusdeploy_framework/resource_username_password_account.go b/octopusdeploy_framework/resource_username_password_account.go new file mode 100644 index 000000000..307ffe3fd --- /dev/null +++ b/octopusdeploy_framework/resource_username_password_account.go @@ -0,0 +1,210 @@ +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/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{} +var _ resource.ResourceWithImportState = &usernamePasswordAccountResource{} + +type usernamePasswordAccountResource struct { + *Config +} + +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 schemas.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 schemas.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 { + if err := errors.ProcessApiErrorV2(ctx, resp, state, err, "usernamePasswordAccountResource"); err != nil { + resp.Diagnostics.AddError("unable to load 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 schemas.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 schemas.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 (r *usernamePasswordAccountResource) 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 username password account", + fmt.Sprintf("Unable to read username password account with ID %s: %s", accountID, err.Error()), + ) + return + } + + usernamePasswordAccount, ok := account.(*accounts.UsernamePasswordAccount) + if !ok { + resp.Diagnostics.AddError( + "Unexpected account type", + fmt.Sprintf("Expected username password account, got: %T", account), + ) + return + } + + state := schemas.UsernamePasswordAccountResourceModel{ + SpaceID: types.StringValue(usernamePasswordAccount.GetSpaceID()), + Name: types.StringValue(usernamePasswordAccount.GetName()), + Description: types.StringValue(usernamePasswordAccount.GetDescription()), + Username: types.StringValue(usernamePasswordAccount.GetUsername()), + TenantedDeploymentParticipation: types.StringValue(string(usernamePasswordAccount.GetTenantedDeploymentMode())), + Environments: flattenStringList(usernamePasswordAccount.GetEnvironmentIDs(), types.ListNull(types.StringType)), + Tenants: flattenStringList(usernamePasswordAccount.GetTenantIDs(), types.ListNull(types.StringType)), + TenantTags: flattenStringList(usernamePasswordAccount.TenantTags, types.ListNull(types.StringType)), + Password: types.StringNull(), + } + state.ID = types.StringValue(usernamePasswordAccount.ID) + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func expandUsernamePasswordAccount(ctx context.Context, model schemas.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(model.Environments)) + account.SetTenantedDeploymentMode(core.TenantedDeploymentMode(model.TenantedDeploymentParticipation.ValueString())) + account.SetTenantIDs(expandStringList(model.Tenants)) + account.SetTenantTags(expandStringList(model.TenantTags)) + + return account +} + +func flattenUsernamePasswordAccount(ctx context.Context, account *accounts.UsernamePasswordAccount, model schemas.UsernamePasswordAccountResourceModel) schemas.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(account.GetEnvironmentIDs(), model.Environments) + model.Tenants = flattenStringList(account.GetTenantIDs(), model.Tenants) + model.TenantTags = flattenStringList(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(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(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 56% rename from octopusdeploy/resource_username_password_account_test.go rename to octopusdeploy_framework/resource_username_password_account_test.go index e1a29bd9f..25977a673 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) { @@ -111,3 +114,59 @@ func TestUsernamePasswordVariableResource(t *testing.T) { t.Fatalf("The variable must have type of UsernamePasswordAccount.") } } + +func TestAccUsernamePasswordAccountImport(t *testing.T) { + localName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) + resourceName := "octopusdeploy_username_password_account." + localName + + description := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) + name := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) + password := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) + tenantedDeploymentParticipation := core.TenantedDeploymentModeTenantedOrUntenanted + username := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { TestAccPreCheck(t) }, + ProtoV6ProviderFactories: ProtoV6ProviderFactories(), + Steps: []resource.TestStep{ + // Create and test the resource + { + Config: testAccUsernamePasswordAccountBasic(localName, description, name, username, password, tenantedDeploymentParticipation), + Check: resource.ComposeTestCheckFunc( + testAccountExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttr(resourceName, "description", description), + resource.TestCheckResourceAttr(resourceName, "username", username), + resource.TestCheckResourceAttr(resourceName, "tenanted_deployment_participation", string(tenantedDeploymentParticipation)), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"password"}, + ImportStateIdFunc: testAccUsernamePasswordAccountImportStateIdFunc(resourceName), + }, + }, + }) +} + +func testAccUsernamePasswordAccountImportStateIdFunc(resourceName string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("Not found: %s", resourceName) + } + + return rs.Primary.ID, nil + } +} +func testAccUsernamePasswordAccountBasic(localName, description, name, username, password string, tenantedDeploymentParticipation core.TenantedDeploymentMode) string { + return fmt.Sprintf(`resource "octopusdeploy_username_password_account" "%s" { + name = "%s" + description = "%s" + username = "%s" + password = "%s" + tenanted_deployment_participation = "%s" + }`, localName, name, description, username, password, tenantedDeploymentParticipation) +} diff --git a/octopusdeploy_framework/schemas/username_password_account.go b/octopusdeploy_framework/schemas/username_password_account.go new file mode 100644 index 000000000..c5bff07fc --- /dev/null +++ b/octopusdeploy_framework/schemas/username_password_account.go @@ -0,0 +1,40 @@ +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().Computed().PlanModifiers(stringplanmodifier.UseStateForUnknown()).Default("").Description("The description of this username/password account.").Build(), + "environments": util.ResourceList(types.StringType).Optional().Computed().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().Optional().Computed().PlanModifiers(stringplanmodifier.UseStateForUnknown()).Description("The tenanted deployment mode of the resource. Valid account types are `Untenanted`, `TenantedOrUntenanted`, or `Tenanted`.").Build(), + "tenants": util.ResourceList(types.StringType).Optional().Computed().Description("A list of tenant IDs associated with this resource.").Build(), + "tenant_tags": util.ResourceList(types.StringType).Optional().Computed().Description("A list of tenant tags associated with this resource.").Build(), + "username": util.ResourceString().Required().Sensitive().Description("The username associated with this resource.").Build(), + }, + } +} + +type UsernamePasswordAccountResourceModel struct { + 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"` + + ResourceModel +} From 44acc60e960ed19d524bcd26852fd3267b4ccf5d Mon Sep 17 00:00:00 2001 From: Isaac Calligeros <101079287+IsaacCalligeros95@users.noreply.github.com> Date: Mon, 12 Aug 2024 14:55:32 +0930 Subject: [PATCH 13/28] Chore!: Migrate script module resource (#712) --- docs/data-sources/script_modules | 26 +-- docs/data-sources/script_modules.md | 26 +-- docs/resources/script_module.md | 8 +- octopusdeploy/data_source_script_modules.go | 48 ---- octopusdeploy/provider.go | 2 - octopusdeploy/resource_script_module.go | 106 --------- octopusdeploy/schema_script_modules.go | 155 ------------- .../data_source_script_modules.go | 74 ++++++ .../data_source_script_modules_test.go | 11 +- octopusdeploy_framework/framework_provider.go | 2 + .../resource_script_module.go | 130 +++++++++++ .../resource_script_module_test.go | 11 +- .../schemas/script_modules.go | 213 ++++++++++++++++++ 13 files changed, 459 insertions(+), 353 deletions(-) delete mode 100644 octopusdeploy/data_source_script_modules.go delete mode 100644 octopusdeploy/resource_script_module.go delete mode 100644 octopusdeploy/schema_script_modules.go create mode 100644 octopusdeploy_framework/data_source_script_modules.go rename {octopusdeploy => octopusdeploy_framework}/data_source_script_modules_test.go (79%) create mode 100644 octopusdeploy_framework/resource_script_module.go rename {octopusdeploy => octopusdeploy_framework}/resource_script_module_test.go (95%) create mode 100644 octopusdeploy_framework/schemas/script_modules.go diff --git a/docs/data-sources/script_modules b/docs/data-sources/script_modules index 6a12509e7..848de86dc 100644 --- a/docs/data-sources/script_modules +++ b/docs/data-sources/script_modules @@ -26,32 +26,32 @@ data "octopusdeploy_script_modules" "example" { ### Optional - `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. +- `partial_name` (String) A filter to search by a partial name. - `skip` (Number) A filter to specify the number of items to skip in the response. -- `space_id` (String) A Space ID to filter by. Will revert what is specified on the provider if not set. +- `space_id` (String) The space ID associated with this script module. - `take` (Number) A filter to specify the number of items to take (or return) in the response. ### Read-Only -- `id` (String) An auto-generated identifier that includes the timestamp when this data source was last modified. -- `script_modules` (List of Object) A list of script modules that match the filter(s). (see [below for nested schema](#nestedatt--script_modules)) +- `id` (String) The unique ID for this resource. +- `script_modules` (Attributes List) (see [below for nested schema](#nestedatt--script_modules)) ### Nested Schema for `script_modules` Read-Only: -- `description` (String) -- `id` (String) -- `name` (String) -- `script` (Set of Object) (see [below for nested schema](#nestedobjatt--script_modules--script)) -- `space_id` (String) -- `variable_set_id` (String) +- `description` (String) The description of this script module. +- `id` (String) The unique ID for this resource. +- `name` (String) The name of this resource. +- `script` (Attributes List) The script associated with this script module. (see [below for nested schema](#nestedatt--script_modules--script)) +- `space_id` (String) The space ID associated with this Script Module. +- `variable_set_id` (String) The variable set ID for this script module. - + ### Nested Schema for `script_modules.script` Read-Only: -- `body` (String) -- `syntax` (String) \ No newline at end of file +- `body` (String) The body of this script module. +- `syntax` (String) The syntax of the script. Valid types are `Bash`, `CSharp`, `FSharp`, `PowerShell`, or `Python`. \ No newline at end of file diff --git a/docs/data-sources/script_modules.md b/docs/data-sources/script_modules.md index 35d7e16a8..2149ce0ca 100644 --- a/docs/data-sources/script_modules.md +++ b/docs/data-sources/script_modules.md @@ -27,34 +27,34 @@ data "octopusdeploy_script_modules" "example" { ### Optional - `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. +- `partial_name` (String) A filter to search by a partial name. - `skip` (Number) A filter to specify the number of items to skip in the response. -- `space_id` (String) A Space ID to filter by. Will revert what is specified on the provider if not set. +- `space_id` (String) The space ID associated with this script module. - `take` (Number) A filter to specify the number of items to take (or return) in the response. ### Read-Only -- `id` (String) An auto-generated identifier that includes the timestamp when this data source was last modified. -- `script_modules` (List of Object) A list of script modules that match the filter(s). (see [below for nested schema](#nestedatt--script_modules)) +- `id` (String) The unique ID for this resource. +- `script_modules` (Attributes List) (see [below for nested schema](#nestedatt--script_modules)) ### Nested Schema for `script_modules` Read-Only: -- `description` (String) -- `id` (String) -- `name` (String) -- `script` (Set of Object) (see [below for nested schema](#nestedobjatt--script_modules--script)) -- `space_id` (String) -- `variable_set_id` (String) +- `description` (String) The description of this script module. +- `id` (String) The unique ID for this resource. +- `name` (String) The name of this resource. +- `script` (Attributes List) The script associated with this script module. (see [below for nested schema](#nestedatt--script_modules--script)) +- `space_id` (String) The space ID associated with this Script Module. +- `variable_set_id` (String) The variable set ID for this script module. - + ### Nested Schema for `script_modules.script` Read-Only: -- `body` (String) -- `syntax` (String) +- `body` (String) The body of this script module. +- `syntax` (String) The syntax of the script. Valid types are `Bash`, `CSharp`, `FSharp`, `PowerShell`, or `Python`. diff --git a/docs/resources/script_module.md b/docs/resources/script_module.md index 15d81fb84..bb484cf04 100644 --- a/docs/resources/script_module.md +++ b/docs/resources/script_module.md @@ -3,12 +3,12 @@ page_title: "octopusdeploy_script_module Resource - terraform-provider-octopusdeploy" subcategory: "" description: |- - This resource manages script modules in Octopus Deploy. + --- # octopusdeploy_script_module (Resource) -This resource manages script modules in Octopus Deploy. + ## Example Usage @@ -30,13 +30,13 @@ resource "octopusdeploy_script_module" "example" { ### Required - `name` (String) The name of this resource. -- `script` (Block Set, Min: 1, Max: 1) The script associated with this script module. (see [below for nested schema](#nestedblock--script)) ### Optional - `description` (String) The description of this script module. - `id` (String) The unique ID for this resource. -- `space_id` (String) The space ID associated with this resource. +- `script` (Block List) The script associated with this script module. (see [below for nested schema](#nestedblock--script)) +- `space_id` (String) The space ID associated with this Script Module. - `variable_set_id` (String) The variable set ID for this script module. diff --git a/octopusdeploy/data_source_script_modules.go b/octopusdeploy/data_source_script_modules.go deleted file mode 100644 index f782e1410..000000000 --- a/octopusdeploy/data_source_script_modules.go +++ /dev/null @@ -1,48 +0,0 @@ -package octopusdeploy - -import ( - "context" - "time" - - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client" - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/scriptmodules" - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/variables" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func dataSourceScriptModules() *schema.Resource { - return &schema.Resource{ - Description: "Provides information about existing script modules.", - ReadContext: dataSourceScriptModulesRead, - Schema: getScriptModuleDataSchema(), - } -} - -func dataSourceScriptModulesRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - query := variables.LibraryVariablesQuery{ - ContentType: "ScriptModule", - IDs: expandArray(d.Get("ids").([]interface{})), - PartialName: d.Get("partial_name").(string), - Skip: d.Get("skip").(int), - Take: d.Get("take").(int), - } - - spaceID := d.Get("space_id").(string) - - client := m.(*client.Client) - existingScriptModules, err := scriptmodules.Get(client, spaceID, query) - if err != nil { - return diag.FromErr(err) - } - - flattenedScriptModules := []interface{}{} - for _, scriptModule := range existingScriptModules.Items { - flattenedScriptModules = append(flattenedScriptModules, flattenScriptModule(scriptModule)) - } - - d.Set("script_modules", flattenedScriptModules) - d.SetId("Script Modules " + time.Now().UTC().String()) - - return nil -} diff --git a/octopusdeploy/provider.go b/octopusdeploy/provider.go index ed362eb12..1c8ff40e9 100644 --- a/octopusdeploy/provider.go +++ b/octopusdeploy/provider.go @@ -25,7 +25,6 @@ func Provider() *schema.Provider { "octopusdeploy_machine_policies": dataSourceMachinePolicies(), "octopusdeploy_offline_package_drop_deployment_targets": dataSourceOfflinePackageDropDeploymentTargets(), "octopusdeploy_polling_tentacle_deployment_targets": dataSourcePollingTentacleDeploymentTargets(), - "octopusdeploy_script_modules": dataSourceScriptModules(), "octopusdeploy_ssh_connection_deployment_targets": dataSourceSSHConnectionDeploymentTargets(), "octopusdeploy_tag_sets": dataSourceTagSets(), "octopusdeploy_teams": dataSourceTeams(), @@ -60,7 +59,6 @@ func Provider() *schema.Provider { "octopusdeploy_project_scheduled_trigger": resourceProjectScheduledTrigger(), "octopusdeploy_runbook_process": resourceRunbookProcess(), "octopusdeploy_scoped_user_role": resourceScopedUserRole(), - "octopusdeploy_script_module": resourceScriptModule(), "octopusdeploy_ssh_connection_deployment_target": resourceSSHConnectionDeploymentTarget(), "octopusdeploy_ssh_key_account": resourceSSHKeyAccount(), "octopusdeploy_static_worker_pool": resourceStaticWorkerPool(), diff --git a/octopusdeploy/resource_script_module.go b/octopusdeploy/resource_script_module.go deleted file mode 100644 index faf0f5232..000000000 --- a/octopusdeploy/resource_script_module.go +++ /dev/null @@ -1,106 +0,0 @@ -package octopusdeploy - -import ( - "context" - "log" - - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client" - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/scriptmodules" - "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 resourceScriptModule() *schema.Resource { - return &schema.Resource{ - CreateContext: resourceScriptModuleCreate, - DeleteContext: resourceScriptModuleDelete, - Description: "This resource manages script modules in Octopus Deploy.", - Importer: getImporter(), - ReadContext: resourceScriptModuleRead, - Schema: getScriptModuleSchema(), - UpdateContext: resourceScriptModuleUpdate, - } -} - -func resourceScriptModuleCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - scriptModule := expandScriptModule(d) - - log.Printf("[INFO] creating script module: %#v", scriptModule) - - client := m.(*client.Client) - createdScriptModule, err := scriptmodules.Add(client, scriptModule) - if err != nil { - return diag.FromErr(err) - } - - if err := setScriptModule(ctx, d, createdScriptModule); err != nil { - return diag.FromErr(err) - } - - d.SetId(createdScriptModule.GetID()) - - log.Printf("[INFO] script module created (%s)", d.Id()) - return nil -} - -func resourceScriptModuleDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - log.Printf("[INFO] deleting script module (%s)", d.Id()) - - var spaceID string - if v, ok := d.GetOk("space_id"); ok { - spaceID = v.(string) - } - - client := m.(*client.Client) - err := scriptmodules.DeleteByID(client, spaceID, d.Id()) - if err != nil { - return diag.FromErr(err) - } - - log.Printf("[INFO] script module deleted (%s)", d.Id()) - d.SetId("") - return nil -} - -func resourceScriptModuleRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - log.Printf("[INFO] reading script module (%s)", d.Id()) - - var spaceID string - if v, ok := d.GetOk("space_id"); ok { - spaceID = v.(string) - } - - client := m.(*client.Client) - - scriptModule, err := scriptmodules.GetByID(client, spaceID, d.Id()) - if err != nil { - return errors.ProcessApiError(ctx, d, err, "script module") - } - - if err := setScriptModule(ctx, d, scriptModule); err != nil { - return diag.FromErr(err) - } - - log.Printf("[INFO] script module read (%s)", d.Id()) - return nil -} - -func resourceScriptModuleUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - log.Printf("[INFO] updating script module (%s)", d.Id()) - - scriptModule := expandScriptModule(d) - - client := m.(*client.Client) - updatedScriptModule, err := scriptmodules.Update(client, scriptModule) - if err != nil { - return diag.FromErr(err) - } - - if err := setScriptModule(ctx, d, updatedScriptModule); err != nil { - return diag.FromErr(err) - } - - log.Printf("[INFO] script module updated (%s)", d.Id()) - return nil -} diff --git a/octopusdeploy/schema_script_modules.go b/octopusdeploy/schema_script_modules.go deleted file mode 100644 index 906d2ea54..000000000 --- a/octopusdeploy/schema_script_modules.go +++ /dev/null @@ -1,155 +0,0 @@ -package octopusdeploy - -import ( - "context" - "fmt" - - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/variables" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" -) - -func expandScriptModule(d *schema.ResourceData) *variables.ScriptModule { - name := d.Get("name").(string) - - scriptModule := variables.NewScriptModule(name) - scriptModule.ID = d.Id() - - if v, ok := d.GetOk("description"); ok { - scriptModule.Description = v.(string) - } - - if v, ok := d.GetOk("script"); ok { - scripts := v.(*schema.Set).List() - for _, script := range scripts { - rawScript := script.(map[string]interface{}) - - if rawScript["body"] != nil { - scriptModule.ScriptBody = rawScript["body"].(string) - } - - if rawScript["syntax"] != nil { - scriptModule.Syntax = rawScript["syntax"].(string) - } - } - } - - if v, ok := d.GetOk("space_id"); ok { - scriptModule.SpaceID = v.(string) - } - - if v, ok := d.GetOk("variable_set_id"); ok { - scriptModule.VariableSetID = v.(string) - } - - return scriptModule -} - -func flattenScript(scriptModule *variables.ScriptModule) []interface{} { - if scriptModule == nil { - return nil - } - - flattenedScriptModules := make([]interface{}, 1) - flattenedScriptModules[0] = map[string]interface{}{ - "body": scriptModule.ScriptBody, - "syntax": scriptModule.Syntax, - } - - return flattenedScriptModules -} - -func flattenScriptModule(scriptModule *variables.ScriptModule) map[string]interface{} { - if scriptModule == nil { - return nil - } - - return map[string]interface{}{ - "description": scriptModule.Description, - "id": scriptModule.GetID(), - "name": scriptModule.Name, - "script": flattenScript(scriptModule), - "space_id": scriptModule.SpaceID, - "variable_set_id": scriptModule.VariableSetID, - } -} - -func getScriptModuleDataSchema() map[string]*schema.Schema { - dataSchema := getScriptModuleSchema() - setDataSchema(&dataSchema) - - return map[string]*schema.Schema{ - "id": getDataSchemaID(), - "space_id": getQuerySpaceID(), - "ids": getQueryIDs(), - "script_modules": { - Computed: true, - Description: "A list of script modules that match the filter(s).", - Elem: &schema.Resource{Schema: dataSchema}, - Optional: false, - Type: schema.TypeList, - }, - "partial_name": getQueryPartialName(), - "skip": getQuerySkip(), - "take": getQueryTake(), - } -} - -func getScriptModuleSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "description": getDescriptionSchema("script module"), - "id": getIDSchema(), - "name": getNameSchema(true), - "script": { - Description: "The script associated with this script module.", - Required: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "body": { - Description: "The body of this script module.", - Required: true, - Type: schema.TypeString, - }, - "syntax": { - Description: "The syntax of the script. Valid types are `Bash`, `CSharp`, `FSharp`, `PowerShell`, or `Python`.", - Required: true, - Type: schema.TypeString, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ - "Bash", - "CSharp", - "FSharp", - "PowerShell", - "Python", - }, false)), - }, - }, - }, - MaxItems: 1, - MinItems: 1, - Type: schema.TypeSet, - }, - "space_id": getSpaceIDSchema(), - "variable_set_id": { - Computed: true, - Description: "The variable set ID for this script module.", - Optional: true, - Type: schema.TypeString, - }, - } -} - -func setScriptModule(ctx context.Context, d *schema.ResourceData, scriptModule *variables.ScriptModule) error { - d.Set("description", scriptModule.Description) - d.Set("name", scriptModule.Name) - - if err := d.Set("script", flattenScript(scriptModule)); err != nil { - return fmt.Errorf("error setting script: %s", err) - } - - d.Set("space_id", scriptModule.SpaceID) - d.Set("variable_set_id", scriptModule.VariableSetID) - - d.SetId(scriptModule.GetID()) - - return nil -} diff --git a/octopusdeploy_framework/data_source_script_modules.go b/octopusdeploy_framework/data_source_script_modules.go new file mode 100644 index 000000000..8374402de --- /dev/null +++ b/octopusdeploy_framework/data_source_script_modules.go @@ -0,0 +1,74 @@ +package octopusdeploy_framework + +import ( + "context" + "fmt" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/scriptmodules" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/variables" + "github.com/hashicorp/terraform-plugin-framework/attr" + "time" + + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/schemas" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +type scriptModulesDataSource struct { + *Config +} + +func NewScriptModuleDataSource() datasource.DataSource { + return &scriptModulesDataSource{} +} + +func (l *scriptModulesDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + tflog.Debug(ctx, "script modules datasource Metadata") + resp.TypeName = util.GetTypeName("script_modules") +} + +func (l *scriptModulesDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + tflog.Debug(ctx, "script modules datasource Schema") + resp.Schema = schemas.GetDatasourceScriptModuleSchema() +} + +func (l *scriptModulesDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + tflog.Debug(ctx, "script modules datasource Configure") + l.Config = DataSourceConfiguration(req, resp) +} + +func (l *scriptModulesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + tflog.Debug(ctx, "script modules datasource Read") + var data schemas.ScriptModuleDataSourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + query := variables.LibraryVariablesQuery{ + ContentType: "ScriptModule", + IDs: util.ExpandStringList(data.IDs), + PartialName: data.PartialName.ValueString(), + Skip: int(data.Skip.ValueInt64()), + Take: int(data.Take.ValueInt64()), + } + + spaceID := data.SpaceID.ValueString() + existingScriptModules, err := scriptmodules.Get(l.Config.Client, spaceID, query) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read script modules, got error: %s", err)) + return + } + + flattenedScriptModules := []attr.Value{} + for _, scriptModule := range existingScriptModules.Items { + flattenedScriptModules = append(flattenedScriptModules, schemas.FlattenScriptModule(scriptModule)) + } + + data.ScriptModules = types.ListValueMust(types.ObjectType{AttrTypes: schemas.ScriptModuleObjectType()}, + flattenedScriptModules) + data.ID = types.StringValue("Script Modules " + time.Now().UTC().String()) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/octopusdeploy/data_source_script_modules_test.go b/octopusdeploy_framework/data_source_script_modules_test.go similarity index 79% rename from octopusdeploy/data_source_script_modules_test.go rename to octopusdeploy_framework/data_source_script_modules_test.go index f5fa0450e..286a8ec5b 100644 --- a/octopusdeploy/data_source_script_modules_test.go +++ b/octopusdeploy_framework/data_source_script_modules_test.go @@ -1,12 +1,11 @@ -package octopusdeploy +package octopusdeploy_framework import ( "fmt" + "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/hashicorp/terraform-plugin-sdk/v2/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) func TestAccDataSourceScriptModules(t *testing.T) { @@ -15,7 +14,7 @@ func TestAccDataSourceScriptModules(t *testing.T) { take := 10 resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { TestAccPreCheck(t) }, ProtoV6ProviderFactories: ProtoV6ProviderFactories(), Steps: []resource.TestStep{ { diff --git a/octopusdeploy_framework/framework_provider.go b/octopusdeploy_framework/framework_provider.go index 0e1ac1453..894f15e82 100644 --- a/octopusdeploy_framework/framework_provider.go +++ b/octopusdeploy_framework/framework_provider.go @@ -71,6 +71,7 @@ func (p *octopusDeployFrameworkProvider) DataSources(ctx context.Context) []func NewVariablesDataSource, NewProjectsDataSource, NewTenantsDataSource, + NewScriptModuleDataSource, } } @@ -97,6 +98,7 @@ func (p *octopusDeployFrameworkProvider) Resources(ctx context.Context) []func() NewUsernamePasswordAccountResource, NewRunbookResource, NewTenantResource, + NewScriptModuleResource, } } diff --git a/octopusdeploy_framework/resource_script_module.go b/octopusdeploy_framework/resource_script_module.go new file mode 100644 index 000000000..8e0a1294f --- /dev/null +++ b/octopusdeploy_framework/resource_script_module.go @@ -0,0 +1,130 @@ +package octopusdeploy_framework + +import ( + "context" + "fmt" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/scriptmodules" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal" + "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/resource/schema" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +type scriptModuleTypeResource struct { + *Config +} + +func NewScriptModuleResource() resource.Resource { + return &scriptModuleTypeResource{} +} + +func (r *scriptModuleTypeResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = util.GetTypeName("script_module") +} + +func (r *scriptModuleTypeResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: schemas.GetScriptModuleResourceSchema(), + Blocks: schemas.GetScriptModuleSchemaBlock(), + } +} + +func (r *scriptModuleTypeResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.Config = ResourceConfiguration(req, resp) +} + +func (r *scriptModuleTypeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + internal.Mutex.Lock() + defer internal.Mutex.Unlock() + + var data *schemas.ScriptModuleResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + scriptModule := schemas.MapFromScriptModuleToState(data) + + tflog.Info(ctx, fmt.Sprintf("creating Script Module: %s", scriptModule.Name)) + + createdScriptModule, err := scriptmodules.Add(r.Config.Client, scriptModule) + if err != nil { + resp.Diagnostics.AddError("unable to create script module", err.Error()) + return + } + + schemas.MapToScriptModuleFromState(data, createdScriptModule) + + tflog.Info(ctx, fmt.Sprintf("Script Module created (%s)", data.ID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *scriptModuleTypeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data *schemas.ScriptModuleResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Info(ctx, fmt.Sprintf("reading Script Module (%s)", data.ID)) + + client := r.Config.Client + scriptModule, err := scriptmodules.GetByID(client, data.SpaceID.ValueString(), data.ID.ValueString()) + if err != nil { + if err := errors.ProcessApiErrorV2(ctx, resp, data, err, "Script Module"); err != nil { + resp.Diagnostics.AddError("unable to load script module", err.Error()) + } + return + } + + schemas.MapToScriptModuleFromState(data, scriptModule) + + tflog.Info(ctx, fmt.Sprintf("Script Module read (%s)", scriptModule.GetID())) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *scriptModuleTypeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data, state *schemas.ScriptModuleResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, fmt.Sprintf("updating script module '%s'", data.ID.ValueString())) + + scriptModule := schemas.MapFromScriptModuleToState(data) + scriptModule.ID = state.ID.ValueString() + + updatedScriptModule, err := scriptmodules.Update(r.Config.Client, scriptModule) + if err != nil { + resp.Diagnostics.AddError("unable to update script module", err.Error()) + return + } + + schemas.MapToScriptModuleFromState(data, updatedScriptModule) + + tflog.Info(ctx, fmt.Sprintf("Script Module updated (%s)", data.ID)) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *scriptModuleTypeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + internal.Mutex.Lock() + defer internal.Mutex.Unlock() + + var data schemas.ScriptModuleResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + if err := scriptmodules.DeleteByID(r.Config.Client, data.SpaceID.ValueString(), data.ID.ValueString()); err != nil { + resp.Diagnostics.AddError("unable to delete script module", err.Error()) + return + } +} diff --git a/octopusdeploy/resource_script_module_test.go b/octopusdeploy_framework/resource_script_module_test.go similarity index 95% rename from octopusdeploy/resource_script_module_test.go rename to octopusdeploy_framework/resource_script_module_test.go index 3893ad972..c7828d6a3 100644 --- a/octopusdeploy/resource_script_module_test.go +++ b/octopusdeploy_framework/resource_script_module_test.go @@ -1,16 +1,15 @@ -package octopusdeploy +package octopusdeploy_framework import ( "fmt" "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" "path/filepath" "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) func TestAccOctopusDeployScriptModuleBasic(t *testing.T) { @@ -24,7 +23,7 @@ func TestAccOctopusDeployScriptModuleBasic(t *testing.T) { resource.Test(t, resource.TestCase{ CheckDestroy: testScriptModuleCheckDestroy, - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { TestAccPreCheck(t) }, ProtoV6ProviderFactories: ProtoV6ProviderFactories(), Steps: []resource.TestStep{ { diff --git a/octopusdeploy_framework/schemas/script_modules.go b/octopusdeploy_framework/schemas/script_modules.go new file mode 100644 index 000000000..f74ab893b --- /dev/null +++ b/octopusdeploy_framework/schemas/script_modules.go @@ -0,0 +1,213 @@ +package schemas + +import ( + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/variables" +) + +type ScriptModuleResourceModel struct { + Description types.String `tfsdk:"description"` + Name types.String `tfsdk:"name"` + SpaceID types.String `tfsdk:"space_id"` + VariableSetId types.String `tfsdk:"variable_set_id"` + Script types.List `tfsdk:"script"` + + ResourceModel +} + +type ScriptModuleDataSourceModel struct { + ID types.String `tfsdk:"id"` + SpaceID types.String `tfsdk:"space_id"` + IDs types.List `tfsdk:"ids"` + PartialName types.String `tfsdk:"partial_name"` + Skip types.Int64 `tfsdk:"skip"` + Take types.Int64 `tfsdk:"take"` + ScriptModules types.List `tfsdk:"script_modules"` +} + +func GetDatasourceScriptModuleSchema() datasourceSchema.Schema { + description := "script module" + return datasourceSchema.Schema{ + Description: "Provides information about existing script modules.", + Attributes: map[string]datasourceSchema.Attribute{ + "id": GetIdDatasourceSchema(true), + "space_id": GetSpaceIdDatasourceSchema(description, false), + "ids": util.GetQueryIDsDatasourceSchema(), + "partial_name": util.GetQueryPartialNameDatasourceSchema(), + "skip": util.GetQuerySkipDatasourceSchema(), + "take": util.GetQueryTakeDatasourceSchema(), + "script_modules": datasourceSchema.ListNestedAttribute{ + Computed: true, + NestedObject: datasourceSchema.NestedAttributeObject{ + Attributes: GetScriptModuleDatasourceSchema(), + }, + }, + }, + } +} + +func GetScriptModuleDatasourceSchema() map[string]datasourceSchema.Attribute { + return map[string]datasourceSchema.Attribute{ + "description": GetReadonlyDescriptionDatasourceSchema("script module"), + "id": GetIdDatasourceSchema(true), + "name": GetReadonlyNameDatasourceSchema(), + "space_id": GetSpaceIdDatasourceSchema("Script Module", true), + "variable_set_id": datasourceSchema.StringAttribute{ + Computed: true, + Description: "The variable set ID for this script module.", + }, + "script": datasourceSchema.ListNestedAttribute{ + Description: "The script associated with this script module.", + Computed: true, + NestedObject: datasourceSchema.NestedAttributeObject{ + Attributes: map[string]datasourceSchema.Attribute{ + "body": datasourceSchema.StringAttribute{ + Description: "The body of this script module.", + Computed: true, + }, + "syntax": datasourceSchema.StringAttribute{ + Description: "The syntax of the script. Valid types are `Bash`, `CSharp`, `FSharp`, `PowerShell`, or `Python`.", + Computed: true, + Validators: []validator.String{ + stringvalidator.OneOfCaseInsensitive( + "Bash", + "CSharp", + "FSharp", + "PowerShell", + "Python"), + }, + }, + }, + }, + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + listvalidator.SizeAtLeast(1), + }, + }, + } +} + +func FlattenScriptModule(scriptModule *variables.ScriptModule) attr.Value { + attrs := map[string]attr.Value{ + "description": types.StringValue(scriptModule.Description), + "id": types.StringValue(scriptModule.GetID()), + "name": types.StringValue(scriptModule.Name), + "script": types.ListValueMust(ScriptObjectType(), flattenScript(scriptModule)), + "space_id": types.StringValue(scriptModule.SpaceID), + "variable_set_id": types.StringValue(scriptModule.VariableSetID), + } + + return types.ObjectValueMust(ScriptModuleObjectType(), attrs) +} + +func ScriptObjectType() types.ObjectType { + return types.ObjectType{AttrTypes: map[string]attr.Type{ + "body": types.StringType, + "syntax": types.StringType, + }} +} + +func ScriptModuleObjectType() map[string]attr.Type { + return map[string]attr.Type{ + "description": types.StringType, + "id": types.StringType, + "name": types.StringType, + "space_id": types.StringType, + "variable_set_id": types.StringType, + "script": types.ListType{ElemType: ScriptObjectType()}, + } +} + +func GetScriptModuleSchemaBlock() map[string]resourceSchema.Block { + return map[string]resourceSchema.Block{ + "script": resourceSchema.ListNestedBlock{ + Description: "The script associated with this script module.", + NestedObject: resourceSchema.NestedBlockObject{ + Attributes: map[string]resourceSchema.Attribute{ + "body": resourceSchema.StringAttribute{ + Description: "The body of this script module.", + Required: true, + }, + "syntax": resourceSchema.StringAttribute{ + Description: "The syntax of the script. Valid types are `Bash`, `CSharp`, `FSharp`, `PowerShell`, or `Python`.", + Required: true, + Validators: []validator.String{ + stringvalidator.OneOfCaseInsensitive( + "Bash", + "CSharp", + "FSharp", + "PowerShell", + "Python"), + }, + }, + }, + }, + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + listvalidator.SizeAtLeast(1), + }, + }, + } +} + +func GetScriptModuleResourceSchema() map[string]resourceSchema.Attribute { + return map[string]resourceSchema.Attribute{ + "description": GetDescriptionResourceSchema("script module"), + "id": GetIdResourceSchema(), + "name": GetNameResourceSchema(true), + "space_id": GetSpaceIdResourceSchema("Script Module"), + "variable_set_id": resourceSchema.StringAttribute{ + Computed: true, + Description: "The variable set ID for this script module.", + Optional: true, + }, + } +} + +func MapFromScriptModuleToState(data *ScriptModuleResourceModel) *variables.ScriptModule { + name := data.Name.ValueString() + scriptModule := variables.NewScriptModule(name) + scriptModule.ID = data.ID.ValueString() + scriptModule.Description = data.Description.ValueString() + // We enforce on the schema a single required script + scriptDetails := data.Script.Elements()[0].(types.Object).Attributes() + scriptModule.Syntax = scriptDetails["syntax"].(types.String).ValueString() + scriptModule.ScriptBody = scriptDetails["body"].(types.String).ValueString() + scriptModule.SpaceID = data.SpaceID.ValueString() + scriptModule.VariableSetID = data.VariableSetId.ValueString() + + return scriptModule +} + +func flattenScript(scriptModule *variables.ScriptModule) []attr.Value { + return []attr.Value{ + types.ObjectValueMust(map[string]attr.Type{ + "body": types.StringType, + "syntax": types.StringType, + }, map[string]attr.Value{ + "body": types.StringValue(scriptModule.ScriptBody), + "syntax": types.StringValue(scriptModule.Syntax), + }), + } +} + +func MapToScriptModuleFromState(data *ScriptModuleResourceModel, scriptModule *variables.ScriptModule) { + data.Description = types.StringValue(scriptModule.Description) + data.Name = types.StringValue(scriptModule.Name) + data.SpaceID = types.StringValue(scriptModule.SpaceID) + data.VariableSetId = types.StringValue(scriptModule.VariableSetID) + data.ID = types.StringValue(scriptModule.ID) + + flattenScript(scriptModule) + + var script, _ = types.ListValue(ScriptObjectType(), flattenScript(scriptModule)) + data.Script = script +} From 5aec963058dc8dddf93290e6a52f21477758a37d Mon Sep 17 00:00:00 2001 From: Isaac Calligeros <101079287+IsaacCalligeros95@users.noreply.github.com> Date: Mon, 12 Aug 2024 15:16:18 +0930 Subject: [PATCH 14/28] Chore!: Migrate tentacle certificate (#716) --- docs/resources/tentacle_certificate.md | 2 +- {octopusdeploy => internal}/random.go | 6 +- octopusdeploy/provider.go | 1 - .../resource_polling_subscription_id.go | 3 +- .../resource_tentacle_certificate.go | 97 ------------- octopusdeploy/schema_tentacle_certificate.go | 25 ---- octopusdeploy_framework/framework_provider.go | 1 + .../resource_tentacle_certificate.go | 129 ++++++++++++++++++ .../resource_tentacle_certificate_test.go | 2 +- .../schemas/schema_tentacle_certificate.go | 44 ++++++ 10 files changed, 181 insertions(+), 129 deletions(-) rename {octopusdeploy => internal}/random.go (88%) delete mode 100644 octopusdeploy/resource_tentacle_certificate.go delete mode 100644 octopusdeploy/schema_tentacle_certificate.go create mode 100644 octopusdeploy_framework/resource_tentacle_certificate.go rename {octopusdeploy => octopusdeploy_framework}/resource_tentacle_certificate_test.go (95%) create mode 100644 octopusdeploy_framework/schemas/schema_tentacle_certificate.go diff --git a/docs/resources/tentacle_certificate.md b/docs/resources/tentacle_certificate.md index a86900df9..7ba18a05e 100644 --- a/docs/resources/tentacle_certificate.md +++ b/docs/resources/tentacle_certificate.md @@ -47,7 +47,7 @@ resource "octopusdeploy_kubernetes_agent_deployment_target" "agent" { ### Read-Only - `base64` (String, Sensitive) The base64 encoded pfx certificate. -- `id` (String) The ID of this resource. +- `id` (String) The unique ID for this resource. - `thumbprint` (String) The SHA1 sum of the certificate represented in hexadecimal. diff --git a/octopusdeploy/random.go b/internal/random.go similarity index 88% rename from octopusdeploy/random.go rename to internal/random.go index 5e7e78487..42f2987c3 100644 --- a/octopusdeploy/random.go +++ b/internal/random.go @@ -1,4 +1,4 @@ -package octopusdeploy +package internal import ( "crypto/rand" @@ -21,7 +21,7 @@ func generateRandomBytes(length int) []byte { return randomBytes } -func generateRandomCryptoString(length int) string { +func GenerateRandomCryptoString(length int) string { result := make([]byte, length) bufferSize := int(float64(length) * 1.3) for i, j, randomBytes := 0, 0, []byte{}; i < length; j++ { @@ -37,7 +37,7 @@ func generateRandomCryptoString(length int) string { return string(result) } -func generateRandomSerialNumber() big.Int { +func GenerateRandomSerialNumber() big.Int { random := rand.Reader randomSerialNumber, err := rand.Int(random, big.NewInt(9223372036854775807)) if err != nil { diff --git a/octopusdeploy/provider.go b/octopusdeploy/provider.go index 1c8ff40e9..4bc99cfc6 100644 --- a/octopusdeploy/provider.go +++ b/octopusdeploy/provider.go @@ -65,7 +65,6 @@ func Provider() *schema.Provider { "octopusdeploy_tag": resourceTag(), "octopusdeploy_tag_set": resourceTagSet(), "octopusdeploy_team": resourceTeam(), - "octopusdeploy_tentacle_certificate": resourceTentacleCertificate(), "octopusdeploy_token_account": resourceTokenAccount(), "octopusdeploy_user": resourceUser(), "octopusdeploy_user_role": resourceUserRole(), diff --git a/octopusdeploy/resource_polling_subscription_id.go b/octopusdeploy/resource_polling_subscription_id.go index e97927ab9..f871377c5 100644 --- a/octopusdeploy/resource_polling_subscription_id.go +++ b/octopusdeploy/resource_polling_subscription_id.go @@ -2,6 +2,7 @@ package octopusdeploy import ( "context" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -27,7 +28,7 @@ func resourcePollingSubscriptionIdDelete(ctx context.Context, d *schema.Resource } func resourcePollingSubscriptionIdCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - var generatedSubscriptionId = generateRandomCryptoString(20) + var generatedSubscriptionId = internal.GenerateRandomCryptoString(20) d.SetId(generatedSubscriptionId) d.Set("polling_uri", "poll://"+generatedSubscriptionId+"/") diff --git a/octopusdeploy/resource_tentacle_certificate.go b/octopusdeploy/resource_tentacle_certificate.go deleted file mode 100644 index 5df7f3402..000000000 --- a/octopusdeploy/resource_tentacle_certificate.go +++ /dev/null @@ -1,97 +0,0 @@ -package octopusdeploy - -import ( - "context" - "crypto/rand" - "crypto/rsa" - "crypto/sha1" - "crypto/x509" - "crypto/x509/pkix" - "encoding/base64" - "encoding/hex" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "software.sslmate.com/src/go-pkcs12" - "strings" - "time" -) - -func resourceTentacleCertificate() *schema.Resource { - return &schema.Resource{ - CreateContext: resourceTentacleCertificateCreate, - DeleteContext: resourceTentacleCertificateDelete, - Description: "Generates a X.509 self-signed certificate for use with a Octopus Deploy Tentacle.", - ReadContext: resourceTentacleCertificateRead, - Schema: getTentacleCertificateSchema(), - } -} - -func resourceTentacleCertificateRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - // Don't need to do anything as all the values are already in state - return nil -} - -func resourceTentacleCertificateDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - d.SetId("") - return nil -} - -func resourceTentacleCertificateCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - certificate, thumbprint, err := generateCertificate("Octopus Tentacle") - - if err != nil { - return diag.FromErr(err) - } - - d.Set("base64", certificate) - d.Set("thumbprint", thumbprint) - - d.SetId(generateRandomCryptoString(20)) - return nil -} - -func generateCertificate(fullName string) (string, string, error) { - random := rand.Reader - - privateKey, err := rsa.GenerateKey(random, 2048) - if err != nil { - return "", "", err - } - - serialNumber := generateRandomSerialNumber() - template := x509.Certificate{ - SerialNumber: &serialNumber, - Subject: pkix.Name{ - CommonName: fullName, - }, - Issuer: pkix.Name{ - CommonName: fullName, - }, - NotBefore: time.Now().AddDate(0, 0, -1), - NotAfter: time.Now().AddDate(100, 0, 0), - KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{ - x509.ExtKeyUsageServerAuth, - }, - IsCA: false, - BasicConstraintsValid: true, - } - - certBytes, err := x509.CreateCertificate(random, &template, &template, &privateKey.PublicKey, privateKey) - if err != nil { - return "", "", err - } - - parsedCert, _ := x509.ParseCertificate(certBytes) - pkcs12Bytes, err := pkcs12.Passwordless.Encode(privateKey, parsedCert, nil, "") - if err != nil { - return "", "", err - } - - pkcs12Base64 := base64.StdEncoding.EncodeToString(pkcs12Bytes) - - thumbprint := sha1.Sum(certBytes) - thumbprintStr := strings.ToUpper(hex.EncodeToString(thumbprint[:])) - - return pkcs12Base64, thumbprintStr, nil -} diff --git a/octopusdeploy/schema_tentacle_certificate.go b/octopusdeploy/schema_tentacle_certificate.go deleted file mode 100644 index 4335996b0..000000000 --- a/octopusdeploy/schema_tentacle_certificate.go +++ /dev/null @@ -1,25 +0,0 @@ -package octopusdeploy - -import "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - -func getTentacleCertificateSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "base64": { - Computed: true, - Sensitive: true, - Description: "The base64 encoded pfx certificate.", - Type: schema.TypeString, - }, - "thumbprint": { - Computed: true, - Description: "The SHA1 sum of the certificate represented in hexadecimal.", - Type: schema.TypeString, - }, - "dependencies": { - Optional: true, - Type: schema.TypeMap, - Description: "Optional map of dependencies that when modified will trigger a re-creation of this resource.", - ForceNew: true, - }, - } -} diff --git a/octopusdeploy_framework/framework_provider.go b/octopusdeploy_framework/framework_provider.go index 894f15e82..5bdf6cc43 100644 --- a/octopusdeploy_framework/framework_provider.go +++ b/octopusdeploy_framework/framework_provider.go @@ -98,6 +98,7 @@ func (p *octopusDeployFrameworkProvider) Resources(ctx context.Context) []func() NewUsernamePasswordAccountResource, NewRunbookResource, NewTenantResource, + NewTentacleCertificateResource, NewScriptModuleResource, } } diff --git a/octopusdeploy_framework/resource_tentacle_certificate.go b/octopusdeploy_framework/resource_tentacle_certificate.go new file mode 100644 index 000000000..6f0e22a76 --- /dev/null +++ b/octopusdeploy_framework/resource_tentacle_certificate.go @@ -0,0 +1,129 @@ +package octopusdeploy_framework + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/hex" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal" + "software.sslmate.com/src/go-pkcs12" + "strings" + "time" + + "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" +) + +type tentacleCertificateResource struct { + *Config +} + +func NewTentacleCertificateResource() resource.Resource { + return &tentacleCertificateResource{} +} + +func (t *tentacleCertificateResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = util.GetTypeName("tentacle_certificate") +} + +func (t *tentacleCertificateResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schemas.GetTentacleCertificateSchema() +} + +func (t *tentacleCertificateResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + t.Config = ResourceConfiguration(req, resp) +} + +func (t *tentacleCertificateResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan schemas.TentacleCertificateResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + certificate, thumbprint, err := generateCertificate("Octopus Tentacle") + + if err != nil { + resp.Diagnostics.AddError("cannot generate tentacle", err.Error()) + return + } + + plan.Base64 = types.StringValue(certificate) + plan.Thumbprint = types.StringValue(thumbprint) + plan.ID = types.StringValue(internal.GenerateRandomCryptoString(20)) + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + +} + +func (t *tentacleCertificateResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + return +} + +func (t *tentacleCertificateResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data schemas.TentacleCertificateResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + data.ID = types.StringValue("") + return +} + +func (r *tentacleCertificateResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + return +} + +func generateCertificate(fullName string) (string, string, error) { + random := rand.Reader + + privateKey, err := rsa.GenerateKey(random, 2048) + if err != nil { + return "", "", err + } + + serialNumber := internal.GenerateRandomSerialNumber() + template := x509.Certificate{ + SerialNumber: &serialNumber, + Subject: pkix.Name{ + CommonName: fullName, + }, + Issuer: pkix.Name{ + CommonName: fullName, + }, + NotBefore: time.Now().AddDate(0, 0, -1), + NotAfter: time.Now().AddDate(100, 0, 0), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageServerAuth, + }, + IsCA: false, + BasicConstraintsValid: true, + } + + certBytes, err := x509.CreateCertificate(random, &template, &template, &privateKey.PublicKey, privateKey) + if err != nil { + return "", "", err + } + + parsedCert, _ := x509.ParseCertificate(certBytes) + pkcs12Bytes, err := pkcs12.Passwordless.Encode(privateKey, parsedCert, nil, "") + if err != nil { + return "", "", err + } + + pkcs12Base64 := base64.StdEncoding.EncodeToString(pkcs12Bytes) + + thumbprint := sha1.Sum(certBytes) + thumbprintStr := strings.ToUpper(hex.EncodeToString(thumbprint[:])) + + return pkcs12Base64, thumbprintStr, nil +} diff --git a/octopusdeploy/resource_tentacle_certificate_test.go b/octopusdeploy_framework/resource_tentacle_certificate_test.go similarity index 95% rename from octopusdeploy/resource_tentacle_certificate_test.go rename to octopusdeploy_framework/resource_tentacle_certificate_test.go index 483d08f08..f07a20aac 100644 --- a/octopusdeploy/resource_tentacle_certificate_test.go +++ b/octopusdeploy_framework/resource_tentacle_certificate_test.go @@ -1,4 +1,4 @@ -package octopusdeploy +package octopusdeploy_framework import ( "github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework/test" diff --git a/octopusdeploy_framework/schemas/schema_tentacle_certificate.go b/octopusdeploy_framework/schemas/schema_tentacle_certificate.go new file mode 100644 index 000000000..74dbfd07a --- /dev/null +++ b/octopusdeploy_framework/schemas/schema_tentacle_certificate.go @@ -0,0 +1,44 @@ +package schemas + +import ( + resourceSchema "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func GetTentacleCertificateSchema() resourceSchema.Schema { + return resourceSchema.Schema{ + Description: "Generates a X.509 self-signed certificate for use with a Octopus Deploy Tentacle.", + Attributes: map[string]resourceSchema.Attribute{ + "id": resourceSchema.StringAttribute{ + Description: "The unique ID for this resource.", + Computed: true, + }, + "base64": resourceSchema.StringAttribute{ + Computed: true, + Sensitive: true, + Description: "The base64 encoded pfx certificate.", + }, + "thumbprint": resourceSchema.StringAttribute{ + Computed: true, + Description: "The SHA1 sum of the certificate represented in hexadecimal.", + }, + "dependencies": resourceSchema.MapAttribute{ + Optional: true, + ElementType: types.StringType, + Description: "Optional map of dependencies that when modified will trigger a re-creation of this resource.", + PlanModifiers: []planmodifier.Map{ + mapplanmodifier.RequiresReplace(), + }, + }, + }, + } +} + +type TentacleCertificateResourceModel struct { + ID types.String `tfsdk:"id"` + Base64 types.String `tfsdk:"base64"` + Thumbprint types.String `tfsdk:"thumbprint"` + Dependencies types.Map `tfsdk:"dependencies"` +} From e4c08d3ee663710280c5f35a30b9ff00df2ed976 Mon Sep 17 00:00:00 2001 From: Huy Nguyen <162080607+HuyPhanNguyen@users.noreply.github.com> Date: Mon, 12 Aug 2024 16:36:34 +1000 Subject: [PATCH 15/28] Migrate tag set resource and datasource (#725) * Add tagset resource and datasource * update tag_set test * refactor * Fix tagset issue * fix issue with tag_set datasource --- octopusdeploy/data_source_tag_sets.go | 45 ---- octopusdeploy/data_source_tag_sets_test.go | 49 ----- octopusdeploy/provider.go | 2 - octopusdeploy/resource_tag_set.go | 103 --------- octopusdeploy/resource_tag_set_test.go | 156 ------------- octopusdeploy/schema_tag_set.go | 83 ------- octopusdeploy/schema_tag_test.go | 36 --- .../datasource_tag_sets.go | 82 +++++++ .../datasource_tag_sets_test.go | 73 +++++++ octopusdeploy_framework/framework_provider.go | 2 + octopusdeploy_framework/resource_tag_set.go | 144 ++++++++++++ .../resource_tag_set_test.go | 205 ++++++++++++++++++ octopusdeploy_framework/schemas/tag_set.go | 113 ++++++++++ octopusdeploy_framework/util/util.go | 1 + 14 files changed, 620 insertions(+), 474 deletions(-) delete mode 100644 octopusdeploy/data_source_tag_sets.go delete mode 100644 octopusdeploy/data_source_tag_sets_test.go delete mode 100644 octopusdeploy/resource_tag_set.go delete mode 100644 octopusdeploy/resource_tag_set_test.go delete mode 100644 octopusdeploy/schema_tag_set.go delete mode 100644 octopusdeploy/schema_tag_test.go create mode 100644 octopusdeploy_framework/datasource_tag_sets.go create mode 100644 octopusdeploy_framework/datasource_tag_sets_test.go create mode 100644 octopusdeploy_framework/resource_tag_set.go create mode 100644 octopusdeploy_framework/resource_tag_set_test.go create mode 100644 octopusdeploy_framework/schemas/tag_set.go diff --git a/octopusdeploy/data_source_tag_sets.go b/octopusdeploy/data_source_tag_sets.go deleted file mode 100644 index 63395248b..000000000 --- a/octopusdeploy/data_source_tag_sets.go +++ /dev/null @@ -1,45 +0,0 @@ -package octopusdeploy - -import ( - "context" - "time" - - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client" - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tagsets" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func dataSourceTagSets() *schema.Resource { - return &schema.Resource{ - Description: "Provides information about existing tag sets.", - ReadContext: dataSourceTagSetsRead, - Schema: getTagSetDataSchema(), - } -} - -func dataSourceTagSetsRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - query := tagsets.TagSetsQuery{ - IDs: expandArray(d.Get("ids").([]interface{})), - PartialName: d.Get("partial_name").(string), - Skip: d.Get("skip").(int), - Take: d.Get("take").(int), - } - spaceID := d.Get("space_id").(string) - - octopus := m.(*client.Client) - existingTagSets, err := tagsets.Get(octopus, spaceID, query) - if err != nil { - return diag.FromErr(err) - } - - flattenedTagSets := []interface{}{} - for _, tagSet := range existingTagSets.Items { - flattenedTagSets = append(flattenedTagSets, flattenTagSet(tagSet)) - } - - d.Set("tag_sets", flattenedTagSets) - d.SetId("TagSets " + time.Now().UTC().String()) - - return nil -} diff --git a/octopusdeploy/data_source_tag_sets_test.go b/octopusdeploy/data_source_tag_sets_test.go deleted file mode 100644 index 2fb8c668e..000000000 --- a/octopusdeploy/data_source_tag_sets_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package octopusdeploy - -import ( - "fmt" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" -) - -func TestAccDataSourceTagSets(t *testing.T) { - localName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) - name := fmt.Sprintf("data.octopusdeploy_tag_sets.%s", localName) - take := 10 - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProtoV6ProviderFactories: ProtoV6ProviderFactories(), - Steps: []resource.TestStep{ - { - Config: testAccDataSourceTagSetsConfig(localName, take), - Check: resource.ComposeTestCheckFunc( - testAccCheckTagSetsDataSourceID(name), - )}, - }, - }) -} - -func testAccCheckTagSetsDataSourceID(n string) resource.TestCheckFunc { - return func(s *terraform.State) error { - all := s.RootModule().Resources - rs, ok := all[n] - if !ok { - return fmt.Errorf("cannot find TagSets data source: %s", n) - } - - if rs.Primary.ID == "" { - return fmt.Errorf("snapshot TagSets source ID not set") - } - return nil - } -} - -func testAccDataSourceTagSetsConfig(localName string, take int) string { - return fmt.Sprintf(`data "octopusdeploy_tag_sets" "%s" { - take = %v - }`, localName, take) -} diff --git a/octopusdeploy/provider.go b/octopusdeploy/provider.go index 4bc99cfc6..cdb60f086 100644 --- a/octopusdeploy/provider.go +++ b/octopusdeploy/provider.go @@ -26,7 +26,6 @@ func Provider() *schema.Provider { "octopusdeploy_offline_package_drop_deployment_targets": dataSourceOfflinePackageDropDeploymentTargets(), "octopusdeploy_polling_tentacle_deployment_targets": dataSourcePollingTentacleDeploymentTargets(), "octopusdeploy_ssh_connection_deployment_targets": dataSourceSSHConnectionDeploymentTargets(), - "octopusdeploy_tag_sets": dataSourceTagSets(), "octopusdeploy_teams": dataSourceTeams(), "octopusdeploy_users": dataSourceUsers(), "octopusdeploy_user_roles": dataSourceUserRoles(), @@ -63,7 +62,6 @@ func Provider() *schema.Provider { "octopusdeploy_ssh_key_account": resourceSSHKeyAccount(), "octopusdeploy_static_worker_pool": resourceStaticWorkerPool(), "octopusdeploy_tag": resourceTag(), - "octopusdeploy_tag_set": resourceTagSet(), "octopusdeploy_team": resourceTeam(), "octopusdeploy_token_account": resourceTokenAccount(), "octopusdeploy_user": resourceUser(), diff --git a/octopusdeploy/resource_tag_set.go b/octopusdeploy/resource_tag_set.go deleted file mode 100644 index c80e19623..000000000 --- a/octopusdeploy/resource_tag_set.go +++ /dev/null @@ -1,103 +0,0 @@ -package octopusdeploy - -import ( - "context" - "fmt" - "log" - - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client" - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tagsets" - "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal/errors" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func resourceTagSet() *schema.Resource { - return &schema.Resource{ - CreateContext: resourceTagSetCreate, - DeleteContext: resourceTagSetDelete, - Description: "This resource manages tag sets in Octopus Deploy.", - Importer: getImporter(), - ReadContext: resourceTagSetRead, - Schema: getTagSetSchema(), - UpdateContext: resourceTagSetUpdate, - } -} - -func resourceTagSetCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - tagSet := expandTagSet(d) - - log.Printf("[INFO] creating tag set: %#v", tagSet) - - octopus := m.(*client.Client) - createdTagSet, err := tagsets.Add(octopus, tagSet) - if err != nil { - return diag.FromErr(err) - } - - if err := setTagSet(ctx, d, createdTagSet); err != nil { - return diag.FromErr(err) - } - - d.SetId(createdTagSet.GetID()) - - log.Printf("[INFO] tag set created (%s)", d.Id()) - return nil -} - -func resourceTagSetDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - log.Printf("[INFO] deleting tag set (%s)", d.Id()) - - octopus := m.(*client.Client) - if err := tagsets.DeleteByID(octopus, d.Get("space_id").(string), d.Id()); err != nil { - return diag.FromErr(err) - } - - d.SetId("") - - log.Printf("[INFO] tag set deleted") - return nil -} - -func resourceTagSetRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Info(ctx, fmt.Sprintf("reading tag set (%s)", d.Id())) - - octopus := m.(*client.Client) - tagSet, err := tagsets.GetByID(octopus, d.Get("space_id").(string), d.Id()) - if err != nil { - return errors.ProcessApiError(ctx, d, err, "tag set") - } - - if err := setTagSet(ctx, d, tagSet); err != nil { - return diag.FromErr(err) - } - - tflog.Info(ctx, fmt.Sprintf("tag set read (%s)", d.Id())) - return nil -} - -func resourceTagSetUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - tagSet := expandTagSet(d) - - log.Printf("[INFO] updating tag set: %#v", tagSet) - - octopus := m.(*client.Client) - existingTagSet, err := tagsets.GetByID(octopus, d.Get("space_id").(string), d.Id()) - if err != nil { - return diag.FromErr(err) - } - tagSet.Tags = existingTagSet.Tags - - updatedTagSet, err := tagsets.Update(octopus, tagSet) - if err != nil { - return diag.FromErr(err) - } - - if err := setTagSet(ctx, d, updatedTagSet); err != nil { - return diag.FromErr(err) - } - - log.Printf("[INFO] tag set updated (%s)", d.Id()) - return nil -} diff --git a/octopusdeploy/resource_tag_set_test.go b/octopusdeploy/resource_tag_set_test.go deleted file mode 100644 index a964ea880..000000000 --- a/octopusdeploy/resource_tag_set_test.go +++ /dev/null @@ -1,156 +0,0 @@ -package octopusdeploy - -import ( - "fmt" - "testing" - - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tagsets" - internalTest "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal/test" - "github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework/octoclient" - "github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework/test" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" -) - -func TestAccOctopusDeployTagSetBasic(t *testing.T) { - internalTest.SkipCI(t, " Error: Unsupported block type") - localName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) - prefix := "octopusdeploy_tag_set." + localName - - name := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) - tagColor := "#6e6e6e" - tagDescription := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) - tagName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) - - resource.Test(t, resource.TestCase{ - CheckDestroy: testTagSetDestroy, - PreCheck: func() { testAccPreCheck(t) }, - ProtoV6ProviderFactories: ProtoV6ProviderFactories(), - Steps: []resource.TestStep{ - { - Check: resource.ComposeTestCheckFunc( - testTagSetExists(prefix), - resource.TestCheckResourceAttr(prefix, "name", name), - resource.TestCheckResourceAttr(prefix, "tag.#", "0"), - ), - Config: testTagSetMinimal(localName, name), - }, - { - Check: resource.ComposeTestCheckFunc( - testTagSetExists(prefix), - resource.TestCheckResourceAttr(prefix, "name", name), - resource.TestCheckResourceAttrSet(prefix, "id"), - resource.TestCheckResourceAttrSet(prefix, "tag.0.id"), - resource.TestCheckResourceAttr(prefix, "tag.0.color", tagColor), - resource.TestCheckResourceAttr(prefix, "tag.0.description", tagDescription), - resource.TestCheckResourceAttr(prefix, "tag.0.name", tagName), - ), - Config: testTagSetComplete(localName, name, tagColor, tagDescription, tagName), - }, - }, - }) -} - -func testTagSetMinimal(localName string, name string) string { - return fmt.Sprintf(`resource "octopusdeploy_tag_set" "%s" { - name = "%s" - }`, localName, name) -} - -func testTagSetComplete(localName string, name string, tagColor string, tagDescription string, tagName string) string { - return fmt.Sprintf(`resource "octopusdeploy_tag_set" "%s" { - name = "%s" - tag { - color = "%s" - description = "%s" - name = "%s" - } - }`, localName, name, tagColor, tagDescription, tagName) -} - -func testTagSetExists(prefix string) resource.TestCheckFunc { - return func(s *terraform.State) error { - tagSetID := s.RootModule().Resources[prefix].Primary.ID - if _, err := octoClient.TagSets.GetByID(tagSetID); err != nil { - return err - } - - return nil - } -} - -func testTagSetDestroy(s *terraform.State) error { - for _, rs := range s.RootModule().Resources { - tagSetID := rs.Primary.ID - tagSet, err := octoClient.TagSets.GetByID(tagSetID) - if err == nil { - if tagSet != nil { - return fmt.Errorf("tag set (%s) still exists", rs.Primary.ID) - } - } - } - - return nil -} - -// TestTagSetResource verifies that a tag set can be reimported with the correct settings -func TestTagSetResource(t *testing.T) { - internalTest.SkipCI(t, " Error: Unsupported block type") - testFramework := test.OctopusContainerTest{} - newSpaceId, err := testFramework.Act(t, octoContainer, "../terraform", "21-tagset", []string{}) - - if err != nil { - t.Fatal(err.Error()) - } - - // Assert - client, err := octoclient.CreateClient(octoContainer.URI, newSpaceId, test.ApiKey) - query := tagsets.TagSetsQuery{ - PartialName: "tag1", - Skip: 0, - Take: 1, - } - - resources, err := client.TagSets.Get(query) - if err != nil { - t.Fatal(err.Error()) - } - - if len(resources.Items) == 0 { - t.Fatalf("Space must have a tag set called \"tag1\"") - } - resource := resources.Items[0] - - if resource.Description != "Test tagset" { - t.Fatal("The tag set must be have a description of \"Test tagset\" (was \"" + resource.Description + "\")") - } - - if resource.SortOrder != 0 { - t.Fatal("The tag set must be have a sort order of \"0\" (was \"" + fmt.Sprint(resource.SortOrder) + "\")") - } - - tagAFound := false - for _, u := range resource.Tags { - if u.Name == "a" { - tagAFound = true - - if u.Description != "tag a" { - t.Fatal("The tag a must be have a description of \"tag a\" (was \"" + u.Description + "\")") - } - - if u.Color != "#333333" { - t.Fatal("The tag a must be have a color of \"#333333\" (was \"" + u.Color + "\")") - } - - if u.SortOrder != 2 { - t.Fatal("The tag a must be have a sort order of \"2\" (was \"" + fmt.Sprint(u.SortOrder) + "\")") - } - } - } - - if !tagAFound { - t.Fatal("Tag Set must have an tag called \"a\"") - } -} diff --git a/octopusdeploy/schema_tag_set.go b/octopusdeploy/schema_tag_set.go deleted file mode 100644 index 953b5c40c..000000000 --- a/octopusdeploy/schema_tag_set.go +++ /dev/null @@ -1,83 +0,0 @@ -package octopusdeploy - -import ( - "context" - - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tagsets" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func expandTagSet(d *schema.ResourceData) *tagsets.TagSet { - name := d.Get("name").(string) - - tagSet := tagsets.NewTagSet(name) - tagSet.ID = d.Id() - - if v, ok := d.GetOk("description"); ok { - tagSet.Description = v.(string) - } - - if v, ok := d.GetOk("sort_order"); ok { - tagSet.SortOrder = int32(v.(int)) - } - - if v, ok := d.GetOk("space_id"); ok { - tagSet.SpaceID = v.(string) - } - - return tagSet -} - -func flattenTagSet(tagSet *tagsets.TagSet) map[string]interface{} { - if tagSet == nil { - return nil - } - - return map[string]interface{}{ - "description": tagSet.Description, - "id": tagSet.GetID(), - "name": tagSet.Name, - "sort_order": tagSet.SortOrder, - "space_id": tagSet.SpaceID, - } -} - -func getTagSetDataSchema() map[string]*schema.Schema { - dataSchema := getTagSetSchema() - setDataSchema(&dataSchema) - - return map[string]*schema.Schema{ - "ids": getQueryIDs(), - "partial_name": getQueryPartialName(), - "skip": getQuerySkip(), - "tag_sets": { - Computed: true, - Description: "A list of tag sets that match the filter(s).", - Elem: &schema.Resource{Schema: dataSchema}, - Optional: false, - Type: schema.TypeList, - }, - "take": getQueryTake(), - "space_id": getSpaceIDSchema(), - } -} - -func getTagSetSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "description": getDescriptionSchema("tag set"), - "id": getIDSchema(), - "name": getNameSchema(true), - "sort_order": getSortOrderSchema(), - "space_id": getSpaceIDSchema(), - } -} - -func setTagSet(ctx context.Context, d *schema.ResourceData, tagSet *tagsets.TagSet) error { - d.Set("description", tagSet.Description) - d.Set("id", tagSet.GetID()) - d.Set("name", tagSet.Name) - d.Set("sort_order", tagSet.SortOrder) - d.Set("space_id", tagSet.SpaceID) - - return nil -} diff --git a/octopusdeploy/schema_tag_test.go b/octopusdeploy/schema_tag_test.go deleted file mode 100644 index 65a9704df..000000000 --- a/octopusdeploy/schema_tag_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package octopusdeploy - -import ( - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/stretchr/testify/require" -) - -func TestExpandTag(t *testing.T) { - canonicalTagName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) - color := "#FF0000" - description := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) - id := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) - name := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) - sortOrder := acctest.RandIntRange(0, 1000) - - resourceDataMap := map[string]interface{}{ - "canonical_tag_name": canonicalTagName, - "color": color, - "description": description, - "id": id, - "name": name, - "sort_order": sortOrder, - } - - d := schema.TestResourceDataRaw(t, getTagSchema(), resourceDataMap) - tag := expandTag(d) - - require.Equal(t, tag.CanonicalTagName, canonicalTagName) - require.Equal(t, tag.Color, color) - require.Equal(t, tag.Description, description) - require.Equal(t, tag.Name, name) - require.Equal(t, tag.SortOrder, sortOrder) -} diff --git a/octopusdeploy_framework/datasource_tag_sets.go b/octopusdeploy_framework/datasource_tag_sets.go new file mode 100644 index 000000000..10566e299 --- /dev/null +++ b/octopusdeploy_framework/datasource_tag_sets.go @@ -0,0 +1,82 @@ +package octopusdeploy_framework + +import ( + "context" + "fmt" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tagsets" + "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/datasource" + "github.com/hashicorp/terraform-plugin-framework/types" + "time" +) + +var _ datasource.DataSource = &tagSetsDataSource{} + +type tagSetsDataSource struct { + *Config +} + +func NewTagSetsDataSource() datasource.DataSource { + return &tagSetsDataSource{} +} + +func (t *tagSetsDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = util.GetTypeName(schemas.TagSetDataSourceName) +} + +func (t *tagSetsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schemas.GetTagSetDataSourceSchema() +} + +func (t *tagSetsDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + t.Config = DataSourceConfiguration(req, resp) +} + +func (t *tagSetsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data schemas.TagSetDataSourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + query := tagsets.TagSetsQuery{ + IDs: schemas.GetIds(data.IDs), + PartialName: data.PartialName.ValueString(), + Skip: int(data.Skip.ValueInt64()), + Take: int(data.Take.ValueInt64()), + } + spaceID := data.SpaceID.ValueString() + + existingTagSets, err := tagsets.Get(t.Client, spaceID, query) + if err != nil { + resp.Diagnostics.AddError("Unable to query tag sets", err.Error()) + return + } + + data.TagSets = flattenTagSets(existingTagSets.Items) + + data.ID = types.StringValue(fmt.Sprintf("TagSets-%s", time.Now().UTC().String())) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func flattenTagSets(tagSets []*tagsets.TagSet) types.List { + if len(tagSets) == 0 { + return types.ListNull(types.ObjectType{AttrTypes: schemas.GetTagSetAttrTypes()}) + } + + tfList := make([]attr.Value, len(tagSets)) + for i, tagSet := range tagSets { + tfList[i] = types.ObjectValueMust(schemas.GetTagSetAttrTypes(), map[string]attr.Value{ + "id": types.StringValue(tagSet.ID), + "name": types.StringValue(tagSet.Name), + "description": types.StringValue(tagSet.Description), + "sort_order": types.Int64Value(int64(tagSet.SortOrder)), + "space_id": types.StringValue(tagSet.SpaceID), + }) + } + + return types.ListValueMust(types.ObjectType{AttrTypes: schemas.GetTagSetAttrTypes()}, tfList) +} diff --git a/octopusdeploy_framework/datasource_tag_sets_test.go b/octopusdeploy_framework/datasource_tag_sets_test.go new file mode 100644 index 000000000..46f34ba32 --- /dev/null +++ b/octopusdeploy_framework/datasource_tag_sets_test.go @@ -0,0 +1,73 @@ +package octopusdeploy_framework + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "testing" +) + +func TestAccDataSourceTagSets(t *testing.T) { + localName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) + tagSetName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) + tagSetResourceName := fmt.Sprintf("octopusdeploy_tag_set.%s", localName) + dataSourceName := fmt.Sprintf("data.octopusdeploy_tag_sets.%s", localName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { TestAccPreCheck(t) }, + ProtoV6ProviderFactories: ProtoV6ProviderFactories(), + Steps: []resource.TestStep{ + // Create a tag set + { + Config: testAccTagSetConfig(localName, tagSetName), + Check: resource.ComposeTestCheckFunc( + testTagSetExists(tagSetResourceName), + resource.TestCheckResourceAttr(tagSetResourceName, "name", tagSetName), + ), + }, + // Query the created tag set using the data source + { + Config: testAccTagSetConfig(localName, tagSetName) + testAccDataSourceTagSetsConfig(localName, tagSetName), + Check: resource.ComposeTestCheckFunc( + testAccCheckTagSetsDataSourceID(dataSourceName), + resource.TestCheckResourceAttrPair(dataSourceName, "tag_sets.0.id", tagSetResourceName, "id"), + resource.TestCheckResourceAttrPair(dataSourceName, "tag_sets.0.name", tagSetResourceName, "name"), + ), + }, + }, + }) +} + +func testAccCheckTagSetsDataSourceID(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("cannot find TagSets data source: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("TagSets data source ID not set") + } + return nil + } +} + +func testAccTagSetConfig(localName, tagSetName string) string { + return fmt.Sprintf(` +resource "octopusdeploy_tag_set" "%s" { + name = "%s" + description = "Test tag set" +} +`, localName, tagSetName) +} + +func testAccDataSourceTagSetsConfig(localName, tagSetName string) string { + return fmt.Sprintf(` +data "octopusdeploy_tag_sets" "%s" { + partial_name = "%s" + skip = 0 + take = 10 +} +`, localName, tagSetName) +} diff --git a/octopusdeploy_framework/framework_provider.go b/octopusdeploy_framework/framework_provider.go index 5bdf6cc43..19bff356a 100644 --- a/octopusdeploy_framework/framework_provider.go +++ b/octopusdeploy_framework/framework_provider.go @@ -71,6 +71,7 @@ func (p *octopusDeployFrameworkProvider) DataSources(ctx context.Context) []func NewVariablesDataSource, NewProjectsDataSource, NewTenantsDataSource, + NewTagSetsDataSource, NewScriptModuleDataSource, } } @@ -95,6 +96,7 @@ func (p *octopusDeployFrameworkProvider) Resources(ctx context.Context) []func() NewVariableResource, NewProjectResource, NewDockerContainerRegistryFeedResource, + NewTagSetResource, NewUsernamePasswordAccountResource, NewRunbookResource, NewTenantResource, diff --git a/octopusdeploy_framework/resource_tag_set.go b/octopusdeploy_framework/resource_tag_set.go new file mode 100644 index 000000000..b14ae7eaa --- /dev/null +++ b/octopusdeploy_framework/resource_tag_set.go @@ -0,0 +1,144 @@ +package octopusdeploy_framework + +import ( + "context" + "fmt" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tagsets" + "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" +) + +var _ resource.Resource = &tagSetResource{} +var _ resource.ResourceWithImportState = &tagSetResource{} + +type tagSetResource struct { + *Config +} + +func NewTagSetResource() resource.Resource { + return &tagSetResource{} +} + +func (r *tagSetResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = util.GetTypeName(schemas.TagSetResourceName) +} + +func (r *tagSetResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schemas.GetTagSetResourceSchema() +} + +func (r *tagSetResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.Config = ResourceConfiguration(req, resp) +} + +func (r *tagSetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan schemas.TagSetResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + tagSet := expandTagSet(plan) + createdTagSet, err := tagsets.Add(r.Client, tagSet) + if err != nil { + resp.Diagnostics.AddError("Error creating tag set", err.Error()) + return + } + + state := flattenTagSet(createdTagSet) + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) +} + +func (r *tagSetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state schemas.TagSetResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + tagSet, err := tagsets.GetByID(r.Client, state.SpaceID.ValueString(), state.ID.ValueString()) + if err != nil { + if err := errors.ProcessApiErrorV2(ctx, resp, state, err, "tagSetResource"); err != nil { + resp.Diagnostics.AddError("unable to load tag set", err.Error()) + } + return + } + + newState := flattenTagSet(tagSet) + resp.Diagnostics.Append(resp.State.Set(ctx, newState)...) +} + +func (r *tagSetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan schemas.TagSetResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + tagSet := expandTagSet(plan) + updatedTagSet, err := tagsets.Update(r.Client, tagSet) + if err != nil { + resp.Diagnostics.AddError("Error updating tag set", err.Error()) + return + } + + state := flattenTagSet(updatedTagSet) + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) +} + +func (r *tagSetResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state schemas.TagSetResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + err := tagsets.DeleteByID(r.Client, state.SpaceID.ValueString(), state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Error deleting tag set", err.Error()) + return + } +} + +func (r *tagSetResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + tagSetID := req.ID + + tagSet, err := tagsets.GetByID(r.Client, r.Client.GetSpaceID(), tagSetID) + if err != nil { + resp.Diagnostics.AddError( + "Error reading tag set", + fmt.Sprintf("Unable to read tag set with ID %s: %s", tagSetID, err.Error()), + ) + return + } + + state := flattenTagSet(tagSet) + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) +} + +func expandTagSet(model schemas.TagSetResourceModel) *tagsets.TagSet { + tagSet := tagsets.NewTagSet(model.Name.ValueString()) + tagSet.ID = model.ID.ValueString() + tagSet.Description = model.Description.ValueString() + tagSet.SpaceID = model.SpaceID.ValueString() + + if !model.SortOrder.IsNull() { + tagSet.SortOrder = int32(model.SortOrder.ValueInt64()) + } + + return tagSet +} + +func flattenTagSet(tagSet *tagsets.TagSet) schemas.TagSetResourceModel { + model := schemas.TagSetResourceModel{ + Name: types.StringValue(tagSet.Name), + Description: types.StringValue(tagSet.Description), + SortOrder: types.Int64Value(int64(tagSet.SortOrder)), + SpaceID: types.StringValue(tagSet.SpaceID), + } + model.ID = types.StringValue(tagSet.ID) + return model +} diff --git a/octopusdeploy_framework/resource_tag_set_test.go b/octopusdeploy_framework/resource_tag_set_test.go new file mode 100644 index 000000000..49b8b04ad --- /dev/null +++ b/octopusdeploy_framework/resource_tag_set_test.go @@ -0,0 +1,205 @@ +package octopusdeploy_framework + +import ( + "fmt" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/schemas" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/stretchr/testify/require" + "testing" + + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tagsets" + "github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework/octoclient" + "github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework/test" +) + +func TestTagSetAndTag(t *testing.T) { + tagSetName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) + tagSetPrefix := "octopusdeploy_tag_set." + tagSetName + tagSetDescription := "TagSet Description" + tagSetName + + tagName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) + tagPrefix := "octopusdeploy_tag." + tagName + tagColor := "#6e6e6e" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { TestAccPreCheck(t) }, + ProtoV6ProviderFactories: ProtoV6ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: testTagSetConfig(tagSetName, tagSetDescription), + Check: resource.ComposeTestCheckFunc( + testTagSetExists(tagSetPrefix), + resource.TestCheckResourceAttr(tagSetPrefix, "name", tagSetName), + resource.TestCheckResourceAttr(tagSetPrefix, "description", tagSetDescription), + ), + }, + { + Config: testTagSetAndTagConfig(tagSetName, tagSetDescription, tagName, tagColor), + Check: resource.ComposeTestCheckFunc( + testTagSetExists(tagSetPrefix), + testTagExists(tagPrefix), + resource.TestCheckResourceAttr(tagSetPrefix, "name", tagSetName), + resource.TestCheckResourceAttr(tagSetPrefix, "description", tagSetDescription), + resource.TestCheckResourceAttr(tagPrefix, "name", tagName), + resource.TestCheckResourceAttr(tagPrefix, "color", tagColor), + resource.TestCheckResourceAttrSet(tagPrefix, "id"), + resource.TestCheckResourceAttrSet(tagPrefix, "tag_set_space_id"), + resource.TestCheckResourceAttrSet(tagPrefix, "tag_set_id"), + resource.TestCheckResourceAttrPair(tagPrefix, "tag_set_id", tagSetPrefix, "id"), + ), + }, + }, + }) +} + +func testTagSetConfig(name, description string) string { + return fmt.Sprintf(` + resource "octopusdeploy_tag_set" "%s" { + name = "%s" + description = "%s" + }`, name, name, description) +} + +func testTagSetAndTagConfig(tagSetName, tagSetDescription, tagName, tagColor string) string { + var tfConfig = fmt.Sprintf(` + resource "octopusdeploy_tag_set" "%s" { + name = "%s" + description = "%s" + } + + resource "octopusdeploy_tag" "%s" { + name = "%s" + color = "%s" + description = "Test tag" + tag_set_id = octopusdeploy_tag_set.%s.id + }`, tagSetName, tagSetName, tagSetDescription, tagName, tagName, tagColor, tagSetName) + return tfConfig +} + +func testTagSetExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no ID is set") + } + + if _, err := tagsets.GetByID(octoClient, rs.Primary.Attributes["space_id"], rs.Primary.ID); err != nil { + return fmt.Errorf("error retrieving tag set: %s", err) + } + + return nil + } +} + +func testTagExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no ID is set") + } + + tagSetID := rs.Primary.Attributes["tag_set_id"] + tagSet, err := tagsets.GetByID(octoClient, rs.Primary.Attributes["space_id"], tagSetID) + if err != nil { + return fmt.Errorf("error retrieving tag set: %s", err) + } + + for _, tag := range tagSet.Tags { + if tag.ID == rs.Primary.ID { + return nil + } + } + + return fmt.Errorf("tag not found in tag set") + } +} + +func TestTagSetResource(t *testing.T) { + testFramework := test.OctopusContainerTest{} + newSpaceId, err := testFramework.Act(t, octoContainer, "../terraform", "21-tagset", []string{}) + + if err != nil { + t.Fatal(err.Error()) + } + + // Assert + client, err := octoclient.CreateClient(octoContainer.URI, newSpaceId, test.ApiKey) + query := tagsets.TagSetsQuery{ + PartialName: "tag1", + Skip: 0, + Take: 1, + } + + resources, err := client.TagSets.Get(query) + if err != nil { + t.Fatal(err.Error()) + } + + if len(resources.Items) == 0 { + t.Fatalf("Space must have a tag set called \"tag1\"") + } + resource := resources.Items[0] + + if resource.Description != "Test tagset" { + t.Fatal("The tag set must be have a description of \"Test tagset\" (was \"" + resource.Description + "\")") + } + + if resource.SortOrder != 0 { + t.Fatal("The tag set must be have a sort order of \"0\" (was \"" + fmt.Sprint(resource.SortOrder) + "\")") + } + + tagAFound := false + for _, u := range resource.Tags { + if u.Name == "a" { + tagAFound = true + + if u.Description != "tag a" { + t.Fatal("The tag a must be have a description of \"tag a\" (was \"" + u.Description + "\")") + } + + if u.Color != "#333333" { + t.Fatal("The tag a must be have a color of \"#333333\" (was \"" + u.Color + "\")") + } + + if u.SortOrder != 2 { + t.Fatal("The tag a must be have a sort order of \"2\" (was \"" + fmt.Sprint(u.SortOrder) + "\")") + } + } + } + + if !tagAFound { + t.Fatal("Tag Set must have an tag called \"a\"") + } +} + +func TestExpandTagSet(t *testing.T) { + name := "Test Tag Set" + description := "This is a test tag set" + sortOrder := int64(10) + spaceID := "Spaces-1" + + tagSetModel := schemas.TagSetResourceModel{ + Name: types.StringValue(name), + Description: types.StringValue(description), + SortOrder: types.Int64Value(sortOrder), + SpaceID: types.StringValue(spaceID), + } + + tagSet := expandTagSet(tagSetModel) + + require.Equal(t, name, tagSet.Name) + require.Equal(t, description, tagSet.Description) + require.Equal(t, int32(sortOrder), tagSet.SortOrder) + require.Equal(t, spaceID, tagSet.SpaceID) +} diff --git a/octopusdeploy_framework/schemas/tag_set.go b/octopusdeploy_framework/schemas/tag_set.go new file mode 100644 index 000000000..a75ef2a5d --- /dev/null +++ b/octopusdeploy_framework/schemas/tag_set.go @@ -0,0 +1,113 @@ +package schemas + +import ( + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" + "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/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +const TagSetDataSourceName = "tag_sets" +const TagSetResourceName = "tag_set" + +func GetTagSetResourceSchema() resourceSchema.Schema { + return resourceSchema.Schema{ + Description: "This resource manages tag sets in Octopus Deploy.", + Attributes: map[string]resourceSchema.Attribute{ + "id": util.ResourceString(). + Optional(). + Computed(). + Description("The unique ID for this resource."). + PlanModifiers(stringplanmodifier.UseStateForUnknown()). + Build(), + "name": util.ResourceString(). + Required(). + Description("The name of this resource."). + Build(), + "description": util.ResourceString(). + Optional(). + Computed(). + Description("The description of this tag set."). + Build(), + "sort_order": util.ResourceInt64(). + Optional(). + Computed(). + Description("The sort order associated with this resource."). + Build(), + "space_id": util.ResourceString(). + Optional(). + Computed(). + Description("The space ID associated with this resource."). + PlanModifiers(stringplanmodifier.UseStateForUnknown()). + Build(), + }, + } +} + +func GetTagSetDataSourceSchema() datasourceSchema.Schema { + return datasourceSchema.Schema{ + Description: "Provides information about existing tag sets.", + Attributes: map[string]datasourceSchema.Attribute{ + "id": util.DataSourceString(). + Computed(). + Description("The ID of this resource."). + Build(), + "space_id": util.DataSourceString(). + Optional(). + Description("The space ID associated with this resource."). + Build(), + "ids": util.DataSourceList(types.StringType). + Optional(). + Description("A filter to search by a list of IDs."). + Build(), + "partial_name": util.DataSourceString(). + Optional(). + Description("A filter to search by the partial match of a name."). + Build(), + "skip": util.DataSourceInt64(). + Optional(). + Description("A filter to specify the number of items to skip in the response."). + Build(), + "take": util.DataSourceInt64(). + Optional(). + Description("A filter to specify the number of items to take (or return) in the response."). + Build(), + "tag_sets": datasourceSchema.ListAttribute{ + Computed: true, + ElementType: types.ObjectType{AttrTypes: GetTagSetAttrTypes()}, + Description: "A list of tag sets that match the filter(s).", + }, + }, + } +} + +func GetTagSetAttrTypes() map[string]attr.Type { + return map[string]attr.Type{ + "id": types.StringType, + "space_id": types.StringType, + "name": types.StringType, + "description": types.StringType, + "sort_order": types.Int64Type, + } +} + +type TagSetDataSourceModel struct { + ID types.String `tfsdk:"id"` + SpaceID types.String `tfsdk:"space_id"` + IDs types.List `tfsdk:"ids"` + PartialName types.String `tfsdk:"partial_name"` + Skip types.Int64 `tfsdk:"skip"` + Take types.Int64 `tfsdk:"take"` + TagSets types.List `tfsdk:"tag_sets"` +} + +type TagSetResourceModel struct { + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + SortOrder types.Int64 `tfsdk:"sort_order"` + SpaceID types.String `tfsdk:"space_id"` + + ResourceModel +} diff --git a/octopusdeploy_framework/util/util.go b/octopusdeploy_framework/util/util.go index 0f167bf80..e0c87bdea 100644 --- a/octopusdeploy_framework/util/util.go +++ b/octopusdeploy_framework/util/util.go @@ -44,6 +44,7 @@ func ExpandStringList(list types.List) []string { } return result } + func SetToStringArray(ctx context.Context, set types.Set) ([]string, diag.Diagnostics) { teams := make([]types.String, 0, len(set.Elements())) diags := diag.Diagnostics{} From 3b1285c8012efd718c3560a7e83ecf9ae156116f Mon Sep 17 00:00:00 2001 From: Henrik Andersson Date: Mon, 12 Aug 2024 17:13:58 -0700 Subject: [PATCH 16/28] chore: add ability to import an environment (#729) --- octopusdeploy_framework/resource_environment.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/octopusdeploy_framework/resource_environment.go b/octopusdeploy_framework/resource_environment.go index 281639f6d..3c78b3493 100644 --- a/octopusdeploy_framework/resource_environment.go +++ b/octopusdeploy_framework/resource_environment.go @@ -8,10 +8,13 @@ import ( "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/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/types" ) +var _ resource.ResourceWithImportState = &environmentTypeResource{} + type environmentTypeResource struct { *Config } @@ -32,6 +35,10 @@ func (r *environmentTypeResource) Configure(_ context.Context, req resource.Conf r.Config = ResourceConfiguration(req, resp) } +func (*environmentTypeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + func (r *environmentTypeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data schemas.EnvironmentTypeResourceModel resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) From c16f12a76e94247c854bafeca96c05e0c89eed06 Mon Sep 17 00:00:00 2001 From: Isaac Calligeros <101079287+IsaacCalligeros95@users.noreply.github.com> Date: Tue, 13 Aug 2024 10:15:17 +0930 Subject: [PATCH 17/28] Migrate tag resource (#719) * Chore!: Migrate tag resource * Migrate tag resource * fix build fail * rename * Add resource_tag_test.go * Fix concurrent issue * Fix update and import * Fix document * Fix wrong schema document * refactor remove duplicate code * tidy * updated default value for description --------- Co-authored-by: Huy Nguyen Co-authored-by: Ben Pearce --- docs/resources/tag.md | 12 +- octopusdeploy/provider.go | 1 - octopusdeploy/resource_tag.go | 304 -------------- octopusdeploy/schema_tag.go | 74 ---- octopusdeploy_framework/framework_provider.go | 1 + octopusdeploy_framework/resource_tag.go | 375 ++++++++++++++++++ octopusdeploy_framework/resource_tag_test.go | 124 ++++++ octopusdeploy_framework/schemas/tag.go | 92 +++++ 8 files changed, 598 insertions(+), 385 deletions(-) delete mode 100644 octopusdeploy/resource_tag.go delete mode 100644 octopusdeploy/schema_tag.go create mode 100644 octopusdeploy_framework/resource_tag.go create mode 100644 octopusdeploy_framework/resource_tag_test.go create mode 100644 octopusdeploy_framework/schemas/tag.go diff --git a/docs/resources/tag.md b/docs/resources/tag.md index 9f40e39d8..314842877 100644 --- a/docs/resources/tag.md +++ b/docs/resources/tag.md @@ -17,19 +17,19 @@ This resource manages tags in Octopus Deploy. ### Required -- `color` (String) -- `name` (String) The name of this resource. +- `color` (String) The color of the tag. +- `name` (String) The name of the tag. - `tag_set_id` (String) The ID of the associated tag set. ### Optional -- `description` (String) The description of this tag. -- `sort_order` (Number) -- `tag_set_space_id` (String) The Space ID of the associated tag set. Required if the tag set is not in the same space as what is configured on the provider +- `description` (String) The description of the tag. +- `sort_order` (Number) The sort order of the tag. +- `tag_set_space_id` (String) The Space ID of the associated tag set. Required if the tag set is not in the same space as what is configured on the provider. ### Read-Only -- `canonical_tag_name` (String) +- `canonical_tag_name` (String) The canonical name of the tag. - `id` (String) The ID of this resource. diff --git a/octopusdeploy/provider.go b/octopusdeploy/provider.go index cdb60f086..a5f8061cb 100644 --- a/octopusdeploy/provider.go +++ b/octopusdeploy/provider.go @@ -61,7 +61,6 @@ func Provider() *schema.Provider { "octopusdeploy_ssh_connection_deployment_target": resourceSSHConnectionDeploymentTarget(), "octopusdeploy_ssh_key_account": resourceSSHKeyAccount(), "octopusdeploy_static_worker_pool": resourceStaticWorkerPool(), - "octopusdeploy_tag": resourceTag(), "octopusdeploy_team": resourceTeam(), "octopusdeploy_token_account": resourceTokenAccount(), "octopusdeploy_user": resourceUser(), diff --git a/octopusdeploy/resource_tag.go b/octopusdeploy/resource_tag.go deleted file mode 100644 index dc81d3d42..000000000 --- a/octopusdeploy/resource_tag.go +++ /dev/null @@ -1,304 +0,0 @@ -package octopusdeploy - -import ( - "context" - "log" - "strings" - - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client" - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tagsets" - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tenants" - "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal" - "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" - "golang.org/x/exp/slices" -) - -func resourceTag() *schema.Resource { - return &schema.Resource{ - CreateContext: resourceTagCreate, - DeleteContext: resourceTagDelete, - Description: "This resource manages tags in Octopus Deploy.", - Importer: getImporter(), - ReadContext: resourceTagRead, - Schema: getTagSchema(), - UpdateContext: resourceTagUpdate, - } -} - -func resourceTagCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - internal.Mutex.Lock() - defer internal.Mutex.Unlock() - - log.Printf("[INFO] creating tag") - - return tagCreate(ctx, d, m) -} - -func tagCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - log.Printf("[INFO] creating tag") - - tagSetID := d.Get("tag_set_id").(string) - tagSetSpaceID := d.Get("tag_set_space_id").(string) - - octopus := m.(*client.Client) - tagSet, err := tagsets.GetByID(octopus, tagSetSpaceID, tagSetID) - if err != nil { - return processUnknownTagSetError(ctx, d, err) - } - - name := d.Get("name").(string) - - for _, tag := range tagSet.Tags { - if tag.Name == name { - return diag.Errorf(`the tag name '%s' is already in use by another tag in this tag set; tag names must be unique`, name) - } - } - - tag := expandTag(d) - if tag.ID != "" { - tag.ID = tagSet.GetID() + "/" + strings.Split(tag.ID, "/")[1] - } - tagSet.Tags = append(tagSet.Tags, tag) - - updatedTagSet, err := tagsets.Update(octopus, tagSet) - if err != nil { - return diag.FromErr(err) - } - - return findByIdOrNameAndSetTag(ctx, d, tag, updatedTagSet) - -} - -func resourceTagDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - internal.Mutex.Lock() - defer internal.Mutex.Unlock() - - tagSetID := d.Get("tag_set_id").(string) - tagSetSpaceID := d.Get("tag_set_space_id").(string) - - log.Printf("[INFO] deleting tag (%s)", d.Id()) - - octopus := m.(*client.Client) - tagSet, err := tagsets.GetByID(octopus, tagSetSpaceID, tagSetID) - if err != nil { - return processUnknownTagSetError(ctx, d, err) - } - - tag := expandTag(d) - - // verify tag is not associated with a tenant - isUsed, err := isTagUsedByTenants(ctx, octopus, tagSetSpaceID, tag) - if err != nil { - d.SetId("") - return diag.FromErr(err) - } - - if isUsed { - d.SetId("") - return diag.Errorf("the tag may not be deleted; it is being used by one or more tenant(s)") - } - - // tag is known and not associated with a tenant, therefore it may be deleted - - for i := 0; i < len(tagSet.Tags); i++ { - if tagSet.Tags[i].ID == d.Id() { - tagSet.Tags = slices.Delete(tagSet.Tags, i, i+1) - - if _, err := tagsets.Update(octopus, tagSet); err != nil { - return diag.FromErr(err) - } - - log.Printf("[INFO] tag deleted (%s)", d.Id()) - d.SetId("") - return nil - } - } - - return errors.DeleteFromState(ctx, d, "tag") -} - -func resourceTagRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - internal.Mutex.Lock() - defer internal.Mutex.Unlock() - - // validate the tag ID - if d.Id() == "" || !strings.Contains(d.Id(), "/") { - return diag.Errorf(`unable to import tag; ID must be "TagSets-{ID}/Tags-{ID}"`) - } - - name := d.Get("name").(string) - tagSetID := d.Get("tag_set_id").(string) - tagSetSpaceID := d.Get("tag_set_space_id").(string) - - // if name and tag set ID are empty then an import is underway - if name == "" && tagSetID == "" { - log.Printf("[INFO] importing tag (%s)", d.Id()) - tagSetID = strings.Split(d.Id(), "/")[0] - } else { - log.Printf("[INFO] reading tag (%s)", d.Id()) - } - - octopus := m.(*client.Client) - tagSet, err := tagsets.GetByID(octopus, tagSetSpaceID, tagSetID) - if err != nil { - return processUnknownTagSetError(ctx, d, err) - } - - tag := expandTag(d) - return findByIdOrNameAndSetTag(ctx, d, tag, tagSet) -} - -func resourceTagUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - internal.Mutex.Lock() - defer internal.Mutex.Unlock() - - name := d.Get("name").(string) - tagSetID := d.Get("tag_set_id").(string) - tagSetSpaceID := d.Get("tag_set_space_id").(string) - - log.Printf("[INFO] updating tag (%s)", d.Id()) - - octopus := m.(*client.Client) - - // if the tag is reassigned to another tag set - if d.HasChange("tag_set_id") { - sourceTagSetID, destinationTagSetID := d.GetChange("tag_set_id") - sourceTagSetSpaceID, destinationTagSetSpaceID := d.GetChange("tag_set_space_id") - - sourceTagSet, err := tagsets.GetByID(octopus, sourceTagSetSpaceID.(string), sourceTagSetID.(string)) - if err != nil { - // if spaceID has change, tag has been deleted, recreate required - if d.HasChange("tag_set_space_id") { - return tagCreate(ctx, d, m) - } - return diag.FromErr(err) - } - - destinationTagSet, err := tagsets.GetByID(octopus, destinationTagSetSpaceID.(string), destinationTagSetID.(string)) - if err != nil { - return diag.FromErr(err) - } - - // check to see if the name already exists in the destination tag set - for _, tag := range destinationTagSet.Tags { - if tag.Name == name { - d.SetId("") - return diag.Errorf(`the tag name '%s' is already in use by another tag in this tag set; tag names must be unique`, name) - } - } - - tag := expandTag(d) - - // check to see that the tag is not applied to a tenant - isUsed, err := isTagUsedByTenants(ctx, octopus, sourceTagSetSpaceID.(string), tag) - if err != nil { - d.SetId("") - return diag.FromErr(err) - } - - if isUsed { - d.SetId("") - return diag.Errorf("the tag may not be transferred; it is being used by one or more tenant(s)") - } - - // all requirements are met; it is OK to transfer the tag - - // remove the tag from the source tag set and update through the API - for i := 0; i < len(sourceTagSet.Tags); i++ { - if sourceTagSet.Tags[i].ID == d.Id() { - sourceTagSet.Tags = slices.Delete(sourceTagSet.Tags, i, i+1) - - if _, err := tagsets.Update(octopus, sourceTagSet); err != nil { - return diag.FromErr(err) - } - } - } - - // update and add the tag to the destination tag set - tag.ID = destinationTagSet.GetID() + "/" + strings.Split(tag.ID, "/")[1] - destinationTagSet.Tags = append(destinationTagSet.Tags, tag) - - updatedTagSet, err := tagsets.Update(octopus, destinationTagSet) - if err != nil { - return diag.FromErr(err) - } - - return findByIdOrNameAndSetTag(ctx, d, tag, updatedTagSet) - } - - tagSet, err := tagsets.GetByID(octopus, tagSetSpaceID, tagSetID) - if err != nil { - return processUnknownTagSetError(ctx, d, err) - } - - // find and update the tag that matches the one updated in configuration - for i := 0; i < len(tagSet.Tags); i++ { - if tagSet.Tags[i].ID == d.Id() { - tagSet.Tags[i] = expandTag(d) - - updatedTagSet, err := tagsets.Update(octopus, tagSet) - if err != nil { - return diag.FromErr(err) - } - - return findByIdOrNameAndSetTag(ctx, d, tagSet.Tags[i], updatedTagSet) - } - } - - return diag.Errorf("unable to update tag") -} - -func isTagUsedByTenants(ctx context.Context, octopus *client.Client, spaceID string, tag *tagsets.Tag) (bool, error) { - tenants, err := tenants.Get(octopus, spaceID, tenants.TenantsQuery{ - Tags: []string{tag.ID}, - }) - if err != nil { - return false, err - } - - return len(tenants.Items) > 0, nil -} - -func findByIdOrNameAndSetTag(ctx context.Context, d *schema.ResourceData, tag *tagsets.Tag, tagSet *tagsets.TagSet) diag.Diagnostics { - for _, t := range tagSet.Tags { - if t.Name == tag.Name { - if err := setTag(ctx, d, t, tagSet); err != nil { - return diag.FromErr(err) - } - - log.Printf("[INFO] tag (%s)", tag.ID) - return nil - } - } - - for _, t := range tagSet.Tags { - if t.ID == tag.ID { - if err := setTag(ctx, d, t, tagSet); err != nil { - return diag.FromErr(err) - } - log.Printf("[INFO] tag (%s)", tag.ID) - return nil - } - } - - return errors.DeleteFromState(ctx, d, "tag") -} - -func processUnknownTagSetError(ctx context.Context, d *schema.ResourceData, err error) diag.Diagnostics { - if err == nil { - return nil - } - - if apiError, ok := err.(*core.APIError); ok { - if apiError.StatusCode == 404 { - log.Printf("[INFO] tag set (%s) not found; deleting tag from state", d.Id()) - d.SetId("") - return nil - } - } - - return diag.FromErr(err) -} diff --git a/octopusdeploy/schema_tag.go b/octopusdeploy/schema_tag.go deleted file mode 100644 index e9657ae61..000000000 --- a/octopusdeploy/schema_tag.go +++ /dev/null @@ -1,74 +0,0 @@ -package octopusdeploy - -import ( - "context" - - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tagsets" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func getTagSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "canonical_tag_name": { - Computed: true, - Type: schema.TypeString, - }, - "color": { - Required: true, - Type: schema.TypeString, - }, - "description": getDescriptionSchema("tag"), - "name": getNameSchema(true), - "sort_order": { - Computed: true, - Optional: true, - Type: schema.TypeInt, - }, - "tag_set_id": { - Description: "The ID of the associated tag set.", - Required: true, - Type: schema.TypeString, - }, - "tag_set_space_id": { - Description: "The Space ID of the associated tag set. Required if the tag set is not in the same space as what is configured on the provider", - Type: schema.TypeString, - Optional: true, - Computed: true, - }, - } -} - -func expandTag(d *schema.ResourceData) *tagsets.Tag { - color := d.Get("color").(string) - name := d.Get("name").(string) - - tag := tagsets.NewTag(name, color) - tag.ID = d.Id() - - if v, ok := d.GetOk("canonical_tag_name"); ok { - tag.CanonicalTagName = v.(string) - } - - if v, ok := d.GetOk("description"); ok { - tag.Description = v.(string) - } - - if v, ok := d.GetOk("sort_order"); ok { - tag.SortOrder = v.(int) - } - - return tag -} - -func setTag(ctx context.Context, d *schema.ResourceData, tag *tagsets.Tag, tagSet *tagsets.TagSet) error { - d.Set("canonical_tag_name", tag.CanonicalTagName) - d.Set("color", tag.Color) - d.Set("description", tag.Description) - d.Set("name", tag.Name) - d.Set("sort_order", tag.SortOrder) - d.Set("tag_set_id", tagSet.GetID()) - d.Set("tag_set_space_id", tagSet.SpaceID) - d.SetId(tag.ID) - - return nil -} diff --git a/octopusdeploy_framework/framework_provider.go b/octopusdeploy_framework/framework_provider.go index 19bff356a..a562879a1 100644 --- a/octopusdeploy_framework/framework_provider.go +++ b/octopusdeploy_framework/framework_provider.go @@ -95,6 +95,7 @@ func (p *octopusDeployFrameworkProvider) Resources(ctx context.Context) []func() NewLibraryVariableSetFeedResource, NewVariableResource, NewProjectResource, + NewTagResource, NewDockerContainerRegistryFeedResource, NewTagSetResource, NewUsernamePasswordAccountResource, diff --git a/octopusdeploy_framework/resource_tag.go b/octopusdeploy_framework/resource_tag.go new file mode 100644 index 000000000..fcc2cc2fd --- /dev/null +++ b/octopusdeploy_framework/resource_tag.go @@ -0,0 +1,375 @@ +package octopusdeploy_framework + +import ( + "context" + "fmt" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tagsets" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tenants" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/schemas" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "log" + "slices" + "strings" +) + +type tagTypeResource struct { + *Config +} + +func NewTagResource() resource.Resource { + return &tagTypeResource{} +} + +func (r *tagTypeResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = util.GetTypeName(schemas.TagResourceName) +} + +func (r *tagTypeResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schemas.GetTagResourceSchema() +} + +func (r *tagTypeResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.Config = ResourceConfiguration(req, resp) +} + +func (r *tagTypeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + internal.Mutex.Lock() + defer internal.Mutex.Unlock() + + var data *schemas.TagResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + if data.ID.ValueString() == "" || !strings.Contains(data.ID.ValueString(), "/") { + resp.Diagnostics.AddError(`unable to import tag; ID must be "TagSets-{ID}/Tags-{ID}"`, "tag ID is null or does not match the required TagSets-{ID}/Tags-{ID} format") + return + } + + name := data.Name.ValueString() + tagSetID := data.TagSetId.ValueString() + tagSetSpaceID := data.TagSetSpaceId.ValueString() + + // if name and tag set ID are empty then an import is underway + if name == "" && tagSetID == "" { + tflog.Info(ctx, fmt.Sprintf("importing tag (%s)", data.ID.ValueString())) + tagSetID = strings.Split(data.ID.ValueString(), "/")[0] + } else { + tflog.Info(ctx, fmt.Sprintf("reading tag (%s)", data.ID.ValueString())) + } + + tagSet, err := tagsets.GetByID(r.Config.Client, tagSetSpaceID, tagSetID) + if err != nil { + processUnknownTagSetError(ctx, data, err, resp.Diagnostics) + } + + tag := schemas.MapFromStateToTag(data) + findByIdOrNameAndSetTag(ctx, data, tag, tagSet) + + tflog.Info(ctx, fmt.Sprintf("Tag read (%s)", tag.ID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *tagTypeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + internal.Mutex.Lock() + defer internal.Mutex.Unlock() + + var data *schemas.TagResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + tagCreate(ctx, data, resp.Diagnostics, r.Client) + + tflog.Info(ctx, fmt.Sprintf("tag created (%s)", data.ID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (t *tagTypeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + internal.Mutex.Lock() + defer internal.Mutex.Unlock() + + var data, state *schemas.TagResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + name := data.Name.ValueString() + tagSetID := data.TagSetId.ValueString() + tagSetSpaceID := data.TagSetSpaceId.ValueString() + + tflog.Info(ctx, fmt.Sprintf("updating tag (%s)", data.ID)) + + // if the tag is reassigned to another tag set + if !data.TagSetId.Equal(state.TagSetId) { + sourceTagSetID, destinationTagSetID := state.TagSetId.ValueString(), data.TagSetId.ValueString() + sourceTagSetSpaceID, destinationTagSetSpaceID := state.TagSetSpaceId.ValueString(), data.TagSetSpaceId.ValueString() + + sourceTagSet, err := tagsets.GetByID(t.Client, sourceTagSetSpaceID, sourceTagSetID) + if err != nil { + // if spaceID has changed, tag has been deleted, recreate required + if !data.TagSetSpaceId.Equal(state.TagSetSpaceId) { + tagCreate(ctx, data, resp.Diagnostics, t.Client) + if !resp.Diagnostics.HasError() { + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + } + return + } + resp.Diagnostics.AddError("Failed to get source tag set", err.Error()) + return + } + + destinationTagSet, err := tagsets.GetByID(t.Client, destinationTagSetSpaceID, destinationTagSetID) + if err != nil { + resp.Diagnostics.AddError("Failed to get destination tag set", err.Error()) + return + } + + // check to see if the name already exists in the destination tag set + for _, tag := range destinationTagSet.Tags { + if tag.Name == name { + resp.Diagnostics.AddError("Tag name already exists", fmt.Sprintf("the tag name '%s' is already in use by another tag in this tag set; tag names must be unique", name)) + return + } + } + + tag := schemas.MapFromStateToTag(data) + + // check to see that the tag is not applied to a tenant + isUsed, err := isTagUsedByTenants(ctx, t.Client, sourceTagSetSpaceID, tag) + if err != nil { + resp.Diagnostics.AddError("Failed to check if tag is used by tenants", err.Error()) + return + } + + if isUsed { + resp.Diagnostics.AddError("Tag in use", "the tag may not be transferred; it is being used by one or more tenant(s)") + return + } + + // all requirements are met; it is OK to transfer the tag + + // remove the tag from the source tag set and update through the API + for i := 0; i < len(sourceTagSet.Tags); i++ { + if sourceTagSet.Tags[i].ID == data.ID.ValueString() { + sourceTagSet.Tags = slices.Delete(sourceTagSet.Tags, i, i+1) + if _, err := tagsets.Update(t.Client, sourceTagSet); err != nil { + resp.Diagnostics.AddError("Failed to update source tag set", err.Error()) + return + } + break + } + } + + // update and add the tag to the destination tag set + tag.ID = destinationTagSet.GetID() + "/" + strings.Split(tag.ID, "/")[1] + destinationTagSet.Tags = append(destinationTagSet.Tags, tag) + + updatedTagSet, err := tagsets.Update(t.Client, destinationTagSet) + if err != nil { + resp.Diagnostics.AddError("Failed to update destination tag set", err.Error()) + return + } + + if err := findByIdOrNameAndSetTag(ctx, data, tag, updatedTagSet); err != nil { + resp.Diagnostics.AddError("Failed to find updated tag", "") + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + return + } + + tagSet, err := tagsets.GetByID(t.Client, tagSetSpaceID, tagSetID) + if err != nil { + processUnknownTagSetError(ctx, data, err, resp.Diagnostics) + return + } + + // find and update the tag that matches the one updated in configuration + var updatedTag *tagsets.Tag + for i := 0; i < len(tagSet.Tags); i++ { + if tagSet.Tags[i].ID == data.ID.ValueString() || tagSet.Tags[i].Name == data.Name.ValueString() { + tagSet.Tags[i] = schemas.MapFromStateToTag(data) + + updatedTagSet, err := tagsets.Update(t.Client, tagSet) + if err != nil { + resp.Diagnostics.AddError("Failed to update tag set", err.Error()) + return + } + + // Find the updated tag in the updatedTagSet + for _, t := range updatedTagSet.Tags { + if t.ID == data.ID.ValueString() || t.Name == data.Name.ValueString() { + updatedTag = t + break + } + } + + if updatedTag == nil { + resp.Diagnostics.AddError("Updated tag not found", "The updated tag was not found in the response from the API") + return + } + + schemas.MapFromTagToState(data, updatedTag, updatedTagSet) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + return + } + } + + resp.Diagnostics.AddError("Unable to update tag", "Tag not found in tag set") +} + +func (r *tagTypeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + internal.Mutex.Lock() + defer internal.Mutex.Unlock() + + var data *schemas.TagResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + tagSetID := data.TagSetId.ValueString() + tagSetSpaceID := data.TagSetSpaceId.ValueString() + + tflog.Info(ctx, fmt.Sprintf("deleting tag (%s)", data.ID)) + + tagSet, err := tagsets.GetByID(r.Config.Client, tagSetSpaceID, tagSetID) + if err != nil { + processUnknownTagSetError(ctx, data, err, resp.Diagnostics) + return + } + + tag := schemas.MapFromStateToTag(data) + + // verify tag is not associated with a tenant + isUsed, err := isTagUsedByTenants(ctx, r.Config.Client, tagSetSpaceID, tag) + if err != nil { + data.ID = types.StringValue("") + resp.Diagnostics.AddError("Failed to check if tag is used by tenants", err.Error()) + } + + if isUsed { + data.ID = types.StringValue("") + resp.Diagnostics.AddError("the tag may not be deleted; it is being used by one or more tenant(s)", "") + } + + // tag is known and not associated with a tenant, therefore it may be deleted + + for i := 0; i < len(tagSet.Tags); i++ { + if tagSet.Tags[i].ID == data.ID.ValueString() { + tagSet.Tags = slices.Delete(tagSet.Tags, i, i+1) + + if _, err := tagsets.Update(r.Config.Client, tagSet); err != nil { + resp.Diagnostics.AddError("Failed to update tag", err.Error()) + return + } + + log.Printf("[INFO] tag deleted (%s)", data.ID.ValueString()) + data.ID = types.StringValue("") + return + } + } +} + +func tagCreate(ctx context.Context, data *schemas.TagResourceModel, diag diag.Diagnostics, client *client.Client) diag.Diagnostics { + tflog.Info(ctx, "creating tag") + + tagSetID := data.TagSetId.ValueString() + tagSetSpaceID := data.TagSetSpaceId.ValueString() + + tagSet, err := tagsets.GetByID(client, tagSetSpaceID, tagSetID) + + if err != nil { + processUnknownTagSetError(ctx, data, err, diag) + return diag + } + + name := data.Name.ValueString() + + for _, tag := range tagSet.Tags { + if tag.Name == name { + diag.AddError(`the tag name '%s' is already in use by another tag in this tag set; tag names must be unique`, name) + } + } + + tag := schemas.MapFromStateToTag(data) + if tag.ID != "" { + tag.ID = tagSet.GetID() + "/" + strings.Split(tag.ID, "/")[1] + } + tagSet.Tags = append(tagSet.Tags, tag) + + updatedTagSet, err := tagsets.Update(client, tagSet) + if err != nil { + diag.AddError(`unable to update tag set`, err.Error()) + } + + return findByIdOrNameAndSetTag(ctx, data, tag, updatedTagSet) +} + +func isTagUsedByTenants(ctx context.Context, octopus *client.Client, spaceID string, tag *tagsets.Tag) (bool, error) { + tenants, err := tenants.Get(octopus, spaceID, tenants.TenantsQuery{ + Tags: []string{tag.ID}, + }) + if err != nil { + return false, err + } + + return len(tenants.Items) > 0, nil +} + +func findByIdOrNameAndSetTag(ctx context.Context, data *schemas.TagResourceModel, tag *tagsets.Tag, tagSet *tagsets.TagSet) diag.Diagnostics { + for _, t := range tagSet.Tags { + if t.Name == tag.Name { + schemas.MapFromTagToState(data, t, tagSet) + + tflog.Info(ctx, fmt.Sprintf("tag (%s)", tag.ID)) + return nil + } + } + + for _, t := range tagSet.Tags { + if t.ID == tag.ID { + schemas.MapFromTagToState(data, t, tagSet) + tflog.Info(ctx, fmt.Sprintf("tag (%s)", tag.ID)) + return nil + } + } + + tflog.Info(ctx, fmt.Sprintf("%s (%s) not found; deleting from state", tag.ID, tag.ID)) + data.ID = types.StringValue("") + return nil +} + +func processUnknownTagSetError(ctx context.Context, data *schemas.TagResourceModel, err error, diag diag.Diagnostics) { + if err == nil { + return + } + + if apiError, ok := err.(*core.APIError); ok { + if apiError.StatusCode == 404 { + tflog.Info(ctx, fmt.Sprintf("tag set (%s) not found; deleting tag from state", data.ID)) + data.ID = types.StringValue("") + return + } + } + + diag.AddError("Processing unknown tag set failed", err.Error()) +} + +func (t *tagTypeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} diff --git a/octopusdeploy_framework/resource_tag_test.go b/octopusdeploy_framework/resource_tag_test.go new file mode 100644 index 000000000..710f8d235 --- /dev/null +++ b/octopusdeploy_framework/resource_tag_test.go @@ -0,0 +1,124 @@ +package octopusdeploy_framework + +import ( + "fmt" + "testing" + + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tagsets" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccTag(t *testing.T) { + tagSetName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) + tagName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) + tagColor := "#6e6e6e" + tagResourceName := "octopusdeploy_tag." + tagName + + resource.Test(t, resource.TestCase{ + PreCheck: func() { TestAccPreCheck(t) }, + ProtoV6ProviderFactories: ProtoV6ProviderFactories(), + CheckDestroy: testAccTagDestroy, + Steps: []resource.TestStep{ + { + Config: testTagConfig(tagSetName, tagName, tagColor), + Check: resource.ComposeTestCheckFunc( + testTagExists(tagResourceName), + resource.TestCheckResourceAttr(tagResourceName, "name", tagName), + resource.TestCheckResourceAttr(tagResourceName, "color", tagColor), + resource.TestCheckResourceAttrSet(tagResourceName, "id"), + resource.TestCheckResourceAttrSet(tagResourceName, "tag_set_id"), + resource.TestCheckResourceAttrSet(tagResourceName, "tag_set_space_id"), + ), + }, + { + Config: testTagConfigUpdate(tagSetName, tagName, "#ff0000"), + Check: resource.ComposeTestCheckFunc( + testTagExists(tagResourceName), + resource.TestCheckResourceAttr(tagResourceName, "name", tagName), + resource.TestCheckResourceAttr(tagResourceName, "color", "#ff0000"), + resource.TestCheckResourceAttrSet(tagResourceName, "id"), + resource.TestCheckResourceAttrSet(tagResourceName, "tag_set_id"), + resource.TestCheckResourceAttrSet(tagResourceName, "tag_set_space_id"), + ), + }, + { + ResourceName: tagResourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: testAccTagImportStateIdFunc(tagResourceName), + }, + }, + }) +} + +func testTagConfig(tagSetName, tagName, tagColor string) string { + var tfConfig = fmt.Sprintf(` + resource "octopusdeploy_tag_set" "%s" { + name = "%s" + description = "Test tag set" + } + + resource "octopusdeploy_tag" "%s" { + name = "%s" + color = "%s" + description = "Test tag" + tag_set_id = octopusdeploy_tag_set.%s.id + } + `, tagSetName, tagSetName, tagName, tagName, tagColor, tagSetName) + return tfConfig +} + +func testTagConfigUpdate(tagSetName, tagName, tagColor string) string { + var tfConfig = fmt.Sprintf(` + resource "octopusdeploy_tag_set" "%s" { + name = "%s" + description = "Test tag set" + } + + resource "octopusdeploy_tag" "%s" { + name = "%s" + color = "%s" + description = "Updated test tag" + tag_set_id = octopusdeploy_tag_set.%s.id + } + `, tagSetName, tagSetName, tagName, tagName, tagColor, tagSetName) + return tfConfig +} + +func testAccTagDestroy(s *terraform.State) error { + + for _, rs := range s.RootModule().Resources { + if rs.Type != "octopusdeploy_tag" { + continue + } + + tagSetID := rs.Primary.Attributes["tag_set_id"] + tagSet, err := tagsets.GetByID(octoClient, rs.Primary.Attributes["space_id"], tagSetID) + if err != nil { + return nil // If the tag set is gone, the tag is gone too + } + + for _, tag := range tagSet.Tags { + if tag.ID == rs.Primary.ID { + return fmt.Errorf("Tag still exists") + } + } + } + + return nil +} + +func testAccTagImportStateIdFunc(resourceName string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("Not found: %s", resourceName) + } + + tagID := rs.Primary.ID + + return tagID, nil + } +} diff --git a/octopusdeploy_framework/schemas/tag.go b/octopusdeploy_framework/schemas/tag.go new file mode 100644 index 000000000..55e2fb42d --- /dev/null +++ b/octopusdeploy_framework/schemas/tag.go @@ -0,0 +1,92 @@ +package schemas + +import ( + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tagsets" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" + resourceSchema "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +const TagResourceName = "tag" + +func GetTagResourceSchema() resourceSchema.Schema { + return resourceSchema.Schema{ + Description: "This resource manages tags in Octopus Deploy.", + Attributes: map[string]resourceSchema.Attribute{ + "id": util.ResourceString(). + Computed(). + Description("The ID of this resource."). + Build(), + "canonical_tag_name": util.ResourceString(). + Computed(). + Description("The canonical name of the tag."). + Build(), + "color": util.ResourceString(). + Required(). + Description("The color of the tag."). + Build(), + "description": util.ResourceString(). + Optional(). + Description("The description of the tag."). + Default(""). + Computed(). + Build(), + "name": util.ResourceString(). + Required(). + Description("The name of the tag."). + Build(), + "sort_order": util.ResourceInt64(). + Optional(). + Computed(). + Description("The sort order of the tag."). + Build(), + "tag_set_id": util.ResourceString(). + Required(). + Description("The ID of the associated tag set."). + Build(), + "tag_set_space_id": util.ResourceString(). + Optional(). + Computed(). + Description("The Space ID of the associated tag set. Required if the tag set is not in the same space as what is configured on the provider."). + Build(), + }, + } +} + +type TagResourceModel struct { + CanonicalTagName types.String `tfsdk:"canonical_tag_name"` + Color types.String `tfsdk:"color"` + Description types.String `tfsdk:"description"` + Name types.String `tfsdk:"name"` + SortOrder types.Int64 `tfsdk:"sort_order"` + TagSetId types.String `tfsdk:"tag_set_id"` + TagSetSpaceId types.String `tfsdk:"tag_set_space_id"` + ResourceModel +} + +func MapFromStateToTag(data *TagResourceModel) *tagsets.Tag { + color := data.Color.ValueString() + name := data.Name.ValueString() + + tag := tagsets.NewTag(name, color) + tag.ID = data.ID.ValueString() + tag.CanonicalTagName = data.CanonicalTagName.ValueString() + tag.Description = data.Description.ValueString() + tag.SortOrder = int(data.SortOrder.ValueInt64()) + + return tag +} + +func MapFromTagToState(data *TagResourceModel, tag *tagsets.Tag, tagSet *tagsets.TagSet) { + + data.CanonicalTagName = types.StringValue(tag.CanonicalTagName) + data.Color = types.StringValue(tag.Color) + data.Description = types.StringValue(tag.Description) + data.Name = types.StringValue(tag.Name) + data.SortOrder = types.Int64Value(int64(tag.SortOrder)) + + data.TagSetId = types.StringValue(tagSet.ID) + data.TagSetSpaceId = types.StringValue(tagSet.SpaceID) + + data.ID = types.StringValue(tag.ID) +} From 131a5650be784c10020f8178a2ce77ef07be4454 Mon Sep 17 00:00:00 2001 From: Isaac Calligeros <101079287+IsaacCalligeros95@users.noreply.github.com> Date: Tue, 13 Aug 2024 10:28:02 +0930 Subject: [PATCH 18/28] Add import state to migrated resources (#731) --- .../resource_artifactory_generic_feed.go | 7 +++++++ .../resource_aws_elastic_container_registry.go | 7 +++++++ .../resource_docker_container_registry.go | 7 +++++++ octopusdeploy_framework/resource_github_repository_feed.go | 7 +++++++ octopusdeploy_framework/resource_helm_feed.go | 7 +++++++ octopusdeploy_framework/resource_library_variable_set.go | 7 +++++++ octopusdeploy_framework/resource_maven_feed.go | 7 +++++++ octopusdeploy_framework/resource_nuget_feed.go | 7 +++++++ octopusdeploy_framework/resource_script_module.go | 7 +++++++ octopusdeploy_framework/resource_tenant.go | 7 +++++++ 10 files changed, 70 insertions(+) diff --git a/octopusdeploy_framework/resource_artifactory_generic_feed.go b/octopusdeploy_framework/resource_artifactory_generic_feed.go index b6c765d51..857043fd9 100644 --- a/octopusdeploy_framework/resource_artifactory_generic_feed.go +++ b/octopusdeploy_framework/resource_artifactory_generic_feed.go @@ -3,6 +3,7 @@ package octopusdeploy_framework import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal/errors" @@ -25,6 +26,8 @@ func NewArtifactoryGenericFeedResource() resource.Resource { return &artifactoryGenericFeedTypeResource{} } +var _ resource.ResourceWithImportState = &artifactoryGenericFeedTypeResource{} + func (r *artifactoryGenericFeedTypeResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = util.GetTypeName("artifactory_generic_feed") } @@ -185,3 +188,7 @@ func updateDataFromArtifactoryGenericFeed(data *schemas.ArtifactoryGenericFeedTy data.PackageAcquisitionLocationOptions = packageAcquisitionLocationOptionsListValue data.ID = types.StringValue(feed.GetID()) } + +func (*artifactoryGenericFeedTypeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} diff --git a/octopusdeploy_framework/resource_aws_elastic_container_registry.go b/octopusdeploy_framework/resource_aws_elastic_container_registry.go index daeb36f31..b54adf9d7 100644 --- a/octopusdeploy_framework/resource_aws_elastic_container_registry.go +++ b/octopusdeploy_framework/resource_aws_elastic_container_registry.go @@ -3,6 +3,7 @@ package octopusdeploy_framework import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/feeds" @@ -20,6 +21,8 @@ type awsElasticContainerRegistryFeedTypeResource struct { *Config } +var _ resource.ResourceWithImportState = &awsElasticContainerRegistryFeedTypeResource{} + func NewAwsElasticContainerRegistryFeedResource() resource.Resource { return &awsElasticContainerRegistryFeedTypeResource{} } @@ -175,3 +178,7 @@ func updateDataFromAwsElasticContainerRegistryFeed(data *schemas.AwsElasticConta data.PackageAcquisitionLocationOptions = packageAcquisitionLocationOptionsListValue data.ID = types.StringValue(feed.GetID()) } + +func (*awsElasticContainerRegistryFeedTypeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} diff --git a/octopusdeploy_framework/resource_docker_container_registry.go b/octopusdeploy_framework/resource_docker_container_registry.go index 0b246633e..97131399b 100644 --- a/octopusdeploy_framework/resource_docker_container_registry.go +++ b/octopusdeploy_framework/resource_docker_container_registry.go @@ -3,6 +3,7 @@ package octopusdeploy_framework import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/feeds" @@ -24,6 +25,8 @@ func NewDockerContainerRegistryFeedResource() resource.Resource { return &dockerContainerRegistryFeedTypeResource{} } +var _ resource.ResourceWithImportState = &dockerContainerRegistryFeedTypeResource{} + func (r *dockerContainerRegistryFeedTypeResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = util.GetTypeName("docker_container_registry") } @@ -185,3 +188,7 @@ func updateDataFromDockerContainerRegistryFeed(data *schemas.DockerContainerRegi data.PackageAcquisitionLocationOptions = packageAcquisitionLocationOptionsListValue data.ID = types.StringValue(feed.ID) } + +func (*dockerContainerRegistryFeedTypeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} diff --git a/octopusdeploy_framework/resource_github_repository_feed.go b/octopusdeploy_framework/resource_github_repository_feed.go index 6b6aabb63..ae4f95977 100644 --- a/octopusdeploy_framework/resource_github_repository_feed.go +++ b/octopusdeploy_framework/resource_github_repository_feed.go @@ -3,6 +3,7 @@ package octopusdeploy_framework import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/feeds" @@ -24,6 +25,8 @@ func NewGitHubRepositoryFeedResource() resource.Resource { return &githubRepositoryFeedTypeResource{} } +var _ resource.ResourceWithImportState = &githubRepositoryFeedTypeResource{} + func (r *githubRepositoryFeedTypeResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = util.GetTypeName("github_repository_feed") } @@ -183,3 +186,7 @@ func updateDataFromGitHubRepositoryFeed(data *schemas.GitHubRepositoryFeedTypeRe data.PackageAcquisitionLocationOptions = packageAcquisitionLocationOptionsListValue data.ID = types.StringValue(feed.GetID()) } + +func (*githubRepositoryFeedTypeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} diff --git a/octopusdeploy_framework/resource_helm_feed.go b/octopusdeploy_framework/resource_helm_feed.go index dd7667eff..c8204a63e 100644 --- a/octopusdeploy_framework/resource_helm_feed.go +++ b/octopusdeploy_framework/resource_helm_feed.go @@ -3,6 +3,7 @@ package octopusdeploy_framework import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal/errors" @@ -25,6 +26,8 @@ func NewHelmFeedResource() resource.Resource { return &helmFeedTypeResource{} } +var _ resource.ResourceWithImportState = &helmFeedTypeResource{} + func (r *helmFeedTypeResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = util.GetTypeName("helm_feed") } @@ -179,3 +182,7 @@ func updateDataFromHelmFeed(data *schemas.HelmFeedTypeResourceModel, spaceId str data.PackageAcquisitionLocationOptions = packageAcquisitionLocationOptionsListValue data.ID = types.StringValue(feed.GetID()) } + +func (*helmFeedTypeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} diff --git a/octopusdeploy_framework/resource_library_variable_set.go b/octopusdeploy_framework/resource_library_variable_set.go index cdf616aa0..795482862 100644 --- a/octopusdeploy_framework/resource_library_variable_set.go +++ b/octopusdeploy_framework/resource_library_variable_set.go @@ -3,6 +3,7 @@ package octopusdeploy_framework import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/path" "log" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/libraryvariablesets" @@ -21,6 +22,8 @@ func NewLibraryVariableSetFeedResource() resource.Resource { return &libraryVariableSetFeedTypeResource{} } +var _ resource.ResourceWithImportState = &libraryVariableSetFeedTypeResource{} + func (r *libraryVariableSetFeedTypeResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = util.GetTypeName("library_variable_set") } @@ -109,3 +112,7 @@ func (r *libraryVariableSetFeedTypeResource) Delete(ctx context.Context, req res return } } + +func (*libraryVariableSetFeedTypeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} diff --git a/octopusdeploy_framework/resource_maven_feed.go b/octopusdeploy_framework/resource_maven_feed.go index 55346da60..e260debcd 100644 --- a/octopusdeploy_framework/resource_maven_feed.go +++ b/octopusdeploy_framework/resource_maven_feed.go @@ -3,6 +3,7 @@ package octopusdeploy_framework import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/feeds" @@ -24,6 +25,8 @@ func NewMavenFeedResource() resource.Resource { return &mavenFeedTypeResource{} } +var _ resource.ResourceWithImportState = &mavenFeedTypeResource{} + func (r *mavenFeedTypeResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = util.GetTypeName("maven_feed") } @@ -181,3 +184,7 @@ func updateDataFromMavenFeed(data *schemas.MavenFeedTypeResourceModel, spaceId s data.PackageAcquisitionLocationOptions = packageAcquisitionLocationOptionsListValue data.ID = types.StringValue(feed.ID) } + +func (*mavenFeedTypeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} diff --git a/octopusdeploy_framework/resource_nuget_feed.go b/octopusdeploy_framework/resource_nuget_feed.go index bf0576248..6a737e8b5 100644 --- a/octopusdeploy_framework/resource_nuget_feed.go +++ b/octopusdeploy_framework/resource_nuget_feed.go @@ -3,6 +3,7 @@ package octopusdeploy_framework import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/feeds" @@ -24,6 +25,8 @@ func NewNugetFeedResource() resource.Resource { return &nugetFeedTypeResource{} } +var _ resource.ResourceWithImportState = &nugetFeedTypeResource{} + func (r *nugetFeedTypeResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = util.GetTypeName("nuget_feed") } @@ -184,3 +187,7 @@ func updateDataFromNugetFeed(data *schemas.NugetFeedTypeResourceModel, spaceId s data.PackageAcquisitionLocationOptions = packageAcquisitionLocationOptionsListValue data.ID = types.StringValue(feed.GetID()) } + +func (*nugetFeedTypeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} diff --git a/octopusdeploy_framework/resource_script_module.go b/octopusdeploy_framework/resource_script_module.go index 8e0a1294f..bb2f5fcee 100644 --- a/octopusdeploy_framework/resource_script_module.go +++ b/octopusdeploy_framework/resource_script_module.go @@ -8,6 +8,7 @@ import ( "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/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-log/tflog" @@ -21,6 +22,8 @@ func NewScriptModuleResource() resource.Resource { return &scriptModuleTypeResource{} } +var _ resource.ResourceWithImportState = &scriptModuleTypeResource{} + func (r *scriptModuleTypeResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = util.GetTypeName("script_module") } @@ -128,3 +131,7 @@ func (r *scriptModuleTypeResource) Delete(ctx context.Context, req resource.Dele return } } + +func (*scriptModuleTypeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} diff --git a/octopusdeploy_framework/resource_tenant.go b/octopusdeploy_framework/resource_tenant.go index 3fcde55b3..cc0b04b67 100644 --- a/octopusdeploy_framework/resource_tenant.go +++ b/octopusdeploy_framework/resource_tenant.go @@ -9,6 +9,7 @@ import ( "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/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/types" @@ -24,6 +25,8 @@ func NewTenantResource() resource.Resource { return &tenantTypeResource{} } +var _ resource.ResourceWithImportState = &tenantTypeResource{} + func (r *tenantTypeResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = util.GetTypeName("tenant") } @@ -172,3 +175,7 @@ func mapTenantToState(data *schemas.TenantModel, tenant *tenants.Tenant) { sort.Strings(tenant.TenantTags) data.TenantTags = util.Ternary(tenant.TenantTags != nil && len(tenant.TenantTags) > 0, util.FlattenStringList(tenant.TenantTags), types.ListValueMust(types.StringType, make([]attr.Value, 0))) } + +func (*tenantTypeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} From 3fecf49b153e008c2f96e1e16ad04140425afe91 Mon Sep 17 00:00:00 2001 From: Henrik Andersson Date: Mon, 12 Aug 2024 20:57:48 -0700 Subject: [PATCH 19/28] chore: return error if we can't find a space matching the name provided (#728) --- octopusdeploy_framework/datasource_space.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/octopusdeploy_framework/datasource_space.go b/octopusdeploy_framework/datasource_space.go index 42077c6cd..c4ab13758 100644 --- a/octopusdeploy_framework/datasource_space.go +++ b/octopusdeploy_framework/datasource_space.go @@ -2,13 +2,15 @@ package octopusdeploy_framework import ( "context" + "fmt" + "strings" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/spaces" "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/schemas" "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/types" - "strings" ) type spaceDataSource struct { @@ -57,6 +59,10 @@ func (b *spaceDataSource) Read(ctx context.Context, req datasource.ReadRequest, matchedSpace = spaceResult } } + if matchedSpace == nil { + resp.Diagnostics.AddError(fmt.Sprintf("unable to find space with name %s", data.Name.ValueString()), "") + return + } mapSpaceToState(ctx, &data, matchedSpace) From 1ba92e16c06d92bda364c53da644c7571ea3b975 Mon Sep 17 00:00:00 2001 From: Isaac Calligeros <101079287+IsaacCalligeros95@users.noreply.github.com> Date: Tue, 13 Aug 2024 15:19:54 +0930 Subject: [PATCH 20/28] Add aws oidc account type (#733) * Add aws oidc account type * Update Schema queries * Update aws oidc account type * Update docs --- docs/data-sources/accounts.md | 2 +- examples/resources/octopusdeploy_account/resource.tf | 2 +- octopusdeploy/schema_queries.go | 4 ++-- octopusdeploy/schema_utilities.go | 3 ++- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/data-sources/accounts.md b/docs/data-sources/accounts.md index 90d281505..a108cfe1d 100644 --- a/docs/data-sources/accounts.md +++ b/docs/data-sources/accounts.md @@ -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`, `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`, `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. diff --git a/examples/resources/octopusdeploy_account/resource.tf b/examples/resources/octopusdeploy_account/resource.tf index c18a1aba5..b44de2669 100644 --- a/examples/resources/octopusdeploy_account/resource.tf +++ b/examples/resources/octopusdeploy_account/resource.tf @@ -7,7 +7,7 @@ resource "octopusdeploy_account" "amazon_web_services_account" { } resource "octopusdeploy_account" "amazon_web_services_openid_connect_account" { - account_type = "AwsOIDCAccount" + account_type = "AmazonWebServicesOidcAccount" name = "AWS OIDC Account (OK to Delete)" role_arn = "arn:aws:iam::sourceAccountId:roleroleName" session_duration = "3600" diff --git a/octopusdeploy/schema_queries.go b/octopusdeploy/schema_queries.go index d7457abec..1322c9311 100644 --- a/octopusdeploy/schema_queries.go +++ b/octopusdeploy/schema_queries.go @@ -7,13 +7,13 @@ 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`, `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`, `None`, `SshKeyPair`, `Token`, or `UsernamePassword`.", Optional: true, Type: schema.TypeString, ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ "AmazonWebServicesAccount", "AmazonWebServicesRoleAccount", - "AwsOIDCAccount", + "AmazonWebServicesOidcAccount", "AzureServicePrincipal", "AzureOIDC", "AzureSubscription", diff --git a/octopusdeploy/schema_utilities.go b/octopusdeploy/schema_utilities.go index 50294a90c..1cb481661 100644 --- a/octopusdeploy/schema_utilities.go +++ b/octopusdeploy/schema_utilities.go @@ -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`, `None`, `SshKeyPair`, `Token`, or `UsernamePassword`.", + Description: "Specifies the type of the account. Valid account types are `AmazonWebServicesAccount`, `AmazonWebServicesRoleAccount`, `AzureServicePrincipal`, `AzureOIDC`, `AzureSubscription`, `AmazonWebServicesOidcAccount`, `None`, `SshKeyPair`, `Token`, or `UsernamePassword`.", ForceNew: true, Type: schema.TypeString, ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ @@ -22,6 +22,7 @@ func getAccountTypeSchema(isRequired bool) *schema.Schema { "SshKeyPair", "Token", "UsernamePassword", + "AmazonWebServicesOidcAccount", }, false)), } From 4d50ff07c61f3fa447c77eaba7a889f025a417be Mon Sep 17 00:00:00 2001 From: Henrik Andersson Date: Mon, 12 Aug 2024 22:53:18 -0700 Subject: [PATCH 21/28] chore: when specifying name in datasource we should only return exact match (#726) --- .../datasource_environments.go | 89 +++++-------------- .../datasource_environments_test.go | 78 ++++++++++++++-- .../schemas/environment.go | 69 ++++++++++++++ 3 files changed, 160 insertions(+), 76 deletions(-) diff --git a/octopusdeploy_framework/datasource_environments.go b/octopusdeploy_framework/datasource_environments.go index 06b52a527..13f884e74 100644 --- a/octopusdeploy_framework/datasource_environments.go +++ b/octopusdeploy_framework/datasource_environments.go @@ -3,13 +3,12 @@ package octopusdeploy_framework import ( "context" "fmt" + "strings" "time" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/environments" - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/extensions" "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/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/types" @@ -85,83 +84,35 @@ func (e *environmentDataSource) Read(ctx context.Context, req datasource.ReadReq Take: util.GetNumber(data.Take), } - existingEnvironments, err := environments.Get(e.Client, data.SpaceID.ValueString(), query) + spaceID := util.Ternary(data.SpaceID.IsNull(), e.Client.GetSpaceID(), data.SpaceID.ValueString()) + + existingEnvironments, err := environments.Get(e.Client, spaceID, query) if err != nil { resp.Diagnostics.AddError("unable to load environments", err.Error()) return } - tflog.Debug(ctx, fmt.Sprintf("environments returned from API: %#v", existingEnvironments)) + var mappedEnvironments []schemas.EnvironmentTypeResourceModel - for _, environment := range existingEnvironments.Items { - var env schemas.EnvironmentTypeResourceModel - env.ID = types.StringValue(environment.ID) - env.SpaceID = types.StringValue(environment.SpaceID) - env.Slug = types.StringValue(environment.Slug) - env.Name = types.StringValue(environment.Name) - env.Description = types.StringValue(environment.Description) - env.AllowDynamicInfrastructure = types.BoolValue(environment.AllowDynamicInfrastructure) - env.SortOrder = types.Int64Value(int64(environment.SortOrder)) - env.UseGuidedFailure = types.BoolValue(environment.UseGuidedFailure) - env.JiraExtensionSettings, _ = types.ListValueFrom(ctx, types.ObjectType{AttrTypes: schemas.JiraExtensionSettingsObjectType()}, []any{}) - env.JiraServiceManagementExtensionSettings, _ = types.ListValueFrom(ctx, types.ObjectType{AttrTypes: schemas.JiraServiceManagementExtensionSettingsObjectType()}, []any{}) - env.ServiceNowExtensionSettings, _ = types.ListValueFrom(ctx, types.ObjectType{AttrTypes: schemas.ServiceNowExtensionSettingsObjectType()}, []any{}) - - for _, extensionSetting := range environment.ExtensionSettings { - switch extensionSetting.ExtensionID() { - case extensions.JiraExtensionID: - if jiraExtension, ok := extensionSetting.(*environments.JiraExtensionSettings); ok { - env.JiraExtensionSettings, _ = types.ListValueFrom( - ctx, - types.ObjectType{AttrTypes: schemas.JiraExtensionSettingsObjectType()}, - []any{schemas.MapJiraExtensionSettings(jiraExtension)}, - ) - } - case extensions.JiraServiceManagementExtensionID: - if jiraServiceManagementExtensionSettings, ok := extensionSetting.(*environments.JiraServiceManagementExtensionSettings); ok { - env.JiraServiceManagementExtensionSettings, _ = types.ListValueFrom( - ctx, - types.ObjectType{AttrTypes: schemas.JiraServiceManagementExtensionSettingsObjectType()}, - []any{schemas.MapJiraServiceManagementExtensionSettings(jiraServiceManagementExtensionSettings)}, - ) - } - case extensions.ServiceNowExtensionID: - if serviceNowExtensionSettings, ok := extensionSetting.(*environments.ServiceNowExtensionSettings); ok { - env.ServiceNowExtensionSettings, _ = types.ListValueFrom( - ctx, - types.ObjectType{AttrTypes: schemas.ServiceNowExtensionSettingsObjectType()}, - []any{schemas.MapServiceNowExtensionSettings(serviceNowExtensionSettings)}, - ) - } + if data.Name.IsNull() { + tflog.Debug(ctx, fmt.Sprintf("environments returned from API: %#v", existingEnvironments)) + for _, environment := range existingEnvironments.Items { + mappedEnvironments = append(mappedEnvironments, schemas.MapFromEnvironment(ctx, environment)) + } + } else { // if name has been specified, match by exact name rather than partial name as the API does + var matchedEnvironment *environments.Environment + tflog.Debug(ctx, fmt.Sprintf("matching environment by name: %s", data.Name)) + for _, env := range existingEnvironments.Items { + if strings.EqualFold(env.Name, data.Name.ValueString()) { + matchedEnvironment = env } } - - mappedEnvironments = append(mappedEnvironments, env) + if matchedEnvironment != nil { + mappedEnvironments = append(mappedEnvironments, schemas.MapFromEnvironment(ctx, matchedEnvironment)) + } } - data.Environments, _ = types.ListValueFrom(ctx, types.ObjectType{AttrTypes: environmentObjectType()}, mappedEnvironments) + data.Environments, _ = types.ListValueFrom(ctx, types.ObjectType{AttrTypes: schemas.EnvironmentObjectType()}, mappedEnvironments) data.ID = types.StringValue("Environments " + time.Now().UTC().String()) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } - -func environmentObjectType() map[string]attr.Type { - return map[string]attr.Type{ - "id": types.StringType, - "name": types.StringType, - "slug": types.StringType, - "description": types.StringType, - schemas.EnvironmentAllowDynamicInfrastructure: types.BoolType, - schemas.EnvironmentSortOrder: types.Int64Type, - schemas.EnvironmentUseGuidedFailure: types.BoolType, - "space_id": types.StringType, - schemas.EnvironmentJiraExtensionSettings: types.ListType{ - ElemType: types.ObjectType{AttrTypes: schemas.JiraExtensionSettingsObjectType()}, - }, - schemas.EnvironmentJiraServiceManagementExtensionSettings: types.ListType{ - ElemType: types.ObjectType{AttrTypes: schemas.JiraServiceManagementExtensionSettingsObjectType()}, - }, - schemas.EnvironmentServiceNowExtensionSettings: types.ListType{ - ElemType: types.ObjectType{AttrTypes: schemas.ServiceNowExtensionSettingsObjectType()}, - }, - } -} diff --git a/octopusdeploy_framework/datasource_environments_test.go b/octopusdeploy_framework/datasource_environments_test.go index 3ca2c90ac..7195d8aa6 100644 --- a/octopusdeploy_framework/datasource_environments_test.go +++ b/octopusdeploy_framework/datasource_environments_test.go @@ -11,8 +11,14 @@ import ( ) func TestAccDataSourceEnvironments(t *testing.T) { - localName := acctest.RandStringFromCharSet(50, acctest.CharSetAlpha) + localName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) prefix := fmt.Sprintf("data.octopusdeploy_environments.%s", localName) + + spaceName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) + + environmentLocalName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) + environmentName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) + take := 10 resource.Test(t, resource.TestCase{ @@ -23,13 +29,34 @@ func TestAccDataSourceEnvironments(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckEnvironmentsDataSourceID(prefix), ), - Config: testAccDataSourceEnvironmentsConfig(localName, take), + Config: testAccDataSourceEnvironmentsEmpty(localName), }, { Check: resource.ComposeTestCheckFunc( testAccCheckEnvironmentsDataSourceID(prefix), + resource.TestCheckResourceAttr(prefix, "environments.#", "3"), + ), + Config: fmt.Sprintf(`%s + + %s`, + createTestAccDataSourceEnvironmentsConfig(spaceName, environmentLocalName, environmentName), + testAccDataSourceEnvironmentsConfig(localName, take, spaceName, environmentLocalName), + ), + }, + { + Check: resource.ComposeTestCheckFunc( + testAccCheckEnvironmentsDataSourceID(prefix), + resource.TestCheckResourceAttr(prefix, "name", environmentName), + resource.TestCheckResourceAttr(prefix, "environments.#", "1"), + resource.TestCheckResourceAttrSet(prefix, "environments.0.id"), + resource.TestCheckResourceAttr(prefix, "environments.0.name", environmentName), + ), + Config: fmt.Sprintf(`%s + + %s`, + createTestAccDataSourceEnvironmentsConfig(spaceName, environmentLocalName, environmentName), + testAccDataSourceEnvironmentByNameConfig(localName, environmentName, spaceName, environmentLocalName), ), - Config: testAccDataSourceEnvironmentsEmpty(localName), }, }, }) @@ -50,12 +77,49 @@ func testAccCheckEnvironmentsDataSourceID(n string) resource.TestCheckFunc { } } -func testAccDataSourceEnvironmentsConfig(localName string, take int) string { - return fmt.Sprintf(`data "octopusdeploy_environments" "%s" { - take = %v - }`, localName, take) +func createTestAccDataSourceEnvironmentsConfig(spaceName string, localName string, name string) string { + return fmt.Sprintf(` + resource "octopusdeploy_space" "%[1]s" { + name = "%[1]s" + is_default = false + is_task_queue_stopped = false + description = "Test space for environments datasource" + space_managers_teams = ["teams-administrators"] + } + + resource "octopusdeploy_environment" "%[2]s" { + name = "%[3]s" + space_id = octopusdeploy_space.%[1]s.id + } + + resource "octopusdeploy_environment" "%[2]s-1" { + name = "%[3]s-1" + space_id = octopusdeploy_space.%[1]s.id + } + + resource "octopusdeploy_environment" "%[2]s-2" { + name = "%[3]s-2" + space_id = octopusdeploy_space.%[1]s.id + } + `, spaceName, localName, name) } func testAccDataSourceEnvironmentsEmpty(localName string) string { return fmt.Sprintf(`data "octopusdeploy_environments" "%s" {}`, localName) } + +func testAccDataSourceEnvironmentsConfig(localName string, take int, spaceName string, environmentLocalName string) string { + return fmt.Sprintf(`data "octopusdeploy_environments" "%s" { + take = %v + space_id = octopusdeploy_space.%s.id + depends_on = [octopusdeploy_environment.%s, octopusdeploy_environment.%[4]s-1, octopusdeploy_environment.%[4]s-2] + }`, localName, take, spaceName, environmentLocalName) +} + +func testAccDataSourceEnvironmentByNameConfig(localName string, name string, spaceName string, environmentLocalName string) string { + return fmt.Sprintf(`data "octopusdeploy_environments" "%s" { + name = "%s" + space_id = octopusdeploy_space.%s.id + depends_on = [octopusdeploy_environment.%s, octopusdeploy_environment.%[4]s-1, octopusdeploy_environment.%[4]s-2] + }`, localName, name, spaceName, environmentLocalName) +} diff --git a/octopusdeploy_framework/schemas/environment.go b/octopusdeploy_framework/schemas/environment.go index ed57bba65..3090f7cc1 100644 --- a/octopusdeploy_framework/schemas/environment.go +++ b/octopusdeploy_framework/schemas/environment.go @@ -1,10 +1,12 @@ package schemas import ( + "context" "fmt" "strings" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/environments" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/extensions" "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -50,6 +52,28 @@ var jiraEnvironmentTypes = []string{ jiraEnvironmentTypeNames.Unmapped, } +func EnvironmentObjectType() map[string]attr.Type { + return map[string]attr.Type{ + "id": types.StringType, + "name": types.StringType, + "slug": types.StringType, + "description": types.StringType, + EnvironmentAllowDynamicInfrastructure: types.BoolType, + EnvironmentSortOrder: types.Int64Type, + EnvironmentUseGuidedFailure: types.BoolType, + "space_id": types.StringType, + EnvironmentJiraExtensionSettings: types.ListType{ + ElemType: types.ObjectType{AttrTypes: JiraExtensionSettingsObjectType()}, + }, + EnvironmentJiraServiceManagementExtensionSettings: types.ListType{ + ElemType: types.ObjectType{AttrTypes: JiraServiceManagementExtensionSettingsObjectType()}, + }, + EnvironmentServiceNowExtensionSettings: types.ListType{ + ElemType: types.ObjectType{AttrTypes: ServiceNowExtensionSettingsObjectType()}, + }, + } +} + func GetEnvironmentDatasourceSchema() map[string]datasourceSchema.Attribute { return map[string]datasourceSchema.Attribute{ "id": GetIdDatasourceSchema(true), @@ -201,6 +225,51 @@ func MapServiceNowExtensionSettings(serviceNowExtensionSettings *environments.Se }) } +func MapFromEnvironment(ctx context.Context, environment *environments.Environment) EnvironmentTypeResourceModel { + var env EnvironmentTypeResourceModel + env.ID = types.StringValue(environment.ID) + env.SpaceID = types.StringValue(environment.SpaceID) + env.Slug = types.StringValue(environment.Slug) + env.Name = types.StringValue(environment.Name) + env.Description = types.StringValue(environment.Description) + env.AllowDynamicInfrastructure = types.BoolValue(environment.AllowDynamicInfrastructure) + env.SortOrder = types.Int64Value(int64(environment.SortOrder)) + env.UseGuidedFailure = types.BoolValue(environment.UseGuidedFailure) + env.JiraExtensionSettings, _ = types.ListValueFrom(ctx, types.ObjectType{AttrTypes: JiraExtensionSettingsObjectType()}, []any{}) + env.JiraServiceManagementExtensionSettings, _ = types.ListValueFrom(ctx, types.ObjectType{AttrTypes: JiraServiceManagementExtensionSettingsObjectType()}, []any{}) + env.ServiceNowExtensionSettings, _ = types.ListValueFrom(ctx, types.ObjectType{AttrTypes: ServiceNowExtensionSettingsObjectType()}, []any{}) + + for _, extensionSetting := range environment.ExtensionSettings { + switch extensionSetting.ExtensionID() { + case extensions.JiraExtensionID: + if jiraExtension, ok := extensionSetting.(*environments.JiraExtensionSettings); ok { + env.JiraExtensionSettings, _ = types.ListValueFrom( + ctx, + types.ObjectType{AttrTypes: JiraExtensionSettingsObjectType()}, + []any{MapJiraExtensionSettings(jiraExtension)}, + ) + } + case extensions.JiraServiceManagementExtensionID: + if jiraServiceManagementExtensionSettings, ok := extensionSetting.(*environments.JiraServiceManagementExtensionSettings); ok { + env.JiraServiceManagementExtensionSettings, _ = types.ListValueFrom( + ctx, + types.ObjectType{AttrTypes: JiraServiceManagementExtensionSettingsObjectType()}, + []any{MapJiraServiceManagementExtensionSettings(jiraServiceManagementExtensionSettings)}, + ) + } + case extensions.ServiceNowExtensionID: + if serviceNowExtensionSettings, ok := extensionSetting.(*environments.ServiceNowExtensionSettings); ok { + env.ServiceNowExtensionSettings, _ = types.ListValueFrom( + ctx, + types.ObjectType{AttrTypes: ServiceNowExtensionSettingsObjectType()}, + []any{MapServiceNowExtensionSettings(serviceNowExtensionSettings)}, + ) + } + } + } + return env +} + type EnvironmentTypeResourceModel struct { Slug types.String `tfsdk:"slug"` Name types.String `tfsdk:"name"` From f11f6f90c1edd9d5da5c4149e1299bf42e6db79b Mon Sep 17 00:00:00 2001 From: Huy Nguyen <162080607+HuyPhanNguyen@users.noreply.github.com> Date: Tue, 13 Aug 2024 16:30:06 +1000 Subject: [PATCH 22/28] chore: Fix some unwanted change in project (#732) * Fix some unwanted change in project * remove default --- octopusdeploy_framework/schemas/project.go | 50 ++++++++++++++-------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/octopusdeploy_framework/schemas/project.go b/octopusdeploy_framework/schemas/project.go index 814710bda..3b02bd3c8 100644 --- a/octopusdeploy_framework/schemas/project.go +++ b/octopusdeploy_framework/schemas/project.go @@ -4,6 +4,11 @@ import ( "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" 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/listplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -19,23 +24,23 @@ func GetProjectResourceSchema() resourceSchema.Schema { "name": util.GetNameResourceSchema(true), "description": util.GetDescriptionResourceSchema(ProjectResourceName), "allow_deployments_to_no_targets": util.ResourceBool().Optional().Deprecated("This value is only valid for an associated connectivity policy and should not be specified here.").Build(), - "auto_create_release": util.ResourceBool().Optional().Computed().Build(), + "auto_create_release": util.ResourceBool().Optional().Computed().PlanModifiers(boolplanmodifier.UseStateForUnknown()).Build(), "cloned_from_project_id": util.ResourceString().Optional().Description("The ID of the project this project was cloned from.").Build(), - "default_guided_failure_mode": util.ResourceString().Optional().Computed().Build(), - "default_to_skip_if_already_installed": util.ResourceBool().Optional().Computed().Build(), - "deployment_changes_template": util.ResourceString().Optional().Computed().Build(), - "discrete_channel_release": util.ResourceBool().Optional().Computed().Description("Treats releases of different channels to the same environment as a separate deployment dimension").Build(), - "is_disabled": util.ResourceBool().Optional().Computed().Build(), - "is_discrete_channel_release": util.ResourceBool().Optional().Computed().Description("Treats releases of different channels to the same environment as a separate deployment dimension").Build(), - "is_version_controlled": util.ResourceBool().Optional().Computed().Build(), + "default_guided_failure_mode": util.ResourceString().Optional().Computed().PlanModifiers(stringplanmodifier.UseStateForUnknown()).Build(), + "default_to_skip_if_already_installed": util.ResourceBool().Optional().Computed().PlanModifiers(boolplanmodifier.UseStateForUnknown()).Build(), + "deployment_changes_template": util.ResourceString().Optional().Computed().PlanModifiers(stringplanmodifier.UseStateForUnknown()).Build(), + "discrete_channel_release": util.ResourceBool().Optional().Computed().PlanModifiers(boolplanmodifier.UseStateForUnknown()).Description("Treats releases of different channels to the same environment as a separate deployment dimension").Build(), + "is_disabled": util.ResourceBool().Optional().Computed().PlanModifiers(boolplanmodifier.UseStateForUnknown()).Build(), + "is_discrete_channel_release": util.ResourceBool().Optional().Computed().PlanModifiers(boolplanmodifier.UseStateForUnknown()).Description("Treats releases of different channels to the same environment as a separate deployment dimension").Build(), + "is_version_controlled": util.ResourceBool().Optional().Computed().PlanModifiers(boolplanmodifier.UseStateForUnknown()).Build(), "lifecycle_id": util.ResourceString().Required().Description("The lifecycle ID associated with this project.").Build(), "project_group_id": util.ResourceString().Required().Description("The project group ID associated with this project.").Build(), - "tenanted_deployment_participation": util.ResourceString().Optional().Computed().Description("The tenanted deployment mode of the resource. Valid account types are `Untenanted`, `TenantedOrUntenanted`, or `Tenanted`.").Build(), - "included_library_variable_sets": util.ResourceList(types.StringType).Optional().Computed().Description("The list of included library variable set IDs.").Build(), - "release_notes_template": util.ResourceString().Optional().Computed().Build(), - "slug": util.ResourceString().Optional().Computed().Description("A human-readable, unique identifier, used to identify a project.").Build(), - "deployment_process_id": util.ResourceString().Computed().Build(), - "variable_set_id": util.ResourceString().Computed().Build(), + "tenanted_deployment_participation": util.ResourceString().Optional().Computed().PlanModifiers(stringplanmodifier.UseStateForUnknown()).Description("The tenanted deployment mode of the resource. Valid account types are `Untenanted`, `TenantedOrUntenanted`, or `Tenanted`.").Build(), + "included_library_variable_sets": util.ResourceList(types.StringType).Optional().Computed().PlanModifiers(listplanmodifier.UseStateForUnknown()).Description("The list of included library variable set IDs.").Build(), + "release_notes_template": util.ResourceString().Optional().Computed().PlanModifiers(stringplanmodifier.UseStateForUnknown()).Build(), + "slug": util.ResourceString().Optional().Computed().PlanModifiers(stringplanmodifier.UseStateForUnknown()).Description("A human-readable, unique identifier, used to identify a project.").Build(), + "deployment_process_id": util.ResourceString().Computed().PlanModifiers(stringplanmodifier.UseStateForUnknown()).Build(), + "variable_set_id": util.ResourceString().Computed().PlanModifiers(stringplanmodifier.UseStateForUnknown()).Build(), }, Blocks: map[string]resourceSchema.Block{ // This is correct object that return from api for project object not a list string. @@ -50,13 +55,19 @@ func GetProjectResourceSchema() resourceSchema.Schema { }, "connectivity_policy": resourceSchema.ListNestedBlock{ NestedObject: resourceSchema.NestedBlockObject{ + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.UseStateForUnknown(), + }, Attributes: map[string]resourceSchema.Attribute{ - "allow_deployments_to_no_targets": util.ResourceBool().Optional().Computed().Build(), - "exclude_unhealthy_targets": util.ResourceBool().Optional().Computed().Build(), - "skip_machine_behavior": util.ResourceString().Optional().Build(), - "target_roles": util.ResourceList(types.StringType).Optional().Computed().Build(), + "allow_deployments_to_no_targets": util.ResourceBool().Optional().Computed().Default(false).PlanModifiers(boolplanmodifier.UseStateForUnknown()).Build(), + "exclude_unhealthy_targets": util.ResourceBool().Optional().Computed().Default(false).PlanModifiers(boolplanmodifier.UseStateForUnknown()).Build(), + "skip_machine_behavior": util.ResourceString().Optional().Computed().Default("None").PlanModifiers(stringplanmodifier.UseStateForUnknown()).Build(), + "target_roles": util.ResourceList(types.StringType).Optional().Computed().PlanModifiers(listplanmodifier.UseStateForUnknown()).Build(), }, }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.UseStateForUnknown(), + }, }, "git_anonymous_persistence_settings": resourceSchema.ListNestedBlock{ NestedObject: resourceSchema.NestedBlockObject{ @@ -133,9 +144,10 @@ func GetProjectResourceSchema() resourceSchema.Schema { }, "versioning_strategy": resourceSchema.ListNestedBlock{ NestedObject: resourceSchema.NestedBlockObject{ + Attributes: map[string]resourceSchema.Attribute{ "donor_package_step_id": util.ResourceString().Optional().Build(), - "template": util.ResourceString().Optional().Build(), + "template": util.ResourceString().Optional().Computed().Build(), }, Blocks: map[string]resourceSchema.Block{ "donor_package": resourceSchema.ListNestedBlock{ From c32f73ff5a4ec7d933860f25bc4b15f2dc4bf5b2 Mon Sep 17 00:00:00 2001 From: Huy Nguyen <162080607+HuyPhanNguyen@users.noreply.github.com> Date: Tue, 13 Aug 2024 17:19:16 +1000 Subject: [PATCH 23/28] fix git credential null description issue (#735) --- octopusdeploy_framework/schemas/gitCredential.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/octopusdeploy_framework/schemas/gitCredential.go b/octopusdeploy_framework/schemas/gitCredential.go index c4b0b4d6f..b11c3fd5a 100644 --- a/octopusdeploy_framework/schemas/gitCredential.go +++ b/octopusdeploy_framework/schemas/gitCredential.go @@ -5,6 +5,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" 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/stringplanmodifier" ) const ( @@ -16,21 +17,23 @@ func GetGitCredentialResourceSchema() resourceSchema.Schema { return resourceSchema.Schema{ Description: "Manages a Git credential in Octopus Deploy.", Attributes: map[string]resourceSchema.Attribute{ - "id": util.ResourceString().Optional().Computed().Description("The unique ID for this resource.").Build(), - "space_id": util.ResourceString().Optional().Computed().Description("The space ID associated with this Git Credential.").Build(), + "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 Git Credential.").Build(), "name": util.ResourceString().Required().Description("The name of this Git Credential.").Build(), - "description": util.ResourceString().Optional().Description("The description of this Git Credential.").Build(), + "description": util.ResourceString().Optional().Computed().Default("").Description("The description of this Git Credential.").Build(), "type": util.ResourceString(). Optional(). Description("The Git credential authentication type."). Build(), "username": util.ResourceString(). Required(). + PlanModifiers(stringplanmodifier.UseStateForUnknown()). Description("The username for the Git credential."). Validators(stringvalidator.LengthAtLeast(1)). Build(), "password": util.ResourceString(). Required(). + PlanModifiers(stringplanmodifier.UseStateForUnknown()). Sensitive(). Description("The password for the Git credential."). Validators(stringvalidator.LengthAtLeast(1)). From 8ef9e5f892f388a820aa4fe7907344a2c819e59c Mon Sep 17 00:00:00 2001 From: Ben Pearce Date: Tue, 13 Aug 2024 17:21:21 +1000 Subject: [PATCH 24/28] chore: fix template ids for project (#734) --- .../resource_project_expand.go | 31 ++++++++++--------- .../resource_project_test.go | 29 +++++++---------- .../resource_tenant_test.go | 2 +- octopusdeploy_framework/schemas/project.go | 2 +- 4 files changed, 29 insertions(+), 35 deletions(-) diff --git a/octopusdeploy_framework/resource_project_expand.go b/octopusdeploy_framework/resource_project_expand.go index e0a3316da..08c9a71c3 100644 --- a/octopusdeploy_framework/resource_project_expand.go +++ b/octopusdeploy_framework/resource_project_expand.go @@ -10,6 +10,7 @@ import ( "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-log/tflog" "net/url" ) @@ -50,9 +51,9 @@ func expandProject(ctx context.Context, model projectResourceModel) *projects.Pr var gitLibrarySettingsList []gitLibraryPersistenceSettingsModel diags := model.GitLibraryPersistenceSettings.ElementsAs(ctx, &gitLibrarySettingsList, false) if diags.HasError() { - fmt.Printf("Error converting Git library persistence settings: %v\n", diags) + tflog.Error(ctx, fmt.Sprintf("Error converting Git library persistence settings: %v\n", diags)) } else { - fmt.Printf("Number of Git library persistence settings: %d\n", len(gitLibrarySettingsList)) + tflog.Debug(ctx, fmt.Sprintf("Number of Git library persistence settings: %d\n", len(gitLibrarySettingsList))) if len(gitLibrarySettingsList) > 0 { project.PersistenceSettings = expandGitLibraryPersistenceSettings(ctx, gitLibrarySettingsList[0]) project.IsVersionControlled = true @@ -62,9 +63,9 @@ func expandProject(ctx context.Context, model projectResourceModel) *projects.Pr var gitUsernamePasswordSettingsList []gitUsernamePasswordPersistenceSettingsModel diags := model.GitUsernamePasswordPersistenceSettings.ElementsAs(ctx, &gitUsernamePasswordSettingsList, false) if diags.HasError() { - fmt.Printf("Error converting Git username/password persistence settings: %v\n", diags) + tflog.Error(ctx, fmt.Sprintf("Error converting Git username/password persistence settings: %v\n", diags)) } else { - fmt.Printf("Number of Git username/password persistence settings: %d\n", len(gitUsernamePasswordSettingsList)) + tflog.Debug(ctx, fmt.Sprintf("Number of Git username/password persistence settings: %d\n", len(gitUsernamePasswordSettingsList))) if len(gitUsernamePasswordSettingsList) > 0 { project.PersistenceSettings = expandGitUsernamePasswordPersistenceSettings(ctx, gitUsernamePasswordSettingsList[0]) project.IsVersionControlled = true @@ -74,9 +75,9 @@ func expandProject(ctx context.Context, model projectResourceModel) *projects.Pr var gitAnonymousSettingsList []gitAnonymousPersistenceSettingsModel diags := model.GitAnonymousPersistenceSettings.ElementsAs(ctx, &gitAnonymousSettingsList, false) if diags.HasError() { - fmt.Printf("Error converting Git anonymous persistence settings: %v\n", diags) + tflog.Error(ctx, fmt.Sprintf("Error converting Git anonymous persistence settings: %v\n", diags)) } else { - fmt.Printf("Number of Git anonymous persistence settings: %d\n", len(gitAnonymousSettingsList)) + tflog.Debug(ctx, fmt.Sprintf("Number of Git anonymous persistence settings: %d\n", len(gitAnonymousSettingsList))) if len(gitAnonymousSettingsList) > 0 { project.PersistenceSettings = expandGitAnonymousPersistenceSettings(ctx, gitAnonymousSettingsList[0]) project.IsVersionControlled = true @@ -116,13 +117,13 @@ func expandProject(ctx context.Context, model projectResourceModel) *projects.Pr var templates []templateModel diags := model.Template.ElementsAs(ctx, &templates, false) if diags.HasError() { - fmt.Printf("Error converting templates: %v\n", diags) + tflog.Error(ctx, fmt.Sprintf("Error converting templates: %v\n", diags)) } else { - fmt.Printf("Number of templates: %d\n", len(templates)) + tflog.Info(ctx, fmt.Sprintf("Number of templates: %d\n", len(templates))) project.Templates = expandTemplates(templates) } } else { - fmt.Println("Template is null") + tflog.Debug(ctx, "Template is null") project.Templates = []actiontemplates.ActionTemplateParameter{} } @@ -138,7 +139,7 @@ func expandProject(ctx context.Context, model projectResourceModel) *projects.Pr } func expandGitLibraryPersistenceSettings(ctx context.Context, model gitLibraryPersistenceSettingsModel) projects.GitPersistenceSettings { - url, _ := url.Parse(model.URL.ValueString()) + gitUrl, _ := url.Parse(model.URL.ValueString()) var protectedBranches []string model.ProtectedBranches.ElementsAs(ctx, &protectedBranches, false) @@ -149,12 +150,12 @@ func expandGitLibraryPersistenceSettings(ctx context.Context, model gitLibraryPe }, model.DefaultBranch.ValueString(), protectedBranches, - url, + gitUrl, ) } func expandGitUsernamePasswordPersistenceSettings(ctx context.Context, model gitUsernamePasswordPersistenceSettingsModel) projects.GitPersistenceSettings { - url, _ := url.Parse(model.URL.ValueString()) + gitUrl, _ := url.Parse(model.URL.ValueString()) var protectedBranches []string model.ProtectedBranches.ElementsAs(ctx, &protectedBranches, false) @@ -168,12 +169,12 @@ func expandGitUsernamePasswordPersistenceSettings(ctx context.Context, model git usernamePasswordCredential, model.DefaultBranch.ValueString(), protectedBranches, - url, + gitUrl, ) } func expandGitAnonymousPersistenceSettings(ctx context.Context, model gitAnonymousPersistenceSettingsModel) projects.GitPersistenceSettings { - url, _ := url.Parse(model.URL.ValueString()) + gitUrl, _ := url.Parse(model.URL.ValueString()) var protectedBranches []string model.ProtectedBranches.ElementsAs(ctx, &protectedBranches, false) @@ -182,7 +183,7 @@ func expandGitAnonymousPersistenceSettings(ctx context.Context, model gitAnonymo &credentials.Anonymous{}, model.DefaultBranch.ValueString(), protectedBranches, - url, + gitUrl, ) } diff --git a/octopusdeploy_framework/resource_project_test.go b/octopusdeploy_framework/resource_project_test.go index 511e771e8..a9971f906 100644 --- a/octopusdeploy_framework/resource_project_test.go +++ b/octopusdeploy_framework/resource_project_test.go @@ -103,7 +103,7 @@ func TestAccProjectWithUpdate(t *testing.T) { resource.TestCheckResourceAttr(prefix, "description", description), resource.TestCheckResourceAttr(prefix, "name", name), ), - Config: testAccProjectBasic(lifecycleLocalName, lifecycleName, projectGroupLocalName, projectGroupName, localName, name, description), + Config: testAccProjectBasic(lifecycleLocalName, lifecycleName, projectGroupLocalName, projectGroupName, localName, name, description, 2), }, { Check: resource.ComposeTestCheckFunc( @@ -114,17 +114,22 @@ func TestAccProjectWithUpdate(t *testing.T) { resource.TestCheckNoResourceAttr(prefix, "deployment_step.0.windows_service.1.step_name"), resource.TestCheckNoResourceAttr(prefix, "deployment_step.0.iis_website.0.step_name"), ), - Config: testAccProjectBasic(lifecycleLocalName, lifecycleName, projectGroupLocalName, projectGroupName, localName, name, description), + Config: testAccProjectBasic(lifecycleLocalName, lifecycleName, projectGroupLocalName, projectGroupName, localName, name, description, 3), }, }, }) } -func testAccProjectBasic(lifecycleLocalName string, lifecycleName string, projectGroupLocalName string, projectGroupName string, localName string, name string, description string) string { +func testAccProjectBasic(lifecycleLocalName string, lifecycleName string, projectGroupLocalName string, projectGroupName string, localName string, name string, description string, templateCount int) string { projectGroup := internaltest.NewProjectGroupTestOptions() projectGroup.LocalName = projectGroupLocalName projectGroup.Resource.Name = projectGroupName + var templates string + for i := 0; i < templateCount; i++ { + templates += fmt.Sprintf("\ntemplate {\n\t\t\t\tname = \"%d\"\n\t\t\t\tdisplay_settings = {\n\t\t\t\t\t\"Octopus.ControlType\": \"SingleLineText\"\n\t\t\t\t}\n\t\t\t}\n", i) + } + return fmt.Sprintf(testAccLifecycle(lifecycleLocalName, lifecycleName)+"\n"+ internaltest.ProjectGroupConfiguration(projectGroup)+"\n"+ `resource "octopusdeploy_project" "%s" { @@ -133,20 +138,8 @@ func testAccProjectBasic(lifecycleLocalName string, lifecycleName string, projec name = "%s" project_group_id = octopusdeploy_project_group.%s.id - template { - name = "2" - display_settings = { - "Octopus.ControlType": "SingleLineText" - } - } - - template { - name = "1" - display_settings = { - "Octopus.ControlType": "SingleLineText" - } - } - + %s + versioning_strategy { template = "#{Octopus.Version.LastMajor}.#{Octopus.Version.LastMinor}.#{Octopus.Version.LastPatch}.#{Octopus.Version.NextRevision}" } @@ -156,7 +149,7 @@ func testAccProjectBasic(lifecycleLocalName string, lifecycleName string, projec skip_machine_behavior = "None" } - }`, localName, description, lifecycleLocalName, name, projectGroupLocalName) + }`, localName, description, lifecycleLocalName, name, projectGroupLocalName, templates) } func testAccProjectCheckDestroy(s *terraform.State) error { diff --git a/octopusdeploy_framework/resource_tenant_test.go b/octopusdeploy_framework/resource_tenant_test.go index cd5a28b72..3d99614c5 100644 --- a/octopusdeploy_framework/resource_tenant_test.go +++ b/octopusdeploy_framework/resource_tenant_test.go @@ -62,7 +62,7 @@ func testAccTenantBasic(lifecycleLocalName string, lifecycleName string, project sortOrder := acctest.RandIntRange(0, 10) useGuidedFailure := false - return fmt.Sprintf(testAccProjectBasic(lifecycleLocalName, lifecycleName, projectGroupLocalName, projectGroupName, projectLocalName, projectName, projectDescription)+"\n"+ + return fmt.Sprintf(testAccProjectBasic(lifecycleLocalName, lifecycleName, projectGroupLocalName, projectGroupName, projectLocalName, projectName, projectDescription, 2)+"\n"+ testAccEnvironment(environmentLocalName, environmentName, environmentDescription, allowDynamicInfrastructure, sortOrder, useGuidedFailure)+"\n"+` resource "octopusdeploy_tenant" "%s" { description = "%s" diff --git a/octopusdeploy_framework/schemas/project.go b/octopusdeploy_framework/schemas/project.go index 3b02bd3c8..6ab43ea08 100644 --- a/octopusdeploy_framework/schemas/project.go +++ b/octopusdeploy_framework/schemas/project.go @@ -129,7 +129,7 @@ func GetProjectResourceSchema() resourceSchema.Schema { "template": resourceSchema.ListNestedBlock{ NestedObject: resourceSchema.NestedBlockObject{ Attributes: map[string]resourceSchema.Attribute{ - "id": util.ResourceString().Optional().Computed().Description("The ID of the template parameter.").Build(), + "id": util.ResourceString().Optional().Computed().PlanModifiers(stringplanmodifier.UseStateForUnknown()).Description("The ID of the template parameter.").Build(), "name": util.ResourceString().Required().Description("The name of the variable set by the parameter. The name can contain letters, digits, dashes and periods.").Build(), "label": util.ResourceString().Optional().Description("The label shown beside the parameter when presented in the deployment process.").Build(), "help_text": util.ResourceString().Optional().Description("The help presented alongside the parameter input.").Build(), From 2261c2a7b519bb1a52d4f99b9b53df6b8e91be43 Mon Sep 17 00:00:00 2001 From: Isaac Calligeros <101079287+IsaacCalligeros95@users.noreply.github.com> Date: Tue, 13 Aug 2024 17:20:16 +0930 Subject: [PATCH 25/28] fix: Fix datasource feeds search not respecting name (#736) --- go.mod | 2 +- go.sum | 6 ++---- octopusdeploy_framework/datasource_feeds.go | 1 + 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index f3ed22597..cd8a3a059 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/OctopusDeploy/terraform-provider-octopusdeploy go 1.21 require ( - github.com/OctopusDeploy/go-octopusdeploy/v2 v2.49.1 + github.com/OctopusDeploy/go-octopusdeploy/v2 v2.49.2 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 diff --git a/go.sum b/go.sum index c0bdd145e..cfe020223 100644 --- a/go.sum +++ b/go.sum @@ -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.49.1 h1:lzC3tcnfvC07Ilqn5J51qhOnW79kGD6nTIxCBOdmAe8= -github.com/OctopusDeploy/go-octopusdeploy/v2 v2.49.1/go.mod h1:ggvOXzMnq+w0pLg6C9zdjz6YBaHfO3B3tqmmB7JQdaw= +github.com/OctopusDeploy/go-octopusdeploy/v2 v2.49.2 h1:ENd1MyQbYIDiW1ZyXRUcZr4OQ0d8j47I5a6DOT9Ez4o= +github.com/OctopusDeploy/go-octopusdeploy/v2 v2.49.2/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= @@ -164,8 +164,6 @@ github.com/hashicorp/terraform-json v0.22.1 h1:xft84GZR0QzjPVWs4lRUwvTcPnegqlyS7 github.com/hashicorp/terraform-json v0.22.1/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A= github.com/hashicorp/terraform-plugin-docs v0.13.0 h1:6e+VIWsVGb6jYJewfzq2ok2smPzZrt1Wlm9koLeKazY= github.com/hashicorp/terraform-plugin-docs v0.13.0/go.mod h1:W0oCmHAjIlTHBbvtppWHe8fLfZ2BznQbuv8+UD8OucQ= -github.com/hashicorp/terraform-plugin-framework v1.9.0 h1:caLcDoxiRucNi2hk8+j3kJwkKfvHznubyFsJMWfZqKU= -github.com/hashicorp/terraform-plugin-framework v1.9.0/go.mod h1:qBXLDn69kM97NNVi/MQ9qgd1uWWsVftGSnygYG1tImM= github.com/hashicorp/terraform-plugin-framework v1.11.0 h1:M7+9zBArexHFXDx/pKTxjE6n/2UCXY6b8FIq9ZYhwfE= github.com/hashicorp/terraform-plugin-framework v1.11.0/go.mod h1:qBXLDn69kM97NNVi/MQ9qgd1uWWsVftGSnygYG1tImM= github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 h1:HOjBuMbOEzl7snOdOoUfE2Jgeto6JOjLVQ39Ls2nksc= diff --git a/octopusdeploy_framework/datasource_feeds.go b/octopusdeploy_framework/datasource_feeds.go index d8c19a863..fad80df86 100644 --- a/octopusdeploy_framework/datasource_feeds.go +++ b/octopusdeploy_framework/datasource_feeds.go @@ -53,6 +53,7 @@ func (e *feedsDataSource) Read(ctx context.Context, req datasource.ReadRequest, } query := feeds.FeedsQuery{ + Name: data.Name.ValueString(), IDs: util.GetIds(data.IDs), PartialName: data.PartialName.ValueString(), Skip: util.GetNumber(data.Skip), From 6a770fa102e38b9d9453573ca31b19f618435ca4 Mon Sep 17 00:00:00 2001 From: Henrik Andersson Date: Wed, 14 Aug 2024 18:56:47 -0700 Subject: [PATCH 26/28] chore: remove plan modifiers from retention period (#738) --- .../schemas/runbook_retention_period.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/octopusdeploy_framework/schemas/runbook_retention_period.go b/octopusdeploy_framework/schemas/runbook_retention_period.go index d8bbdc8b6..efe7833d4 100644 --- a/octopusdeploy_framework/schemas/runbook_retention_period.go +++ b/octopusdeploy_framework/schemas/runbook_retention_period.go @@ -10,9 +10,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" resourceSchema "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -41,9 +38,6 @@ func getRunbookRetentionPeriodSchema() map[string]resourceSchema.Attribute { Validators: []validator.Int64{ int64validator.AtLeast(0), }, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.UseStateForUnknown(), - }, }, runbookRetentionPeriodSchemeAttributeNames.ShouldKeepForever: resourceSchema.BoolAttribute{ Description: "Indicates if items should never be deleted. The default value is `false`.", @@ -53,9 +47,6 @@ func getRunbookRetentionPeriodSchema() map[string]resourceSchema.Attribute { Validators: []validator.Bool{ boolvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName(runbookRetentionPeriodSchemeAttributeNames.QuantityToKeep)), }, - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.UseStateForUnknown(), - }, }, } } From 0678c067bd0154f31bf937cbacb5f9075375daaa Mon Sep 17 00:00:00 2001 From: Henrik Andersson Date: Wed, 14 Aug 2024 21:20:19 -0700 Subject: [PATCH 27/28] chore: should pass in space_id from config as it'll grab the space id from the client if nil (#737) --- octopusdeploy_framework/datasource_environments.go | 4 +--- octopusdeploy_framework/resource_variable.go | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/octopusdeploy_framework/datasource_environments.go b/octopusdeploy_framework/datasource_environments.go index 13f884e74..da219658c 100644 --- a/octopusdeploy_framework/datasource_environments.go +++ b/octopusdeploy_framework/datasource_environments.go @@ -84,9 +84,7 @@ func (e *environmentDataSource) Read(ctx context.Context, req datasource.ReadReq Take: util.GetNumber(data.Take), } - spaceID := util.Ternary(data.SpaceID.IsNull(), e.Client.GetSpaceID(), data.SpaceID.ValueString()) - - existingEnvironments, err := environments.Get(e.Client, spaceID, query) + existingEnvironments, err := environments.Get(e.Client, data.SpaceID.ValueString(), query) if err != nil { resp.Diagnostics.AddError("unable to load environments", err.Error()) return diff --git a/octopusdeploy_framework/resource_variable.go b/octopusdeploy_framework/resource_variable.go index 78d48b5a2..af4e5b775 100644 --- a/octopusdeploy_framework/resource_variable.go +++ b/octopusdeploy_framework/resource_variable.go @@ -211,7 +211,7 @@ func (r *variableTypeResource) Delete(ctx context.Context, req resource.DeleteRe return } - if _, err := variables.DeleteSingle(r.Config.Client, r.Config.SpaceID, variableOwnerID.ValueString(), data.ID.ValueString()); err != nil { + if _, err := variables.DeleteSingle(r.Config.Client, data.SpaceID.ValueString(), variableOwnerID.ValueString(), data.ID.ValueString()); err != nil { resp.Diagnostics.AddError("unable to delete variable", err.Error()) return } From d7fc15c6c8f1a69681c5da470899e0883c20df97 Mon Sep 17 00:00:00 2001 From: Isaac Calligeros <101079287+IsaacCalligeros95@users.noreply.github.com> Date: Thu, 15 Aug 2024 14:21:45 +0930 Subject: [PATCH 28/28] Chore!: fix lvs datasource unexpected changes and resource Known After Apply differences. (#739) * Fix an issue with help text and templates when empty * Fix library variable set resource and data source schema + changes * Remove template ids plan modifier --- docs/resources/library_variable_set.md | 2 +- .../schemas/library_variable_set.go | 10 +++++++--- octopusdeploy_framework/schemas/schema.go | 8 ++++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/resources/library_variable_set.md b/docs/resources/library_variable_set.md index 9bff4bf78..1eac0caca 100644 --- a/docs/resources/library_variable_set.md +++ b/docs/resources/library_variable_set.md @@ -25,10 +25,10 @@ This resource manages library variable sets in Octopus Deploy. - `id` (String) The unique ID for this resource. - `space_id` (String) The space ID associated with this library variable set. - `template` (Block List) (see [below for nested schema](#nestedblock--template)) -- `template_ids` (Map of String) ### Read-Only +- `template_ids` (Map of String) - `variable_set_id` (String) diff --git a/octopusdeploy_framework/schemas/library_variable_set.go b/octopusdeploy_framework/schemas/library_variable_set.go index 76ba84d14..055db386c 100644 --- a/octopusdeploy_framework/schemas/library_variable_set.go +++ b/octopusdeploy_framework/schemas/library_variable_set.go @@ -7,6 +7,8 @@ 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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" types "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -93,10 +95,12 @@ func GetLibraryVariableSetResourceSchema() resourceSchema.Schema { "template_ids": resourceSchema.MapAttribute{ ElementType: types.StringType, Computed: true, - Optional: true, }, "variable_set_id": resourceSchema.StringAttribute{ Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, }, Description: "This resource manages library variable sets in Octopus Deploy.", @@ -135,7 +139,7 @@ func MapToLibraryVariableSet(data *LibraryVariableSetResourceModel) *variables.L func FlattenTemplates(actionTemplateParameters []actiontemplates.ActionTemplateParameter) types.List { if len(actionTemplateParameters) == 0 { - return types.ListNull(types.ObjectType{AttrTypes: TemplateObjectType()}) + return types.ListValueMust(types.ObjectType{AttrTypes: TemplateObjectType()}, []attr.Value{}) } actionTemplateList := make([]attr.Value, 0, len(actionTemplateParameters)) @@ -143,7 +147,7 @@ func FlattenTemplates(actionTemplateParameters []actiontemplates.ActionTemplateP attrs := map[string]attr.Value{ "default_value": util.Ternary(actionTemplateParams.DefaultValue.Value != "", types.StringValue(actionTemplateParams.DefaultValue.Value), types.StringNull()), "display_settings": flattenDisplaySettingsMap(actionTemplateParams.DisplaySettings), - "help_text": util.Ternary(actionTemplateParams.HelpText != "", types.StringValue(actionTemplateParams.HelpText), types.StringNull()), + "help_text": util.Ternary(actionTemplateParams.HelpText != "", types.StringValue(actionTemplateParams.HelpText), types.StringValue("")), "id": types.StringValue(actionTemplateParams.GetID()), "label": util.Ternary(actionTemplateParams.Label != "", types.StringValue(actionTemplateParams.Label), types.StringNull()), "name": types.StringValue(actionTemplateParams.Name), diff --git a/octopusdeploy_framework/schemas/schema.go b/octopusdeploy_framework/schemas/schema.go index 496dbc913..e67c11dc3 100644 --- a/octopusdeploy_framework/schemas/schema.go +++ b/octopusdeploy_framework/schemas/schema.go @@ -2,6 +2,8 @@ package schemas import ( "fmt" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" @@ -145,6 +147,9 @@ func GetIdResourceSchema() resourceSchema.Attribute { Description: "The unique ID for this resource.", Computed: true, Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, } } @@ -153,6 +158,9 @@ func GetSpaceIdResourceSchema(resourceDescription string) resourceSchema.Attribu Description: "The space ID associated with this " + resourceDescription + ".", Computed: true, Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, } }