From 560d63d096a44681ca8a75540dc903959a020fed Mon Sep 17 00:00:00 2001 From: Alexander Dahmen Date: Mon, 30 Mar 2026 08:50:58 +0200 Subject: [PATCH 1/2] feat(serverupdate): Onboard enable update service STACKITTPR-586 Signed-off-by: Alexander Dahmen --- docs/data-sources/server_update_enable.md | 30 ++ docs/resources/server_update_enable.md | 31 ++ docs/resources/server_update_schedule.md | 8 + .../resource.tf | 8 + .../serverupdate/enable/datasource.go | 179 ++++++++++ .../serverupdate/enable/datasource_test.go | 75 ++++ .../services/serverupdate/enable/resource.go | 322 ++++++++++++++++++ .../serverupdate/enable/resource_test.go | 76 +++++ .../serverupdate/schedule/resource.go | 7 + .../serverupdate/serverupdate_acc_test.go | 31 +- .../serverupdate/testdata/datasource.tf | 15 + .../serverupdate/testdata/resource-max.tf | 19 +- .../serverupdate/testdata/resource-min.tf | 18 +- stackit/provider.go | 3 + 14 files changed, 798 insertions(+), 24 deletions(-) create mode 100644 docs/data-sources/server_update_enable.md create mode 100644 docs/resources/server_update_enable.md create mode 100644 stackit/internal/services/serverupdate/enable/datasource.go create mode 100644 stackit/internal/services/serverupdate/enable/datasource_test.go create mode 100644 stackit/internal/services/serverupdate/enable/resource.go create mode 100644 stackit/internal/services/serverupdate/enable/resource_test.go create mode 100644 stackit/internal/services/serverupdate/testdata/datasource.tf diff --git a/docs/data-sources/server_update_enable.md b/docs/data-sources/server_update_enable.md new file mode 100644 index 000000000..e3ad0a04d --- /dev/null +++ b/docs/data-sources/server_update_enable.md @@ -0,0 +1,30 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_server_update_enable Data Source - stackit" +subcategory: "" +description: |- + Server update enable datasource schema. Must have a region specified in the provider configuration. +--- + +# stackit_server_update_enable (Data Source) + +Server update enable datasource schema. Must have a `region` specified in the provider configuration. + + + + +## Schema + +### Required + +- `project_id` (String) STACKIT Project ID to which the server update enable is associated. +- `server_id` (String) Server ID to which the server update enable is associated. + +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + +### Read-Only + +- `enabled` (Boolean) Set to true if the service is enabled. +- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`server_id`,`region`". diff --git a/docs/resources/server_update_enable.md b/docs/resources/server_update_enable.md new file mode 100644 index 000000000..6911270c1 --- /dev/null +++ b/docs/resources/server_update_enable.md @@ -0,0 +1,31 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_server_update_enable Resource - stackit" +subcategory: "" +description: |- + Server update enable resource schema. Must have a region specified in the provider configuration. Always use only one enable resource per server. +--- + +# stackit_server_update_enable (Resource) + +Server update enable resource schema. Must have a `region` specified in the provider configuration. Always use only one enable resource per server. + + + + +## Schema + +### Required + +- `project_id` (String) STACKIT Project ID to which the server update enable is associated. +- `server_id` (String) Server ID to which the server update enable is associated. + +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. +- `update_policy_id` (String) The update policy ID. + +### Read-Only + +- `enabled` (Boolean) Set to true if the service is enabled. +- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`server_id`,`region`". diff --git a/docs/resources/server_update_schedule.md b/docs/resources/server_update_schedule.md index ea7988d1d..66e867b06 100644 --- a/docs/resources/server_update_schedule.md +++ b/docs/resources/server_update_schedule.md @@ -20,6 +20,14 @@ resource "stackit_server_update_schedule" "example" { rrule = "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1" enabled = true maintenance_window = 1 + depends_on = [ + stackit_server_update_enable.enable + ] +} + +resource "stackit_server_update_enable" "enable" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + server_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" } # Only use the import statement, if you want to import an existing server update schedule diff --git a/examples/resources/stackit_server_update_schedule/resource.tf b/examples/resources/stackit_server_update_schedule/resource.tf index bfc86d7bb..bd905be1c 100644 --- a/examples/resources/stackit_server_update_schedule/resource.tf +++ b/examples/resources/stackit_server_update_schedule/resource.tf @@ -5,6 +5,14 @@ resource "stackit_server_update_schedule" "example" { rrule = "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1" enabled = true maintenance_window = 1 + depends_on = [ + stackit_server_update_enable.enable + ] +} + +resource "stackit_server_update_enable" "enable" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + server_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" } # Only use the import statement, if you want to import an existing server update schedule diff --git a/stackit/internal/services/serverupdate/enable/datasource.go b/stackit/internal/services/serverupdate/enable/datasource.go new file mode 100644 index 000000000..662a09555 --- /dev/null +++ b/stackit/internal/services/serverupdate/enable/datasource.go @@ -0,0 +1,179 @@ +package enable + +import ( + "context" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + serverUpdateUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverupdate/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &serverUpdateEnableDataSource{} +) + +type DataModel struct { + Id types.String `tfsdk:"id"` // needed by TF + ProjectId types.String `tfsdk:"project_id"` + ServerId types.String `tfsdk:"server_id"` + Enabled types.Bool `tfsdk:"enabled"` + Region types.String `tfsdk:"region"` +} + +// NewServerUpdateEnableDataSource is a helper function to simplify the provider implementation. +func NewServerUpdateEnableDataSource() datasource.DataSource { + return &serverUpdateEnableDataSource{} +} + +// serverUpdateEnableDataSource is the data source implementation. +type serverUpdateEnableDataSource struct { + client *serverupdate.APIClient + providerData core.ProviderData +} + +// Metadata returns the data source type name. +func (d *serverUpdateEnableDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_server_update_enable" +} + +// Configure adds the provider configured client to the data source. +func (d *serverUpdateEnableDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + apiClient := serverUpdateUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + d.client = apiClient + tflog.Info(ctx, "Server update client client configured") +} + +// Schema defines the schema for the resource. +func (d *serverUpdateEnableDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + descriptions := map[string]string{ + "main": "Server update enable datasource schema. Must have a `region` specified in the provider configuration.", + "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`server_id`,`region`\".", + "project_id": "STACKIT Project ID to which the server update enable is associated.", + "server_id": "Server ID to which the server update enable is associated.", + "enabled": "Set to true if the service is enabled.", + "region": "The resource region. If not defined, the provider region is used.", + } + + resp.Schema = schema.Schema{ + Description: descriptions["main"], + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + }, + "project_id": schema.StringAttribute{ + Description: descriptions["project_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "server_id": schema.StringAttribute{ + Description: descriptions["server_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "enabled": schema.BoolAttribute{ + Description: descriptions["enabled"], + Computed: true, + }, + "region": schema.StringAttribute{ + Optional: true, + // the region cannot be found automatically, so it has to be passed + Description: descriptions["region"], + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (d *serverUpdateEnableDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model DataModel + diags := req.Config.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + serverId := model.ServerId.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) + + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "server_id", serverId) + ctx = tflog.SetField(ctx, "region", region) + + serviceResp, err := d.client.GetServiceResource(ctx, projectId, serverId, region).Execute() + if err != nil { + utils.LogError( + ctx, + &resp.Diagnostics, + err, + "Reading server update enable", + fmt.Sprintf("Server update enable does not exist for this server %q.", serverId), + map[int]string{ + http.StatusForbidden: fmt.Sprintf("Project with ID %q or server with ID %q not found or forbidden access", projectId, serverId), + }, + ) + resp.State.RemoveResource(ctx) + return + } + + ctx = core.LogResponse(ctx) + + // Map response body to schema + err = mapDataFields(serviceResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading server update enable", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Server update enable read") +} + +func mapDataFields(serviceResp *serverupdate.GetUpdateServiceResponse, model *DataModel, region string) error { + if serviceResp == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), model.ServerId.ValueString(), region) + model.Region = types.StringValue(region) + model.Enabled = types.BoolPointerValue(serviceResp.Enabled) + + return nil +} diff --git a/stackit/internal/services/serverupdate/enable/datasource_test.go b/stackit/internal/services/serverupdate/enable/datasource_test.go new file mode 100644 index 000000000..a50621f99 --- /dev/null +++ b/stackit/internal/services/serverupdate/enable/datasource_test.go @@ -0,0 +1,75 @@ +package enable + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" +) + +func TestDataMapFields(t *testing.T) { + const testRegion = "eu01" + id := fmt.Sprintf("%s,%s,%s", "pid", "sid", testRegion) + tests := []struct { + description string + input *serverupdate.GetUpdateServiceResponse + expected DataModel + isValid bool + }{ + { + "default_values", + &serverupdate.GetUpdateServiceResponse{}, + DataModel{ + Id: types.StringValue(id), + ProjectId: types.StringValue("pid"), + ServerId: types.StringValue("sid"), + Region: types.StringValue("eu01"), + }, + true, + }, + { + "simple_values", + &serverupdate.GetUpdateServiceResponse{ + Enabled: utils.Ptr(true), + }, + DataModel{ + Id: types.StringValue(id), + ProjectId: types.StringValue("pid"), + ServerId: types.StringValue("sid"), + Region: types.StringValue("eu01"), + Enabled: types.BoolValue(true), + }, + true, + }, + { + "nil_response", + nil, + DataModel{}, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + model := &DataModel{ + ProjectId: tt.expected.ProjectId, + ServerId: tt.expected.ServerId, + } + err := mapDataFields(tt.input, model, "eu01") + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(model, &tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} diff --git a/stackit/internal/services/serverupdate/enable/resource.go b/stackit/internal/services/serverupdate/enable/resource.go new file mode 100644 index 000000000..a22d90128 --- /dev/null +++ b/stackit/internal/services/serverupdate/enable/resource.go @@ -0,0 +1,322 @@ +package enable + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + serverUpdateUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverupdate/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &serverUpdateEnableResource{} + _ resource.ResourceWithConfigure = &serverUpdateEnableResource{} +) + +type Model struct { + Id types.String `tfsdk:"id"` // needed by TF + ProjectId types.String `tfsdk:"project_id"` + ServerId types.String `tfsdk:"server_id"` + UpdatePolicyId types.String `tfsdk:"update_policy_id"` + Enabled types.Bool `tfsdk:"enabled"` + Region types.String `tfsdk:"region"` +} + +// NewServerUpdateEnableResource is a helper function to simplify the provider implementation. +func NewServerUpdateEnableResource() resource.Resource { + return &serverUpdateEnableResource{} +} + +// serverUpdateEnableResource is the resource implementation. +type serverUpdateEnableResource struct { + client *serverupdate.APIClient + providerData core.ProviderData +} + +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *serverUpdateEnableResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + +// Metadata returns the resource type name. +func (r *serverUpdateEnableResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_server_update_enable" +} + +// Configure adds the provider configured client to the resource. +func (r *serverUpdateEnableResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + apiClient := serverUpdateUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + r.client = apiClient + tflog.Info(ctx, "Server update client configured") +} + +// Schema defines the schema for the resource. +func (r *serverUpdateEnableResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + descriptions := map[string]string{ + "main": "Server update enable resource schema. Must have a `region` specified in the provider configuration. Always use only one enable resource per server.", + "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`server_id`,`region`\".", + "project_id": "STACKIT Project ID to which the server update enable is associated.", + "server_id": "Server ID to which the server update enable is associated.", + "region": "The resource region. If not defined, the provider region is used.", + "enabled": "Set to true if the service is enabled.", + "update_policy_id": "The update policy ID.", + } + + resp.Schema = schema.Schema{ + Description: descriptions["main"], + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "project_id": schema.StringAttribute{ + Description: descriptions["project_id"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "server_id": schema.StringAttribute{ + Description: descriptions["server_id"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "update_policy_id": schema.StringAttribute{ + Description: descriptions["update_policy_id"], + Optional: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "enabled": schema.BoolAttribute{ + Description: descriptions["enabled"], + Computed: true, + }, + "region": schema.StringAttribute{ + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + Description: descriptions["region"], + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *serverUpdateEnableResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + serverId := model.ServerId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "server_id", serverId) + ctx = tflog.SetField(ctx, "region", region) + + err := r.client.EnableServiceResource(ctx, projectId, serverId, region).EnableServiceResourcePayload(serverupdate.EnableServiceResourcePayload{}).Execute() + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) + + if !(ok && oapiErr.StatusCode == http.StatusConflict) { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server update enable", fmt.Sprintf("Calling API: %v", err)) + return + } + tflog.Info(ctx, "Server update is already enabled for this server. Please check duplicate resources.") + } + + serviceResp, err := r.client.GetServiceResource(ctx, projectId, serverId, region).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading server update enable", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + // Map response body to schema + err = mapFields(serviceResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading server update enable", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Info(ctx, "Server update enable created") +} + +// Read refreshes the Terraform state with the latest data. +func (r *serverUpdateEnableResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + projectId := model.ProjectId.ValueString() + serverId := model.ServerId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + + ctx = core.InitProviderContext(ctx) + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "server_id", serverId) + ctx = tflog.SetField(ctx, "region", region) + + serviceResp, err := r.client.GetServiceResource(ctx, projectId, serverId, region).Execute() + if err != nil { + oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped + if ok && oapiErr.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading server update enable", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + // Map response body to schema + err = mapFields(serviceResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading server update enable", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Server update enable read") +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *serverUpdateEnableResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform + // Update shouldn't be called + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server update enable", "Server update enable can't be updated") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *serverUpdateEnableResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + serverId := model.ServerId.ValueString() + region := model.Region.ValueString() + + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "server_id", serverId) + ctx = tflog.SetField(ctx, "region", region) + + err := r.client.DisableServiceResource(ctx, projectId, serverId, region).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting server update enable", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + tflog.Info(ctx, "Server update enable deleted") +} + +func mapFields(serviceResp *serverupdate.GetUpdateServiceResponse, model *Model, region string) error { + if serviceResp == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), model.ServerId.ValueString(), region) + model.Region = types.StringValue(region) + model.Enabled = types.BoolPointerValue(serviceResp.Enabled) + + return nil +} diff --git a/stackit/internal/services/serverupdate/enable/resource_test.go b/stackit/internal/services/serverupdate/enable/resource_test.go new file mode 100644 index 000000000..9fecee90e --- /dev/null +++ b/stackit/internal/services/serverupdate/enable/resource_test.go @@ -0,0 +1,76 @@ +package enable + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" +) + +func TestMapFields(t *testing.T) { + const testRegion = "eu01" + id := fmt.Sprintf("%s,%s,%s", "pid", "sid", testRegion) + tests := []struct { + description string + input *serverupdate.GetUpdateServiceResponse + expected Model + isValid bool + }{ + { + "default_values", + &serverupdate.GetUpdateServiceResponse{}, + Model{ + Id: types.StringValue(id), + ProjectId: types.StringValue("pid"), + ServerId: types.StringValue("sid"), + Region: types.StringValue("eu01"), + }, + true, + }, + { + "simple_values", + &serverupdate.GetUpdateServiceResponse{ + Enabled: utils.Ptr(true), + }, + Model{ + Id: types.StringValue(id), + ProjectId: types.StringValue("pid"), + ServerId: types.StringValue("sid"), + Region: types.StringValue("eu01"), + Enabled: types.BoolValue(true), + }, + true, + }, + { + "nil_response", + nil, + Model{}, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + model := &Model{ + ProjectId: tt.expected.ProjectId, + ServerId: tt.expected.ServerId, + } + err := mapFields(tt.input, model, "eu01") + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(model, &tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} diff --git a/stackit/internal/services/serverupdate/schedule/resource.go b/stackit/internal/services/serverupdate/schedule/resource.go index c359e9d0c..b5ebdd995 100644 --- a/stackit/internal/services/serverupdate/schedule/resource.go +++ b/stackit/internal/services/serverupdate/schedule/resource.go @@ -224,7 +224,13 @@ func (r *scheduleResource) Create(ctx context.Context, req resource.CreateReques ctx = tflog.SetField(ctx, "server_id", serverId) ctx = tflog.SetField(ctx, "region", region) + // Deprecation warning for enableUpdateService. + resp.Diagnostics.AddWarning("Deprecation warning", + "This resource is using a built in function to enable the update service which will be removed on 28.09.2026. "+ + "Use the new `server_update_enable` resource instead to prevent unexpected behavior.") + // Enable updates if not already enabled + // Deprecated: This function will be removed on 26.09.2026. Use `server_update_enable` resource instead. err := enableUpdatesService(ctx, &model, r.client, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server update schedule", fmt.Sprintf("Enabling server update project before creation: %v", err)) @@ -443,6 +449,7 @@ func mapFields(schedule *serverupdate.UpdateSchedule, model *Model, region strin } // If already enabled, just continues +// Deprecated: This function will be removed on 26.09.2026. Use `server_update_enable` resource instead. func enableUpdatesService(ctx context.Context, model *Model, client *serverupdate.APIClient, region string) error { projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() diff --git a/stackit/internal/services/serverupdate/serverupdate_acc_test.go b/stackit/internal/services/serverupdate/serverupdate_acc_test.go index 0655e87ee..62e83c3d6 100644 --- a/stackit/internal/services/serverupdate/serverupdate_acc_test.go +++ b/stackit/internal/services/serverupdate/serverupdate_acc_test.go @@ -29,6 +29,9 @@ var ( //go:embed testdata/resource-max.tf resourceMaxConfig string + + //go:embed testdata/datasource.tf + datasourceConfig string ) var testConfigVarsMin = config.Variables{ @@ -108,11 +111,15 @@ func TestAccServerUpdateScheduleMinResource(t *testing.T) { // server resource.TestCheckResourceAttrSet("stackit_server_update_schedule.test_schedule", "server_id"), + + // enable + resource.TestCheckResourceAttrSet("stackit_server_update_enable.enable", "server_id"), + resource.TestCheckResourceAttr("stackit_server_update_enable.enable", "enabled", "true"), ), }, // data source { - Config: testutil.ServerUpdateProviderConfig() + "\n" + resourceMinConfig, + Config: testutil.ServerUpdateProviderConfig() + "\n" + resourceMinConfig + "\n" + datasourceConfig, ConfigVariables: testConfigVarsMin, Check: resource.ComposeAggregateTestCheckFunc( // Server update schedule data @@ -129,6 +136,10 @@ func TestAccServerUpdateScheduleMinResource(t *testing.T) { // server resource.TestCheckResourceAttrSet("data.stackit_server_update_schedules.schedules_data_test", "server_id"), + + // enable + resource.TestCheckResourceAttrSet("data.stackit_server_update_enable.enable_test", "server_id"), + resource.TestCheckResourceAttr("data.stackit_server_update_enable.enable_test", "enabled", "true"), ), }, // Import @@ -169,6 +180,10 @@ func TestAccServerUpdateScheduleMinResource(t *testing.T) { // server resource.TestCheckResourceAttrSet("stackit_server_update_schedule.test_schedule", "server_id"), + + // enable + resource.TestCheckResourceAttrSet("stackit_server_update_enable.enable", "server_id"), + resource.TestCheckResourceAttr("stackit_server_update_enable.enable", "enabled", "true"), ), }, // Deletion is done by the framework implicitly @@ -207,11 +222,15 @@ func TestAccServerUpdateScheduleMaxResource(t *testing.T) { // server resource.TestCheckResourceAttrSet("stackit_server_update_schedule.test_schedule", "server_id"), + + // enable + resource.TestCheckResourceAttrSet("stackit_server_update_enable.enable", "server_id"), + resource.TestCheckResourceAttr("stackit_server_update_enable.enable", "enabled", "true"), ), }, // data source { - Config: testutil.ServerUpdateProviderConfig() + "\n" + resourceMaxConfig, + Config: testutil.ServerUpdateProviderConfig() + "\n" + resourceMaxConfig + "\n" + datasourceConfig, ConfigVariables: testConfigVarsMax, Check: resource.ComposeAggregateTestCheckFunc( // Server update schedule data @@ -228,6 +247,10 @@ func TestAccServerUpdateScheduleMaxResource(t *testing.T) { // server resource.TestCheckResourceAttrSet("data.stackit_server_update_schedules.schedules_data_test", "server_id"), + + // enable + resource.TestCheckResourceAttrSet("data.stackit_server_update_enable.enable_test", "server_id"), + resource.TestCheckResourceAttr("data.stackit_server_update_enable.enable_test", "enabled", "true"), ), }, // Import @@ -268,6 +291,10 @@ func TestAccServerUpdateScheduleMaxResource(t *testing.T) { // server resource.TestCheckResourceAttrSet("stackit_server_update_schedule.test_schedule", "server_id"), + + // enable + resource.TestCheckResourceAttrSet("stackit_server_update_enable.enable", "server_id"), + resource.TestCheckResourceAttr("stackit_server_update_enable.enable", "enabled", "true"), ), }, // Deletion is done by the framework implicitly diff --git a/stackit/internal/services/serverupdate/testdata/datasource.tf b/stackit/internal/services/serverupdate/testdata/datasource.tf new file mode 100644 index 000000000..cc97e4363 --- /dev/null +++ b/stackit/internal/services/serverupdate/testdata/datasource.tf @@ -0,0 +1,15 @@ +data "stackit_server_update_enable" "enable_test" { + project_id = var.project_id + server_id = stackit_server.server.server_id +} + +data "stackit_server_update_schedule" "test_schedule" { + project_id = var.project_id + server_id = stackit_server.server.server_id + update_schedule_id = stackit_server_update_schedule.test_schedule.update_schedule_id +} + +data "stackit_server_update_schedules" "schedules_data_test" { + project_id = var.project_id + server_id = stackit_server.server.server_id +} \ No newline at end of file diff --git a/stackit/internal/services/serverupdate/testdata/resource-max.tf b/stackit/internal/services/serverupdate/testdata/resource-max.tf index da5531321..0b094e6a5 100644 --- a/stackit/internal/services/serverupdate/testdata/resource-max.tf +++ b/stackit/internal/services/serverupdate/testdata/resource-max.tf @@ -37,6 +37,11 @@ resource "stackit_server" "server" { ] } +resource "stackit_server_update_enable" "enable" { + project_id = var.project_id + server_id = stackit_server.server.server_id +} + resource "stackit_server_update_schedule" "test_schedule" { project_id = var.project_id server_id = stackit_server.server.server_id @@ -45,15 +50,7 @@ resource "stackit_server_update_schedule" "test_schedule" { enabled = var.enabled maintenance_window = var.maintenance_window region = var.region -} - -data "stackit_server_update_schedule" "test_schedule" { - project_id = var.project_id - server_id = stackit_server.server.server_id - update_schedule_id = stackit_server_update_schedule.test_schedule.update_schedule_id -} - -data "stackit_server_update_schedules" "schedules_data_test" { - project_id = var.project_id - server_id = stackit_server.server.server_id + depends_on = [ + stackit_server_update_enable.enable + ] } diff --git a/stackit/internal/services/serverupdate/testdata/resource-min.tf b/stackit/internal/services/serverupdate/testdata/resource-min.tf index 1e1129279..47df927aa 100644 --- a/stackit/internal/services/serverupdate/testdata/resource-min.tf +++ b/stackit/internal/services/serverupdate/testdata/resource-min.tf @@ -36,6 +36,10 @@ resource "stackit_server" "server" { ] } +resource "stackit_server_update_enable" "enable" { + project_id = var.project_id + server_id = stackit_server.server.server_id +} resource "stackit_server_update_schedule" "test_schedule" { project_id = var.project_id @@ -44,15 +48,7 @@ resource "stackit_server_update_schedule" "test_schedule" { rrule = var.rrule enabled = var.enabled maintenance_window = var.maintenance_window -} - -data "stackit_server_update_schedule" "test_schedule" { - project_id = var.project_id - server_id = stackit_server.server.server_id - update_schedule_id = stackit_server_update_schedule.test_schedule.update_schedule_id -} - -data "stackit_server_update_schedules" "schedules_data_test" { - project_id = var.project_id - server_id = stackit_server.server.server_id + depends_on = [ + stackit_server_update_enable.enable + ] } diff --git a/stackit/provider.go b/stackit/provider.go index edd904d30..2ea383a1f 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -100,6 +100,7 @@ import ( secretsManagerUser "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/user" serverBackupEnable "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverbackup/enable" serverBackupSchedule "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverbackup/schedule" + serverUpdateEnable "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverupdate/enable" serverUpdateSchedule "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverupdate/schedule" serviceAccount "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceaccount/account" serviceAccounts "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceaccount/accounts" @@ -686,6 +687,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource snapshots.NewResourcePoolSnapshotDataSource, compliancelock.NewComplianceLockDataSource, serverBackupEnable.NewServerBackupEnableDataSource, + serverUpdateEnable.NewServerUpdateEnableDataSource, } dataSources = append(dataSources, customRole.NewCustomRoleDataSources()...) @@ -773,6 +775,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { exportpolicy.NewExportPolicyResource, compliancelock.NewComplianceLockResource, serverBackupEnable.NewServerBackupEnableResource, + serverUpdateEnable.NewServerUpdateEnableResource, } resources = append(resources, roleAssignements.NewRoleAssignmentResources()...) resources = append(resources, customRole.NewCustomRoleResources()...) From 905fa25d76a52435b060343d4925da58fe538d2d Mon Sep 17 00:00:00 2001 From: Alexander Dahmen Date: Mon, 30 Mar 2026 12:20:30 +0200 Subject: [PATCH 2/2] Update stackit/internal/services/serverupdate/schedule/resource.go Co-authored-by: cgoetz-inovex --- stackit/internal/services/serverupdate/schedule/resource.go | 1 + 1 file changed, 1 insertion(+) diff --git a/stackit/internal/services/serverupdate/schedule/resource.go b/stackit/internal/services/serverupdate/schedule/resource.go index b5ebdd995..5ef737d90 100644 --- a/stackit/internal/services/serverupdate/schedule/resource.go +++ b/stackit/internal/services/serverupdate/schedule/resource.go @@ -449,6 +449,7 @@ func mapFields(schedule *serverupdate.UpdateSchedule, model *Model, region strin } // If already enabled, just continues + // Deprecated: This function will be removed on 26.09.2026. Use `server_update_enable` resource instead. func enableUpdatesService(ctx context.Context, model *Model, client *serverupdate.APIClient, region string) error { projectId := model.ProjectId.ValueString()