diff --git a/.changelog/3195.txt b/.changelog/3195.txt new file mode 100644 index 0000000000..76447e66de --- /dev/null +++ b/.changelog/3195.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/mongodbatlas_maintenance_window: Adds `protected_hours` and `time_zone_id` +``` + +```release-note:enhancement +data-source/mongodbatlas_maintenance_window: Adds `protected_hours` and `time_zone_id` +``` diff --git a/docs/data-sources/maintenance_window.md b/docs/data-sources/maintenance_window.md index 69cf2e639a..541d1d1db1 100644 --- a/docs/data-sources/maintenance_window.md +++ b/docs/data-sources/maintenance_window.md @@ -44,4 +44,12 @@ In addition to all arguments above, the following attributes are exported: * `start_asap` - Flag indicating whether project maintenance has been directed to start immediately. If you request that maintenance begin immediately, this field returns true from the time the request was made until the time the maintenance event completes. * `number_of_deferrals` - Number of times the current maintenance event for this project has been deferred, there can be a maximum of 2 deferrals. * `auto_defer_once_enabled` - Flag that indicates whether you want to defer all maintenance windows one week they would be triggered. +* `protected_hours` - (Optional) Defines the time period during which there will be no standard updates to the clusters. See [Protected Hours](#protected-hours). +* `time_zone_id` - Identifier for the current time zone of the maintenance window. This can only be updated via the Project Settings UI. + +### Protected Hours +* `start_hour_of_day` - Zero-based integer that represents the beginning hour of the day for the protected hours window. +* `end_hour_of_day` - Zero-based integer that represents the end hour of the day for the protected hours window. + + For more information see: [MongoDB Atlas API Reference.](https://docs.atlas.mongodb.com/reference/api/maintenance-windows/) \ No newline at end of file diff --git a/docs/resources/maintenance_window.md b/docs/resources/maintenance_window.md index 83b89a6cb2..bea1a9c461 100644 --- a/docs/resources/maintenance_window.md +++ b/docs/resources/maintenance_window.md @@ -21,6 +21,11 @@ Once maintenance is scheduled for your cluster, you cannot change your maintenan project_id = "" day_of_week = 3 hour_of_day = 4 + + protected_hours { + start_hour_of_day = 9 + end_hour_of_day = 17 + } } ``` @@ -41,14 +46,20 @@ Once maintenance is scheduled for your cluster, you cannot change your maintenan * `defer` - Defer the next scheduled maintenance for the given project for one week. * `auto_defer` - Defer any scheduled maintenance for the given project for one week. * `auto_defer_once_enabled` - Flag that indicates whether you want to defer all maintenance windows one week they would be triggered. +* `protected_hours` - (Optional) Defines the time period during which there will be no standard updates to the clusters. See [Protected Hours](#protected-hours). -> **NOTE:** The `start_asap` attribute can't be used because of breaks the Terraform flow, but you can enable via API. +### Protected Hours +* `start_hour_of_day` - Zero-based integer that represents the beginning hour of the day for the protected hours window. +- `end_hour_of_day` - Zero-based integer that represents the end hour of the day for the protected hours window. + ## Attributes Reference In addition to all arguments above, the following attributes are exported: * `number_of_deferrals` - Number of times the current maintenance event for this project has been deferred, there can be a maximum of 2 deferrals. +* `time_zone_id` - Identifier for the current time zone of the maintenance window. This can only be updated via the Project Settings UI. ## Import diff --git a/examples/mongodbatlas_maintenance_window/README.md b/examples/mongodbatlas_maintenance_window/README.md new file mode 100644 index 0000000000..3d988c7dc8 --- /dev/null +++ b/examples/mongodbatlas_maintenance_window/README.md @@ -0,0 +1,11 @@ +# MongoDB Atlas Provider - Configure Maintenance Window + +This example demonstrates how to configure maintenance windows for your Atlas project in Terraform. + +Required variables to set: + +- `public_key`: Atlas public key +- `private_key`: Atlas private key +- `org_id`: Unique 24-hexadecimal digit string that identifies the organization that contains the project and cluster. + +For additional information you can visit the [Maintenance Window Documentation](https://www.mongodb.com/docs/atlas/tutorial/cluster-maintenance-window/). \ No newline at end of file diff --git a/examples/mongodbatlas_maintenance_window/main.tf b/examples/mongodbatlas_maintenance_window/main.tf new file mode 100644 index 0000000000..5c18db523d --- /dev/null +++ b/examples/mongodbatlas_maintenance_window/main.tf @@ -0,0 +1,23 @@ +resource "mongodbatlas_project" "example" { + name = "project-name" + org_id = var.org_id +} + +resource "mongodbatlas_maintenance_window" "example" { + project_id = mongodbatlas_project.example.id + auto_defer_once_enabled = true + hour_of_day = 23 + day_of_week = 1 + protected_hours { + start_hour_of_day = 9 + end_hour_of_day = 17 + } +} + +data "mongodbatlas_maintenance_window" "example" { + project_id = mongodbatlas_maintenance_window.example.project_id +} + +output "time_zone_id" { + value = data.mongodbatlas_maintenance_window.example.time_zone_id +} \ No newline at end of file diff --git a/examples/mongodbatlas_maintenance_window/provider.tf b/examples/mongodbatlas_maintenance_window/provider.tf new file mode 100644 index 0000000000..e5aeda8033 --- /dev/null +++ b/examples/mongodbatlas_maintenance_window/provider.tf @@ -0,0 +1,4 @@ +provider "mongodbatlas" { + public_key = var.public_key + private_key = var.private_key +} \ No newline at end of file diff --git a/examples/mongodbatlas_maintenance_window/variables.tf b/examples/mongodbatlas_maintenance_window/variables.tf new file mode 100644 index 0000000000..503476e252 --- /dev/null +++ b/examples/mongodbatlas_maintenance_window/variables.tf @@ -0,0 +1,12 @@ +variable "public_key" { + description = "Public API key to authenticate to Atlas" + type = string +} +variable "private_key" { + description = "Private API key to authenticate to Atlas" + type = string +} +variable "org_id" { + description = "Atlas Organization ID" + type = string +} \ No newline at end of file diff --git a/examples/mongodbatlas_maintenance_window/versions.tf b/examples/mongodbatlas_maintenance_window/versions.tf new file mode 100644 index 0000000000..1888453805 --- /dev/null +++ b/examples/mongodbatlas_maintenance_window/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_providers { + mongodbatlas = { + source = "mongodb/mongodbatlas" + version = "~> 1.0" + } + } + required_version = ">= 1.0" +} diff --git a/internal/service/maintenancewindow/data_source_maintenance_window.go b/internal/service/maintenancewindow/data_source_maintenance_window.go index 311af7794a..a3440cfa9c 100644 --- a/internal/service/maintenancewindow/data_source_maintenance_window.go +++ b/internal/service/maintenancewindow/data_source_maintenance_window.go @@ -2,9 +2,11 @@ package maintenancewindow import ( "context" + "fmt" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/config" ) @@ -36,6 +38,26 @@ func DataSource() *schema.Resource { Type: schema.TypeBool, Computed: true, }, + "time_zone_id": { + Type: schema.TypeString, + Computed: true, + }, + "protected_hours": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "end_hour_of_day": { + Type: schema.TypeInt, + Computed: true, + }, + "start_hour_of_day": { + Type: schema.TypeInt, + Computed: true, + }, + }, + }, + }, }, } } @@ -69,6 +91,16 @@ func dataSourceRead(ctx context.Context, d *schema.ResourceData, meta any) diag. return diag.Errorf(errorMaintenanceRead, projectID, err) } + if err := d.Set("time_zone_id", maintenance.GetTimeZoneId()); err != nil { + return diag.FromErr(fmt.Errorf(errorMaintenanceRead, projectID, err)) + } + + if maintenance.ProtectedHours != nil { + if err := d.Set("protected_hours", flattenProtectedHours(maintenance.GetProtectedHours())); err != nil { + return diag.FromErr(fmt.Errorf(errorMaintenanceRead, projectID, err)) + } + } + d.SetId(projectID) return nil diff --git a/internal/service/maintenancewindow/data_source_maintenance_window_test.go b/internal/service/maintenancewindow/data_source_maintenance_window_test.go index 6d44f7377d..79e8fd9cce 100644 --- a/internal/service/maintenancewindow/data_source_maintenance_window_test.go +++ b/internal/service/maintenancewindow/data_source_maintenance_window_test.go @@ -6,8 +6,9 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/mongodb/terraform-provider-mongodbatlas/internal/testutil/acc" "github.com/spf13/cast" + + "github.com/mongodb/terraform-provider-mongodbatlas/internal/testutil/acc" ) const dataSourceName = "mongodbatlas_maintenance_window.test" @@ -32,6 +33,7 @@ func TestAccConfigDSMaintenanceWindow_basic(t *testing.T) { resource.TestCheckResourceAttr(dataSourceName, "day_of_week", cast.ToString(dayOfWeek)), resource.TestCheckResourceAttr(dataSourceName, "hour_of_day", cast.ToString(hourOfDay)), resource.TestCheckResourceAttr(dataSourceName, "auto_defer_once_enabled", "true"), + resource.TestCheckResourceAttrSet(dataSourceName, "time_zone_id"), ), }, }, diff --git a/internal/service/maintenancewindow/resource_maintenance_window.go b/internal/service/maintenancewindow/resource_maintenance_window.go index 7a0b2e52cd..79cbe54288 100644 --- a/internal/service/maintenancewindow/resource_maintenance_window.go +++ b/internal/service/maintenancewindow/resource_maintenance_window.go @@ -6,10 +6,11 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/spf13/cast" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/validate" "github.com/mongodb/terraform-provider-mongodbatlas/internal/config" - "github.com/spf13/cast" "go.mongodb.org/atlas-sdk/v20250312002/admin" ) @@ -84,6 +85,27 @@ func Resource() *schema.Resource { Optional: true, Computed: true, }, + "time_zone_id": { + Type: schema.TypeString, + Computed: true, + }, + "protected_hours": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "end_hour_of_day": { + Type: schema.TypeInt, + Required: true, + }, + "start_hour_of_day": { + Type: schema.TypeInt, + Required: true, + }, + }, + }, + }, }, } } @@ -110,6 +132,7 @@ func resourceCreate(ctx context.Context, d *schema.ResourceData, meta any) diag. params.AutoDeferOnceEnabled = conversion.Pointer(autoDeferOnceEnabled.(bool)) } + params.ProtectedHours = newProtectedHours(d) _, err := connV2.MaintenanceWindowsApi.UpdateMaintenanceWindow(ctx, projectID, params).Execute() if err != nil { return diag.FromErr(fmt.Errorf(errorMaintenanceCreate, projectID, err)) @@ -127,6 +150,19 @@ func resourceCreate(ctx context.Context, d *schema.ResourceData, meta any) diag. return resourceRead(ctx, d, meta) } +func newProtectedHours(d *schema.ResourceData) *admin.ProtectedHours { + if protectedHours, ok := d.Get("protected_hours").([]any); ok && conversion.HasElementsSliceOrMap(protectedHours) { + item := protectedHours[0].(map[string]any) + + return &admin.ProtectedHours{ + EndHourOfDay: conversion.IntPtr(item["end_hour_of_day"].(int)), + StartHourOfDay: conversion.IntPtr(item["start_hour_of_day"].(int)), + } + } + + return nil +} + func resourceRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { connV2 := meta.(*config.MongoDBClient).AtlasV2 projectID := d.Id() @@ -165,9 +201,27 @@ func resourceRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Di return diag.FromErr(fmt.Errorf(errorMaintenanceRead, projectID, err)) } + if err := d.Set("time_zone_id", maintenanceWindow.GetTimeZoneId()); err != nil { + return diag.FromErr(fmt.Errorf(errorMaintenanceRead, projectID, err)) + } + + if maintenanceWindow.ProtectedHours != nil { + if err := d.Set("protected_hours", flattenProtectedHours(maintenanceWindow.GetProtectedHours())); err != nil { + return diag.FromErr(fmt.Errorf(errorMaintenanceRead, projectID, err)) + } + } return nil } +func flattenProtectedHours(protectedHours admin.ProtectedHours) []map[string]int { + res := make([]map[string]int, 0) + res = append(res, map[string]int{ + "end_hour_of_day": protectedHours.GetEndHourOfDay(), + "start_hour_of_day": protectedHours.GetStartHourOfDay(), + }) + return res +} + func resourceUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { connV2 := meta.(*config.MongoDBClient).AtlasV2 projectID := d.Id() @@ -190,6 +244,20 @@ func resourceUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag. params.AutoDeferOnceEnabled = conversion.Pointer(d.Get("auto_defer_once_enabled").(bool)) } + if oldPAny, newPAny := d.GetChange("protected_hours"); d.HasChange("protected_hours") { + oldP := oldPAny.([]any) + newP := newPAny.([]any) + + if len(oldP) == 1 && len(newP) == 0 { + params.ProtectedHours = &admin.ProtectedHours{ + StartHourOfDay: nil, + EndHourOfDay: nil, + } + } else { + params.ProtectedHours = newProtectedHours(d) + } + } + _, err := connV2.MaintenanceWindowsApi.UpdateMaintenanceWindow(ctx, projectID, params).Execute() if err != nil { return diag.FromErr(fmt.Errorf(errorMaintenanceUpdate, projectID, err)) diff --git a/internal/service/maintenancewindow/resource_maintenance_window_migration_test.go b/internal/service/maintenancewindow/resource_maintenance_window_migration_test.go index 01562a759f..a6705dd74f 100644 --- a/internal/service/maintenancewindow/resource_maintenance_window_migration_test.go +++ b/internal/service/maintenancewindow/resource_maintenance_window_migration_test.go @@ -17,7 +17,7 @@ func TestMigConfigMaintenanceWindow_basic(t *testing.T) { projectName = acc.RandomProjectName() dayOfWeek = 7 hourOfDay = 3 - config = configBasic(orgID, projectName, dayOfWeek, conversion.Pointer(hourOfDay)) + config = configBasic(orgID, projectName, dayOfWeek, conversion.Pointer(hourOfDay), nil) ) resource.ParallelTest(t, resource.TestCase{ diff --git a/internal/service/maintenancewindow/resource_maintenance_window_test.go b/internal/service/maintenancewindow/resource_maintenance_window_test.go index bc21be4342..b2054a7f42 100644 --- a/internal/service/maintenancewindow/resource_maintenance_window_test.go +++ b/internal/service/maintenancewindow/resource_maintenance_window_test.go @@ -9,13 +9,26 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/spf13/cast" + "go.mongodb.org/atlas-sdk/v20250312002/admin" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" "github.com/mongodb/terraform-provider-mongodbatlas/internal/testutil/acc" - "github.com/spf13/cast" ) const resourceName = "mongodbatlas_maintenance_window.test" +var ( + defaultProtectedHours = &admin.ProtectedHours{ + StartHourOfDay: conversion.Pointer(9), + EndHourOfDay: conversion.Pointer(17), + } + updatedProtectedHours = &admin.ProtectedHours{ + StartHourOfDay: conversion.Pointer(10), + EndHourOfDay: conversion.Pointer(15), + } +) + func TestAccConfigRSMaintenanceWindow_basic(t *testing.T) { var ( orgID = os.Getenv("MONGODB_ATLAS_ORG_ID") @@ -32,20 +45,20 @@ func TestAccConfigRSMaintenanceWindow_basic(t *testing.T) { Steps: []resource.TestStep{ { // testing hour_of_day set to 0 during creation phase does not return errors - Config: configBasic(orgID, projectName, dayOfWeek, conversion.Pointer(hourOfDay)), - Check: checkBasic(dayOfWeek, hourOfDay), + Config: configBasic(orgID, projectName, dayOfWeek, conversion.Pointer(hourOfDay), defaultProtectedHours), + Check: checkBasic(dayOfWeek, hourOfDay, defaultProtectedHours), }, { - Config: configBasic(orgID, projectName, dayOfWeek, conversion.Pointer(hourOfDayUpdated)), - Check: checkBasic(dayOfWeek, hourOfDayUpdated), + Config: configBasic(orgID, projectName, dayOfWeek, conversion.Pointer(hourOfDayUpdated), updatedProtectedHours), + Check: checkBasic(dayOfWeek, hourOfDayUpdated, updatedProtectedHours), }, { - Config: configBasic(orgID, projectName, dayOfWeekUpdated, conversion.Pointer(hourOfDay)), - Check: checkBasic(dayOfWeekUpdated, hourOfDay), + Config: configBasic(orgID, projectName, dayOfWeekUpdated, conversion.Pointer(hourOfDay), nil), + Check: checkBasic(dayOfWeekUpdated, hourOfDay, nil), }, { - Config: configBasic(orgID, projectName, dayOfWeek, conversion.Pointer(hourOfDay)), - Check: checkBasic(dayOfWeek, hourOfDay), + Config: configBasic(orgID, projectName, dayOfWeek, conversion.Pointer(hourOfDay), defaultProtectedHours), + Check: checkBasic(dayOfWeek, hourOfDay, defaultProtectedHours), }, { ResourceName: resourceName, @@ -69,8 +82,8 @@ func TestAccConfigRSMaintenanceWindow_emptyHourOfDay(t *testing.T) { ProtoV6ProviderFactories: acc.TestAccProviderV6Factories, Steps: []resource.TestStep{ { - Config: configBasic(orgID, projectName, dayOfWeek, nil), - Check: checkBasic(dayOfWeek, 0), + Config: configBasic(orgID, projectName, dayOfWeek, nil, defaultProtectedHours), + Check: checkBasic(dayOfWeek, 0, defaultProtectedHours), }, }, }) @@ -97,6 +110,7 @@ func TestAccConfigRSMaintenanceWindow_autoDeferActivated(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "hour_of_day", cast.ToString(hourOfDay)), resource.TestCheckResourceAttr(resourceName, "number_of_deferrals", "0"), resource.TestCheckResourceAttr(resourceName, "auto_defer_once_enabled", "true"), + resource.TestCheckResourceAttrSet(resourceName, "time_zone_id"), ), }, }, @@ -132,11 +146,20 @@ func importStateIDFunc(resourceName string) resource.ImportStateIdFunc { } } -func configBasic(orgID, projectName string, dayOfWeek int, hourOfDay *int) string { +func configBasic(orgID, projectName string, dayOfWeek int, hourOfDay *int, protectedHours *admin.ProtectedHours) string { hourOfDayAttr := "" if hourOfDay != nil { hourOfDayAttr = fmt.Sprintf("hour_of_day = %d", *hourOfDay) } + protectedHoursStr := "" + if protectedHours != nil { + protectedHoursStr = fmt.Sprintf(` + protected_hours { + start_hour_of_day = %[1]d + end_hour_of_day = %[2]d + }`, *protectedHours.StartHourOfDay, *protectedHours.EndHourOfDay) + } + return fmt.Sprintf(` resource "mongodbatlas_project" "test" { name = %[2]q @@ -146,7 +169,9 @@ func configBasic(orgID, projectName string, dayOfWeek int, hourOfDay *int) strin project_id = mongodbatlas_project.test.id day_of_week = %[3]d %[4]s - }`, orgID, projectName, dayOfWeek, hourOfDayAttr) + %[5]s + + }`, orgID, projectName, dayOfWeek, hourOfDayAttr, protectedHoursStr) } func configWithAutoDeferEnabled(orgID, projectName string, dayOfWeek, hourOfDay int) string { @@ -163,12 +188,21 @@ func configWithAutoDeferEnabled(orgID, projectName string, dayOfWeek, hourOfDay }`, orgID, projectName, dayOfWeek, hourOfDay) } -func checkBasic(dayOfWeek, hourOfDay int) resource.TestCheckFunc { - return resource.ComposeAggregateTestCheckFunc( +func checkBasic(dayOfWeek, hourOfDay int, protectedHours *admin.ProtectedHours) resource.TestCheckFunc { + checks := []resource.TestCheckFunc{ checkExists(resourceName), resource.TestCheckResourceAttrSet(resourceName, "project_id"), resource.TestCheckResourceAttr(resourceName, "day_of_week", cast.ToString(dayOfWeek)), resource.TestCheckResourceAttr(resourceName, "hour_of_day", cast.ToString(hourOfDay)), resource.TestCheckResourceAttr(resourceName, "number_of_deferrals", "0"), - ) + } + if protectedHours != nil { + checks = append(checks, + resource.TestCheckResourceAttr(resourceName, "protected_hours.0.start_hour_of_day", cast.ToString(*protectedHours.StartHourOfDay)), + resource.TestCheckResourceAttr(resourceName, "protected_hours.0.end_hour_of_day", cast.ToString(*protectedHours.EndHourOfDay)), + ) + } else { + checks = append(checks, resource.TestCheckResourceAttr(resourceName, "protected_hours.#", "0")) + } + return resource.ComposeAggregateTestCheckFunc(checks...) }