Skip to content

Commit

Permalink
Add support recurring freeze
Browse files Browse the repository at this point in the history
  • Loading branch information
HuyPhanNguyen committed Dec 5, 2024
1 parent 8ff4bde commit 55692e7
Show file tree
Hide file tree
Showing 6 changed files with 286 additions and 31 deletions.
35 changes: 35 additions & 0 deletions docs/resources/deployment_freeze.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.

<a id="nestedatt--recurring_schedule"></a>
### Nested Schema for `recurring_schedule`

Required:

- `end_type` (String) When the recurring schedule should end (Never, OnDate, AfterOccurrences)
- `type` (String) Type of recurring schedule (OnceDaily, DaysPerWeek, DaysPerMonth, 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) 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
- `monthly_schedule_type` (String) Type of monthly schedule (DayOfMonth, DateOfMonth)


12 changes: 12 additions & 0 deletions examples/resources/octopusdeploy_deployment_freeze/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
173 changes: 153 additions & 20 deletions octopusdeploy_framework/resource_deployment_freeze.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
}
Expand Down Expand Up @@ -170,6 +192,69 @@ 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 endOnDate *time.Time
var endAfterOccurrences *int
var daysOfWeek []string

if !state.RecurringSchedule.EndOnDate.IsNull() {
date, diagsDate := state.RecurringSchedule.EndOnDate.ValueRFC3339Time()
if diagsDate.HasError() {
diags.Append(diagsDate...)
return nil, diags
}
endOnDate = &date
}

if !state.RecurringSchedule.EndAfterOccurrences.IsNull() {
occurrences := int(state.RecurringSchedule.EndAfterOccurrences.ValueInt64())
endAfterOccurrences = &occurrences
}

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()),
EndOnDate: endOnDate,
EndAfterOccurrences: endAfterOccurrences,
MonthlyScheduleType: getOptionalString(state.RecurringSchedule.MonthlyScheduleType),
DateOfMonth: getOptionalStringPointer(state.RecurringSchedule.DateOfMonth),
DayNumberOfMonth: getOptionalStringPointer(state.RecurringSchedule.DayNumberOfMonth),
DaysOfWeek: daysOfWeek,
DayOfWeek: getOptionalStringPointer(state.RecurringSchedule.DayOfWeek),
}
}

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)
Expand All @@ -186,6 +271,49 @@ 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()
}

if deploymentFreeze.RecurringSchedule.EndAfterOccurrences != nil {
state.RecurringSchedule.EndAfterOccurrences = types.Int64Value(int64(*deploymentFreeze.RecurringSchedule.EndAfterOccurrences))
} else {
state.RecurringSchedule.EndAfterOccurrences = types.Int64Null()
}

state.RecurringSchedule.DateOfMonth = mapOptionalStringPointer(deploymentFreeze.RecurringSchedule.DateOfMonth)
state.RecurringSchedule.DayNumberOfMonth = mapOptionalStringPointer(deploymentFreeze.RecurringSchedule.DayNumberOfMonth)
state.RecurringSchedule.DayOfWeek = mapOptionalStringPointer(deploymentFreeze.RecurringSchedule.DayOfWeek)
}

return nil
}

Expand All @@ -210,25 +338,30 @@ 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)
}

freeze := deploymentfreezes.DeploymentFreeze{
Name: state.Name.ValueString(),
Start: &start,
End: &end,
func mapOptionalStringPointer(value *string) types.String {
if value == nil {
return types.StringNull()
}
return types.StringValue(*value)
}

freeze.ID = state.ID.String()
return &freeze, nil
func getOptionalString(value types.String) string {
if value.IsNull() {
return ""
}
return value.ValueString()
}
53 changes: 42 additions & 11 deletions octopusdeploy_framework/resource_deployment_freeze_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,27 +38,43 @@ 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(
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, tenantName, false),
Config: testDeploymentFreezeBasic(localName, name+"1", start, updatedEnd, spaceName, []string{environmentName1, environmentName2}, projectName, projectGroupName, lifecycleName, tenantName, false, false),
},
{
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", "DaysPerWeek"),
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, false, true),
},
{
Check: resource.ComposeTestCheckFunc(
testDeploymentFreezeExists(resourceName),
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),
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)
Expand All @@ -74,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 = "DaysPerWeek"
unit = 24
end_type = "AfterOccurrences"
end_after_occurrences = 5
days_of_week = ["Monday", "Wednesday", "Friday"]
}`
}

freezeConfig += `
}`

config := fmt.Sprintf(`
# Space Configuration
%s
Expand All @@ -91,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
Expand All @@ -107,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, ","))

Expand Down
Loading

0 comments on commit 55692e7

Please sign in to comment.