Skip to content

Commit e0f3e51

Browse files
authored
feat: Add support for project deployment freezes (#27)
* chore: move function to a shared place * feat: add project deployment freeze resource * chore(docs): add example usages of project deployment freezes * chore(docs): update docs * fix: project environment scopes should not be preserved as they live on the freeze resource now * fix: add support for specifying users utc offset on recurring schedule * chore(docs): update docs * chore: add back moved functions
1 parent c5967e7 commit e0f3e51

File tree

8 files changed

+792
-77
lines changed

8 files changed

+792
-77
lines changed
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "octopusdeploy_project_deployment_freeze Resource - terraform-provider-octopusdeploy"
4+
subcategory: ""
5+
description: |-
6+
7+
---
8+
9+
# octopusdeploy_project_deployment_freeze (Resource)
10+
11+
12+
13+
## Example Usage
14+
15+
```terraform
16+
# basic freeze with no environment scopes
17+
resource "octopusdeploy_project_deployment_freeze" "freeze" {
18+
owner_id = "Projects-123"
19+
name = "Xmas"
20+
start = "2024-12-25T00:00:00+10:00"
21+
end = "2024-12-27T00:00:00+08:00"
22+
}
23+
24+
# Freeze with different timezones and single environment scope
25+
resource "octopusdeploy_deployment_freeze" "freeze" {
26+
owner_id = "Projects-123"
27+
name = "Xmas"
28+
start = "2024-12-25T00:00:00+10:00"
29+
end = "2024-12-27T00:00:00+08:00"
30+
environment_ids = ["Environments-123"]
31+
}
32+
33+
# Freeze recurring freeze yearly on Xmas
34+
resource "octopusdeploy_deployment_freeze" "freeze" {
35+
owner_id = "Projects-123"
36+
name = "Xmas"
37+
start = "2024-12-25T00:00:00+10:00"
38+
end = "2024-12-27T00:00:00+08:00"
39+
recurring_schedule = {
40+
type = "Annually"
41+
unit = 1
42+
end_type = "Never"
43+
}
44+
}
45+
46+
resource "octopusdeploy_deployment_freeze_project" "project_freeze" {
47+
deploymentfreeze_id= octopusdeploy_deployment_freeze.freeze.id
48+
project_id = "Projects-123"
49+
environment_ids = [ "Environments-123", "Environments-456" ]
50+
}
51+
52+
# Freeze with ids sourced from resources.
53+
resource "octopusdeploy_deployment_freeze" "freeze" {
54+
owner_id = resource.octopusdeploy_project.project1.id
55+
name = "End of financial year shutdown"
56+
start = "2025-06-30T00:00:00+10:00"
57+
end = "2025-07-02T00:00:00+10:00"
58+
environment_ids = [resource.octopusdeploy_environment.production.id]
59+
}
60+
61+
# Freeze with tenant environment scope and ids sourced from datasources.
62+
resource "octopusdeploy_deployment_freeze" "freeze" {
63+
owner_id = data.octopusdeploy_project.project1.id
64+
name = "End of financial year shutdown"
65+
start = "2025-06-30T00:00:00+10:00"
66+
end = "2025-07-02T00:00:00+10:00"
67+
environment_ids = [data.octopusdeploy_environments.default_environment.environments[0].id]
68+
}
69+
70+
resource "octopusdeploy_deployment_freeze_tenant" "tenant_freeze" {
71+
deploymentfreeze_id = octopusdeploy_deployment_freeze.freeze.id
72+
tenant_id = data.octopusdeploy_tenants.default_tenant.tenants[0].id
73+
project_id = data.octopusdeploy_project.project1.id
74+
environment_id = data.octopusdeploy_environments.default_environment.environments[0].id
75+
}
76+
```
77+
78+
<!-- schema generated by tfplugindocs -->
79+
## Schema
80+
81+
### Required
82+
83+
- `end` (String) The end time of the freeze, must be RFC3339 format
84+
- `name` (String) The name of this resource.
85+
- `owner_id` (String) The Owner ID of the freeze
86+
- `start` (String) The start time of the freeze, must be RFC3339 format
87+
88+
### Optional
89+
90+
- `environment_ids` (List of String) The environment IDs associated with this project deployment freeze scope
91+
- `recurring_schedule` (Attributes) (see [below for nested schema](#nestedatt--recurring_schedule))
92+
93+
### Read-Only
94+
95+
- `id` (String) The unique ID for this resource.
96+
97+
<a id="nestedatt--recurring_schedule"></a>
98+
### Nested Schema for `recurring_schedule`
99+
100+
Required:
101+
102+
- `end_type` (String) When the recurring schedule should end (Never, OnDate, AfterOccurrences)
103+
- `type` (String) Type of recurring schedule (Daily, Weekly, Monthly, Annually)
104+
- `unit` (Number) The unit value for the schedule
105+
106+
Optional:
107+
108+
- `date_of_month` (String) The date of the month for monthly schedules
109+
- `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
110+
- `day_of_week` (String) The day of the week for monthly schedules when using DayOfMonth type
111+
- `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
112+
- `end_after_occurrences` (Number) Number of occurrences after which the schedule should end
113+
- `end_on_date` (String) The date when the recurring schedule should end
114+
- `monthly_schedule_type` (String) Type of monthly schedule (DayOfMonth, DateOfMonth)
115+
- `utc_offset_in_minutes` (Number) The UTC offset in minutes of the timezone the schedule should run in, will use offset from freeze start date if not specified.
116+
117+
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# basic freeze with no environment scopes
2+
resource "octopusdeploy_project_deployment_freeze" "freeze" {
3+
owner_id = "Projects-123"
4+
name = "Xmas"
5+
start = "2024-12-25T00:00:00+10:00"
6+
end = "2024-12-27T00:00:00+08:00"
7+
}
8+
9+
# Freeze with different timezones and single environment scope
10+
resource "octopusdeploy_deployment_freeze" "freeze" {
11+
owner_id = "Projects-123"
12+
name = "Xmas"
13+
start = "2024-12-25T00:00:00+10:00"
14+
end = "2024-12-27T00:00:00+08:00"
15+
environment_ids = ["Environments-123"]
16+
}
17+
18+
# Freeze recurring freeze yearly on Xmas
19+
resource "octopusdeploy_deployment_freeze" "freeze" {
20+
owner_id = "Projects-123"
21+
name = "Xmas"
22+
start = "2024-12-25T00:00:00+10:00"
23+
end = "2024-12-27T00:00:00+08:00"
24+
recurring_schedule = {
25+
type = "Annually"
26+
unit = 1
27+
end_type = "Never"
28+
}
29+
}
30+
31+
resource "octopusdeploy_deployment_freeze_project" "project_freeze" {
32+
deploymentfreeze_id= octopusdeploy_deployment_freeze.freeze.id
33+
project_id = "Projects-123"
34+
environment_ids = [ "Environments-123", "Environments-456" ]
35+
}
36+
37+
# Freeze with ids sourced from resources.
38+
resource "octopusdeploy_deployment_freeze" "freeze" {
39+
owner_id = resource.octopusdeploy_project.project1.id
40+
name = "End of financial year shutdown"
41+
start = "2025-06-30T00:00:00+10:00"
42+
end = "2025-07-02T00:00:00+10:00"
43+
environment_ids = [resource.octopusdeploy_environment.production.id]
44+
}
45+
46+
# Freeze with tenant environment scope and ids sourced from datasources.
47+
resource "octopusdeploy_deployment_freeze" "freeze" {
48+
owner_id = data.octopusdeploy_project.project1.id
49+
name = "End of financial year shutdown"
50+
start = "2025-06-30T00:00:00+10:00"
51+
end = "2025-07-02T00:00:00+10:00"
52+
environment_ids = [data.octopusdeploy_environments.default_environment.environments[0].id]
53+
}
54+
55+
resource "octopusdeploy_deployment_freeze_tenant" "tenant_freeze" {
56+
deploymentfreeze_id = octopusdeploy_deployment_freeze.freeze.id
57+
tenant_id = data.octopusdeploy_tenants.default_tenant.tenants[0].id
58+
project_id = data.octopusdeploy_project.project1.id
59+
environment_id = data.octopusdeploy_environments.default_environment.environments[0].id
60+
}

octopusdeploy_framework/framework_provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ func (p *octopusDeployFrameworkProvider) Resources(ctx context.Context) []func()
143143
NewProcessChildStepsOrderResource,
144144
NewProcessTemplatedStepResource,
145145
NewProcessTemplatedChildStepResource,
146+
NewProjectDeploymentFreezeResource,
146147
}
147148
}
148149

octopusdeploy_framework/resource_deployment_freeze.go

Lines changed: 14 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package octopusdeploy_framework
22

33
import (
44
"context"
5+
56
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/deploymentfreezes"
67
"github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal"
78
"github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal/errors"
@@ -12,14 +13,14 @@ import (
1213
"github.com/hashicorp/terraform-plugin-framework/diag"
1314
"github.com/hashicorp/terraform-plugin-framework/resource"
1415
"github.com/hashicorp/terraform-plugin-framework/types"
15-
"time"
1616
)
1717

1818
const deploymentFreezeResourceName = "deployment_freeze"
1919

2020
type recurringScheduleModel struct {
2121
Type types.String `tfsdk:"type"`
2222
Unit types.Int64 `tfsdk:"unit"`
23+
UtcOffsetInMinutes types.Int64 `tfsdk:"utc_offset_in_minutes"`
2324
EndType types.String `tfsdk:"end_type"`
2425
EndOnDate timetypes.RFC3339 `tfsdk:"end_on_date"`
2526
EndAfterOccurrences types.Int64 `tfsdk:"end_after_occurrences"`
@@ -38,14 +39,6 @@ type deploymentFreezeModel struct {
3839
schemas.ResourceModel
3940
}
4041

41-
func getStringPointer(s types.String) *string {
42-
if s.IsNull() {
43-
return nil
44-
}
45-
value := s.ValueString()
46-
return &value
47-
}
48-
4942
type deploymentFreezeResource struct {
5043
*Config
5144
}
@@ -230,12 +223,12 @@ func mapFromState(state *deploymentFreezeModel) (*deploymentfreezes.DeploymentFr
230223
Type: deploymentfreezes.RecurringScheduleType(state.RecurringSchedule.Type.ValueString()),
231224
Unit: int(state.RecurringSchedule.Unit.ValueInt64()),
232225
EndType: deploymentfreezes.RecurringScheduleEndType(state.RecurringSchedule.EndType.ValueString()),
233-
EndAfterOccurrences: getOptionalIntValue(state.RecurringSchedule.EndAfterOccurrences),
234-
MonthlyScheduleType: getOptionalString(state.RecurringSchedule.MonthlyScheduleType),
235-
DateOfMonth: getOptionalString(state.RecurringSchedule.DateOfMonth),
236-
DayNumberOfMonth: getOptionalString(state.RecurringSchedule.DayNumberOfMonth),
226+
EndAfterOccurrences: util.GetOptionalIntValue(state.RecurringSchedule.EndAfterOccurrences),
227+
MonthlyScheduleType: util.GetOptionalString(state.RecurringSchedule.MonthlyScheduleType),
228+
DateOfMonth: util.GetOptionalString(state.RecurringSchedule.DateOfMonth),
229+
DayNumberOfMonth: util.GetOptionalString(state.RecurringSchedule.DayNumberOfMonth),
237230
DaysOfWeek: daysOfWeek,
238-
DayOfWeek: getOptionalString(state.RecurringSchedule.DayOfWeek),
231+
DayOfWeek: util.GetOptionalString(state.RecurringSchedule.DayOfWeek),
239232
}
240233

241234
if !state.RecurringSchedule.EndOnDate.IsNull() {
@@ -255,13 +248,13 @@ func mapToState(ctx context.Context, state *deploymentFreezeModel, deploymentFre
255248
state.ID = types.StringValue(deploymentFreeze.ID)
256249
state.Name = types.StringValue(deploymentFreeze.Name)
257250

258-
updatedStart, diags := calculateStateTime(ctx, state.Start, *deploymentFreeze.Start)
251+
updatedStart, diags := util.CalculateStateTime(ctx, state.Start, *deploymentFreeze.Start)
259252
if diags.HasError() {
260253
return diags
261254
}
262255
state.Start = updatedStart
263256

264-
updatedEnd, diags := calculateStateTime(ctx, state.End, *deploymentFreeze.End)
257+
updatedEnd, diags := util.CalculateStateTime(ctx, state.End, *deploymentFreeze.End)
265258
if diags.HasError() {
266259
return diags
267260
}
@@ -290,7 +283,7 @@ func mapToState(ctx context.Context, state *deploymentFreezeModel, deploymentFre
290283
Unit: types.Int64Value(int64(deploymentFreeze.RecurringSchedule.Unit)),
291284
EndType: types.StringValue(string(deploymentFreeze.RecurringSchedule.EndType)),
292285
DaysOfWeek: daysOfWeek,
293-
MonthlyScheduleType: mapOptionalStringValue(deploymentFreeze.RecurringSchedule.MonthlyScheduleType),
286+
MonthlyScheduleType: util.MapOptionalStringValue(deploymentFreeze.RecurringSchedule.MonthlyScheduleType),
294287
}
295288

296289
if deploymentFreeze.RecurringSchedule.EndOnDate != nil {
@@ -299,66 +292,11 @@ func mapToState(ctx context.Context, state *deploymentFreezeModel, deploymentFre
299292
state.RecurringSchedule.EndOnDate = timetypes.NewRFC3339Null()
300293
}
301294

302-
state.RecurringSchedule.EndAfterOccurrences = mapOptionalIntValue(deploymentFreeze.RecurringSchedule.EndAfterOccurrences)
303-
state.RecurringSchedule.DateOfMonth = mapOptionalStringValue(deploymentFreeze.RecurringSchedule.DateOfMonth)
304-
state.RecurringSchedule.DayNumberOfMonth = mapOptionalStringValue(deploymentFreeze.RecurringSchedule.DayNumberOfMonth)
305-
state.RecurringSchedule.DayOfWeek = mapOptionalStringValue(deploymentFreeze.RecurringSchedule.DayOfWeek)
295+
state.RecurringSchedule.EndAfterOccurrences = util.MapOptionalIntValue(deploymentFreeze.RecurringSchedule.EndAfterOccurrences)
296+
state.RecurringSchedule.DateOfMonth = util.MapOptionalStringValue(deploymentFreeze.RecurringSchedule.DateOfMonth)
297+
state.RecurringSchedule.DayNumberOfMonth = util.MapOptionalStringValue(deploymentFreeze.RecurringSchedule.DayNumberOfMonth)
298+
state.RecurringSchedule.DayOfWeek = util.MapOptionalStringValue(deploymentFreeze.RecurringSchedule.DayOfWeek)
306299
}
307300

308301
return nil
309302
}
310-
311-
func calculateStateTime(ctx context.Context, stateValue timetypes.RFC3339, updatedValue time.Time) (timetypes.RFC3339, diag.Diagnostics) {
312-
stateTime, diags := stateValue.ValueRFC3339Time()
313-
if diags.HasError() {
314-
return timetypes.RFC3339{}, diags
315-
}
316-
stateTimeUTC := timetypes.NewRFC3339TimeValue(stateTime.UTC())
317-
updatedValueUTC := updatedValue.UTC()
318-
valuesAreEqual, diags := stateTimeUTC.StringSemanticEquals(ctx, timetypes.NewRFC3339TimeValue(updatedValueUTC))
319-
if diags.HasError() {
320-
return timetypes.NewRFC3339Null(), diags
321-
}
322-
323-
if valuesAreEqual {
324-
return stateValue, diags
325-
}
326-
327-
location := stateTime.Location()
328-
newValue := timetypes.NewRFC3339TimeValue(updatedValueUTC.In(location))
329-
return newValue, diags
330-
}
331-
332-
func getOptionalStringPointer(value types.String) *string {
333-
if value.IsNull() {
334-
return nil
335-
}
336-
str := value.ValueString()
337-
return &str
338-
}
339-
func mapOptionalStringValue(value string) types.String {
340-
if value == "" {
341-
return types.StringNull()
342-
}
343-
return types.StringValue(value)
344-
}
345-
func getOptionalIntValue(value types.Int64) int {
346-
if value.IsNull() {
347-
return 0
348-
}
349-
return int(value.ValueInt64())
350-
}
351-
352-
func mapOptionalIntValue(value int) types.Int64 {
353-
if value == 0 {
354-
return types.Int64Null()
355-
}
356-
return types.Int64Value(int64(value))
357-
}
358-
359-
func getOptionalString(value types.String) string {
360-
if value.IsNull() {
361-
return ""
362-
}
363-
return value.ValueString()
364-
}

0 commit comments

Comments
 (0)