diff --git a/docs/data-sources/accounts.md b/docs/data-sources/accounts.md
index a108cfe1d..27bb16f6b 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`, `AmazonWebServicesOidcAccount`, `AzureServicePrincipal`, `AzureSubscription`, `None`, `SshKeyPair`, `Token`, or `UsernamePassword`.
+- `account_type` (String) A filter to search by a list of account types. Valid account types are `AmazonWebServicesAccount`, `AmazonWebServicesRoleAccount`, `AmazonWebServicesOidcAccount`, `AzureServicePrincipal`, `AzureSubscription`, `GenericOidcAccount`, `None`, `SshKeyPair`, `Token`, or `UsernamePassword`.
- `ids` (List of String) A filter to search by a list of IDs.
- `partial_name` (String) A filter to search by the partial match of a name.
- `skip` (Number) A filter to specify the number of items to skip in the response.
diff --git a/docs/data-sources/deployment_freezes.md b/docs/data-sources/deployment_freezes.md
new file mode 100644
index 000000000..a4a365baf
--- /dev/null
+++ b/docs/data-sources/deployment_freezes.md
@@ -0,0 +1,45 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "octopusdeploy_deployment_freezes Data Source - terraform-provider-octopusdeploy"
+subcategory: ""
+description: |-
+ Provides information about deployment freezes
+---
+
+# octopusdeploy_deployment_freezes (Data Source)
+
+Provides information about deployment freezes
+
+
+
+
+## Schema
+
+### Optional
+
+- `environment_ids` (List of String) A filter to search by a list of environment IDs
+- `ids` (List of String) A filter to search by a list of IDs.
+- `include_complete` (Boolean) Include deployment freezes that completed, default is true
+- `partial_name` (String) A filter to search by a partial name.
+- `project_ids` (List of String) A filter to search by a list of project IDs
+- `skip` (Number) A filter to specify the number of items to skip in the response.
+- `status` (String) Filter by the status of the deployment freeze, value values are Expired, Active, Scheduled (case-insensitive)
+- `take` (Number) A filter to specify the number of items to take (or return) in the response.
+
+### Read-Only
+
+- `deployment_freezes` (Attributes List) (see [below for nested schema](#nestedatt--deployment_freezes))
+- `id` (String) The unique ID for this resource.
+
+
+### Nested Schema for `deployment_freezes`
+
+Read-Only:
+
+- `end` (String) The end time of the freeze
+- `id` (String) The unique ID for this resource.
+- `name` (String) The name of this resource.
+- `project_environment_scope` (Map of List of String) The project environment scope of the deployment freeze
+- `start` (String) The start time of the freeze
+
+
diff --git a/docs/guides/2-provider-configuration.md b/docs/guides/2-provider-configuration.md
index 158c59df3..94deaf8af 100644
--- a/docs/guides/2-provider-configuration.md
+++ b/docs/guides/2-provider-configuration.md
@@ -15,7 +15,7 @@ subcategory: "Guides"
terraform {
required_providers {
octopusdeploy = {
- source = OctopusDeployLabs/octopusdeploy
+ source = "OctopusDeployLabs/octopusdeploy"
}
}
}
@@ -39,7 +39,7 @@ The environment variable fallback values that the Terraform Provider search for
terraform {
required_providers {
octopusdeploy = {
- source = OctopusDeployLabs/octopusdeploy
+ source = "OctopusDeployLabs/octopusdeploy"
}
}
}
diff --git a/docs/resources/deployment_freeze.md b/docs/resources/deployment_freeze.md
new file mode 100644
index 000000000..9f0188139
--- /dev/null
+++ b/docs/resources/deployment_freeze.md
@@ -0,0 +1,69 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "octopusdeploy_deployment_freeze Resource - terraform-provider-octopusdeploy"
+subcategory: ""
+description: |-
+
+---
+
+# octopusdeploy_deployment_freeze (Resource)
+
+
+
+## Example Usage
+
+```terraform
+# basic freeze with no project scopes
+resource "octopusdeploy_deployment_freeze" "freeze" {
+ name = "Xmas"
+ start = "2024-12-25T00:00:00+10:00"
+ end = "2024-12-27T00:00:00+08:00"
+}
+
+# Freeze with different timezones and single project/environment scope
+resource "octopusdeploy_deployment_freeze" "freeze" {
+ name = "Xmas"
+ start = "2024-12-25T00:00:00+10:00"
+ end = "2024-12-27T00:00:00+08:00"
+}
+
+resource "octopusdeploy_deployment_freeze_project" "project_freeze" {
+ deploymentfreeze_id= octopusdeploy_deployment_freeze.freeze.id
+ project_id = "Projects-123"
+ environment_ids = [ "Environments-123", "Environments-456" ]
+}
+
+# Freeze with ids sourced from resources and datasources. Projects can be sourced from different spaces, a single scope can only reference projects and environments from the same space.
+resource "octopusdeploy_deployment_freeze" "freeze" {
+ name = "End of financial year shutdown"
+ start = "2025-06-30T00:00:00+10:00"
+ end = "2025-07-02T00:00:00+10:00"
+}
+
+resource "octopusdeploy_deployment_freeze_project" "project_freeze" {
+ deploymentfreeze_id = octopusdeploy_deployment_freeze.freeze.id
+ project_id = resource.octopusdeploy_project.project1.id
+ environment_ids = [resource.octopusdeploy_environment.production.id]
+}
+
+resource "octopusdeploy_deployment_freeze_project" "project_freeze" {
+ deploymentfreeze_id = octopusdeploy_deployment_freeze.freeze.id
+ project_id = data.octopusdeploy_projects.second_project.projects[0].id
+ environment_ids = [ data.octopusdeploy_environments.default_environment.environments[0].id ]
+}
+```
+
+
+## Schema
+
+### Required
+
+- `end` (String) The end time of the freeze, must be RFC3339 format
+- `name` (String) The name of this resource.
+- `start` (String) The start time of the freeze, must be RFC3339 format
+
+### Read-Only
+
+- `id` (String) The unique ID for this resource.
+
+
diff --git a/docs/resources/deployment_freeze_project.md b/docs/resources/deployment_freeze_project.md
new file mode 100644
index 000000000..829dc8b67
--- /dev/null
+++ b/docs/resources/deployment_freeze_project.md
@@ -0,0 +1,31 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "octopusdeploy_deployment_freeze_project Resource - terraform-provider-octopusdeploy"
+subcategory: ""
+description: |-
+
+---
+
+# octopusdeploy_deployment_freeze_project (Resource)
+
+
+
+
+
+
+## Schema
+
+### Required
+
+- `deploymentfreeze_id` (String) The deployment freeze ID associated with this freeze scope.
+- `project_id` (String) The project ID associated with this freeze scope.
+
+### Optional
+
+- `environment_ids` (List of String) The environment IDs associated with this project deployment freeze scope.
+
+### Read-Only
+
+- `id` (String) The unique ID for this resource.
+
+
diff --git a/docs/resources/generic_oidc_account.md b/docs/resources/generic_oidc_account.md
new file mode 100644
index 000000000..b6be4b96b
--- /dev/null
+++ b/docs/resources/generic_oidc_account.md
@@ -0,0 +1,51 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "octopusdeploy_generic_oidc_account Resource - terraform-provider-octopusdeploy"
+subcategory: ""
+description: |-
+ This resource manages a Generic OIDC Account in Octopus Deploy.
+---
+
+# octopusdeploy_generic_oidc_account (Resource)
+
+This resource manages a Generic OIDC Account in Octopus Deploy.
+
+## Example Usage
+
+```terraform
+resource "octopusdeploy_generic_oidc_account" "example" {
+ name = "Generic OpenID Connect Account (OK to Delete)"
+ execution_subject_keys = ["space", "project"]
+ audience = "api://default"
+}
+```
+
+
+## Schema
+
+### Required
+
+- `name` (String) The name of the generic oidc account.
+
+### Optional
+
+- `audience` (String) The audience associated with this resource.
+- `description` (String) The description of this generic oidc account.
+- `environments` (List of String) A list of environment IDs associated with this resource.
+- `execution_subject_keys` (List of String) Keys to include in a deployment or runbook. Valid options are `space`, `environment`, `project`, `tenant`, `runbook`, `account`, `type`.
+- `space_id` (String) The space ID associated with this resource.
+- `tenant_tags` (List of String) A list of tenant tags associated with this resource.
+- `tenanted_deployment_participation` (String) The tenanted deployment mode of the resource. Valid account types are `Untenanted`, `TenantedOrUntenanted`, or `Tenanted`.
+- `tenants` (List of String) A list of tenant IDs associated with this resource.
+
+### Read-Only
+
+- `id` (String) The unique ID for this resource.
+
+## Import
+
+Import is supported using the following syntax:
+
+```shell
+terraform import [options] octopusdeploy_generic_oidc_account.
+```
diff --git a/docs/resources/project.md b/docs/resources/project.md
index 218c4259f..d64796886 100644
--- a/docs/resources/project.md
+++ b/docs/resources/project.md
@@ -97,7 +97,7 @@ resource "octopusdeploy_project" "example" {
- `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 List) (see [below for nested schema](#nestedblock--versioning_strategy))
+- `versioning_strategy` (Block List, Deprecated) (see [below for nested schema](#nestedblock--versioning_strategy))
### Read-Only
diff --git a/docs/resources/project_versioning_strategy.md b/docs/resources/project_versioning_strategy.md
new file mode 100644
index 000000000..eb86b23f6
--- /dev/null
+++ b/docs/resources/project_versioning_strategy.md
@@ -0,0 +1,100 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "octopusdeploy_project_versioning_strategy Resource - terraform-provider-octopusdeploy"
+subcategory: ""
+description: |-
+
+---
+
+# octopusdeploy_project_versioning_strategy (Resource)
+
+
+
+## Example Usage
+
+```terraform
+resource "octopusdeploy_project_group" "tp" {
+ name = "DevOps Projects"
+ description = "My DevOps projects group"
+}
+
+resource "octopusdeploy_project" "tp" {
+ name = "My DevOps Project"
+ description = "test project"
+ lifecycle_id = "Lifecycles-1"
+ project_group_id = octopusdeploy_project_group.tp.id
+
+ depends_on = [octopusdeploy_project_group.tp]
+}
+
+resource "octopusdeploy_deployment_process" "process" {
+ project_id = octopusdeploy_project.tp.id
+
+ step {
+ name = "Hello World"
+ target_roles = [ "hello-world" ]
+ start_trigger = "StartAfterPrevious"
+ package_requirement = "LetOctopusDecide"
+ condition = "Success"
+
+ run_script_action {
+ name = "Hello World"
+ is_disabled = false
+ is_required = true
+ script_body = "Write-Host 'hello world'"
+ script_syntax = "PowerShell"
+ can_be_used_for_project_versioning = true
+ sort_order = 1
+
+
+ package {
+ name = "Package"
+ feed_id = "feeds-builtin"
+ package_id = "myExpressApp"
+ acquisition_location = "Server"
+ extract_during_deployment = true
+ }
+ }
+ }
+
+ depends_on = [octopusdeploy_project.tp]
+}
+
+resource "octopusdeploy_project_versioning_strategy" "tp" {
+ project_id = octopusdeploy_project.tp.id
+ space_id = octopusdeploy_project.tp.space_id
+ donor_package_step_id = octopusdeploy_deployment_process.process.step[0].run_script_action[0].id
+ donor_package = {
+ deployment_action = "Hello World"
+ package_reference = "Package"
+ }
+ depends_on = [
+ octopusdeploy_project_group.tp,
+ octopusdeploy_deployment_process.process
+ ]
+}
+```
+
+
+## Schema
+
+### Required
+
+- `donor_package` (Attributes) Donor Packages. (see [below for nested schema](#nestedatt--donor_package))
+- `project_id` (String) The associated project ID.
+- `space_id` (String) Space ID of the associated project.
+
+### Optional
+
+- `donor_package_step_id` (String) The associated donor package step ID.
+- `template` (String)
+
+
+### Nested Schema for `donor_package`
+
+Optional:
+
+- `deployment_action` (String) Deployment action.
+- `package_reference` (String) Package reference.
+
+
diff --git a/examples/resources/octopusdeploy_deployment_freeze/resource.tf b/examples/resources/octopusdeploy_deployment_freeze/resource.tf
new file mode 100644
index 000000000..d868261c8
--- /dev/null
+++ b/examples/resources/octopusdeploy_deployment_freeze/resource.tf
@@ -0,0 +1,38 @@
+# basic freeze with no project scopes
+resource "octopusdeploy_deployment_freeze" "freeze" {
+ name = "Xmas"
+ start = "2024-12-25T00:00:00+10:00"
+ end = "2024-12-27T00:00:00+08:00"
+}
+
+# Freeze with different timezones and single project/environment scope
+resource "octopusdeploy_deployment_freeze" "freeze" {
+ name = "Xmas"
+ start = "2024-12-25T00:00:00+10:00"
+ end = "2024-12-27T00:00:00+08:00"
+}
+
+resource "octopusdeploy_deployment_freeze_project" "project_freeze" {
+ deploymentfreeze_id= octopusdeploy_deployment_freeze.freeze.id
+ project_id = "Projects-123"
+ environment_ids = [ "Environments-123", "Environments-456" ]
+}
+
+# Freeze with ids sourced from resources and datasources. Projects can be sourced from different spaces, a single scope can only reference projects and environments from the same space.
+resource "octopusdeploy_deployment_freeze" "freeze" {
+ name = "End of financial year shutdown"
+ start = "2025-06-30T00:00:00+10:00"
+ end = "2025-07-02T00:00:00+10:00"
+}
+
+resource "octopusdeploy_deployment_freeze_project" "project_freeze" {
+ deploymentfreeze_id = octopusdeploy_deployment_freeze.freeze.id
+ project_id = resource.octopusdeploy_project.project1.id
+ environment_ids = [resource.octopusdeploy_environment.production.id]
+}
+
+resource "octopusdeploy_deployment_freeze_project" "project_freeze" {
+ deploymentfreeze_id = octopusdeploy_deployment_freeze.freeze.id
+ project_id = data.octopusdeploy_projects.second_project.projects[0].id
+ environment_ids = [ data.octopusdeploy_environments.default_environment.environments[0].id ]
+}
diff --git a/examples/resources/octopusdeploy_generic_oidc_account/import.sh b/examples/resources/octopusdeploy_generic_oidc_account/import.sh
new file mode 100644
index 000000000..f9540307d
--- /dev/null
+++ b/examples/resources/octopusdeploy_generic_oidc_account/import.sh
@@ -0,0 +1 @@
+terraform import [options] octopusdeploy_generic_oidc_account.
diff --git a/examples/resources/octopusdeploy_generic_oidc_account/resource.tf b/examples/resources/octopusdeploy_generic_oidc_account/resource.tf
new file mode 100644
index 000000000..6ad25b7da
--- /dev/null
+++ b/examples/resources/octopusdeploy_generic_oidc_account/resource.tf
@@ -0,0 +1,5 @@
+resource "octopusdeploy_generic_oidc_account" "example" {
+ name = "Generic OpenID Connect Account (OK to Delete)"
+ execution_subject_keys = ["space", "project"]
+ audience = "api://default"
+}
diff --git a/examples/resources/octopusdeploy_project_versioning_strategy/resource.tf b/examples/resources/octopusdeploy_project_versioning_strategy/resource.tf
new file mode 100644
index 000000000..dfcc6bbec
--- /dev/null
+++ b/examples/resources/octopusdeploy_project_versioning_strategy/resource.tf
@@ -0,0 +1,60 @@
+resource "octopusdeploy_project_group" "tp" {
+ name = "DevOps Projects"
+ description = "My DevOps projects group"
+}
+
+resource "octopusdeploy_project" "tp" {
+ name = "My DevOps Project"
+ description = "test project"
+ lifecycle_id = "Lifecycles-1"
+ project_group_id = octopusdeploy_project_group.tp.id
+
+ depends_on = [octopusdeploy_project_group.tp]
+}
+
+resource "octopusdeploy_deployment_process" "process" {
+ project_id = octopusdeploy_project.tp.id
+
+ step {
+ name = "Hello World"
+ target_roles = [ "hello-world" ]
+ start_trigger = "StartAfterPrevious"
+ package_requirement = "LetOctopusDecide"
+ condition = "Success"
+
+ run_script_action {
+ name = "Hello World"
+ is_disabled = false
+ is_required = true
+ script_body = "Write-Host 'hello world'"
+ script_syntax = "PowerShell"
+ can_be_used_for_project_versioning = true
+ sort_order = 1
+
+
+ package {
+ name = "Package"
+ feed_id = "feeds-builtin"
+ package_id = "myExpressApp"
+ acquisition_location = "Server"
+ extract_during_deployment = true
+ }
+ }
+ }
+
+ depends_on = [octopusdeploy_project.tp]
+}
+
+resource "octopusdeploy_project_versioning_strategy" "tp" {
+ project_id = octopusdeploy_project.tp.id
+ space_id = octopusdeploy_project.tp.space_id
+ donor_package_step_id = octopusdeploy_deployment_process.process.step[0].run_script_action[0].id
+ donor_package = {
+ deployment_action = "Hello World"
+ package_reference = "Package"
+ }
+ depends_on = [
+ octopusdeploy_project_group.tp,
+ octopusdeploy_deployment_process.process
+ ]
+}
diff --git a/go.mod b/go.mod
index 7e10c51b0..42cb09455 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.60.0
+ github.com/OctopusDeploy/go-octopusdeploy/v2 v2.63.0
github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework v0.0.0-20240729041805-46db6fb717b4
github.com/google/uuid v1.6.0
github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637
@@ -77,6 +77,7 @@ require (
github.com/hashicorp/logutils v1.0.0 // indirect
github.com/hashicorp/terraform-exec v0.21.0 // indirect
github.com/hashicorp/terraform-json v0.22.1 // indirect
+ github.com/hashicorp/terraform-plugin-framework-timetypes v0.5.0 // indirect
github.com/hashicorp/terraform-registry-address v0.2.3 // indirect
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
diff --git a/go.sum b/go.sum
index d8eeb086a..2dbca8771 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.60.0 h1:9j4IQ1UcAuaTytlBzQ7Mmoy/dLtofYfSGNiM22+sLXs=
-github.com/OctopusDeploy/go-octopusdeploy/v2 v2.60.0/go.mod h1:ggvOXzMnq+w0pLg6C9zdjz6YBaHfO3B3tqmmB7JQdaw=
+github.com/OctopusDeploy/go-octopusdeploy/v2 v2.63.0 h1:TshwN+IqKt21uY9aXzj0ou0Ew92uIi3+ZGTccVd9Z8g=
+github.com/OctopusDeploy/go-octopusdeploy/v2 v2.63.0/go.mod h1:ggvOXzMnq+w0pLg6C9zdjz6YBaHfO3B3tqmmB7JQdaw=
github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework v0.0.0-20240729041805-46db6fb717b4 h1:QfbVf0bOIRMp/WHAWsuVDB7KHoWnRsGbvDuOf2ua7k4=
github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework v0.0.0-20240729041805-46db6fb717b4/go.mod h1:Oq9KbiRNDBB5jFmrwnrgLX0urIqR/1ptY18TzkqXm7M=
github.com/ProtonMail/go-crypto v1.1.0-alpha.2 h1:bkyFVUP+ROOARdgCiJzNQo2V2kiB97LyUpzH9P6Hrlg=
@@ -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.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-timetypes v0.5.0 h1:v3DapR8gsp3EM8fKMh6up9cJUFQ2iRaFsYLP8UJnCco=
+github.com/hashicorp/terraform-plugin-framework-timetypes v0.5.0/go.mod h1:c3PnGE9pHBDfdEVG9t1S1C9ia5LW+gkFR0CygXlM8ak=
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 733b46675..ecd9d1076 100644
--- a/internal/errors/error.go
+++ b/internal/errors/error.go
@@ -2,12 +2,12 @@ package errors
import (
"context"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
"log"
"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"
)
diff --git a/octopusdeploy/schema_queries.go b/octopusdeploy/schema_queries.go
index 1322c9311..651fc55d8 100644
--- a/octopusdeploy/schema_queries.go
+++ b/octopusdeploy/schema_queries.go
@@ -7,7 +7,7 @@ import (
func getQueryAccountType() *schema.Schema {
return &schema.Schema{
- Description: "A filter to search by a list of account types. Valid account types are `AmazonWebServicesAccount`, `AmazonWebServicesRoleAccount`, `AmazonWebServicesOidcAccount`, `AzureServicePrincipal`, `AzureSubscription`, `None`, `SshKeyPair`, `Token`, or `UsernamePassword`.",
+ Description: "A filter to search by a list of account types. Valid account types are `AmazonWebServicesAccount`, `AmazonWebServicesRoleAccount`, `AmazonWebServicesOidcAccount`, `AzureServicePrincipal`, `AzureSubscription`, `GenericOidcAccount`, `None`, `SshKeyPair`, `Token`, or `UsernamePassword`.",
Optional: true,
Type: schema.TypeString,
ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{
@@ -17,6 +17,7 @@ func getQueryAccountType() *schema.Schema {
"AzureServicePrincipal",
"AzureOIDC",
"AzureSubscription",
+ "GenericOidcAccount",
"None",
"SshKeyPair",
"Token",
diff --git a/octopusdeploy/schema_utilities.go b/octopusdeploy/schema_utilities.go
index 1cb481661..3b80f9098 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`, `AmazonWebServicesOidcAccount`, `None`, `SshKeyPair`, `Token`, or `UsernamePassword`.",
+ Description: "Specifies the type of the account. Valid account types are `AmazonWebServicesAccount`, `AmazonWebServicesRoleAccount`, `AzureServicePrincipal`, `AzureOIDC`, `AzureSubscription`, `AmazonWebServicesOidcAccount`, `GenericOidcAccount`, `None`, `SshKeyPair`, `Token`, or `UsernamePassword`.",
ForceNew: true,
Type: schema.TypeString,
ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{
@@ -18,6 +18,7 @@ func getAccountTypeSchema(isRequired bool) *schema.Schema {
"AzureServicePrincipal",
"AzureOIDC",
"AzureSubscription",
+ "GenericOidcAccount",
"None",
"SshKeyPair",
"Token",
diff --git a/octopusdeploy_framework/datasource_deployment_freeze.go b/octopusdeploy_framework/datasource_deployment_freeze.go
new file mode 100644
index 000000000..e02e71e20
--- /dev/null
+++ b/octopusdeploy_framework/datasource_deployment_freeze.go
@@ -0,0 +1,122 @@
+package octopusdeploy_framework
+
+import (
+ "context"
+ "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/deploymentfreezes"
+ "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/diag"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "time"
+)
+
+const deploymentFreezeDatasourceName = "deployment_freezes"
+
+type deploymentFreezesModel struct {
+ ID types.String `tfsdk:"id"`
+ IDs types.List `tfsdk:"ids"`
+ PartialName types.String `tfsdk:"partial_name"`
+ ProjectIDs types.List `tfsdk:"project_ids"`
+ EnvironmentIDs types.List `tfsdk:"environment_ids"`
+ IncludeComplete types.Bool `tfsdk:"include_complete"`
+ Status types.String `tfsdk:"status"`
+ Skip types.Int64 `tfsdk:"skip"`
+ Take types.Int64 `tfsdk:"take"`
+ DeploymentFreezes types.List `tfsdk:"deployment_freezes"`
+}
+
+type deploymentFreezeDataSource struct {
+ *Config
+}
+
+func (d *deploymentFreezeDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ d.Config = DataSourceConfiguration(req, resp)
+}
+
+func NewDeploymentFreezeDataSource() datasource.DataSource {
+ return &deploymentFreezeDataSource{}
+}
+
+func (d *deploymentFreezeDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = util.GetTypeName(deploymentFreezeDatasourceName)
+}
+
+func (d *deploymentFreezeDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schemas.DeploymentFreezeSchema{}.GetDatasourceSchema()
+}
+
+func (d *deploymentFreezeDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data deploymentFreezesModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ query := deploymentfreezes.DeploymentFreezeQuery{
+ IDs: util.Ternary(data.IDs.IsNull(), []string{}, util.ExpandStringList(data.IDs)),
+ PartialName: data.PartialName.ValueString(),
+ ProjectIds: util.Ternary(data.ProjectIDs.IsNull(), []string{}, util.ExpandStringList(data.ProjectIDs)),
+ EnvironmentIds: util.Ternary(data.EnvironmentIDs.IsNull(), []string{}, util.ExpandStringList(data.EnvironmentIDs)),
+ IncludeComplete: data.IncludeComplete.ValueBool(),
+ Status: data.Status.ValueString(),
+ Skip: int(data.Skip.ValueInt64()),
+ Take: int(data.Take.ValueInt64()),
+ }
+
+ util.DatasourceReading(ctx, "deployment freezes", query)
+
+ existingFreezes, err := deploymentfreezes.Get(d.Client, query)
+ if err != nil {
+ resp.Diagnostics.AddError("unable to load deployment freezes", err.Error())
+ return
+ }
+
+ flattenedFreezes := []interface{}{}
+ for _, freeze := range existingFreezes.DeploymentFreezes {
+ flattenedFreeze, diags := mapFreezeToAttribute(ctx, freeze)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+ flattenedFreezes = append(flattenedFreezes, flattenedFreeze)
+ }
+
+ data.ID = types.StringValue("Deployment Freezes " + time.Now().UTC().String())
+ data.DeploymentFreezes, _ = types.ListValueFrom(ctx, types.ObjectType{AttrTypes: freezeObjectType()}, flattenedFreezes)
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+var _ datasource.DataSource = &deploymentFreezeDataSource{}
+var _ datasource.DataSourceWithConfigure = &deploymentFreezeDataSource{}
+
+func mapFreezeToAttribute(ctx context.Context, freeze deploymentfreezes.DeploymentFreeze) (attr.Value, diag.Diagnostics) {
+ projectScopes := make(map[string]attr.Value)
+ for projectId, environmentScopes := range freeze.ProjectEnvironmentScope {
+ projectScopes[projectId] = util.FlattenStringList(environmentScopes)
+ }
+
+ scopeType, diags := types.MapValueFrom(ctx, types.ListType{ElemType: types.StringType}, projectScopes)
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ return types.ObjectValueMust(freezeObjectType(), map[string]attr.Value{
+ "id": types.StringValue(freeze.ID),
+ "name": types.StringValue(freeze.Name),
+ "start": types.StringValue(freeze.Start.Format(time.RFC3339)),
+ "end": types.StringValue(freeze.End.Format(time.RFC3339)),
+ "project_environment_scope": scopeType,
+ }), diags
+}
+
+func freezeObjectType() map[string]attr.Type {
+ return map[string]attr.Type{
+ "id": types.StringType,
+ "name": types.StringType,
+ "start": types.StringType,
+ "end": types.StringType,
+ "project_environment_scope": types.MapType{ElemType: types.ListType{ElemType: types.StringType}},
+ }
+}
diff --git a/octopusdeploy_framework/framework_provider.go b/octopusdeploy_framework/framework_provider.go
index 555dc283b..d5be31d90 100644
--- a/octopusdeploy_framework/framework_provider.go
+++ b/octopusdeploy_framework/framework_provider.go
@@ -86,6 +86,7 @@ func (p *octopusDeployFrameworkProvider) DataSources(ctx context.Context) []func
NewUsersDataSource,
NewServiceAccountOIDCIdentityDataSource,
NewWorkersDataSource,
+ NewDeploymentFreezeDataSource,
NewTeamsDataSource,
}
}
@@ -114,6 +115,7 @@ func (p *octopusDeployFrameworkProvider) Resources(ctx context.Context) []func()
NewLibraryVariableSetFeedResource,
NewVariableResource,
NewProjectResource,
+ NewProjectVersioningStrategyResource,
NewMachineProxyResource,
NewTagResource,
NewDockerContainerRegistryFeedResource,
@@ -126,7 +128,10 @@ func (p *octopusDeployFrameworkProvider) Resources(ctx context.Context) []func()
NewSSHConnectionWorkerResource,
NewScriptModuleResource,
NewUserResource,
+ NewDeploymentFreezeResource,
+ NewDeploymentFreezeProjectResource,
NewServiceAccountOIDCIdentity,
+ NewGenericOidcResource,
}
}
diff --git a/octopusdeploy_framework/resource_deployment_freeze.go b/octopusdeploy_framework/resource_deployment_freeze.go
new file mode 100644
index 000000000..6298faa48
--- /dev/null
+++ b/octopusdeploy_framework/resource_deployment_freeze.go
@@ -0,0 +1,234 @@
+package octopusdeploy_framework
+
+import (
+ "context"
+ "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/deploymentfreezes"
+ "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-timetypes/timetypes"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "time"
+)
+
+const deploymentFreezeResourceName = "deployment_freeze"
+
+type deploymentFreezeModel struct {
+ Name types.String `tfsdk:"name"`
+ Start timetypes.RFC3339 `tfsdk:"start"`
+ End timetypes.RFC3339 `tfsdk:"end"`
+
+ schemas.ResourceModel
+}
+
+type deploymentFreezeResource struct {
+ *Config
+}
+
+var _ resource.Resource = &deploymentFreezeResource{}
+
+func NewDeploymentFreezeResource() resource.Resource {
+ return &deploymentFreezeResource{}
+}
+
+func (f *deploymentFreezeResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = util.GetTypeName(deploymentFreezeResourceName)
+}
+
+func (f *deploymentFreezeResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schemas.DeploymentFreezeSchema{}.GetResourceSchema()
+}
+
+func (f *deploymentFreezeResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ f.Config = ResourceConfiguration(req, resp)
+}
+
+func (f *deploymentFreezeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ internal.Mutex.Lock()
+ defer internal.Mutex.Unlock()
+
+ var state *deploymentFreezeModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ deploymentFreeze, err := deploymentfreezes.GetById(f.Config.Client, state.GetID())
+ if err != nil {
+ if err := errors.ProcessApiErrorV2(ctx, resp, state, err, "deployment freeze"); err != nil {
+ resp.Diagnostics.AddError("unable to load deployment freeze", err.Error())
+ }
+ return
+ }
+
+ if deploymentFreeze.Name != state.Name.ValueString() {
+ state.Name = types.StringValue(deploymentFreeze.Name)
+ }
+
+ mapToState(ctx, state, deploymentFreeze)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (f *deploymentFreezeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ internal.Mutex.Lock()
+ defer internal.Mutex.Unlock()
+
+ var plan *deploymentFreezeModel
+ diags := req.Plan.Get(ctx, &plan)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ deploymentFreeze, diags := mapFromState(plan)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+
+ createdFreeze, err := deploymentfreezes.Add(f.Config.Client, deploymentFreeze)
+ if err != nil {
+ resp.Diagnostics.AddError("error while creating deployment freeze", err.Error())
+ return
+ }
+
+ diags.Append(mapToState(ctx, plan, createdFreeze)...)
+ if diags.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, plan)...)
+}
+
+func (f *deploymentFreezeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ internal.Mutex.Lock()
+ defer internal.Mutex.Unlock()
+
+ var plan *deploymentFreezeModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ existingFreeze, err := deploymentfreezes.GetById(f.Config.Client, plan.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("unable to load deployment freeze", err.Error())
+ return
+ }
+
+ updatedFreeze, diags := mapFromState(plan)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+
+ // this resource doesn't include scopes, need to copy it from the fetched resource
+ updatedFreeze.ProjectEnvironmentScope = existingFreeze.ProjectEnvironmentScope
+
+ updatedFreeze.SetID(existingFreeze.GetID())
+ updatedFreeze.Links = existingFreeze.Links
+
+ updatedFreeze, err = deploymentfreezes.Update(f.Config.Client, updatedFreeze)
+ if err != nil {
+ resp.Diagnostics.AddError("error while updating deployment freeze", err.Error())
+ return
+ }
+
+ diags.Append(mapToState(ctx, plan, updatedFreeze)...)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, plan)...)
+}
+
+func (f *deploymentFreezeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ internal.Mutex.Lock()
+ defer internal.Mutex.Unlock()
+
+ var state *deploymentFreezeModel
+ diags := req.State.Get(ctx, &state)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ freeze, err := deploymentfreezes.GetById(f.Config.Client, state.GetID())
+ if err != nil {
+ resp.Diagnostics.AddError("unable to load deployment freeze", err.Error())
+ return
+ }
+
+ err = deploymentfreezes.Delete(f.Config.Client, freeze)
+ if err != nil {
+ resp.Diagnostics.AddError("unable to delete deployment freeze", err.Error())
+ }
+
+ resp.State.RemoveResource(ctx)
+}
+
+func mapToState(ctx context.Context, state *deploymentFreezeModel, deploymentFreeze *deploymentfreezes.DeploymentFreeze) diag.Diagnostics {
+ state.ID = types.StringValue(deploymentFreeze.ID)
+ state.Name = types.StringValue(deploymentFreeze.Name)
+
+ updatedStart, diags := calculateStateTime(ctx, state.Start, *deploymentFreeze.Start)
+ if diags.HasError() {
+ return diags
+ }
+ state.Start = updatedStart
+
+ updatedEnd, diags := calculateStateTime(ctx, state.End, *deploymentFreeze.End)
+ if diags.HasError() {
+ return diags
+ }
+ state.End = updatedEnd
+
+ return nil
+}
+
+func calculateStateTime(ctx context.Context, stateValue timetypes.RFC3339, updatedValue time.Time) (timetypes.RFC3339, diag.Diagnostics) {
+ stateTime, diags := stateValue.ValueRFC3339Time()
+ if diags.HasError() {
+ return timetypes.RFC3339{}, diags
+ }
+ stateTimeUTC := timetypes.NewRFC3339TimeValue(stateTime.UTC())
+ updatedValueUTC := updatedValue.UTC()
+ valuesAreEqual, diags := stateTimeUTC.StringSemanticEquals(ctx, timetypes.NewRFC3339TimeValue(updatedValueUTC))
+ if diags.HasError() {
+ return timetypes.NewRFC3339Null(), diags
+ }
+
+ if valuesAreEqual {
+ return stateValue, diags
+ }
+
+ location := stateTime.Location()
+ newValue := timetypes.NewRFC3339TimeValue(updatedValueUTC.In(location))
+ return newValue, diags
+}
+
+func mapFromState(state *deploymentFreezeModel) (*deploymentfreezes.DeploymentFreeze, diag.Diagnostics) {
+ start, diags := state.Start.ValueRFC3339Time()
+ if diags.HasError() {
+ return nil, diags
+ }
+ start = start.UTC()
+
+ end, diags := state.End.ValueRFC3339Time()
+ if diags.HasError() {
+ return nil, diags
+ }
+ end = end.UTC()
+
+ freeze := deploymentfreezes.DeploymentFreeze{
+ Name: state.Name.ValueString(),
+ Start: &start,
+ End: &end,
+ }
+
+ freeze.ID = state.ID.String()
+ return &freeze, nil
+}
diff --git a/octopusdeploy_framework/resource_deployment_freeze_project.go b/octopusdeploy_framework/resource_deployment_freeze_project.go
new file mode 100644
index 000000000..4dd5465f8
--- /dev/null
+++ b/octopusdeploy_framework/resource_deployment_freeze_project.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/deploymentfreezes"
+ "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/attr"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+ "net/http"
+)
+
+type deploymentFreezeProjectResource struct {
+ *Config
+}
+
+const description = "deployment freeze project scope"
+
+var _ resource.Resource = &deploymentFreezeProjectResource{}
+var _ resource.ResourceWithConfigure = &deploymentFreezeProjectResource{}
+
+func NewDeploymentFreezeProjectResource() resource.Resource {
+ return &deploymentFreezeProjectResource{}
+}
+
+func (d *deploymentFreezeProjectResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = util.GetTypeName("deployment_freeze_project")
+}
+
+func (d *deploymentFreezeProjectResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schemas.DeploymentFreezeProjectSchema{}.GetResourceSchema()
+}
+
+func (d *deploymentFreezeProjectResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ d.Config = ResourceConfiguration(req, resp)
+}
+
+func (d *deploymentFreezeProjectResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ internal.Mutex.Lock()
+ defer internal.Mutex.Unlock()
+
+ util.Create(ctx, description)
+
+ var plan schemas.DeploymentFreezeProjectResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ tflog.Debug(ctx, fmt.Sprintf("adding project (%s) to deployment freeze (%s)", plan.ProjectID.ValueString(), plan.DeploymentFreezeID.ValueString()))
+ freeze, err := deploymentfreezes.GetById(d.Client, plan.DeploymentFreezeID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("cannot load deployment freeze", err.Error())
+ return
+ }
+ freeze.ProjectEnvironmentScope[plan.ProjectID.ValueString()] = util.ExpandStringList(plan.EnvironmentIDs)
+
+ freeze, err = deploymentfreezes.Update(d.Client, freeze)
+ if err != nil {
+ resp.Diagnostics.AddError("error while updating deployment freeze", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(util.BuildCompositeId(plan.DeploymentFreezeID.ValueString(), plan.ProjectID.ValueString()))
+ plan.EnvironmentIDs = mapEnvironmentIds(freeze.ProjectEnvironmentScope[plan.ProjectID.ValueString()])
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ tflog.Debug(ctx, fmt.Sprintf("scope for project (%s) added to deployment freeze (%s)", plan.ProjectID, plan.DeploymentFreezeID))
+ util.Created(ctx, description)
+}
+
+func (d *deploymentFreezeProjectResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ util.Reading(ctx, description)
+ var data schemas.DeploymentFreezeProjectResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ bits := util.SplitCompositeId(data.ID.ValueString())
+ freezeId := bits[0]
+ projectId := bits[1]
+
+ freeze, err := deploymentfreezes.GetById(d.Client, freezeId)
+ if err != nil {
+ apiError := err.(*core.APIError)
+ if apiError.StatusCode != http.StatusNotFound {
+ resp.Diagnostics.AddError("unable to load deployment freeze", err.Error())
+ return
+ }
+ }
+
+ data.EnvironmentIDs = mapEnvironmentIds(freeze.ProjectEnvironmentScope[projectId])
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+ util.Read(ctx, description)
+}
+
+func (d *deploymentFreezeProjectResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ internal.Mutex.Lock()
+ defer internal.Mutex.Unlock()
+
+ util.Update(ctx, description)
+
+ var plan, state schemas.DeploymentFreezeProjectResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ freeze, err := deploymentfreezes.GetById(d.Client, state.DeploymentFreezeID.ValueString())
+ if err != nil {
+ apiError := err.(*core.APIError)
+ if apiError.StatusCode != http.StatusNotFound {
+ resp.Diagnostics.AddError("unable to load deployment freeze", err.Error())
+ return
+ }
+ }
+
+ tflog.Debug(ctx, fmt.Sprintf("updating project (%s) to deployment freeze (%s)", plan.ProjectID.ValueString(), plan.DeploymentFreezeID.ValueString()))
+ freeze.ProjectEnvironmentScope[plan.ProjectID.ValueString()] = util.ExpandStringList(plan.EnvironmentIDs)
+ _, err = deploymentfreezes.Update(d.Client, freeze)
+ if err != nil {
+ resp.Diagnostics.AddError("error while updating deployment freeze", err.Error())
+ return
+ }
+
+ plan.ID = types.StringValue(util.BuildCompositeId(plan.DeploymentFreezeID.ValueString(), plan.ProjectID.ValueString()))
+ plan.EnvironmentIDs = mapEnvironmentIds(freeze.ProjectEnvironmentScope[plan.ProjectID.ValueString()])
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ tflog.Debug(ctx, fmt.Sprintf("updated project (%s) to deployment freeze (%s)", plan.ProjectID.ValueString(), plan.DeploymentFreezeID.ValueString()))
+ util.Updated(ctx, description)
+}
+
+func (d *deploymentFreezeProjectResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ internal.Mutex.Lock()
+ defer internal.Mutex.Unlock()
+
+ util.Delete(ctx, description)
+
+ var data schemas.DeploymentFreezeProjectResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ freeze, err := deploymentfreezes.GetById(d.Client, data.DeploymentFreezeID.ValueString())
+ if err != nil {
+ apiError := err.(*core.APIError)
+ if apiError.StatusCode != http.StatusNotFound {
+ resp.Diagnostics.AddError("unable to load deployment freeze", err.Error())
+ return
+ }
+ }
+
+ delete(freeze.ProjectEnvironmentScope, data.ProjectID.ValueString())
+ freeze, err = deploymentfreezes.Update(d.Client, freeze)
+ if err != nil {
+ resp.Diagnostics.AddError(fmt.Sprintf("cannot remove project scope (%s) from deployment freeze (%s)", data.ProjectID.ValueString(), data.DeploymentFreezeID.ValueString()), err.Error())
+ }
+
+ tflog.Debug(ctx, fmt.Sprintf("scope for project (%s) removed from deployment freeze (%s)", data.ProjectID.ValueString(), data.DeploymentFreezeID.ValueString()))
+ util.Deleted(ctx, description)
+}
+
+func mapEnvironmentIds(ids []string) basetypes.ListValue {
+ environmentIDs := make([]attr.Value, len(ids))
+ for i, envID := range ids {
+ environmentIDs[i] = types.StringValue(envID)
+ }
+ environmentIdList, _ := types.ListValue(types.StringType, environmentIDs)
+ return environmentIdList
+}
diff --git a/octopusdeploy_framework/resource_deployment_freeze_test.go b/octopusdeploy_framework/resource_deployment_freeze_test.go
new file mode 100644
index 000000000..1098c1fd9
--- /dev/null
+++ b/octopusdeploy_framework/resource_deployment_freeze_test.go
@@ -0,0 +1,125 @@
+package octopusdeploy_framework
+
+import (
+ "fmt"
+ "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/deploymentfreezes"
+ "github.com/hashicorp/terraform-plugin-testing/helper/acctest"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/hashicorp/terraform-plugin-testing/terraform"
+ "strings"
+ "testing"
+ "time"
+)
+
+func TestNewDeploymentFreezeResource(t *testing.T) {
+ localName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)
+ resourceName := "octopusdeploy_deployment_freeze." + localName
+ name := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)
+ start := fmt.Sprintf("%d-11-21T06:30:00+10:00", time.Now().Year()+1)
+ end := fmt.Sprintf("%d-11-21T08:30:00+10:00", time.Now().Year()+1)
+ updatedEnd := fmt.Sprintf("%d-11-21T08:30:00+10:00", time.Now().Year()+2)
+ projectName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)
+ environmentName1 := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)
+ environmentName2 := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)
+ spaceName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)
+ projectGroupName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)
+ lifecycleName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)
+
+ resource.Test(t, resource.TestCase{
+ CheckDestroy: testDeploymentFreezeCheckDestroy,
+ PreCheck: func() { TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: ProtoV6ProviderFactories(),
+ Steps: []resource.TestStep{
+ {
+ Check: resource.ComposeTestCheckFunc(
+ testDeploymentFreezeExists(resourceName),
+ resource.TestCheckResourceAttr(resourceName, "name", name),
+ resource.TestCheckResourceAttr(resourceName, "start", start),
+ resource.TestCheckResourceAttr(resourceName, "end", end)),
+ Config: testDeploymentFreezeBasic(localName, name, start, end, spaceName, []string{environmentName1}, projectName, projectGroupName, lifecycleName),
+ },
+ {
+ Check: resource.ComposeTestCheckFunc(
+ testDeploymentFreezeExists(resourceName),
+ resource.TestCheckResourceAttr(resourceName, "name", name+"1"),
+ resource.TestCheckResourceAttr(resourceName, "start", start),
+ resource.TestCheckResourceAttr(resourceName, "end", updatedEnd)),
+ Config: testDeploymentFreezeBasic(localName, name+"1", start, updatedEnd, spaceName, []string{environmentName1, environmentName2}, projectName, projectGroupName, lifecycleName),
+ },
+ },
+ })
+}
+
+func testDeploymentFreezeBasic(localName string, freezeName string, start string, end string, spaceName string, environments []string, projectName string, projectGroupName string, lifecycleName string) string {
+ spaceLocalName := fmt.Sprintf("space_%s", localName)
+ projectScopeLocalName := fmt.Sprintf("project_scope_%s", localName)
+ projectLocalName := fmt.Sprintf("project_%s", localName)
+ lifecycleLocalName := fmt.Sprintf("lifecycle_%s", localName)
+ projectGroupLocalName := fmt.Sprintf("project_group_%s", localName)
+ environmentScopes := make([]string, 0, len(environments))
+ environmentResources := ""
+ for i, environmentName := range environments {
+ environmentLocalName := fmt.Sprintf("environment_%d_%s", i, localName)
+ environmentResources += fmt.Sprintln(createEnvironment(spaceLocalName, environmentLocalName, environmentName))
+ environmentScopes = append(environmentScopes, fmt.Sprintf("resource.octopusdeploy_environment.%s.id", environmentLocalName))
+ }
+
+ projectScopes := fmt.Sprintf(`resource "octopusdeploy_deployment_freeze_project" "%s" {
+ deploymentfreeze_id = octopusdeploy_deployment_freeze.%s.id
+ project_id = octopusdeploy_project.%s.id
+ environment_ids = [ %s ]
+ }`, projectScopeLocalName, localName, projectLocalName, strings.Join(environmentScopes, ","))
+
+ fmt.Println(projectScopes)
+
+ return fmt.Sprintf(`
+ %s
+
+ %s
+
+ %s
+
+ %s
+
+ %s
+
+ resource "octopusdeploy_deployment_freeze" "%s" {
+ name = "%s"
+ start = "%s"
+ end = "%s"
+ }
+
+ %s`,
+ createSpace(spaceLocalName, spaceName),
+ environmentResources,
+ createLifecycle(spaceLocalName, lifecycleLocalName, lifecycleName),
+ createProjectGroup(spaceLocalName, projectGroupLocalName, projectGroupName),
+ createProject(spaceLocalName, projectLocalName, projectName, lifecycleLocalName, projectGroupLocalName),
+ localName, freezeName, start, end, projectScopes)
+}
+
+func testDeploymentFreezeExists(prefix string) resource.TestCheckFunc {
+ return func(s *terraform.State) error {
+ freezeId := s.RootModule().Resources[prefix].Primary.ID
+ if _, err := deploymentfreezes.GetById(octoClient, freezeId); err != nil {
+ return err
+ }
+
+ return nil
+ }
+}
+
+func testDeploymentFreezeCheckDestroy(s *terraform.State) error {
+ for _, rs := range s.RootModule().Resources {
+ if rs.Type != "octopusdeploy_deployment_freeze" {
+ continue
+ }
+
+ feed, err := deploymentfreezes.GetById(octoClient, rs.Primary.ID)
+ if err == nil && feed != nil {
+ return fmt.Errorf("Deployment Freeze (%s) still exists", rs.Primary.ID)
+ }
+ }
+
+ return nil
+}
diff --git a/octopusdeploy_framework/resource_generic_oidc_account.go b/octopusdeploy_framework/resource_generic_oidc_account.go
new file mode 100644
index 000000000..5de260ca5
--- /dev/null
+++ b/octopusdeploy_framework/resource_generic_oidc_account.go
@@ -0,0 +1,179 @@
+package octopusdeploy_framework
+
+import (
+ "context"
+ "fmt"
+ "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/accounts"
+ "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core"
+ "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal/errors"
+ "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/schemas"
+ "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+)
+
+var _ resource.Resource = &genericOidcAccountResource{}
+var _ resource.ResourceWithImportState = &genericOidcAccountResource{}
+
+type genericOidcAccountResource struct {
+ *Config
+}
+
+func NewGenericOidcResource() resource.Resource {
+ return &genericOidcAccountResource{}
+}
+
+func (r *genericOidcAccountResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = util.GetTypeName("generic_oidc_account")
+}
+
+func (r *genericOidcAccountResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schemas.GenericOidcAccountSchema{}.GetResourceSchema()
+}
+
+func (r *genericOidcAccountResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ r.Config = ResourceConfiguration(req, resp)
+}
+func (r *genericOidcAccountResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan schemas.GenericOidcAccountResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ tflog.Debug(ctx, "Creating generic oidc account", map[string]interface{}{
+ "name": plan.Name.ValueString(),
+ })
+
+ account := expandGenericOidcAccountResource(ctx, plan)
+ createdAccount, err := accounts.Add(r.Client, account)
+ if err != nil {
+ resp.Diagnostics.AddError("Error creating generic oidc account", err.Error())
+ return
+ }
+
+ state := flattenGenericOidcAccountResource(ctx, createdAccount.(*accounts.GenericOIDCAccount), plan)
+ resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
+}
+
+func (r *genericOidcAccountResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state schemas.GenericOidcAccountResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ account, err := accounts.GetByID(r.Client, state.SpaceID.ValueString(), state.ID.ValueString())
+ if err != nil {
+ if err := errors.ProcessApiErrorV2(ctx, resp, state, err, "genericOidcAccountResource"); err != nil {
+ resp.Diagnostics.AddError("unable to load generic oidc account", err.Error())
+ }
+ return
+ }
+
+ newState := flattenGenericOidcAccountResource(ctx, account.(*accounts.GenericOIDCAccount), state)
+ resp.Diagnostics.Append(resp.State.Set(ctx, newState)...)
+}
+
+func (r *genericOidcAccountResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan schemas.GenericOidcAccountResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ account := expandGenericOidcAccountResource(ctx, plan)
+ updatedAccount, err := accounts.Update(r.Client, account)
+ if err != nil {
+ resp.Diagnostics.AddError("Error updating generic oidc account", err.Error())
+ return
+ }
+
+ state := flattenGenericOidcAccountResource(ctx, updatedAccount.(*accounts.GenericOIDCAccount), plan)
+ resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
+}
+
+func (r *genericOidcAccountResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state schemas.GenericOidcAccountResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ err := accounts.DeleteByID(r.Client, state.SpaceID.ValueString(), state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Error deleting generic oidc account", err.Error())
+ return
+ }
+}
+
+func (r *genericOidcAccountResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ accountID := req.ID
+
+ account, err := accounts.GetByID(r.Client, r.Client.GetSpaceID(), accountID)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error reading generic oidc account",
+ fmt.Sprintf("Unable to read generic oidc account with ID %s: %s", accountID, err.Error()),
+ )
+ return
+ }
+
+ genericOidcAccount, ok := account.(*accounts.GenericOIDCAccount)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected account type",
+ fmt.Sprintf("Expected generic oidc account, got: %T", account),
+ )
+ return
+ }
+
+ state := schemas.GenericOidcAccountResourceModel{
+ SpaceID: types.StringValue(genericOidcAccount.GetSpaceID()),
+ Name: types.StringValue(genericOidcAccount.GetName()),
+ Description: types.StringValue(genericOidcAccount.GetDescription()),
+ TenantedDeploymentParticipation: types.StringValue(string(genericOidcAccount.GetTenantedDeploymentMode())),
+ Environments: flattenStringList(genericOidcAccount.GetEnvironmentIDs(), types.ListNull(types.StringType)),
+ Tenants: flattenStringList(genericOidcAccount.GetTenantIDs(), types.ListNull(types.StringType)),
+ TenantTags: flattenStringList(genericOidcAccount.TenantTags, types.ListNull(types.StringType)),
+ ExecutionSubjectKeys: flattenStringList(genericOidcAccount.DeploymentSubjectKeys, types.ListNull(types.StringType)),
+ Audience: types.StringValue(genericOidcAccount.Audience),
+ }
+ state.ID = types.StringValue(genericOidcAccount.ID)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func expandGenericOidcAccountResource(ctx context.Context, model schemas.GenericOidcAccountResourceModel) *accounts.GenericOIDCAccount {
+ account, _ := accounts.NewGenericOIDCAccount(model.Name.ValueString())
+
+ account.SetID(model.ID.ValueString())
+ account.SetDescription(model.Description.ValueString())
+ account.SetSpaceID(model.SpaceID.ValueString())
+ account.SetEnvironmentIDs(util.ExpandStringList(model.Environments))
+ account.SetTenantedDeploymentMode(core.TenantedDeploymentMode(model.TenantedDeploymentParticipation.ValueString()))
+ account.SetTenantIDs(util.ExpandStringList(model.Tenants))
+ account.SetTenantTags(util.ExpandStringList(model.TenantTags))
+ account.DeploymentSubjectKeys = util.ExpandStringList(model.ExecutionSubjectKeys)
+ account.Audience = model.Audience.ValueString()
+
+ return account
+}
+
+func flattenGenericOidcAccountResource(ctx context.Context, account *accounts.GenericOIDCAccount, model schemas.GenericOidcAccountResourceModel) schemas.GenericOidcAccountResourceModel {
+ model.ID = types.StringValue(account.GetID())
+ model.SpaceID = types.StringValue(account.GetSpaceID())
+ model.Name = types.StringValue(account.GetName())
+ model.Description = types.StringValue(account.GetDescription())
+ model.TenantedDeploymentParticipation = types.StringValue(string(account.GetTenantedDeploymentMode()))
+
+ model.Environments = util.FlattenStringList(account.GetEnvironmentIDs())
+ model.Tenants = util.FlattenStringList(account.GetTenantIDs())
+ model.TenantTags = util.FlattenStringList(account.TenantTags)
+
+ model.ExecutionSubjectKeys = util.FlattenStringList(account.DeploymentSubjectKeys)
+ model.Audience = types.StringValue(account.Audience)
+
+ return model
+}
diff --git a/octopusdeploy_framework/resource_generic_oidc_account_test.go b/octopusdeploy_framework/resource_generic_oidc_account_test.go
new file mode 100644
index 000000000..69e12c053
--- /dev/null
+++ b/octopusdeploy_framework/resource_generic_oidc_account_test.go
@@ -0,0 +1,78 @@
+package octopusdeploy_framework
+
+import (
+ "fmt"
+ "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core"
+ "github.com/hashicorp/terraform-plugin-testing/helper/acctest"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "strings"
+ "testing"
+)
+
+func TestAccGenericOidcAccountBasic(t *testing.T) {
+ localName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)
+ resourceName := "octopusdeploy_generic_oidc_account." + localName
+
+ description := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)
+ name := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)
+ tenantedDeploymentParticipation := core.TenantedDeploymentModeTenantedOrUntenanted
+
+ executionKeys := []string{"space"}
+ audience := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)
+ updatedAudience := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)
+
+ config := testGenericOidcAccountBasic(localName, name, description, tenantedDeploymentParticipation, executionKeys, audience)
+ updateConfig := testGenericOidcAccountBasic(localName, name, description, tenantedDeploymentParticipation, executionKeys, updatedAudience)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: ProtoV6ProviderFactories(),
+ Steps: []resource.TestStep{
+ {
+ Config: config,
+ Check: resource.ComposeTestCheckFunc(
+ testAccountExists(resourceName),
+ resource.TestCheckResourceAttr(resourceName, "description", description),
+ resource.TestCheckResourceAttrSet(resourceName, "id"),
+ resource.TestCheckResourceAttr(resourceName, "name", name),
+ resource.TestCheckResourceAttrSet(resourceName, "space_id"),
+ resource.TestCheckResourceAttr(resourceName, "tenanted_deployment_participation", string(tenantedDeploymentParticipation)),
+ resource.TestCheckResourceAttr(resourceName, "execution_subject_keys.0", executionKeys[0]),
+ resource.TestCheckResourceAttr(resourceName, "audience", audience),
+ ),
+ ResourceName: resourceName,
+ },
+ {
+ Config: updateConfig,
+ Check: resource.ComposeTestCheckFunc(
+ testAccountExists(resourceName),
+ resource.TestCheckResourceAttr(resourceName, "description", description),
+ resource.TestCheckResourceAttrSet(resourceName, "id"),
+ resource.TestCheckResourceAttr(resourceName, "name", name),
+ resource.TestCheckResourceAttrSet(resourceName, "space_id"),
+ resource.TestCheckResourceAttr(resourceName, "tenanted_deployment_participation", string(tenantedDeploymentParticipation)),
+ resource.TestCheckResourceAttr(resourceName, "execution_subject_keys.0", executionKeys[0]),
+ resource.TestCheckResourceAttr(resourceName, "audience", updatedAudience),
+ ),
+ ResourceName: resourceName,
+ },
+ },
+ })
+}
+
+func testGenericOidcAccountBasic(localName string, name string, description string, tenantedDeploymentParticipation core.TenantedDeploymentMode, execution_subject_keys []string, audience string) string {
+
+ execKeysStr := fmt.Sprintf(`["%s"]`, strings.Join(execution_subject_keys, `", "`))
+
+ return fmt.Sprintf(`resource "octopusdeploy_generic_oidc_account" "%s" {
+ description = "%s"
+ name = "%s"
+ tenanted_deployment_participation = "%s"
+ execution_subject_keys = %s
+ audience = "%s"
+ }
+
+ data "octopusdeploy_accounts" "test" {
+ ids = [octopusdeploy_generic_oidc_account.%s.id]
+ }`, localName, description, name, tenantedDeploymentParticipation, execKeysStr, audience, localName)
+}
diff --git a/octopusdeploy_framework/resource_project_flatten.go b/octopusdeploy_framework/resource_project_flatten.go
index 6e411b08d..834d5caf9 100644
--- a/octopusdeploy_framework/resource_project_flatten.go
+++ b/octopusdeploy_framework/resource_project_flatten.go
@@ -260,7 +260,7 @@ func flattenTemplates(templates []actiontemplates.ActionTemplateParameter) types
"default_value": util.StringOrNull(template.DefaultValue.Value),
"display_settings": types.MapValueMust(
types.StringType,
- convertMapStringToMapAttrValue(template.DisplaySettings),
+ util.ConvertMapStringToMapAttrValue(template.DisplaySettings),
),
})
@@ -310,14 +310,6 @@ func flattenReleaseCreationStrategy(strategy *projects.ReleaseCreationStrategy)
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()})
diff --git a/octopusdeploy_framework/resource_project_versioning_strategy.go b/octopusdeploy_framework/resource_project_versioning_strategy.go
new file mode 100644
index 000000000..bf587d864
--- /dev/null
+++ b/octopusdeploy_framework/resource_project_versioning_strategy.go
@@ -0,0 +1,190 @@
+package octopusdeploy_framework
+
+import (
+ "context"
+ "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core"
+ "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/packages"
+ "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"
+ "log"
+ "net/http"
+)
+
+var _ resource.Resource = &projectVersioningStrategyResource{}
+
+type projectVersioningStrategyResource struct {
+ *Config
+}
+
+func NewProjectVersioningStrategyResource() resource.Resource {
+ return &projectVersioningStrategyResource{}
+}
+
+func (r *projectVersioningStrategyResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = util.GetTypeName(schemas.ProjectVersioningStrategyResourceName)
+}
+
+func (r *projectVersioningStrategyResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schemas.ProjectVersioningStrategySchema{}.GetResourceSchema()
+}
+
+func (r *projectVersioningStrategyResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ r.Config = ResourceConfiguration(req, resp)
+}
+
+func (r *projectVersioningStrategyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan schemas.ProjectVersioningStrategyModel
+ diags := req.Plan.Get(ctx, &plan)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ project, err := projects.GetByID(r.Client, plan.SpaceID.ValueString(), plan.ProjectID.ValueString())
+ if err != nil {
+ if apiError, ok := err.(*core.APIError); ok {
+ if apiError.StatusCode == http.StatusNotFound {
+ log.Printf("[INFO] associated project (%s) not found; deleting version strategy from state", plan.ProjectID.ValueString())
+ resp.State.RemoveResource(ctx)
+ }
+ } else {
+ resp.Diagnostics.AddError("Failed to read associated project", err.Error())
+ }
+ return
+ }
+ versioningStrategy := mapStateToProjectVersioningStrategy(&plan)
+ project.VersioningStrategy = versioningStrategy
+
+ _, err = projects.Update(r.Client, project)
+ if err != nil {
+ resp.Diagnostics.AddError("Error updating associated project", err.Error())
+ return
+ }
+
+ updatedProject, err := projects.GetByID(r.Client, plan.SpaceID.ValueString(), plan.ProjectID.ValueString())
+ if err != nil {
+ if apiError, ok := err.(*core.APIError); ok {
+ if apiError.StatusCode == http.StatusNotFound {
+ log.Printf("[INFO] associated project (%s) not found; deleting version strategy from state", plan.ProjectID.ValueString())
+ resp.State.RemoveResource(ctx)
+ }
+ } else {
+ resp.Diagnostics.AddError("Failed to read associated project", err.Error())
+ }
+ return
+ }
+
+ mapProjectVersioningStrategyToState(updatedProject.VersioningStrategy, &plan)
+ resp.Diagnostics.Append(resp.State.Set(ctx, plan)...)
+}
+
+func (r *projectVersioningStrategyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state schemas.ProjectVersioningStrategyModel
+ diags := req.State.Get(ctx, &state)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ project, err := projects.GetByID(r.Client, state.SpaceID.ValueString(), state.ProjectID.ValueString())
+ if err != nil {
+ if apiError, ok := err.(*core.APIError); ok {
+ if apiError.StatusCode == http.StatusNotFound {
+ log.Printf("[INFO] associated project (%s) not found; deleting version strategy from state", state.ProjectID.ValueString())
+ resp.State.RemoveResource(ctx)
+ }
+ } else {
+ resp.Diagnostics.AddError("Failed to read associated project", err.Error())
+ }
+ return
+ }
+ mapProjectVersioningStrategyToState(project.VersioningStrategy, &state)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
+}
+
+func (r *projectVersioningStrategyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan schemas.ProjectVersioningStrategyModel
+ 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.ProjectID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Error retrieving associated project", err.Error())
+ return
+ }
+
+ versioningStrategy := mapStateToProjectVersioningStrategy(&plan)
+ existingProject.VersioningStrategy = versioningStrategy
+
+ _, err = projects.Update(r.Client, existingProject)
+ if err != nil {
+ resp.Diagnostics.AddError("Error updating associated project", err.Error())
+ return
+ }
+
+ updatedProject, err := projects.GetByID(r.Client, plan.SpaceID.ValueString(), plan.ProjectID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Error retrieving associated project", err.Error())
+ return
+ }
+
+ mapProjectVersioningStrategyToState(updatedProject.VersioningStrategy, &plan)
+ resp.Diagnostics.Append(resp.State.Set(ctx, plan)...)
+}
+
+func (r *projectVersioningStrategyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state schemas.ProjectVersioningStrategyModel
+ diags := req.State.Get(ctx, &state)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ project, err := projects.GetByID(r.Client, state.SpaceID.ValueString(), state.ProjectID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Error retrieving project", err.Error())
+ return
+ }
+
+ project.VersioningStrategy = &projects.VersioningStrategy{}
+ _, err = projects.Update(r.Client, project)
+ if err != nil {
+ resp.Diagnostics.AddError("Error updating project to remove versioning strategy", err.Error())
+ return
+ }
+
+ resp.State.RemoveResource(ctx)
+}
+
+func mapStateToProjectVersioningStrategy(state *schemas.ProjectVersioningStrategyModel) *projects.VersioningStrategy {
+ var donorPackageStepID *string
+ donorPackageStepIDString := state.DonorPackageStepID.ValueString()
+ if donorPackageStepIDString != "" {
+ donorPackageStepID = &donorPackageStepIDString
+ }
+
+ return &projects.VersioningStrategy{
+ Template: state.Template.ValueString(),
+ DonorPackageStepID: donorPackageStepID,
+ DonorPackage: &packages.DeploymentActionPackage{
+ DeploymentAction: state.DonorPackage.DeploymentAction.ValueString(),
+ PackageReference: state.DonorPackage.PackageReference.ValueString(),
+ },
+ }
+}
+
+func mapProjectVersioningStrategyToState(versioningStrategy *projects.VersioningStrategy, state *schemas.ProjectVersioningStrategyModel) {
+ if versioningStrategy.DonorPackageStepID != nil {
+ state.DonorPackageStepID = types.StringValue(*versioningStrategy.DonorPackageStepID)
+ }
+ state.Template = types.StringValue(versioningStrategy.Template)
+ state.DonorPackage.PackageReference = types.StringValue(versioningStrategy.DonorPackage.PackageReference)
+ state.DonorPackage.DeploymentAction = types.StringValue(versioningStrategy.DonorPackage.DeploymentAction)
+}
diff --git a/octopusdeploy_framework/resource_step_template.go b/octopusdeploy_framework/resource_step_template.go
index 01bf45428..009805903 100644
--- a/octopusdeploy_framework/resource_step_template.go
+++ b/octopusdeploy_framework/resource_step_template.go
@@ -210,19 +210,7 @@ func mapStepTemplateResourceModelToActionTemplate(ctx context.Context, data sche
at.Packages = make([]packages.PackageReference, len(pkgs))
if len(pkgs) > 0 {
for i, val := range pkgs {
- pkgProps := make(map[string]string, len(val.Properties.Attributes()))
- for key, prop := range val.Properties.Attributes() {
- if prop.Type(ctx) == types.StringType {
- pkgProps[key] = prop.(types.String).ValueString()
- } else {
- // We should not get this error unless we add a field to package properties in the schema that is not a string
- resp.AddError("Unexpected value type in package properties.",
- fmt.Sprintf("Expected [%s] to have value of string but got [%s].", key, prop.String()))
- }
- }
- if resp.HasError() {
- return at, resp
- }
+ pkgProps := convertAttributeStepTemplatePackageProperty(val.Properties.Attributes())
pkgRef := packages.PackageReference{
AcquisitionLocation: val.AcquisitionLocation.ValueString(),
FeedID: val.FeedID.ValueString(),
@@ -335,28 +323,28 @@ func convertStepTemplatePackagePropertyAttribute(atpp map[string]string) (types.
diags := diag.Diagnostics{}
// We need to manually convert the string map to ensure all fields are set.
- if extract, ok := atpp["extract"]; ok {
+ if extract, ok := atpp["Extract"]; ok {
prop["extract"] = types.StringValue(extract)
} else {
diags.AddWarning("Package property missing value.", "extract value missing from package property")
prop["extract"] = types.StringNull()
}
- if purpose, ok := atpp["purpose"]; ok {
+ if purpose, ok := atpp["Purpose"]; ok {
prop["purpose"] = types.StringValue(purpose)
} else {
diags.AddWarning("Package property missing value.", "purpose value missing from package property")
prop["purpose"] = types.StringNull()
}
- if purpose, ok := atpp["package_parameter_name"]; ok {
+ if purpose, ok := atpp["PackageParameterName"]; ok {
prop["package_parameter_name"] = types.StringValue(purpose)
} else {
diags.AddWarning("Package property missing value.", "package_parameter_name value missing from package property")
prop["package_parameter_name"] = types.StringNull()
}
- if selectionMode, ok := atpp["selection_mode"]; ok {
+ if selectionMode, ok := atpp["SelectionMode"]; ok {
prop["selection_mode"] = types.StringValue(selectionMode)
} else {
diags.AddWarning("Package property missing value.", "selection_mode value missing from package property")
@@ -370,3 +358,24 @@ func convertStepTemplatePackagePropertyAttribute(atpp map[string]string) (types.
}
return propMap, diags
}
+
+func convertAttributeStepTemplatePackageProperty(prop map[string]attr.Value) map[string]string {
+ atpp := make(map[string]string)
+
+ if extract, ok := prop["extract"]; ok {
+ atpp["Extract"] = extract.(types.String).ValueString()
+ }
+
+ if purpose, ok := prop["purpose"]; ok {
+ atpp["Purpose"] = purpose.(types.String).ValueString()
+ }
+
+ if purpose, ok := prop["package_parameter_name"]; ok {
+ atpp["PackageParameterName"] = purpose.(types.String).ValueString()
+ }
+
+ if selectionMode, ok := prop["selection_mode"]; ok {
+ atpp["SelectionMode"] = selectionMode.(types.String).ValueString()
+ }
+ return atpp
+}
diff --git a/octopusdeploy_framework/resource_tenant_project.go b/octopusdeploy_framework/resource_tenant_project.go
index a5f68eac7..3ee22ee5e 100644
--- a/octopusdeploy_framework/resource_tenant_project.go
+++ b/octopusdeploy_framework/resource_tenant_project.go
@@ -2,9 +2,10 @@ package octopusdeploy_framework
import (
"context"
+ "errors"
"fmt"
+ internalErrors "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal/errors"
"net/http"
- "strings"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tenants"
@@ -66,9 +67,10 @@ func (t *tenantProjectResource) Create(ctx context.Context, req resource.CreateR
_, err = tenants.Update(t.Client, tenant)
if err != nil {
resp.Diagnostics.AddError("cannot update tenant environment", err.Error())
+ return
}
- plan.ID = types.StringValue(schemas.BuildTenantProjectID(spaceId, plan.TenantID.ValueString(), plan.ProjectID.ValueString()))
+ plan.ID = types.StringValue(util.BuildCompositeId(spaceId, plan.TenantID.ValueString(), plan.ProjectID.ValueString()))
plan.SpaceID = types.StringValue(spaceId)
plan.EnvironmentIDs = util.FlattenStringList(tenant.ProjectEnvironments[plan.ProjectID.ValueString()])
@@ -83,18 +85,17 @@ func (t *tenantProjectResource) Read(ctx context.Context, req resource.ReadReque
return
}
- bits := strings.Split(data.ID.ValueString(), ":")
+ bits := util.SplitCompositeId(data.ID.ValueString())
spaceID := bits[0]
tenantID := bits[1]
projectID := bits[2]
tenant, err := tenants.GetByID(t.Client, spaceID, tenantID)
if err != nil {
- apiError := err.(*core.APIError)
- if apiError.StatusCode != http.StatusNotFound {
+ if err := internalErrors.ProcessApiErrorV2(ctx, resp, data, err, "tenant"); err != nil {
resp.Diagnostics.AddError("unable to load tenant", err.Error())
- return
}
+ return
}
data.EnvironmentIDs = util.FlattenStringList(tenant.ProjectEnvironments[projectID])
@@ -132,7 +133,7 @@ func (t *tenantProjectResource) Update(ctx context.Context, req resource.UpdateR
resp.Diagnostics.AddError("cannot update tenant environment", err.Error())
}
- plan.ID = types.StringValue(schemas.BuildTenantProjectID(spaceId, plan.TenantID.ValueString(), plan.ProjectID.ValueString()))
+ plan.ID = types.StringValue(util.BuildCompositeId(spaceId, plan.TenantID.ValueString(), plan.ProjectID.ValueString()))
plan.SpaceID = types.StringValue(spaceId)
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
@@ -162,10 +163,12 @@ func (t *tenantProjectResource) Delete(ctx context.Context, req resource.DeleteR
tenant, err := tenants.GetByID(t.Client, spaceId, data.TenantID.ValueString())
if err != nil {
- apiError := err.(*core.APIError)
- if apiError.StatusCode == http.StatusNotFound {
- tflog.Info(ctx, fmt.Sprintf("tenant (%s) no longer exists", data.TenantID.ValueString()))
- return
+ var apiError *core.APIError
+ if errors.As(err, &apiError) {
+ if apiError.StatusCode == http.StatusNotFound {
+ tflog.Info(ctx, fmt.Sprintf("tenant (%s) no longer exists", data.TenantID.ValueString()))
+ return
+ }
} else {
resp.Diagnostics.AddError("cannot load tenant", err.Error())
return
diff --git a/octopusdeploy_framework/resource_tenant_project_variable.go b/octopusdeploy_framework/resource_tenant_project_variable.go
index 39a8957ce..6057aaa6d 100644
--- a/octopusdeploy_framework/resource_tenant_project_variable.go
+++ b/octopusdeploy_framework/resource_tenant_project_variable.go
@@ -125,6 +125,11 @@ func (t *tenantProjectVariableResource) Read(ctx context.Context, req resource.R
return
}
+ if !checkIfTemplateExists(tenantVariables, state) {
+ // The template no longer exists, so the variable can no longer exist either
+ return
+ }
+
isSensitive, err := checkIfVariableIsSensitive(tenantVariables, state)
if err != nil {
resp.Diagnostics.AddError("Error checking if variable is sensitive", err.Error())
@@ -251,6 +256,17 @@ func (t *tenantProjectVariableResource) ImportState(ctx context.Context, req res
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("template_id"), idParts[3])...)
}
+func checkIfTemplateExists(tenantVariables *variables.TenantVariables, plan tenantProjectVariableResourceModel) bool {
+ if projectVariable, ok := tenantVariables.ProjectVariables[plan.ProjectID.ValueString()]; ok {
+ for _, template := range projectVariable.Templates {
+ if template.GetID() == plan.TemplateID.ValueString() {
+ return true
+ }
+ }
+ }
+ return false
+}
+
func checkIfVariableIsSensitive(tenantVariables *variables.TenantVariables, plan tenantProjectVariableResourceModel) (bool, error) {
if projectVariable, ok := tenantVariables.ProjectVariables[plan.ProjectID.ValueString()]; ok {
for _, template := range projectVariable.Templates {
diff --git a/octopusdeploy_framework/resource_variable.go b/octopusdeploy_framework/resource_variable.go
index d28763803..9010ced7b 100644
--- a/octopusdeploy_framework/resource_variable.go
+++ b/octopusdeploy_framework/resource_variable.go
@@ -267,13 +267,14 @@ func validateVariable(variableSet *variables.VariableSet, newVariable *variables
for _, v := range variableSet.Variables {
if v.Name == newVariable.Name && v.Type == newVariable.Type && (v.IsSensitive || v.Value == newVariable.Value) && v.Description == newVariable.Description && v.IsSensitive == newVariable.IsSensitive {
scopeMatches, _, err := variables.MatchesScope(v.Scope, &newVariable.Scope)
- if err != nil || !scopeMatches {
+ if err != nil {
return err
}
- if scopeMatches {
- newVariable.ID = v.GetID()
- return nil
+ if !scopeMatches {
+ continue
}
+ newVariable.ID = v.GetID()
+ return nil
}
}
diff --git a/octopusdeploy_framework/schemas/deployment_freeze.go b/octopusdeploy_framework/schemas/deployment_freeze.go
new file mode 100644
index 000000000..6baa8dd0d
--- /dev/null
+++ b/octopusdeploy_framework/schemas/deployment_freeze.go
@@ -0,0 +1,77 @@
+package schemas
+
+import (
+ 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"
+)
+
+type DeploymentFreezeSchema struct{}
+
+func (d DeploymentFreezeSchema) GetResourceSchema() resourceSchema.Schema {
+ return resourceSchema.Schema{
+ Attributes: map[string]resourceSchema.Attribute{
+ "id": GetIdResourceSchema(),
+ "name": GetNameResourceSchema(true),
+ "start": GetDateTimeResourceSchema("The start time of the freeze, must be RFC3339 format", true),
+ "end": GetDateTimeResourceSchema("The end time of the freeze, must be RFC3339 format", true),
+ },
+ }
+}
+
+func (d DeploymentFreezeSchema) GetDatasourceSchema() datasourceSchema.Schema {
+ return datasourceSchema.Schema{
+ Description: "Provides information about deployment freezes",
+ Attributes: map[string]datasourceSchema.Attribute{
+ "id": GetIdDatasourceSchema(true),
+ "ids": GetQueryIDsDatasourceSchema(),
+ "skip": GetQuerySkipDatasourceSchema(),
+ "take": GetQueryTakeDatasourceSchema(),
+ "partial_name": GetQueryPartialNameDatasourceSchema(),
+ "project_ids": datasourceSchema.ListAttribute{
+ Description: "A filter to search by a list of project IDs",
+ ElementType: types.StringType,
+ Optional: true,
+ },
+ "environment_ids": datasourceSchema.ListAttribute{
+ Description: "A filter to search by a list of environment IDs",
+ ElementType: types.StringType,
+ Optional: true,
+ },
+ "include_complete": GetBooleanDatasourceAttribute("Include deployment freezes that completed, default is true", true),
+ "status": datasourceSchema.StringAttribute{
+ Description: "Filter by the status of the deployment freeze, value values are Expired, Active, Scheduled (case-insensitive)",
+ Optional: true,
+ },
+ "deployment_freezes": datasourceSchema.ListNestedAttribute{
+ NestedObject: datasourceSchema.NestedAttributeObject{
+ Attributes: map[string]datasourceSchema.Attribute{
+ "id": GetIdDatasourceSchema(true),
+ "name": GetReadonlyNameDatasourceSchema(),
+ "start": datasourceSchema.StringAttribute{
+ Description: "The start time of the freeze",
+ Optional: false,
+ Computed: true,
+ },
+ "end": datasourceSchema.StringAttribute{
+ Description: "The end time of the freeze",
+ Optional: false,
+ Computed: true,
+ },
+ "project_environment_scope": datasourceSchema.MapAttribute{
+ ElementType: types.ListType{ElemType: types.StringType},
+ Description: "The project environment scope of the deployment freeze",
+ Optional: false,
+ Computed: true,
+ },
+ },
+ },
+ Optional: false,
+ Computed: true,
+ },
+ },
+ }
+
+}
+
+var _ EntitySchema = &DeploymentFreezeSchema{}
diff --git a/octopusdeploy_framework/schemas/deployment_freeze_project.go b/octopusdeploy_framework/schemas/deployment_freeze_project.go
new file mode 100644
index 000000000..930303376
--- /dev/null
+++ b/octopusdeploy_framework/schemas/deployment_freeze_project.go
@@ -0,0 +1,57 @@
+package schemas
+
+import (
+ "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
+ 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"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+type DeploymentFreezeProjectSchema struct{}
+
+type DeploymentFreezeProjectResourceModel struct {
+ DeploymentFreezeID types.String `tfsdk:"deploymentfreeze_id"`
+ ProjectID types.String `tfsdk:"project_id"`
+ EnvironmentIDs types.List `tfsdk:"environment_ids"`
+ ResourceModel
+}
+
+func (d DeploymentFreezeProjectSchema) GetResourceSchema() resourceSchema.Schema {
+ return resourceSchema.Schema{
+ Attributes: map[string]resourceSchema.Attribute{
+ "id": GetIdResourceSchema(),
+ "deploymentfreeze_id": resourceSchema.StringAttribute{
+ Description: "The deployment freeze ID associated with this freeze scope.",
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ "project_id": resourceSchema.StringAttribute{
+ Description: "The project ID associated with this freeze scope.",
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ "environment_ids": resourceSchema.ListAttribute{
+ Description: "The environment IDs associated with this project deployment freeze scope.",
+ ElementType: types.StringType,
+ Optional: true,
+ Validators: []validator.List{
+ listvalidator.SizeAtLeast(1),
+ },
+ },
+ },
+ }
+}
+
+func (d DeploymentFreezeProjectSchema) GetDatasourceSchema() datasourceSchema.Schema {
+ //TODO implement me
+ panic("implement me")
+}
+
+var _ EntitySchema = DeploymentFreezeProjectSchema{}
diff --git a/octopusdeploy_framework/schemas/generic_oidc_account.go b/octopusdeploy_framework/schemas/generic_oidc_account.go
new file mode 100644
index 000000000..a68f46f77
--- /dev/null
+++ b/octopusdeploy_framework/schemas/generic_oidc_account.go
@@ -0,0 +1,86 @@
+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/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+type GenericOidcAccountSchema struct{}
+
+var _ EntitySchema = GenericOidcAccountSchema{}
+
+func (a GenericOidcAccountSchema) GetDatasourceSchema() datasourceSchema.Schema {
+ return datasourceSchema.Schema{}
+}
+
+func (a GenericOidcAccountSchema) GetResourceSchema() resourceSchema.Schema {
+ return resourceSchema.Schema{
+ Description: "This resource manages a Generic OIDC Account in Octopus Deploy.",
+ Attributes: map[string]resourceSchema.Attribute{
+ "description": util.ResourceString().
+ Optional().
+ Computed().
+ PlanModifiers(stringplanmodifier.UseStateForUnknown()).
+ Default("").
+ Description("The description of this generic oidc account.").
+ Build(),
+ "environments": util.ResourceList(types.StringType).
+ Optional().
+ Computed().
+ Description("A list of environment IDs associated with this resource.").
+ Build(),
+ "id": GetIdResourceSchema(),
+ "name": util.ResourceString().
+ Required().
+ Description("The name of the generic oidc account.").
+ Build(),
+ "space_id": util.ResourceString().
+ Optional().
+ Computed().
+ PlanModifiers(stringplanmodifier.UseStateForUnknown()).
+ Description("The space ID associated with this resource.").
+ 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(),
+ "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(),
+ "execution_subject_keys": util.ResourceList(types.StringType).
+ Optional().
+ Description("Keys to include in a deployment or runbook. Valid options are `space`, `environment`, `project`, `tenant`, `runbook`, `account`, `type`.").
+ Build(),
+ "audience": util.ResourceString().
+ Optional().
+ Description("The audience associated with this resource.").
+ Build(),
+ },
+ }
+}
+
+type GenericOidcAccountResourceModel struct {
+ Description types.String `tfsdk:"description"`
+ Environments types.List `tfsdk:"environments"`
+ Name types.String `tfsdk:"name"`
+ SpaceID types.String `tfsdk:"space_id"`
+ TenantedDeploymentParticipation types.String `tfsdk:"tenanted_deployment_participation"`
+ Tenants types.List `tfsdk:"tenants"`
+ TenantTags types.List `tfsdk:"tenant_tags"`
+ ExecutionSubjectKeys types.List `tfsdk:"execution_subject_keys"`
+ Audience types.String `tfsdk:"audience"`
+
+ ResourceModel
+}
diff --git a/octopusdeploy_framework/schemas/project.go b/octopusdeploy_framework/schemas/project.go
index ac7898ab2..93d7b32a9 100644
--- a/octopusdeploy_framework/schemas/project.go
+++ b/octopusdeploy_framework/schemas/project.go
@@ -147,8 +147,8 @@ func (p ProjectSchema) GetResourceSchema() resourceSchema.Schema {
},
},
"versioning_strategy": resourceSchema.ListNestedBlock{
+ DeprecationMessage: "versioning_strategy is deprecated in favor of resource project_versioning strategy",
NestedObject: resourceSchema.NestedBlockObject{
-
Attributes: map[string]resourceSchema.Attribute{
"donor_package_step_id": util.ResourceString().Optional().Build(),
"template": util.ResourceString().Optional().Computed().Build(),
diff --git a/octopusdeploy_framework/schemas/project_versioning_strategy.go b/octopusdeploy_framework/schemas/project_versioning_strategy.go
new file mode 100644
index 000000000..c581f8037
--- /dev/null
+++ b/octopusdeploy_framework/schemas/project_versioning_strategy.go
@@ -0,0 +1,71 @@
+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/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+type ProjectVersioningStrategySchema struct{}
+
+var _ EntitySchema = ProjectVersioningStrategySchema{}
+
+const ProjectVersioningStrategyResourceName = "project_versioning_strategy"
+
+func (p ProjectVersioningStrategySchema) GetResourceSchema() resourceSchema.Schema {
+ return resourceSchema.Schema{
+ Attributes: map[string]resourceSchema.Attribute{
+ "project_id": util.ResourceString().
+ Description("The associated project ID.").
+ PlanModifiers(stringplanmodifier.RequiresReplace()).
+ Required().
+ Build(),
+ "space_id": util.ResourceString().
+ Description("Space ID of the associated project.").
+ Required().
+ Build(),
+ "donor_package_step_id": util.ResourceString().
+ Description("The associated donor package step ID.").
+ Optional().
+ Build(),
+ "template": util.ResourceString().
+ Optional().
+ Computed().
+ Build(),
+ "donor_package": resourceSchema.SingleNestedAttribute{
+ Required: true,
+ Description: "Donor Packages.",
+ Attributes: map[string]resourceSchema.Attribute{
+ "deployment_action": util.ResourceString().
+ Description("Deployment action.").
+ Optional().
+ Build(),
+ "package_reference": util.ResourceString().
+ Description("Package reference.").
+ Optional().
+ Build(),
+ },
+ },
+ },
+ }
+}
+
+func (p ProjectVersioningStrategySchema) GetDatasourceSchema() datasourceSchema.Schema {
+ // no datasource required, returned as part of project datasource
+ return datasourceSchema.Schema{}
+}
+
+type ProjectVersioningStrategyModel struct {
+ ProjectID types.String `tfsdk:"project_id"`
+ SpaceID types.String `tfsdk:"space_id"`
+ DonorPackageStepID types.String `tfsdk:"donor_package_step_id"`
+ Template types.String `tfsdk:"template"`
+ DonorPackage DonorPackageModel `tfsdk:"donor_package"`
+}
+
+type DonorPackageModel struct {
+ DeploymentAction types.String `tfsdk:"deployment_action"`
+ PackageReference types.String `tfsdk:"package_reference"`
+}
diff --git a/octopusdeploy_framework/schemas/schema.go b/octopusdeploy_framework/schemas/schema.go
index ad468ca8d..4e95c45f6 100644
--- a/octopusdeploy_framework/schemas/schema.go
+++ b/octopusdeploy_framework/schemas/schema.go
@@ -2,10 +2,12 @@ package schemas
import (
"fmt"
+ "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
"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"
+ "regexp"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
@@ -432,3 +434,15 @@ func GetSensitiveResourceSchema(description string, isRequired bool) resourceSch
return s
}
+
+func GetDateTimeResourceSchema(description string, isRequired bool) resourceSchema.Attribute {
+ regex := "^((?:(\\d{4}-\\d{2}-\\d{2})T(\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?))(?:Z|[\\+-]\\d{2}:\\d{2})?)"
+ return resourceSchema.StringAttribute{
+ Description: description,
+ Required: isRequired,
+ CustomType: timetypes.RFC3339Type{},
+ Validators: []validator.String{
+ stringvalidator.RegexMatches(regexp.MustCompile(regex), fmt.Sprintf("must match RFC3339 format, %s", regex)),
+ },
+ }
+}
diff --git a/octopusdeploy_framework/schemas/step_template.go b/octopusdeploy_framework/schemas/step_template.go
index 7daa17bb3..fab264a27 100644
--- a/octopusdeploy_framework/schemas/step_template.go
+++ b/octopusdeploy_framework/schemas/step_template.go
@@ -163,6 +163,7 @@ func GetStepTemplateParameterResourceSchema() rs.ListNestedAttribute {
"label": rs.StringAttribute{
Description: "The label shown beside the parameter when presented in the deployment process. Example: `Server name`.",
Optional: true,
+ Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
@@ -213,7 +214,7 @@ func GetStepTemplatePackageResourceSchema() rs.ListNestedAttribute {
Optional: true,
Computed: true,
Validators: []validator.String{
- stringvalidator.RegexMatches(regexp.MustCompile("^(True|Fasle)$"), "Extract must be True or False"),
+ stringvalidator.RegexMatches(regexp.MustCompile("^(True|False)$"), "Extract must be True or False"),
},
},
"package_parameter_name": rs.StringAttribute{
diff --git a/octopusdeploy_framework/schemas/tenant_projects.go b/octopusdeploy_framework/schemas/tenant_projects.go
index c5f2effb6..98a9c4f9b 100644
--- a/octopusdeploy_framework/schemas/tenant_projects.go
+++ b/octopusdeploy_framework/schemas/tenant_projects.go
@@ -1,8 +1,8 @@
package schemas
import (
- "fmt"
"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"
resourceSchema "github.com/hashicorp/terraform-plugin-framework/resource/schema"
@@ -94,10 +94,6 @@ func (t TenantProjectsSchema) GetResourceSchema() resourceSchema.Schema {
}}
}
-func BuildTenantProjectID(spaceID string, tenantID string, projectID string) string {
- return fmt.Sprintf("%s:%s:%s", spaceID, tenantID, projectID)
-}
-
func TenantProjectType() map[string]attr.Type {
return map[string]attr.Type{
"id": types.StringType,
@@ -116,7 +112,7 @@ func MapTenantToTenantProject(tenant *tenants.Tenant, projectID string) attr.Val
environmentIdList, _ := types.ListValue(types.StringType, environmentIDs)
return types.ObjectValueMust(TenantProjectType(), map[string]attr.Value{
- "id": types.StringValue(BuildTenantProjectID(tenant.SpaceID, tenant.ID, projectID)),
+ "id": types.StringValue(util.BuildCompositeId(tenant.SpaceID, tenant.ID, projectID)),
"tenant_id": types.StringValue(tenant.ID),
"project_id": types.StringValue(projectID),
"environment_ids": environmentIdList,
diff --git a/octopusdeploy_framework/util/util.go b/octopusdeploy_framework/util/util.go
index b67394f71..41ac73295 100644
--- a/octopusdeploy_framework/util/util.go
+++ b/octopusdeploy_framework/util/util.go
@@ -6,6 +6,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types"
+ "strings"
)
func GetProviderName() string {
@@ -141,3 +142,34 @@ func GetNumber(val types.Int64) int {
return v
}
+
+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 ConvertMapStringArrayToMapAttrValue(ctx context.Context, m map[string][]string) (map[string]attr.Value, diag.Diagnostics) {
+ var diags diag.Diagnostics
+ result := make(map[string]attr.Value, len(m))
+ for k, v := range m {
+ values := make([]attr.Value, len(v))
+ for i, s := range v {
+ values[i] = types.StringValue(s)
+ }
+ result[k], diags = types.SetValueFrom(ctx, types.StringType, v)
+ }
+
+ return result, diags
+}
+
+const sep = ":"
+
+func BuildCompositeId(keys ...string) string {
+ return strings.Join(keys, sep)
+}
+func SplitCompositeId(id string) []string {
+ return strings.Split(id, sep)
+}
diff --git a/templates/guides/2-provider-configuration.md.tmpl b/templates/guides/2-provider-configuration.md.tmpl
index 158c59df3..94deaf8af 100644
--- a/templates/guides/2-provider-configuration.md.tmpl
+++ b/templates/guides/2-provider-configuration.md.tmpl
@@ -15,7 +15,7 @@ subcategory: "Guides"
terraform {
required_providers {
octopusdeploy = {
- source = OctopusDeployLabs/octopusdeploy
+ source = "OctopusDeployLabs/octopusdeploy"
}
}
}
@@ -39,7 +39,7 @@ The environment variable fallback values that the Terraform Provider search for
terraform {
required_providers {
octopusdeploy = {
- source = OctopusDeployLabs/octopusdeploy
+ source = "OctopusDeployLabs/octopusdeploy"
}
}
}