diff --git a/docs/data-sources/deployment_freezes.md b/docs/data-sources/deployment_freezes.md
index a4a365ba..148cb1b2 100644
--- a/docs/data-sources/deployment_freezes.md
+++ b/docs/data-sources/deployment_freezes.md
@@ -10,7 +10,29 @@ description: |-
Provides information about deployment freezes
+## Example Usage
+```terraform
+data "octopusdeploy_deployment_freezes" "test_freeze" {
+ ids = null
+ partial_name = "Freeze Name"
+ skip = 5
+ take = 100
+}
+
+
+data "octopusdeploy_deployment_freezes" "project_freezes" {
+ project_ids = ["projects-1"]
+ skip = 0
+ take = 5
+}
+
+data "octopusdeploy_deployment_freezes" "tenant_freezes" {
+ tenant_ids = ["tenants-1"]
+ skip = 0
+ take = 10
+}
+```
## Schema
@@ -25,6 +47,7 @@ Provides information about deployment freezes
- `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.
+- `tenant_ids` (List of String) A filter to search by a list of tenant IDs
### Read-Only
@@ -40,6 +63,34 @@ Read-Only:
- `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
+- `recurring_schedule` (Attributes) (see [below for nested schema](#nestedatt--deployment_freezes--recurring_schedule))
- `start` (String) The start time of the freeze
+- `tenant_project_environment_scope` (Attributes List) The tenant project environment scope of the deployment freeze (see [below for nested schema](#nestedatt--deployment_freezes--tenant_project_environment_scope))
+
+
+### Nested Schema for `deployment_freezes.recurring_schedule`
+
+Read-Only:
+
+- `date_of_month` (String) The date of the month for monthly schedules
+- `day_number_of_month` (String) The day number of the month for monthly schedules
+- `day_of_week` (String) The day of the week for monthly schedules
+- `days_of_week` (List of String) List of days of the week for weekly schedules
+- `end_after_occurrences` (Number) Number of occurrences after which the schedule should end
+- `end_on_date` (String) The date when the recurring schedule should end
+- `end_type` (String) When the recurring schedule should end (Never, OnDate, AfterOccurrences)
+- `monthly_schedule_type` (String) Type of monthly schedule (DayOfMonth, DateOfMonth)
+- `type` (String) Type of recurring schedule (OnceDaily, DaysPerWeek, DaysPerMonth, Annually)
+- `unit` (Number) The unit value for the schedule
+
+
+
+### Nested Schema for `deployment_freezes.tenant_project_environment_scope`
+
+Read-Only:
+
+- `environment_id` (String) The environment ID
+- `project_id` (String) The project ID
+- `tenant_id` (String) The tenant ID
diff --git a/docs/resources/deployment_freeze.md b/docs/resources/deployment_freeze.md
index 70813e81..397a2474 100644
--- a/docs/resources/deployment_freeze.md
+++ b/docs/resources/deployment_freeze.md
@@ -27,6 +27,18 @@ resource "octopusdeploy_deployment_freeze" "freeze" {
end = "2024-12-27T00:00:00+08:00"
}
+# Freeze recurring freeze yearly on Xmas
+resource "octopusdeploy_deployment_freeze" "freeze" {
+ name = "Xmas"
+ start = "2024-12-25T00:00:00+10:00"
+ end = "2024-12-27T00:00:00+08:00"
+ recurring_schedule = {
+ type = "Annually"
+ unit = 1
+ end_type = "Never"
+ }
+}
+
resource "octopusdeploy_deployment_freeze_project" "project_freeze" {
deploymentfreeze_id= octopusdeploy_deployment_freeze.freeze.id
project_id = "Projects-123"
@@ -69,8 +81,31 @@ resource "octopusdeploy_deployment_freeze_tenant" "tenant_freeze" {
- `name` (String) The name of this resource.
- `start` (String) The start time of the freeze, must be RFC3339 format
+### Optional
+
+- `recurring_schedule` (Attributes) (see [below for nested schema](#nestedatt--recurring_schedule))
+
### Read-Only
- `id` (String) The unique ID for this resource.
+
+### Nested Schema for `recurring_schedule`
+
+Required:
+
+- `end_type` (String) When the recurring schedule should end (Never, OnDate, AfterOccurrences)
+- `type` (String) Type of recurring schedule (Daily, Weekly, Monthly, Annually)
+- `unit` (Number) The unit value for the schedule
+
+Optional:
+
+- `date_of_month` (String) The date of the month for monthly schedules
+- `day_number_of_month` (String) Specifies which weekday position in the month. Valid values: 1 (First), 2 (Second), 3 (Third), 4 (Fourth), L (Last). Used with day_of_week
+- `day_of_week` (String) The day of the week for monthly schedules when using DayOfMonth type
+- `days_of_week` (List of String) List of days of the week for weekly schedules. Must follow order: Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday
+- `end_after_occurrences` (Number) Number of occurrences after which the schedule should end
+- `end_on_date` (String) The date when the recurring schedule should end
+- `monthly_schedule_type` (String) Type of monthly schedule (DayOfMonth, DateOfMonth)
+
diff --git a/examples/data-sources/octopusdeploy_deployment_freezes/data-source.tf b/examples/data-sources/octopusdeploy_deployment_freezes/data-source.tf
new file mode 100644
index 00000000..9cb90699
--- /dev/null
+++ b/examples/data-sources/octopusdeploy_deployment_freezes/data-source.tf
@@ -0,0 +1,19 @@
+data "octopusdeploy_deployment_freezes" "test_freeze" {
+ ids = null
+ partial_name = "Freeze Name"
+ skip = 5
+ take = 100
+}
+
+
+data "octopusdeploy_deployment_freezes" "project_freezes" {
+ project_ids = ["projects-1"]
+ skip = 0
+ take = 5
+}
+
+data "octopusdeploy_deployment_freezes" "tenant_freezes" {
+ tenant_ids = ["tenants-1"]
+ skip = 0
+ take = 10
+}
diff --git a/examples/resources/octopusdeploy_deployment_freeze/resource.tf b/examples/resources/octopusdeploy_deployment_freeze/resource.tf
index 291928c7..746c9738 100644
--- a/examples/resources/octopusdeploy_deployment_freeze/resource.tf
+++ b/examples/resources/octopusdeploy_deployment_freeze/resource.tf
@@ -12,6 +12,18 @@ resource "octopusdeploy_deployment_freeze" "freeze" {
end = "2024-12-27T00:00:00+08:00"
}
+# Freeze recurring freeze yearly on Xmas
+resource "octopusdeploy_deployment_freeze" "freeze" {
+ name = "Xmas"
+ start = "2024-12-25T00:00:00+10:00"
+ end = "2024-12-27T00:00:00+08:00"
+ recurring_schedule = {
+ type = "Annually"
+ unit = 1
+ end_type = "Never"
+ }
+}
+
resource "octopusdeploy_deployment_freeze_project" "project_freeze" {
deploymentfreeze_id= octopusdeploy_deployment_freeze.freeze.id
project_id = "Projects-123"
diff --git a/go.mod b/go.mod
index f5a145f5..cbfd298d 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.22
toolchain go1.22.3
require (
- github.com/OctopusDeploy/go-octopusdeploy/v2 v2.63.1
+ github.com/OctopusDeploy/go-octopusdeploy/v2 v2.64.0
github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework v0.0.0-20241206032352-dbc62b2d16cf
github.com/google/uuid v1.6.0
github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637
diff --git a/go.sum b/go.sum
index 0a7a2e40..df4a0452 100644
--- a/go.sum
+++ b/go.sum
@@ -18,8 +18,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
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.63.1 h1:ShfJ3VZqNblU5V0UnA/UNjQuUrgkljZdDp9Vxh8rhcI=
-github.com/OctopusDeploy/go-octopusdeploy/v2 v2.63.1/go.mod h1:ggvOXzMnq+w0pLg6C9zdjz6YBaHfO3B3tqmmB7JQdaw=
+github.com/OctopusDeploy/go-octopusdeploy/v2 v2.64.0 h1:NWqQ/7JLUfEJQ8QHrkek7AfePuN121+f6+tUi3xP6vE=
+github.com/OctopusDeploy/go-octopusdeploy/v2 v2.64.0/go.mod h1:ggvOXzMnq+w0pLg6C9zdjz6YBaHfO3B3tqmmB7JQdaw=
github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework v0.0.0-20241206032352-dbc62b2d16cf h1:wuUJ6DbSZEHE4a3SfSJIcoeTQCSI6lbQ+i46ibY14+Q=
github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework v0.0.0-20241206032352-dbc62b2d16cf/go.mod h1:xVv8DvYhhwxtQUQQDfOYA6CY8KTkHXccxQ2RfRj6IJ0=
github.com/ProtonMail/go-crypto v1.1.0-alpha.2 h1:bkyFVUP+ROOARdgCiJzNQo2V2kiB97LyUpzH9P6Hrlg=
diff --git a/octopusdeploy_framework/datasource_deployment_freeze.go b/octopusdeploy_framework/datasource_deployment_freeze.go
index e02e71e2..5db7bfff 100644
--- a/octopusdeploy_framework/datasource_deployment_freeze.go
+++ b/octopusdeploy_framework/datasource_deployment_freeze.go
@@ -14,12 +14,13 @@ import (
const deploymentFreezeDatasourceName = "deployment_freezes"
-type deploymentFreezesModel struct {
+type deploymentFreezesDatasourceModel 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"`
+ TenantIDs types.List `tfsdk:"tenant_ids"`
IncludeComplete types.Bool `tfsdk:"include_complete"`
Status types.String `tfsdk:"status"`
Skip types.Int64 `tfsdk:"skip"`
@@ -48,7 +49,7 @@ func (d *deploymentFreezeDataSource) Schema(ctx context.Context, req datasource.
}
func (d *deploymentFreezeDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
- var data deploymentFreezesModel
+ var data deploymentFreezesDatasourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
@@ -59,6 +60,7 @@ func (d *deploymentFreezeDataSource) Read(ctx context.Context, req datasource.Re
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)),
+ TenantIds: util.Ternary(data.TenantIDs.IsNull(), []string{}, util.ExpandStringList(data.TenantIDs)),
IncludeComplete: data.IncludeComplete.ValueBool(),
Status: data.Status.ValueString(),
Skip: int(data.Skip.ValueInt64()),
@@ -92,31 +94,134 @@ var _ datasource.DataSource = &deploymentFreezeDataSource{}
var _ datasource.DataSourceWithConfigure = &deploymentFreezeDataSource{}
func mapFreezeToAttribute(ctx context.Context, freeze deploymentfreezes.DeploymentFreeze) (attr.Value, diag.Diagnostics) {
+ var diags 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() {
+ scopeType, scopeDiags := types.MapValueFrom(ctx, types.ListType{ElemType: types.StringType}, projectScopes)
+ if scopeDiags.HasError() {
+ diags.Append(scopeDiags...)
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
+ tenantScopes := make([]attr.Value, 0)
+ for _, scope := range freeze.TenantProjectEnvironmentScope {
+ tenantScope, tDiags := types.ObjectValue(tenantScopeObjectType(), map[string]attr.Value{
+ "tenant_id": types.StringValue(scope.TenantId),
+ "project_id": types.StringValue(scope.ProjectId),
+ "environment_id": types.StringValue(scope.EnvironmentId),
+ })
+ if tDiags.HasError() {
+ diags.Append(tDiags...)
+ return nil, diags
+ }
+ tenantScopes = append(tenantScopes, tenantScope)
+ }
+
+ tenantScopesList, tsDiags := types.ListValue(
+ types.ObjectType{AttrTypes: tenantScopeObjectType()},
+ tenantScopes,
+ )
+ if tsDiags.HasError() {
+ diags.Append(tsDiags...)
+ return nil, diags
+ }
+
+ attrs := 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,
+ "tenant_project_environment_scope": tenantScopesList,
+ }
+
+ if freeze.RecurringSchedule != nil {
+ daysOfWeek, daysDiags := types.ListValueFrom(ctx, types.StringType, freeze.RecurringSchedule.DaysOfWeek)
+ if daysDiags.HasError() {
+ diags.Append(daysDiags...)
+ return nil, diags
+ }
+
+ endOnDate := types.StringNull()
+ if freeze.RecurringSchedule.EndOnDate != nil {
+ endOnDate = types.StringValue(freeze.RecurringSchedule.EndOnDate.Format(time.RFC3339))
+ }
+
+ endAfterOccurrences := types.Int64Value(int64(freeze.RecurringSchedule.EndAfterOccurrences))
+
+ monthlyScheduleType := types.StringNull()
+ if freeze.RecurringSchedule.MonthlyScheduleType != "" {
+ monthlyScheduleType = types.StringValue(freeze.RecurringSchedule.MonthlyScheduleType)
+ }
+
+ dateOfMonth := types.StringValue(freeze.RecurringSchedule.DateOfMonth)
+
+ dayNumberOfMonth := types.StringValue(freeze.RecurringSchedule.DayNumberOfMonth)
+
+ dayOfWeek := types.StringValue(freeze.RecurringSchedule.DayOfWeek)
+
+ scheduleAttrs := map[string]attr.Value{
+ "type": types.StringValue(string(freeze.RecurringSchedule.Type)),
+ "unit": types.Int64Value(int64(freeze.RecurringSchedule.Unit)),
+ "end_type": types.StringValue(string(freeze.RecurringSchedule.EndType)),
+ "end_on_date": endOnDate,
+ "end_after_occurrences": endAfterOccurrences,
+ "monthly_schedule_type": monthlyScheduleType,
+ "date_of_month": dateOfMonth,
+ "day_number_of_month": dayNumberOfMonth,
+ "days_of_week": daysOfWeek,
+ "day_of_week": dayOfWeek,
+ }
+
+ recurringSchedule, rsDiags := types.ObjectValue(freezeRecurringScheduleObjectType(), scheduleAttrs)
+ if rsDiags.HasError() {
+ diags.Append(rsDiags...)
+ return nil, diags
+ }
+
+ attrs["recurring_schedule"] = recurringSchedule
+ } else {
+ attrs["recurring_schedule"] = types.ObjectNull(freezeRecurringScheduleObjectType())
+ }
+
+ return types.ObjectValueMust(freezeObjectType(), attrs), diags
+}
+
+func freezeRecurringScheduleObjectType() map[string]attr.Type {
+ return map[string]attr.Type{
+ "type": types.StringType,
+ "unit": types.Int64Type,
+ "end_type": types.StringType,
+ "end_on_date": types.StringType,
+ "end_after_occurrences": types.Int64Type,
+ "monthly_schedule_type": types.StringType,
+ "date_of_month": types.StringType,
+ "day_number_of_month": types.StringType,
+ "days_of_week": types.ListType{ElemType: types.StringType},
+ "day_of_week": types.StringType,
+ }
+}
+
+func tenantScopeObjectType() map[string]attr.Type {
+ return map[string]attr.Type{
+ "tenant_id": types.StringType,
+ "project_id": types.StringType,
+ "environment_id": types.StringType,
+ }
}
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}},
+ "id": types.StringType,
+ "name": types.StringType,
+ "start": types.StringType,
+ "end": types.StringType,
+ "project_environment_scope": types.MapType{ElemType: types.ListType{ElemType: types.StringType}},
+ "tenant_project_environment_scope": types.ListType{ElemType: types.ObjectType{AttrTypes: tenantScopeObjectType()}},
+ "recurring_schedule": types.ObjectType{AttrTypes: freezeRecurringScheduleObjectType()},
}
}
diff --git a/octopusdeploy_framework/datasource_deployment_freeze_test.go b/octopusdeploy_framework/datasource_deployment_freeze_test.go
new file mode 100644
index 00000000..230a3fd5
--- /dev/null
+++ b/octopusdeploy_framework/datasource_deployment_freeze_test.go
@@ -0,0 +1,181 @@
+package octopusdeploy_framework
+
+import (
+ "fmt"
+ "github.com/hashicorp/terraform-plugin-testing/helper/acctest"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "strings"
+ "testing"
+ "time"
+)
+
+func TestAccDataSourceDeploymentFreezes(t *testing.T) {
+ spaceName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)
+ freezeName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)
+ tenantName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)
+ projectName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)
+ environmentName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)
+ dataSourceName := "data.octopusdeploy_deployment_freezes.test_freeze"
+
+ startTime := time.Now().AddDate(1, 0, 0).UTC()
+ endTime := startTime.Add(24 * time.Hour)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: ProtoV6ProviderFactories(),
+ Steps: []resource.TestStep{
+ {
+ Config: testAccDataSourceDeploymentFreezesConfig(spaceName, freezeName, startTime, endTime, false, false, projectName, environmentName, tenantName),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttrSet(dataSourceName, "id"),
+ resource.TestCheckResourceAttr(dataSourceName, "partial_name", freezeName),
+ resource.TestCheckResourceAttr(dataSourceName, "deployment_freezes.#", "1"),
+ resource.TestCheckResourceAttr(dataSourceName, "deployment_freezes.0.name", freezeName),
+ resource.TestCheckResourceAttr(dataSourceName, "deployment_freezes.0.tenant_project_environment_scope.#", "0"),
+ resource.TestCheckResourceAttr(dataSourceName, "deployment_freezes.0.project_environment_scope.%", "0"),
+ ),
+ },
+ {
+ Config: testAccDataSourceDeploymentFreezesConfig(spaceName, freezeName, startTime, endTime, true, false, projectName, environmentName, tenantName),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttrSet(dataSourceName, "id"),
+ resource.TestCheckResourceAttr(dataSourceName, "deployment_freezes.0.tenant_project_environment_scope.#", "0"),
+ resource.TestCheckResourceAttr(dataSourceName, "deployment_freezes.0.project_environment_scope.%", "1"),
+ ),
+ },
+ {
+ Config: testAccDataSourceDeploymentFreezesConfig(spaceName, freezeName, startTime, endTime, true, true, projectName, environmentName, tenantName),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttrSet(dataSourceName, "id"),
+ resource.TestCheckResourceAttr(dataSourceName, "deployment_freezes.0.tenant_project_environment_scope.#", "1"),
+ resource.TestCheckResourceAttrSet(dataSourceName, "deployment_freezes.0.tenant_project_environment_scope.0.tenant_id"),
+ resource.TestCheckResourceAttrSet(dataSourceName, "deployment_freezes.0.tenant_project_environment_scope.0.project_id"),
+ resource.TestCheckResourceAttrSet(dataSourceName, "deployment_freezes.0.tenant_project_environment_scope.0.environment_id"),
+ resource.TestCheckResourceAttr(dataSourceName, "deployment_freezes.0.project_environment_scope.%", "1"),
+ ),
+ },
+ },
+ })
+}
+
+func testAccDataSourceDeploymentFreezesConfig(spaceName, freezeName string, startTime, endTime time.Time, includeProject bool, includeTenant bool, projectName, environmentName, tenantName string) string {
+ baseConfig := fmt.Sprintf(`
+resource "octopusdeploy_space" "test_space" {
+ name = "%s"
+ is_default = false
+ is_task_queue_stopped = false
+ description = "Test space for deployment freeze datasource"
+ space_managers_teams = ["teams-administrators"]
+}
+
+resource "octopusdeploy_environment" "test_environment" {
+ name = "%s"
+ description = "Test environment"
+ space_id = octopusdeploy_space.test_space.id
+}
+
+resource "octopusdeploy_project_group" "test_project_group" {
+ name = "Test Project Group"
+ description = "Test project group for deployment freeze datasource"
+ space_id = octopusdeploy_space.test_space.id
+}
+
+resource "octopusdeploy_lifecycle" "test_lifecycle" {
+ name = "Test Lifecycle"
+ space_id = octopusdeploy_space.test_space.id
+}
+
+resource "octopusdeploy_project" "test_project" {
+ name = "%s"
+ lifecycle_id = octopusdeploy_lifecycle.test_lifecycle.id
+ project_group_id = octopusdeploy_project_group.test_project_group.id
+ space_id = octopusdeploy_space.test_space.id
+}
+
+resource "octopusdeploy_deployment_freeze" "test_freeze" {
+ name = "%s"
+ start = "%s"
+ end = "%s"
+
+ recurring_schedule = {
+ type = "Weekly"
+ unit = 24
+ end_type = "AfterOccurrences"
+ end_after_occurrences = 5
+ days_of_week = ["Monday", "Wednesday", "Friday"]
+ }
+
+ depends_on = [octopusdeploy_space.test_space]
+}
+`, spaceName, environmentName, projectName, freezeName, startTime.Format(time.RFC3339), endTime.Format(time.RFC3339))
+
+ if includeProject {
+ projectConfig := fmt.Sprintf(`
+resource "octopusdeploy_deployment_freeze_project" "test_project_scope" {
+ deploymentfreeze_id = octopusdeploy_deployment_freeze.test_freeze.id
+ project_id = octopusdeploy_project.test_project.id
+ environment_ids = [octopusdeploy_environment.test_environment.id]
+}
+`)
+ baseConfig = baseConfig + projectConfig
+ }
+
+ if includeTenant {
+ tenantConfig := fmt.Sprintf(`
+resource "octopusdeploy_tenant" "test_tenant" {
+ name = "%s"
+ space_id = octopusdeploy_space.test_space.id
+}
+
+resource "octopusdeploy_tenant_project" "test_tenant_project" {
+ tenant_id = octopusdeploy_tenant.test_tenant.id
+ project_id = octopusdeploy_project.test_project.id
+ environment_ids = [octopusdeploy_environment.test_environment.id]
+ space_id = octopusdeploy_space.test_space.id
+}
+
+resource "octopusdeploy_deployment_freeze_tenant" "test_tenant_scope" {
+ deploymentfreeze_id = octopusdeploy_deployment_freeze.test_freeze.id
+ tenant_id = octopusdeploy_tenant.test_tenant.id
+ project_id = octopusdeploy_project.test_project.id
+ environment_id = octopusdeploy_environment.test_environment.id
+
+ depends_on = [
+ octopusdeploy_tenant_project.test_tenant_project
+ ]
+}
+`, tenantName)
+ baseConfig = baseConfig + tenantConfig
+ }
+
+ datasourceConfig := `
+data "octopusdeploy_deployment_freezes" "test_freeze" {
+ partial_name = "%s"
+ skip = 0
+ take = 1
+ depends_on = [`
+
+ deps := []string{"octopusdeploy_deployment_freeze.test_freeze"}
+ if includeProject {
+ deps = append(deps, "octopusdeploy_deployment_freeze_project.test_project_scope")
+ }
+ if includeTenant {
+ deps = append(deps, "octopusdeploy_deployment_freeze_tenant.test_tenant_scope")
+ }
+
+ datasourceConfig += "\n " + strings.Join(deps, ",\n ") + "\n "
+
+ datasourceConfig += `]
+}
+
+output "octopus_space_id" {
+ value = octopusdeploy_space.test_space.id
+}
+
+output "octopus_freeze_id" {
+ value = data.octopusdeploy_deployment_freezes.test_freeze.deployment_freezes[0].id
+}
+`
+ var config = baseConfig + fmt.Sprintf(datasourceConfig, freezeName)
+ return config
+}
diff --git a/octopusdeploy_framework/resource_deployment_freeze.go b/octopusdeploy_framework/resource_deployment_freeze.go
index 6298faa4..ad204b44 100644
--- a/octopusdeploy_framework/resource_deployment_freeze.go
+++ b/octopusdeploy_framework/resource_deployment_freeze.go
@@ -8,6 +8,7 @@ import (
"github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/schemas"
"github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util"
"github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
@@ -16,14 +17,35 @@ import (
const deploymentFreezeResourceName = "deployment_freeze"
-type deploymentFreezeModel struct {
- Name types.String `tfsdk:"name"`
- Start timetypes.RFC3339 `tfsdk:"start"`
- End timetypes.RFC3339 `tfsdk:"end"`
+type recurringScheduleModel struct {
+ Type types.String `tfsdk:"type"`
+ Unit types.Int64 `tfsdk:"unit"`
+ EndType types.String `tfsdk:"end_type"`
+ EndOnDate timetypes.RFC3339 `tfsdk:"end_on_date"`
+ EndAfterOccurrences types.Int64 `tfsdk:"end_after_occurrences"`
+ MonthlyScheduleType types.String `tfsdk:"monthly_schedule_type"`
+ DateOfMonth types.String `tfsdk:"date_of_month"`
+ DayNumberOfMonth types.String `tfsdk:"day_number_of_month"`
+ DaysOfWeek types.List `tfsdk:"days_of_week"`
+ DayOfWeek types.String `tfsdk:"day_of_week"`
+}
+type deploymentFreezeModel struct {
+ Name types.String `tfsdk:"name"`
+ Start timetypes.RFC3339 `tfsdk:"start"`
+ End timetypes.RFC3339 `tfsdk:"end"`
+ RecurringSchedule *recurringScheduleModel `tfsdk:"recurring_schedule"`
schemas.ResourceModel
}
+func getStringPointer(s types.String) *string {
+ if s.IsNull() {
+ return nil
+ }
+ value := s.ValueString()
+ return &value
+}
+
type deploymentFreezeResource struct {
*Config
}
@@ -125,8 +147,9 @@ func (f *deploymentFreezeResource) Update(ctx context.Context, req resource.Upda
return
}
- // this resource doesn't include scopes, need to copy it from the fetched resource
+ // Preserve both project and tenant scopes from the existing freeze
updatedFreeze.ProjectEnvironmentScope = existingFreeze.ProjectEnvironmentScope
+ updatedFreeze.TenantProjectEnvironmentScope = existingFreeze.TenantProjectEnvironmentScope
updatedFreeze.SetID(existingFreeze.GetID())
updatedFreeze.Links = existingFreeze.Links
@@ -169,7 +192,60 @@ func (f *deploymentFreezeResource) Delete(ctx context.Context, req resource.Dele
resp.State.RemoveResource(ctx)
}
+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,
+ }
+
+ if state.RecurringSchedule != nil {
+ var daysOfWeek []string
+
+ if !state.RecurringSchedule.DaysOfWeek.IsNull() {
+ diags.Append(state.RecurringSchedule.DaysOfWeek.ElementsAs(context.TODO(), &daysOfWeek, false)...)
+ if diags.HasError() {
+ return nil, diags
+ }
+ }
+
+ freeze.RecurringSchedule = &deploymentfreezes.RecurringSchedule{
+ Type: deploymentfreezes.RecurringScheduleType(state.RecurringSchedule.Type.ValueString()),
+ Unit: int(state.RecurringSchedule.Unit.ValueInt64()),
+ EndType: deploymentfreezes.RecurringScheduleEndType(state.RecurringSchedule.EndType.ValueString()),
+ EndAfterOccurrences: getOptionalIntValue(state.RecurringSchedule.EndAfterOccurrences),
+ MonthlyScheduleType: getOptionalString(state.RecurringSchedule.MonthlyScheduleType),
+ DateOfMonth: getOptionalString(state.RecurringSchedule.DateOfMonth),
+ DayNumberOfMonth: getOptionalString(state.RecurringSchedule.DayNumberOfMonth),
+ DaysOfWeek: daysOfWeek,
+ DayOfWeek: getOptionalString(state.RecurringSchedule.DayOfWeek),
+ }
+
+ if !state.RecurringSchedule.EndOnDate.IsNull() {
+ date, diagsDate := state.RecurringSchedule.EndOnDate.ValueRFC3339Time()
+ if diagsDate.HasError() {
+ diags.Append(diagsDate...)
+ return nil, diags
+ }
+ freeze.RecurringSchedule.EndOnDate = &date
+ }
+ }
+
+ freeze.ID = state.ID.String()
+ return &freeze, nil
+}
func mapToState(ctx context.Context, state *deploymentFreezeModel, deploymentFreeze *deploymentfreezes.DeploymentFreeze) diag.Diagnostics {
state.ID = types.StringValue(deploymentFreeze.ID)
state.Name = types.StringValue(deploymentFreeze.Name)
@@ -186,6 +262,44 @@ func mapToState(ctx context.Context, state *deploymentFreezeModel, deploymentFre
}
state.End = updatedEnd
+ if deploymentFreeze.RecurringSchedule != nil {
+ var daysOfWeek types.List
+ if len(deploymentFreeze.RecurringSchedule.DaysOfWeek) > 0 {
+ elements := make([]attr.Value, len(deploymentFreeze.RecurringSchedule.DaysOfWeek))
+ for i, day := range deploymentFreeze.RecurringSchedule.DaysOfWeek {
+ elements[i] = types.StringValue(day)
+ }
+
+ var listDiags diag.Diagnostics
+ daysOfWeek, listDiags = types.ListValue(types.StringType, elements)
+ if listDiags.HasError() {
+ diags.Append(listDiags...)
+ return diags
+ }
+ } else {
+ daysOfWeek = types.ListNull(types.StringType)
+ }
+
+ state.RecurringSchedule = &recurringScheduleModel{
+ Type: types.StringValue(string(deploymentFreeze.RecurringSchedule.Type)),
+ Unit: types.Int64Value(int64(deploymentFreeze.RecurringSchedule.Unit)),
+ EndType: types.StringValue(string(deploymentFreeze.RecurringSchedule.EndType)),
+ DaysOfWeek: daysOfWeek,
+ MonthlyScheduleType: mapOptionalStringValue(deploymentFreeze.RecurringSchedule.MonthlyScheduleType),
+ }
+
+ if deploymentFreeze.RecurringSchedule.EndOnDate != nil {
+ state.RecurringSchedule.EndOnDate = timetypes.NewRFC3339TimeValue(*deploymentFreeze.RecurringSchedule.EndOnDate)
+ } else {
+ state.RecurringSchedule.EndOnDate = timetypes.NewRFC3339Null()
+ }
+
+ state.RecurringSchedule.EndAfterOccurrences = mapOptionalIntValue(deploymentFreeze.RecurringSchedule.EndAfterOccurrences)
+ state.RecurringSchedule.DateOfMonth = mapOptionalStringValue(deploymentFreeze.RecurringSchedule.DateOfMonth)
+ state.RecurringSchedule.DayNumberOfMonth = mapOptionalStringValue(deploymentFreeze.RecurringSchedule.DayNumberOfMonth)
+ state.RecurringSchedule.DayOfWeek = mapOptionalStringValue(deploymentFreeze.RecurringSchedule.DayOfWeek)
+ }
+
return nil
}
@@ -210,25 +324,36 @@ func calculateStateTime(ctx context.Context, stateValue timetypes.RFC3339, updat
return newValue, diags
}
-func mapFromState(state *deploymentFreezeModel) (*deploymentfreezes.DeploymentFreeze, diag.Diagnostics) {
- start, diags := state.Start.ValueRFC3339Time()
- if diags.HasError() {
- return nil, diags
+func getOptionalStringPointer(value types.String) *string {
+ if value.IsNull() {
+ return nil
}
- start = start.UTC()
-
- end, diags := state.End.ValueRFC3339Time()
- if diags.HasError() {
- return nil, diags
+ str := value.ValueString()
+ return &str
+}
+func mapOptionalStringValue(value string) types.String {
+ if value == "" {
+ return types.StringNull()
}
- end = end.UTC()
+ return types.StringValue(value)
+}
+func getOptionalIntValue(value types.Int64) int {
+ if value.IsNull() {
+ return 0
+ }
+ return int(value.ValueInt64())
+}
- freeze := deploymentfreezes.DeploymentFreeze{
- Name: state.Name.ValueString(),
- Start: &start,
- End: &end,
+func mapOptionalIntValue(value int) types.Int64 {
+ if value == 0 {
+ return types.Int64Null()
}
+ return types.Int64Value(int64(value))
+}
- freeze.ID = state.ID.String()
- return &freeze, nil
+func getOptionalString(value types.String) string {
+ if value.IsNull() {
+ return ""
+ }
+ return value.ValueString()
}
diff --git a/octopusdeploy_framework/resource_deployment_freeze_tenant.go b/octopusdeploy_framework/resource_deployment_freeze_tenant.go
index 3f4160ef..329fa54a 100644
--- a/octopusdeploy_framework/resource_deployment_freeze_tenant.go
+++ b/octopusdeploy_framework/resource_deployment_freeze_tenant.go
@@ -52,7 +52,7 @@ func (d *deploymentFreezeTenantResource) Create(ctx context.Context, req resourc
return
}
- tflog.Info(ctx, fmt.Sprintf("adding tenant (%s) to deployment freeze (%s)", plan.TenantID.ValueString(), plan.DeploymentFreezeID.ValueString()))
+ tflog.Debug(ctx, fmt.Sprintf("adding tenant (%s) to deployment freeze (%s)", plan.TenantID.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())
@@ -86,7 +86,6 @@ func (d *deploymentFreezeTenantResource) Create(ctx context.Context, req resourc
tflog.Debug(ctx, fmt.Sprintf("tenant scope (%s) added to deployment freeze (%s)", plan.TenantID.ValueString(), plan.DeploymentFreezeID.ValueString()))
util.Created(ctx, tenantDescription)
}
-
func (d *deploymentFreezeTenantResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
util.Reading(ctx, tenantDescription)
@@ -104,14 +103,21 @@ func (d *deploymentFreezeTenantResource) Read(ctx context.Context, req resource.
freeze, err := deploymentfreezes.GetById(d.Client, freezeId)
if err != nil {
- apiError := err.(*core.APIError)
- if apiError.StatusCode != http.StatusNotFound {
+ apiError, ok := err.(*core.APIError)
+ if !ok {
resp.Diagnostics.AddError("unable to load deployment freeze", err.Error())
return
}
+
+ if apiError.StatusCode == http.StatusNotFound {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+
+ resp.Diagnostics.AddError("unable to load deployment freeze", apiError.Error())
+ return
}
- // Verify the tenant scope still exists
exists := false
for _, scope := range freeze.TenantProjectEnvironmentScope {
if scope.TenantId == tenantId && scope.ProjectId == projectId && scope.EnvironmentId == environmentId {
diff --git a/octopusdeploy_framework/resource_deployment_freeze_test.go b/octopusdeploy_framework/resource_deployment_freeze_test.go
index 2080963c..b0cb96f7 100644
--- a/octopusdeploy_framework/resource_deployment_freeze_test.go
+++ b/octopusdeploy_framework/resource_deployment_freeze_test.go
@@ -1,13 +1,11 @@
package octopusdeploy_framework
import (
- "context"
"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"
- "os"
"strings"
"testing"
"time"
@@ -40,7 +38,7 @@ func TestNewDeploymentFreezeResource(t *testing.T) {
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, tenantName, false),
+ Config: testDeploymentFreezeBasic(localName, name, start, end, spaceName, []string{environmentName1}, projectName, projectGroupName, lifecycleName, tenantName, false, false),
},
{
Check: resource.ComposeTestCheckFunc(
@@ -48,19 +46,35 @@ func TestNewDeploymentFreezeResource(t *testing.T) {
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, tenantName, false),
+ Config: testDeploymentFreezeBasic(localName, name+"1", start, updatedEnd, spaceName, []string{environmentName1, environmentName2}, projectName, projectGroupName, lifecycleName, tenantName, false, false),
},
{
Check: resource.ComposeTestCheckFunc(
testDeploymentFreezeExists(resourceName),
- testDeploymentFreezeTenantExists(fmt.Sprintf("octopusdeploy_deployment_freeze_tenant.tenant_%s", localName), t)),
- Config: testDeploymentFreezeBasic(localName, name+"1", start, updatedEnd, spaceName, []string{environmentName1, environmentName2}, projectName, projectGroupName, lifecycleName, tenantName, true),
+ testDeploymentFreezeTenantExists(fmt.Sprintf("octopusdeploy_deployment_freeze_tenant.tenant_%s", localName))),
+ Config: testDeploymentFreezeBasic(localName, name+"1", start, updatedEnd, spaceName, []string{environmentName1, environmentName2}, projectName, projectGroupName, lifecycleName, tenantName, true, true),
+ },
+ {
+ Check: resource.ComposeTestCheckFunc(
+ testDeploymentFreezeExists(resourceName),
+ resource.TestCheckResourceAttr(resourceName, "name", name+"1"),
+ resource.TestCheckResourceAttr(resourceName, "start", start),
+ resource.TestCheckResourceAttr(resourceName, "end", updatedEnd),
+ resource.TestCheckResourceAttr(resourceName, "recurring_schedule.type", "Weekly"),
+ resource.TestCheckResourceAttr(resourceName, "recurring_schedule.unit", "24"),
+ resource.TestCheckResourceAttr(resourceName, "recurring_schedule.end_type", "AfterOccurrences"),
+ resource.TestCheckResourceAttr(resourceName, "recurring_schedule.end_after_occurrences", "5"),
+ resource.TestCheckResourceAttr(resourceName, "recurring_schedule.days_of_week.#", "3"),
+ resource.TestCheckResourceAttr(resourceName, "recurring_schedule.days_of_week.0", "Monday"),
+ resource.TestCheckResourceAttr(resourceName, "recurring_schedule.days_of_week.1", "Wednesday"),
+ resource.TestCheckResourceAttr(resourceName, "recurring_schedule.days_of_week.2", "Friday")),
+ Config: testDeploymentFreezeBasic(localName, name+"1", start, updatedEnd, spaceName, []string{environmentName1, environmentName2}, projectName, projectGroupName, lifecycleName, tenantName, true, true),
},
},
})
}
-func testDeploymentFreezeBasic(localName string, freezeName string, start string, end string, spaceName string, environments []string, projectName string, projectGroupName string, lifecycleName string, tenantName string, includeTenant bool) string {
+func testDeploymentFreezeBasic(localName string, freezeName string, start string, end string, spaceName string, environments []string, projectName string, projectGroupName string, lifecycleName string, tenantName string, includeTenant bool, includeRecurringSchedule bool) string {
spaceLocalName := fmt.Sprintf("space_%s", localName)
projectScopeLocalName := fmt.Sprintf("project_scope_%s", localName)
projectLocalName := fmt.Sprintf("project_%s", localName)
@@ -76,8 +90,27 @@ func testDeploymentFreezeBasic(localName string, freezeName string, start string
environmentScopes = append(environmentScopes, fmt.Sprintf("resource.octopusdeploy_environment.%s.id", environmentLocalName))
}
+ freezeConfig := fmt.Sprintf(`
+ resource "octopusdeploy_deployment_freeze" "%s" {
+ name = "%s"
+ start = "%s"
+ end = "%s"`, localName, freezeName, start, end)
+
+ if includeRecurringSchedule {
+ freezeConfig += `
+ recurring_schedule = {
+ type = "Weekly"
+ unit = 24
+ end_type = "AfterOccurrences"
+ end_after_occurrences = 5
+ days_of_week = ["Monday", "Wednesday", "Friday"]
+ }`
+ }
+
+ freezeConfig += `
+ }`
+
config := fmt.Sprintf(`
-
# Space Configuration
%s
@@ -93,11 +126,7 @@ func testDeploymentFreezeBasic(localName string, freezeName string, start string
# Project Configuration
%s
- resource "octopusdeploy_deployment_freeze" "%s" {
- name = "%s"
- start = "%s"
- end = "%s"
- }
+ %s
resource "octopusdeploy_deployment_freeze_project" "%s" {
deploymentfreeze_id = octopusdeploy_deployment_freeze.%s.id
@@ -109,7 +138,7 @@ func testDeploymentFreezeBasic(localName string, freezeName string, start string
createLifecycle(spaceLocalName, lifecycleLocalName, lifecycleName),
createProjectGroup(spaceLocalName, projectGroupLocalName, projectGroupName),
createProject(spaceLocalName, projectLocalName, projectName, lifecycleLocalName, projectGroupLocalName),
- localName, freezeName, start, end,
+ freezeConfig,
projectScopeLocalName, localName, projectLocalName,
strings.Join(environmentScopes, ","))
@@ -155,11 +184,11 @@ func testDeploymentFreezeExists(prefix string) resource.TestCheckFunc {
}
}
-func testDeploymentFreezeTenantExists(prefix string, t *testing.T) resource.TestCheckFunc {
+func testDeploymentFreezeTenantExists(prefix string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[prefix]
if !ok {
- return fmt.Errorf("Resource not found: %s", prefix)
+ return fmt.Errorf("Not found: %s", prefix)
}
bits := strings.Split(rs.Primary.ID, ":")
@@ -172,47 +201,18 @@ func testDeploymentFreezeTenantExists(prefix string, t *testing.T) resource.Test
projectId := bits[2]
environmentId := bits[3]
- retryErr := resource.RetryContext(context.Background(), 2*time.Minute, func() *resource.RetryError {
- freeze, err := deploymentfreezes.GetById(octoClient, freezeId)
- if err != nil {
- t.Logf("Failed to get deployment freeze: %v", err)
- return resource.NonRetryableError(fmt.Errorf("Error getting deployment freeze: %v", err))
- }
-
- t.Logf("Retrieved deployment freeze with %d tenant scopes", len(freeze.TenantProjectEnvironmentScope))
-
- for i, scope := range freeze.TenantProjectEnvironmentScope {
- t.Logf("Scope %d - Tenant: %s, Project: %s, Environment: %s",
- i+1, scope.TenantId, scope.ProjectId, scope.EnvironmentId)
- }
-
- for _, scope := range freeze.TenantProjectEnvironmentScope {
- if scope.TenantId == tenantId && scope.ProjectId == projectId && scope.EnvironmentId == environmentId {
- t.Log("Found matching tenant scope in deployment freeze")
- return nil
- }
- }
+ freeze, err := deploymentfreezes.GetById(octoClient, freezeId)
+ if err != nil {
+ return err
+ }
- t.Log("Tenant scope not yet found, will retry...")
- return resource.RetryableError(fmt.Errorf("Tenant scope not yet found in deployment freeze (freezeId: %s)", freezeId))
- })
-
- if retryErr != nil {
- freeze, err := deploymentfreezes.GetById(octoClient, freezeId)
- if err != nil {
- t.Logf("Final attempt to get deployment freeze failed: %v", err)
- } else {
- t.Logf("Final state - Deployment freeze has %d tenant scopes", len(freeze.TenantProjectEnvironmentScope))
- for i, scope := range freeze.TenantProjectEnvironmentScope {
- t.Logf("Final Scope %d - Tenant: %s, Project: %s, Environment: %s",
- i+1, scope.TenantId, scope.ProjectId, scope.EnvironmentId)
- }
+ for _, scope := range freeze.TenantProjectEnvironmentScope {
+ if scope.TenantId == tenantId && scope.ProjectId == projectId && scope.EnvironmentId == environmentId {
+ return nil
}
-
- return fmt.Errorf("Failed to find tenant scope after retries. Error: %v", retryErr)
}
- return nil
+ return fmt.Errorf("Tenant scope not found in deployment freeze")
}
}
@@ -227,7 +227,5 @@ func testDeploymentFreezeCheckDestroy(s *terraform.State) error {
return fmt.Errorf("Deployment Freeze (%s) still exists", rs.Primary.ID)
}
}
-
- os.Setenv("TF_LOG", "")
return nil
}
diff --git a/octopusdeploy_framework/schemas/deployment_freeze.go b/octopusdeploy_framework/schemas/deployment_freeze.go
index 6baa8dd0..59bdd9ba 100644
--- a/octopusdeploy_framework/schemas/deployment_freeze.go
+++ b/octopusdeploy_framework/schemas/deployment_freeze.go
@@ -1,8 +1,10 @@
package schemas
import (
+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
datasourceSchema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
resourceSchema "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
)
@@ -15,6 +17,70 @@ func (d DeploymentFreezeSchema) GetResourceSchema() resourceSchema.Schema {
"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),
+ "recurring_schedule": resourceSchema.SingleNestedAttribute{
+ Optional: true,
+ Validators: []validator.Object{
+ NewRecurringScheduleValidator(),
+ },
+ Attributes: map[string]resourceSchema.Attribute{
+ "type": resourceSchema.StringAttribute{
+ Description: "Type of recurring schedule (Daily, Weekly, Monthly, Annually)",
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf("Daily", "Weekly", "Monthly", "Annually"),
+ },
+ },
+ "unit": resourceSchema.Int64Attribute{
+ Description: "The unit value for the schedule",
+ Required: true,
+ },
+ "end_type": resourceSchema.StringAttribute{
+ Description: "When the recurring schedule should end (Never, OnDate, AfterOccurrences)",
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf("Never", "OnDate", "AfterOccurrences"),
+ },
+ },
+ "end_on_date": GetDateTimeResourceSchema("The date when the recurring schedule should end", false),
+ "end_after_occurrences": resourceSchema.Int64Attribute{
+ Description: "Number of occurrences after which the schedule should end",
+ Optional: true,
+ },
+ "monthly_schedule_type": resourceSchema.StringAttribute{
+ Description: "Type of monthly schedule (DayOfMonth, DateOfMonth)",
+ Optional: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf("DayOfMonth", "DateOfMonth"),
+ },
+ },
+ "date_of_month": resourceSchema.StringAttribute{
+ Description: "The date of the month for monthly schedules",
+ Optional: true,
+ },
+ "day_number_of_month": resourceSchema.StringAttribute{
+ Description: "Specifies which weekday position in the month. Valid values: 1 (First), 2 (Second), 3 (Third), 4 (Fourth), L (Last). Used with day_of_week",
+ Optional: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf("1", "2", "3", "4", "L"),
+ },
+ },
+ "days_of_week": resourceSchema.ListAttribute{
+ Description: "List of days of the week for weekly schedules. Must follow order: Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday",
+ Optional: true,
+ ElementType: types.StringType,
+ Validators: []validator.List{
+ NewDaysOfWeekValidator(),
+ },
+ },
+ "day_of_week": resourceSchema.StringAttribute{
+ Description: "The day of the week for monthly schedules when using DayOfMonth type",
+ Optional: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"),
+ },
+ },
+ },
+ },
},
}
}
@@ -33,6 +99,11 @@ func (d DeploymentFreezeSchema) GetDatasourceSchema() datasourceSchema.Schema {
ElementType: types.StringType,
Optional: true,
},
+ "tenant_ids": datasourceSchema.ListAttribute{
+ Description: "A filter to search by a list of tenant IDs",
+ ElementType: types.StringType,
+ Optional: true,
+ },
"environment_ids": datasourceSchema.ListAttribute{
Description: "A filter to search by a list of environment IDs",
ElementType: types.StringType,
@@ -64,14 +135,79 @@ func (d DeploymentFreezeSchema) GetDatasourceSchema() datasourceSchema.Schema {
Optional: false,
Computed: true,
},
+ "tenant_project_environment_scope": datasourceSchema.ListNestedAttribute{
+ Description: "The tenant project environment scope of the deployment freeze",
+ Optional: false,
+ Computed: true,
+ NestedObject: datasourceSchema.NestedAttributeObject{
+ Attributes: map[string]datasourceSchema.Attribute{
+ "tenant_id": datasourceSchema.StringAttribute{
+ Description: "The tenant ID",
+ Computed: true,
+ },
+ "project_id": datasourceSchema.StringAttribute{
+ Description: "The project ID",
+ Computed: true,
+ },
+ "environment_id": datasourceSchema.StringAttribute{
+ Description: "The environment ID",
+ Computed: true,
+ },
+ },
+ },
+ },
+ "recurring_schedule": datasourceSchema.SingleNestedAttribute{
+ Computed: true,
+ Attributes: map[string]datasourceSchema.Attribute{
+ "type": datasourceSchema.StringAttribute{
+ Description: "Type of recurring schedule (OnceDaily, DaysPerWeek, DaysPerMonth, Annually)",
+ Computed: true,
+ },
+ "unit": datasourceSchema.Int64Attribute{
+ Description: "The unit value for the schedule",
+ Computed: true,
+ },
+ "end_type": datasourceSchema.StringAttribute{
+ Description: "When the recurring schedule should end (Never, OnDate, AfterOccurrences)",
+ Computed: true,
+ },
+ "end_on_date": datasourceSchema.StringAttribute{
+ Description: "The date when the recurring schedule should end",
+ Computed: true,
+ },
+ "end_after_occurrences": datasourceSchema.Int64Attribute{
+ Description: "Number of occurrences after which the schedule should end",
+ Computed: true,
+ },
+ "monthly_schedule_type": datasourceSchema.StringAttribute{
+ Description: "Type of monthly schedule (DayOfMonth, DateOfMonth)",
+ Computed: true,
+ },
+ "date_of_month": datasourceSchema.StringAttribute{
+ Description: "The date of the month for monthly schedules",
+ Computed: true,
+ },
+ "day_number_of_month": datasourceSchema.StringAttribute{
+ Description: "The day number of the month for monthly schedules",
+ Computed: true,
+ },
+ "days_of_week": datasourceSchema.ListAttribute{
+ Description: "List of days of the week for weekly schedules",
+ Computed: true,
+ ElementType: types.StringType,
+ },
+ "day_of_week": datasourceSchema.StringAttribute{
+ Description: "The day of the week for monthly schedules",
+ Computed: true,
+ },
+ },
+ },
},
},
- Optional: false,
Computed: true,
},
},
}
-
}
var _ EntitySchema = &DeploymentFreezeSchema{}
diff --git a/octopusdeploy_framework/schemas/deployment_freeze_validation.go b/octopusdeploy_framework/schemas/deployment_freeze_validation.go
new file mode 100644
index 00000000..6effbb39
--- /dev/null
+++ b/octopusdeploy_framework/schemas/deployment_freeze_validation.go
@@ -0,0 +1,201 @@
+package schemas
+
+import (
+ "context"
+ "fmt"
+ "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes"
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+)
+
+type daysOfWeekValidator struct{}
+
+func NewDaysOfWeekValidator() daysOfWeekValidator {
+ return daysOfWeekValidator{}
+}
+
+func (v daysOfWeekValidator) Description(ctx context.Context) string {
+ return "validates that days of the week are valid and in correct order (Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday)"
+}
+
+func (v daysOfWeekValidator) MarkdownDescription(ctx context.Context) string {
+ return v.Description(ctx)
+}
+
+func (v daysOfWeekValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) {
+ if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
+ return
+ }
+
+ validDays := map[string]int{
+ "Sunday": 0,
+ "Monday": 1,
+ "Tuesday": 2,
+ "Wednesday": 3,
+ "Thursday": 4,
+ "Friday": 5,
+ "Saturday": 6,
+ }
+
+ var days []string
+ req.ConfigValue.ElementsAs(ctx, &days, false)
+
+ for i := 1; i < len(days); i++ {
+ currentDay := days[i]
+ previousDay := days[i-1]
+
+ currentPos, currentExists := validDays[currentDay]
+ previousPos, previousExists := validDays[previousDay]
+
+ if !currentExists {
+ resp.Diagnostics.AddError(
+ "Invalid day of week",
+ fmt.Sprintf("'%s' is not a valid day of week. Must be one of: Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday", currentDay),
+ )
+ return
+ }
+
+ if !previousExists {
+ resp.Diagnostics.AddError(
+ "Invalid day of week",
+ fmt.Sprintf("'%s' is not a valid day of week. Must be one of: Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday", previousDay),
+ )
+ return
+ }
+
+ if currentPos <= previousPos {
+ resp.Diagnostics.AddError(
+ "Invalid day order",
+ fmt.Sprintf("Days of the week must be in order (Sunday through Saturday). Found '%s' after '%s'", currentDay, previousDay),
+ )
+ return
+ }
+ }
+}
+
+type recurringScheduleValidator struct{}
+
+func NewRecurringScheduleValidator() recurringScheduleValidator {
+ return recurringScheduleValidator{}
+}
+
+func (v recurringScheduleValidator) Description(_ context.Context) string {
+ return "validates that required fields are set based on the schedule type"
+}
+
+func (v recurringScheduleValidator) MarkdownDescription(ctx context.Context) string {
+ return v.Description(ctx)
+}
+
+func (v recurringScheduleValidator) ValidateObject(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) {
+ if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
+ return
+ }
+
+ var schedule struct {
+ Type types.String `tfsdk:"type"`
+ Unit types.Int64 `tfsdk:"unit"`
+ EndType types.String `tfsdk:"end_type"`
+ EndOnDate timetypes.RFC3339 `tfsdk:"end_on_date"`
+ EndAfterOccurrences types.Int64 `tfsdk:"end_after_occurrences"`
+ MonthlyScheduleType types.String `tfsdk:"monthly_schedule_type"`
+ DateOfMonth types.String `tfsdk:"date_of_month"`
+ DayNumberOfMonth types.String `tfsdk:"day_number_of_month"`
+ DaysOfWeek types.List `tfsdk:"days_of_week"`
+ DayOfWeek types.String `tfsdk:"day_of_week"`
+ }
+
+ resp.Diagnostics.Append(req.ConfigValue.As(ctx, &schedule, basetypes.ObjectAsOptions{})...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ scheduleType := schedule.Type.ValueString()
+
+ switch scheduleType {
+ case "Daily":
+ // Daily only requires type and unit which are already marked as required
+
+ case "Weekly":
+ if schedule.DaysOfWeek.IsNull() {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("days_of_week"),
+ "Missing Required Field",
+ "days_of_week must be set when schedule type is DaysPerWeek",
+ )
+ }
+
+ case "Monthly":
+ if schedule.MonthlyScheduleType.IsNull() {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("monthly_schedule_type"),
+ "Missing Required Field",
+ "monthly_schedule_type must be set when schedule type is DaysPerMonth",
+ )
+ return
+ }
+
+ monthlyType := schedule.MonthlyScheduleType.ValueString()
+ switch monthlyType {
+ case "DateOfMonth":
+ if schedule.DateOfMonth.IsNull() {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("date_of_month"),
+ "Missing Required Field",
+ "date_of_month must be set when monthly_schedule_type is DateOfMonth",
+ )
+ }
+
+ case "DayOfMonth":
+ if schedule.DayNumberOfMonth.IsNull() {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("day_number_of_month"),
+ "Missing Required Field",
+ "day_number_of_month must be set when monthly_schedule_type is DayOfMonth",
+ )
+ } else {
+ dayNum := schedule.DayNumberOfMonth.ValueString()
+ validDayNums := map[string]bool{
+ "1": true,
+ "2": true,
+ "3": true,
+ "4": true,
+ "L": true,
+ }
+ if !validDayNums[dayNum] {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("day_number_of_month"),
+ "Invalid Day Number",
+ fmt.Sprintf("day_number_of_month must be one of: 1, 2, 3, 4, L, got: %s", dayNum),
+ )
+ }
+ }
+ if schedule.DayOfWeek.IsNull() {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("day_of_week"),
+ "Missing Required Field",
+ "day_of_week must be set when monthly_schedule_type is DayOfMonth",
+ )
+ }
+
+ default:
+ resp.Diagnostics.AddAttributeError(
+ path.Root("monthly_schedule_type"),
+ "Invalid Monthly Schedule Type",
+ fmt.Sprintf("monthly_schedule_type must be either DateOfMonth or DayOfMonth, got: %s", monthlyType),
+ )
+ }
+
+ case "Annually":
+ // Annually only requires type and unit which are already marked as required
+
+ default:
+ resp.Diagnostics.AddAttributeError(
+ path.Root("type"),
+ "Invalid Schedule Type",
+ fmt.Sprintf("type must be one of: Daily, Weekly, Monthly, Annually, got: %s", scheduleType),
+ )
+ }
+}
diff --git a/octopusdeploy_framework/schemas/schema.go b/octopusdeploy_framework/schemas/schema.go
index 4e95c45f..576996a0 100644
--- a/octopusdeploy_framework/schemas/schema.go
+++ b/octopusdeploy_framework/schemas/schema.go
@@ -440,6 +440,7 @@ func GetDateTimeResourceSchema(description string, isRequired bool) resourceSche
return resourceSchema.StringAttribute{
Description: description,
Required: isRequired,
+ Optional: !isRequired,
CustomType: timetypes.RFC3339Type{},
Validators: []validator.String{
stringvalidator.RegexMatches(regexp.MustCompile(regex), fmt.Sprintf("must match RFC3339 format, %s", regex)),