feat: region adjustment serverupdate (#742)

This commit is contained in:
Marcel Jacek 2025-03-31 09:56:54 +02:00 committed by GitHub
parent 435de4c9eb
commit 862db91f84
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 196 additions and 76 deletions

View file

@ -18,6 +18,7 @@ import (
"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/terraform-provider-stackit/stackit/internal/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
@ -39,6 +40,7 @@ var (
_ resource.Resource = &scheduleResource{}
_ resource.ResourceWithConfigure = &scheduleResource{}
_ resource.ResourceWithImportState = &scheduleResource{}
_ resource.ResourceWithModifyPlan = &scheduleResource{}
)
type Model struct {
@ -50,6 +52,7 @@ type Model struct {
Rrule types.String `tfsdk:"rrule"`
Enabled types.Bool `tfsdk:"enabled"`
MaintenanceWindow types.Int64 `tfsdk:"maintenance_window"`
Region types.String `tfsdk:"region"`
}
// NewScheduleResource is a helper function to simplify the provider implementation.
@ -59,7 +62,38 @@ func NewScheduleResource() resource.Resource {
// scheduleResource is the resource implementation.
type scheduleResource struct {
client *serverupdate.APIClient
client *serverupdate.APIClient
providerData core.ProviderData
}
// ModifyPlan implements resource.ResourceWithModifyPlan.
// Use the modifier to set the effective region in the current plan.
func (r *scheduleResource) 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.
@ -74,14 +108,15 @@ func (r *scheduleResource) Configure(ctx context.Context, req resource.Configure
return
}
providerData, ok := req.ProviderData.(core.ProviderData)
var ok bool
r.providerData, ok = req.ProviderData.(core.ProviderData)
if !ok {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
return
}
if !resourceBetaCheckDone {
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_server_update_schedule", "resource")
features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_server_update_schedule", "resource")
if resp.Diagnostics.HasError() {
return
}
@ -90,16 +125,15 @@ func (r *scheduleResource) Configure(ctx context.Context, req resource.Configure
var apiClient *serverupdate.APIClient
var err error
if providerData.ServerUpdateCustomEndpoint != "" {
ctx = tflog.SetField(ctx, "server_update_custom_endpoint", providerData.ServerUpdateCustomEndpoint)
if r.providerData.ServerUpdateCustomEndpoint != "" {
ctx = tflog.SetField(ctx, "server_update_custom_endpoint", r.providerData.ServerUpdateCustomEndpoint)
apiClient, err = serverupdate.NewAPIClient(
config.WithCustomAuth(providerData.RoundTripper),
config.WithEndpoint(providerData.ServerUpdateCustomEndpoint),
config.WithCustomAuth(r.providerData.RoundTripper),
config.WithEndpoint(r.providerData.ServerUpdateCustomEndpoint),
)
} else {
apiClient, err = serverupdate.NewAPIClient(
config.WithCustomAuth(providerData.RoundTripper),
config.WithRegion(providerData.GetRegion()),
config.WithCustomAuth(r.providerData.RoundTripper),
)
}
@ -119,7 +153,7 @@ func (r *scheduleResource) Schema(_ context.Context, _ resource.SchemaRequest, r
MarkdownDescription: features.AddBetaDescription("Server update schedule resource schema. Must have a `region` specified in the provider configuration."),
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`server_id`,`update_schedule_id`\".",
Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`server_id`,`update_schedule_id`\".",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
@ -194,6 +228,15 @@ func (r *scheduleResource) Schema(_ context.Context, _ resource.SchemaRequest, r
int64validator.AtMost(24),
},
},
"region": schema.StringAttribute{
Optional: true,
// must be computed to allow for storing the override value from the provider
Computed: true,
Description: "The resource region. If not defined, the provider region is used.",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
},
}
}
@ -208,11 +251,13 @@ func (r *scheduleResource) Create(ctx context.Context, req resource.CreateReques
}
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)
// Enable updates if not already enabled
err := enableUpdatesService(ctx, &model, r.client)
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))
return
@ -224,7 +269,7 @@ func (r *scheduleResource) Create(ctx context.Context, req resource.CreateReques
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server update schedule", fmt.Sprintf("Creating API payload: %v", err))
return
}
scheduleResp, err := r.client.CreateUpdateSchedule(ctx, projectId, serverId).CreateUpdateSchedulePayload(*payload).Execute()
scheduleResp, err := r.client.CreateUpdateSchedule(ctx, projectId, serverId, region).CreateUpdateSchedulePayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server update schedule", fmt.Sprintf("Calling API: %v", err))
return
@ -232,7 +277,7 @@ func (r *scheduleResource) Create(ctx context.Context, req resource.CreateReques
ctx = tflog.SetField(ctx, "update_schedule_id", *scheduleResp.Id)
// Map response body to schema
err = mapFields(scheduleResp, &model)
err = mapFields(scheduleResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server update schedule", fmt.Sprintf("Processing API payload: %v", err))
return
@ -256,11 +301,16 @@ func (r *scheduleResource) Read(ctx context.Context, req resource.ReadRequest, r
projectId := model.ProjectId.ValueString()
serverId := model.ServerId.ValueString()
updateScheduleId := model.UpdateScheduleId.ValueInt64()
region := model.Region.ValueString()
if region == "" {
region = r.providerData.GetRegion()
}
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "server_id", serverId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "update_schedule_id", updateScheduleId)
scheduleResp, err := r.client.GetUpdateSchedule(ctx, projectId, serverId, strconv.FormatInt(updateScheduleId, 10)).Execute()
scheduleResp, err := r.client.GetUpdateSchedule(ctx, projectId, serverId, strconv.FormatInt(updateScheduleId, 10), 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 {
@ -272,7 +322,7 @@ func (r *scheduleResource) Read(ctx context.Context, req resource.ReadRequest, r
}
// Map response body to schema
err = mapFields(scheduleResp, &model)
err = mapFields(scheduleResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading update schedule", fmt.Sprintf("Processing API payload: %v", err))
return
@ -298,8 +348,10 @@ func (r *scheduleResource) Update(ctx context.Context, req resource.UpdateReques
projectId := model.ProjectId.ValueString()
serverId := model.ServerId.ValueString()
updateScheduleId := model.UpdateScheduleId.ValueInt64()
region := model.Region.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "server_id", serverId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "update_schedule_id", updateScheduleId)
// Update schedule
@ -309,14 +361,14 @@ func (r *scheduleResource) Update(ctx context.Context, req resource.UpdateReques
return
}
scheduleResp, err := r.client.UpdateUpdateSchedule(ctx, projectId, serverId, strconv.FormatInt(updateScheduleId, 10)).UpdateUpdateSchedulePayload(*payload).Execute()
scheduleResp, err := r.client.UpdateUpdateSchedule(ctx, projectId, serverId, strconv.FormatInt(updateScheduleId, 10), region).UpdateUpdateSchedulePayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server update schedule", fmt.Sprintf("Calling API: %v", err))
return
}
// Map response body to schema
err = mapFields(scheduleResp, &model)
err = mapFields(scheduleResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server update schedule", fmt.Sprintf("Processing API payload: %v", err))
return
@ -340,11 +392,13 @@ func (r *scheduleResource) Delete(ctx context.Context, req resource.DeleteReques
projectId := model.ProjectId.ValueString()
serverId := model.ServerId.ValueString()
updateScheduleId := model.UpdateScheduleId.ValueInt64()
region := model.Region.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "server_id", serverId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "update_schedule_id", updateScheduleId)
err := r.client.DeleteUpdateSchedule(ctx, projectId, serverId, strconv.FormatInt(updateScheduleId, 10)).Execute()
err := r.client.DeleteUpdateSchedule(ctx, projectId, serverId, strconv.FormatInt(updateScheduleId, 10), region).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting server update schedule", fmt.Sprintf("Calling API: %v", err))
return
@ -356,15 +410,15 @@ func (r *scheduleResource) Delete(ctx context.Context, req resource.DeleteReques
// The expected format of the resource import identifier is: // project_id,server_id,schedule_id
func (r *scheduleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
idParts := strings.Split(req.ID, core.Separator)
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" {
core.LogAndAddError(ctx, &resp.Diagnostics,
"Error importing server update schedule",
fmt.Sprintf("Expected import identifier with format [project_id],[server_id],[update_schedule_id], got %q", req.ID),
fmt.Sprintf("Expected import identifier with format [project_id],[region],[server_id],[update_schedule_id], got %q", req.ID),
)
return
}
intId, err := strconv.ParseInt(idParts[2], 10, 64)
intId, err := strconv.ParseInt(idParts[3], 10, 64)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics,
"Error importing server update schedule",
@ -374,12 +428,13 @@ func (r *scheduleResource) ImportState(ctx context.Context, req resource.ImportS
}
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("server_id"), idParts[1])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("server_id"), idParts[2])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("update_schedule_id"), intId)...)
tflog.Info(ctx, "Server update schedule state imported.")
}
func mapFields(schedule *serverupdate.UpdateSchedule, model *Model) error {
func mapFields(schedule *serverupdate.UpdateSchedule, model *Model, region string) error {
if schedule == nil {
return fmt.Errorf("response input is nil")
}
@ -393,6 +448,7 @@ func mapFields(schedule *serverupdate.UpdateSchedule, model *Model) error {
model.UpdateScheduleId = types.Int64PointerValue(schedule.Id)
idParts := []string{
model.ProjectId.ValueString(),
region,
model.ServerId.ValueString(),
strconv.FormatInt(model.UpdateScheduleId.ValueInt64(), 10),
}
@ -403,17 +459,18 @@ func mapFields(schedule *serverupdate.UpdateSchedule, model *Model) error {
model.Rrule = types.StringPointerValue(schedule.Rrule)
model.Enabled = types.BoolPointerValue(schedule.Enabled)
model.MaintenanceWindow = types.Int64PointerValue(schedule.MaintenanceWindow)
model.Region = types.StringValue(region)
return nil
}
// If already enabled, just continues
func enableUpdatesService(ctx context.Context, model *Model, client *serverupdate.APIClient) error {
func enableUpdatesService(ctx context.Context, model *Model, client *serverupdate.APIClient, region string) error {
projectId := model.ProjectId.ValueString()
serverId := model.ServerId.ValueString()
enableServicePayload := serverupdate.EnableServicePayload{}
payload := serverupdate.EnableServiceResourcePayload{}
tflog.Debug(ctx, "Enabling server update service")
err := client.EnableService(ctx, projectId, serverId).EnableServicePayload(enableServicePayload).Execute()
err := client.EnableServiceResource(ctx, projectId, serverId, region).EnableServiceResourcePayload(payload).Execute()
if err != nil {
if strings.Contains(err.Error(), "Tried to activate already active service") {
tflog.Debug(ctx, "Service for server update already enabled")

View file

@ -10,9 +10,11 @@ import (
)
func TestMapFields(t *testing.T) {
const testRegion = "region"
tests := []struct {
description string
input *sdk.UpdateSchedule
region string
expected Model
isValid bool
}{
@ -21,11 +23,13 @@ func TestMapFields(t *testing.T) {
&sdk.UpdateSchedule{
Id: utils.Ptr(int64(5)),
},
testRegion,
Model{
ID: types.StringValue("project_uid,server_uid,5"),
ID: types.StringValue("project_uid,region,server_uid,5"),
ProjectId: types.StringValue("project_uid"),
ServerId: types.StringValue("server_uid"),
UpdateScheduleId: types.Int64Value(5),
Region: types.StringValue(testRegion),
},
true,
},
@ -38,27 +42,31 @@ func TestMapFields(t *testing.T) {
Rrule: utils.Ptr("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"),
MaintenanceWindow: utils.Ptr(int64(1)),
},
testRegion,
Model{
ServerId: types.StringValue("server_uid"),
ProjectId: types.StringValue("project_uid"),
UpdateScheduleId: types.Int64Value(5),
ID: types.StringValue("project_uid,server_uid,5"),
ID: types.StringValue("project_uid,region,server_uid,5"),
Name: types.StringValue("update_schedule_name_1"),
Rrule: types.StringValue("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"),
Enabled: types.BoolValue(true),
MaintenanceWindow: types.Int64Value(1),
Region: types.StringValue(testRegion),
},
true,
},
{
"nil_response",
nil,
testRegion,
Model{},
false,
},
{
"no_resource_id",
&sdk.UpdateSchedule{},
testRegion,
Model{},
false,
},
@ -69,7 +77,7 @@ func TestMapFields(t *testing.T) {
ProjectId: tt.expected.ProjectId,
ServerId: tt.expected.ServerId,
}
err := mapFields(tt.input, state)
err := mapFields(tt.input, state, tt.region)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}

View file

@ -10,6 +10,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
@ -37,7 +38,8 @@ func NewScheduleDataSource() datasource.DataSource {
// scheduleDataSource is the data source implementation.
type scheduleDataSource struct {
client *serverupdate.APIClient
client *serverupdate.APIClient
providerData core.ProviderData
}
// Metadata returns the data source type name.
@ -52,14 +54,15 @@ func (r *scheduleDataSource) Configure(ctx context.Context, req datasource.Confi
return
}
providerData, ok := req.ProviderData.(core.ProviderData)
var ok bool
r.providerData, ok = req.ProviderData.(core.ProviderData)
if !ok {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
return
}
if !scheduleDataSourceBetaCheckDone {
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_server_update_schedule", "data source")
features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_server_update_schedule", "data source")
if resp.Diagnostics.HasError() {
return
}
@ -68,16 +71,15 @@ func (r *scheduleDataSource) Configure(ctx context.Context, req datasource.Confi
var apiClient *serverupdate.APIClient
var err error
if providerData.ServerUpdateCustomEndpoint != "" {
ctx = tflog.SetField(ctx, "server_update_custom_endpoint", providerData.ServerUpdateCustomEndpoint)
if r.providerData.ServerUpdateCustomEndpoint != "" {
ctx = tflog.SetField(ctx, "server_update_custom_endpoint", r.providerData.ServerUpdateCustomEndpoint)
apiClient, err = serverupdate.NewAPIClient(
config.WithCustomAuth(providerData.RoundTripper),
config.WithEndpoint(providerData.ServerUpdateCustomEndpoint),
config.WithCustomAuth(r.providerData.RoundTripper),
config.WithEndpoint(r.providerData.ServerUpdateCustomEndpoint),
)
} else {
apiClient, err = serverupdate.NewAPIClient(
config.WithCustomAuth(providerData.RoundTripper),
config.WithRegion(providerData.GetRegion()),
config.WithCustomAuth(r.providerData.RoundTripper),
)
}
@ -97,7 +99,7 @@ func (r *scheduleDataSource) Schema(_ context.Context, _ datasource.SchemaReques
MarkdownDescription: features.AddBetaDescription("Server update schedule datasource schema. Must have a `region` specified in the provider configuration."),
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`server_id`,`update_schedule_id`\".",
Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`server_id`,`update_schedule_id`\".",
Computed: true,
},
"name": schema.StringAttribute{
@ -136,6 +138,11 @@ func (r *scheduleDataSource) Schema(_ context.Context, _ datasource.SchemaReques
Description: "Maintenance window [1..24].",
Computed: true,
},
"region": schema.StringAttribute{
// the region cannot be found, so it has to be passed
Optional: true,
Description: "The resource region. If not defined, the provider region is used.",
},
},
}
}
@ -151,11 +158,18 @@ func (r *scheduleDataSource) Read(ctx context.Context, req datasource.ReadReques
projectId := model.ProjectId.ValueString()
serverId := model.ServerId.ValueString()
updateScheduleId := model.UpdateScheduleId.ValueInt64()
var region string
if utils.IsUndefined(model.Region) {
region = r.providerData.GetRegion()
} else {
region = model.Region.ValueString()
}
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "server_id", serverId)
ctx = tflog.SetField(ctx, "update_schedule_id", updateScheduleId)
ctx = tflog.SetField(ctx, "region", region)
scheduleResp, err := r.client.GetUpdateSchedule(ctx, projectId, serverId, strconv.FormatInt(updateScheduleId, 10)).Execute()
scheduleResp, err := r.client.GetUpdateSchedule(ctx, projectId, serverId, strconv.FormatInt(updateScheduleId, 10), 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 {
@ -166,7 +180,7 @@ func (r *scheduleDataSource) Read(ctx context.Context, req datasource.ReadReques
}
// Map response body to schema
err = mapFields(scheduleResp, &model)
err = mapFields(scheduleResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading server update schedule", fmt.Sprintf("Processing API payload: %v", err))
return

View file

@ -12,6 +12,7 @@ import (
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
@ -37,7 +38,8 @@ func NewSchedulesDataSource() datasource.DataSource {
// schedulesDataSource is the data source implementation.
type schedulesDataSource struct {
client *serverupdate.APIClient
client *serverupdate.APIClient
providerData core.ProviderData
}
// Metadata returns the data source type name.
@ -52,14 +54,15 @@ func (r *schedulesDataSource) Configure(ctx context.Context, req datasource.Conf
return
}
providerData, ok := req.ProviderData.(core.ProviderData)
var ok bool
r.providerData, ok = req.ProviderData.(core.ProviderData)
if !ok {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
return
}
if !schedulesDataSourceBetaCheckDone {
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_server_update_schedules", "data source")
features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_server_update_schedules", "data source")
if resp.Diagnostics.HasError() {
return
}
@ -68,16 +71,15 @@ func (r *schedulesDataSource) Configure(ctx context.Context, req datasource.Conf
var apiClient *serverupdate.APIClient
var err error
if providerData.ServerUpdateCustomEndpoint != "" {
ctx = tflog.SetField(ctx, "server_update_custom_endpoint", providerData.ServerUpdateCustomEndpoint)
if r.providerData.ServerUpdateCustomEndpoint != "" {
ctx = tflog.SetField(ctx, "server_update_custom_endpoint", r.providerData.ServerUpdateCustomEndpoint)
apiClient, err = serverupdate.NewAPIClient(
config.WithCustomAuth(providerData.RoundTripper),
config.WithEndpoint(providerData.ServerUpdateCustomEndpoint),
config.WithCustomAuth(r.providerData.RoundTripper),
config.WithEndpoint(r.providerData.ServerUpdateCustomEndpoint),
)
} else {
apiClient, err = serverupdate.NewAPIClient(
config.WithCustomAuth(providerData.RoundTripper),
config.WithRegion(providerData.GetRegion()),
config.WithCustomAuth(r.providerData.RoundTripper),
)
}
@ -97,7 +99,7 @@ func (r *schedulesDataSource) Schema(_ context.Context, _ datasource.SchemaReque
MarkdownDescription: features.AddBetaDescription("Server update schedules datasource schema. Must have a `region` specified in the provider configuration."),
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal data source identifier. It is structured as \"`project_id`,`server_id`\".",
Description: "Terraform's internal data source identifier. It is structured as \"`project_id`,`region`,`server_id`\".",
Computed: true,
},
"project_id": schema.StringAttribute{
@ -142,6 +144,11 @@ func (r *schedulesDataSource) Schema(_ context.Context, _ datasource.SchemaReque
},
},
},
"region": schema.StringAttribute{
// the region cannot be found, so it has to be passed
Optional: true,
Description: "The resource region. If not defined, the provider region is used.",
},
},
}
}
@ -152,6 +159,7 @@ type schedulesDataSourceModel struct {
ProjectId types.String `tfsdk:"project_id"`
ServerId types.String `tfsdk:"server_id"`
Items []schedulesDatasourceItemModel `tfsdk:"items"`
Region types.String `tfsdk:"region"`
}
// schedulesDatasourceItemModel maps schedule schema data.
@ -173,10 +181,16 @@ func (r *schedulesDataSource) Read(ctx context.Context, req datasource.ReadReque
}
projectId := model.ProjectId.ValueString()
serverId := model.ServerId.ValueString()
var region string
if utils.IsUndefined(model.Region) {
region = r.providerData.GetRegion()
} else {
region = model.Region.ValueString()
}
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "server_id", serverId)
schedules, err := r.client.ListUpdateSchedules(ctx, projectId, serverId).Execute()
schedules, err := r.client.ListUpdateSchedules(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 {
@ -187,7 +201,7 @@ func (r *schedulesDataSource) Read(ctx context.Context, req datasource.ReadReque
}
// Map response body to schema
err = mapSchedulesDatasourceFields(ctx, schedules, &model)
err = mapSchedulesDatasourceFields(ctx, schedules, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading server update schedules", fmt.Sprintf("Processing API payload: %v", err))
return
@ -202,7 +216,7 @@ func (r *schedulesDataSource) Read(ctx context.Context, req datasource.ReadReque
tflog.Info(ctx, "Server update schedules read")
}
func mapSchedulesDatasourceFields(ctx context.Context, schedules *serverupdate.GetUpdateSchedulesResponse, model *schedulesDataSourceModel) error {
func mapSchedulesDatasourceFields(ctx context.Context, schedules *serverupdate.GetUpdateSchedulesResponse, model *schedulesDataSourceModel, region string) error {
if schedules == nil {
return fmt.Errorf("response input is nil")
}
@ -214,10 +228,11 @@ func mapSchedulesDatasourceFields(ctx context.Context, schedules *serverupdate.G
projectId := model.ProjectId.ValueString()
serverId := model.ServerId.ValueString()
idParts := []string{projectId, serverId}
idParts := []string{projectId, region, serverId}
model.ID = types.StringValue(
strings.Join(idParts, core.Separator),
)
model.Region = types.StringValue(region)
for _, schedule := range *schedules.Items {
scheduleState := schedulesDatasourceItemModel{

View file

@ -11,9 +11,11 @@ import (
)
func TestMapSchedulesDataSourceFields(t *testing.T) {
const testRegion = "region"
tests := []struct {
description string
input *sdk.GetUpdateSchedulesResponse
region string
expected schedulesDataSourceModel
isValid bool
}{
@ -22,11 +24,13 @@ func TestMapSchedulesDataSourceFields(t *testing.T) {
&sdk.GetUpdateSchedulesResponse{
Items: &[]sdk.UpdateSchedule{},
},
testRegion,
schedulesDataSourceModel{
ID: types.StringValue("project_uid,server_uid"),
ID: types.StringValue("project_uid,region,server_uid"),
ProjectId: types.StringValue("project_uid"),
ServerId: types.StringValue("server_uid"),
Items: nil,
Region: types.StringValue(testRegion),
},
true,
},
@ -43,8 +47,9 @@ func TestMapSchedulesDataSourceFields(t *testing.T) {
},
},
},
testRegion,
schedulesDataSourceModel{
ID: types.StringValue("project_uid,server_uid"),
ID: types.StringValue("project_uid,region,server_uid"),
ServerId: types.StringValue("server_uid"),
ProjectId: types.StringValue("project_uid"),
Items: []schedulesDatasourceItemModel{
@ -56,12 +61,14 @@ func TestMapSchedulesDataSourceFields(t *testing.T) {
MaintenanceWindow: types.Int64Value(1),
},
},
Region: types.StringValue(testRegion),
},
true,
},
{
"nil_response",
nil,
testRegion,
schedulesDataSourceModel{},
false,
},
@ -73,7 +80,7 @@ func TestMapSchedulesDataSourceFields(t *testing.T) {
ServerId: tt.expected.ServerId,
}
ctx := context.TODO()
err := mapSchedulesDatasourceFields(ctx, tt.input, state)
err := mapSchedulesDatasourceFields(ctx, tt.input, state, tt.region)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}

View file

@ -26,7 +26,11 @@ var serverUpdateScheduleResource = map[string]string{
"maintenance_window": "1",
}
func resourceConfig(maintenanceWindow int64) string {
func resourceConfig(maintenanceWindow int64, region *string) string {
var regionConfig string
if region != nil {
regionConfig = fmt.Sprintf(`region = %q`, *region)
}
return fmt.Sprintf(`
%s
@ -37,6 +41,7 @@ func resourceConfig(maintenanceWindow int64) string {
rrule = "%s"
enabled = true
maintenance_window = %d
%s
}
`,
testutil.ServerUpdateProviderConfig(),
@ -45,6 +50,7 @@ func resourceConfig(maintenanceWindow int64) string {
serverUpdateScheduleResource["name"],
serverUpdateScheduleResource["rrule"],
maintenanceWindow,
regionConfig,
)
}
@ -53,6 +59,7 @@ func TestAccServerUpdateScheduleResource(t *testing.T) {
fmt.Println("TF_ACC_SERVER_ID not set, skipping test")
return
}
testRegion := utils.Ptr("eu01")
var invalidMaintenanceWindow int64 = 0
var validMaintenanceWindow int64 = 15
var updatedMaintenanceWindow int64 = 8
@ -62,12 +69,12 @@ func TestAccServerUpdateScheduleResource(t *testing.T) {
Steps: []resource.TestStep{
// Creation fail
{
Config: resourceConfig(invalidMaintenanceWindow),
Config: resourceConfig(invalidMaintenanceWindow, testRegion),
ExpectError: regexp.MustCompile(`.*maintenance_window value must be at least 1*`),
},
// Creation
{
Config: resourceConfig(validMaintenanceWindow),
Config: resourceConfig(validMaintenanceWindow, testRegion),
Check: resource.ComposeAggregateTestCheckFunc(
// Update schedule data
resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "project_id", serverUpdateScheduleResource["project_id"]),
@ -77,6 +84,7 @@ func TestAccServerUpdateScheduleResource(t *testing.T) {
resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "name", serverUpdateScheduleResource["name"]),
resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "rrule", serverUpdateScheduleResource["rrule"]),
resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "enabled", strconv.FormatBool(true)),
resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "region", *testRegion),
),
},
// data source
@ -94,7 +102,7 @@ func TestAccServerUpdateScheduleResource(t *testing.T) {
server_id = stackit_server_update_schedule.test_schedule.server_id
update_schedule_id = stackit_server_update_schedule.test_schedule.update_schedule_id
}`,
resourceConfig(validMaintenanceWindow),
resourceConfig(validMaintenanceWindow, testRegion),
),
Check: resource.ComposeAggregateTestCheckFunc(
// Server update schedule data
@ -124,14 +132,14 @@ func TestAccServerUpdateScheduleResource(t *testing.T) {
if !ok {
return "", fmt.Errorf("couldn't find attribute update_schedule_id")
}
return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.ServerId, scheduleId), nil
return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, testutil.ServerId, scheduleId), nil
},
ImportState: true,
ImportStateVerify: true,
},
// Update
{
Config: resourceConfig(updatedMaintenanceWindow),
Config: resourceConfig(updatedMaintenanceWindow, nil),
Check: resource.ComposeAggregateTestCheckFunc(
// Update schedule data
resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "project_id", serverUpdateScheduleResource["project_id"]),
@ -154,9 +162,7 @@ func testAccCheckServerUpdateScheduleDestroy(s *terraform.State) error {
var client *serverupdate.APIClient
var err error
if testutil.ServerUpdateCustomEndpoint == "" {
client, err = serverupdate.NewAPIClient(
config.WithRegion("eu01"),
)
client, err = serverupdate.NewAPIClient()
} else {
client, err = serverupdate.NewAPIClient(
config.WithEndpoint(testutil.ServerUpdateCustomEndpoint),
@ -176,7 +182,7 @@ func testAccCheckServerUpdateScheduleDestroy(s *terraform.State) error {
schedulesToDestroy = append(schedulesToDestroy, scheduleId)
}
schedulesResp, err := client.ListUpdateSchedules(ctx, testutil.ProjectId, testutil.ServerId).Execute()
schedulesResp, err := client.ListUpdateSchedules(ctx, testutil.ProjectId, testutil.ServerId, testutil.Region).Execute()
if err != nil {
return fmt.Errorf("getting schedulesResp: %w", err)
}
@ -188,7 +194,7 @@ func testAccCheckServerUpdateScheduleDestroy(s *terraform.State) error {
}
scheduleId := strconv.FormatInt(*schedules[i].Id, 10)
if utils.Contains(schedulesToDestroy, scheduleId) {
err := client.DeleteUpdateScheduleExecute(ctx, testutil.ProjectId, testutil.ServerId, scheduleId)
err := client.DeleteUpdateScheduleExecute(ctx, testutil.ProjectId, testutil.ServerId, scheduleId, testutil.Region)
if err != nil {
return fmt.Errorf("destroying server update schedule %s during CheckDestroy: %w", scheduleId, err)
}

View file

@ -360,7 +360,8 @@ func ServerUpdateProviderConfig() string {
if ServerUpdateCustomEndpoint == "" {
return `
provider "stackit" {
region = "eu01"
default_region = "eu01"
enable_beta_resources = true
}`
}
return fmt.Sprintf(`