From a2ed2b6068c0aba1ddde14ace27d15d8b6cd69c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BCdiger=20Schmitz?= Date: Wed, 22 Jan 2025 14:48:52 +0100 Subject: [PATCH] fix: separated models for resource and data-source (#640) --- .../services/iaas/server/datasource.go | 115 ++++++++++++- .../services/iaas/server/datasource_test.go | 162 ++++++++++++++++++ 2 files changed, 271 insertions(+), 6 deletions(-) create mode 100644 stackit/internal/services/iaas/server/datasource_test.go diff --git a/stackit/internal/services/iaas/server/datasource.go b/stackit/internal/services/iaas/server/datasource.go index 312089fd..84635ba1 100644 --- a/stackit/internal/services/iaas/server/datasource.go +++ b/stackit/internal/services/iaas/server/datasource.go @@ -4,11 +4,15 @@ import ( "context" "fmt" "net/http" + "strings" + "time" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" @@ -29,6 +33,25 @@ var ( _ datasource.DataSource = &serverDataSource{} ) +type DataSourceModel struct { + Id types.String `tfsdk:"id"` // needed by TF + ProjectId types.String `tfsdk:"project_id"` + ServerId types.String `tfsdk:"server_id"` + MachineType types.String `tfsdk:"machine_type"` + Name types.String `tfsdk:"name"` + AvailabilityZone types.String `tfsdk:"availability_zone"` + BootVolume types.Object `tfsdk:"boot_volume"` + ImageId types.String `tfsdk:"image_id"` + NetworkInterfaces types.List `tfsdk:"network_interfaces"` + KeypairName types.String `tfsdk:"keypair_name"` + Labels types.Map `tfsdk:"labels"` + AffinityGroup types.String `tfsdk:"affinity_group"` + UserData types.String `tfsdk:"user_data"` + CreatedAt types.String `tfsdk:"created_at"` + LaunchedAt types.String `tfsdk:"launched_at"` + UpdatedAt types.String `tfsdk:"updated_at"` +} + // NewServerDataSource is a helper function to simplify the provider implementation. func NewServerDataSource() datasource.DataSource { return &serverDataSource{} @@ -185,17 +208,13 @@ func (r *serverDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, Description: "Date-time when the server was updated", Computed: true, }, - "desired_status": schema.StringAttribute{ - Description: "The desired status of the server resource." + utils.SupportedValuesDocumentation(desiredStatusOptions), - Computed: true, - }, }, } } // // Read refreshes the Terraform state with the latest data. func (r *serverDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model + var model DataSourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -220,7 +239,7 @@ func (r *serverDataSource) Read(ctx context.Context, req datasource.ReadRequest, } // Map response body to schema - err = mapFields(ctx, serverResp, &model) + err = mapDataSourceFields(ctx, serverResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading server", fmt.Sprintf("Processing API payload: %v", err)) return @@ -233,3 +252,87 @@ func (r *serverDataSource) Read(ctx context.Context, req datasource.ReadRequest, } tflog.Info(ctx, "server read") } + +func mapDataSourceFields(ctx context.Context, serverResp *iaas.Server, model *DataSourceModel) error { + if serverResp == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + var serverId string + if model.ServerId.ValueString() != "" { + serverId = model.ServerId.ValueString() + } else if serverResp.Id != nil { + serverId = *serverResp.Id + } else { + return fmt.Errorf("server id not present") + } + + idParts := []string{ + model.ProjectId.ValueString(), + serverId, + } + model.Id = types.StringValue( + strings.Join(idParts, core.Separator), + ) + + labels, diags := types.MapValueFrom(ctx, types.StringType, map[string]interface{}{}) + if diags.HasError() { + return fmt.Errorf("convert labels to StringValue map: %w", core.DiagsToError(diags)) + } + if serverResp.Labels != nil && len(*serverResp.Labels) != 0 { + var diags diag.Diagnostics + labels, diags = types.MapValueFrom(ctx, types.StringType, *serverResp.Labels) + if diags.HasError() { + return fmt.Errorf("convert labels to StringValue map: %w", core.DiagsToError(diags)) + } + } else if model.Labels.IsNull() { + labels = types.MapNull(types.StringType) + } + var createdAt basetypes.StringValue + if serverResp.CreatedAt != nil { + createdAtValue := *serverResp.CreatedAt + createdAt = types.StringValue(createdAtValue.Format(time.RFC3339)) + } + var updatedAt basetypes.StringValue + if serverResp.UpdatedAt != nil { + updatedAtValue := *serverResp.UpdatedAt + updatedAt = types.StringValue(updatedAtValue.Format(time.RFC3339)) + } + var launchedAt basetypes.StringValue + if serverResp.LaunchedAt != nil { + launchedAtValue := *serverResp.LaunchedAt + launchedAt = types.StringValue(launchedAtValue.Format(time.RFC3339)) + } + if serverResp.Nics != nil { + var respNics []string + for _, nic := range *serverResp.Nics { + respNics = append(respNics, *nic.NicId) + } + nicTF, diags := types.ListValueFrom(ctx, types.StringType, respNics) + if diags.HasError() { + return fmt.Errorf("failed to map networkInterfaces: %w", core.DiagsToError(diags)) + } + + model.NetworkInterfaces = nicTF + } else { + model.NetworkInterfaces = types.ListNull(types.StringType) + } + + model.AvailabilityZone = types.StringPointerValue(serverResp.AvailabilityZone) + model.ServerId = types.StringValue(serverId) + model.MachineType = types.StringPointerValue(serverResp.MachineType) + + model.Name = types.StringPointerValue(serverResp.Name) + model.Labels = labels + model.ImageId = types.StringPointerValue(serverResp.ImageId) + model.KeypairName = types.StringPointerValue(serverResp.KeypairName) + model.AffinityGroup = types.StringPointerValue(serverResp.AffinityGroup) + model.CreatedAt = createdAt + model.UpdatedAt = updatedAt + model.LaunchedAt = launchedAt + + return nil +} diff --git a/stackit/internal/services/iaas/server/datasource_test.go b/stackit/internal/services/iaas/server/datasource_test.go new file mode 100644 index 00000000..bb709d15 --- /dev/null +++ b/stackit/internal/services/iaas/server/datasource_test.go @@ -0,0 +1,162 @@ +package server + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +func TestMapDataSourceFields(t *testing.T) { + tests := []struct { + description string + state DataSourceModel + input *iaas.Server + expected DataSourceModel + isValid bool + }{ + { + "default_values", + DataSourceModel{ + ProjectId: types.StringValue("pid"), + ServerId: types.StringValue("sid"), + }, + &iaas.Server{ + Id: utils.Ptr("sid"), + }, + DataSourceModel{ + Id: types.StringValue("pid,sid"), + ProjectId: types.StringValue("pid"), + ServerId: types.StringValue("sid"), + Name: types.StringNull(), + AvailabilityZone: types.StringNull(), + Labels: types.MapNull(types.StringType), + ImageId: types.StringNull(), + NetworkInterfaces: types.ListNull(types.StringType), + KeypairName: types.StringNull(), + AffinityGroup: types.StringNull(), + UserData: types.StringNull(), + CreatedAt: types.StringNull(), + UpdatedAt: types.StringNull(), + LaunchedAt: types.StringNull(), + }, + true, + }, + { + "simple_values", + DataSourceModel{ + ProjectId: types.StringValue("pid"), + ServerId: types.StringValue("sid"), + }, + &iaas.Server{ + Id: utils.Ptr("sid"), + Name: utils.Ptr("name"), + AvailabilityZone: utils.Ptr("zone"), + Labels: &map[string]interface{}{ + "key": "value", + }, + ImageId: utils.Ptr("image_id"), + Nics: &[]iaas.ServerNetwork{ + { + NicId: utils.Ptr("nic1"), + }, + { + NicId: utils.Ptr("nic2"), + }, + }, + KeypairName: utils.Ptr("keypair_name"), + AffinityGroup: utils.Ptr("group_id"), + CreatedAt: utils.Ptr(testTimestamp()), + UpdatedAt: utils.Ptr(testTimestamp()), + LaunchedAt: utils.Ptr(testTimestamp()), + Status: utils.Ptr("active"), + }, + DataSourceModel{ + Id: types.StringValue("pid,sid"), + ProjectId: types.StringValue("pid"), + ServerId: types.StringValue("sid"), + Name: types.StringValue("name"), + AvailabilityZone: types.StringValue("zone"), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "key": types.StringValue("value"), + }), + ImageId: types.StringValue("image_id"), + NetworkInterfaces: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("nic1"), + types.StringValue("nic2"), + }), + KeypairName: types.StringValue("keypair_name"), + AffinityGroup: types.StringValue("group_id"), + CreatedAt: types.StringValue(testTimestampValue), + UpdatedAt: types.StringValue(testTimestampValue), + LaunchedAt: types.StringValue(testTimestampValue), + }, + true, + }, + { + "empty_labels", + DataSourceModel{ + ProjectId: types.StringValue("pid"), + ServerId: types.StringValue("sid"), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), + }, + &iaas.Server{ + Id: utils.Ptr("sid"), + }, + DataSourceModel{ + Id: types.StringValue("pid,sid"), + ProjectId: types.StringValue("pid"), + ServerId: types.StringValue("sid"), + Name: types.StringNull(), + AvailabilityZone: types.StringNull(), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), + ImageId: types.StringNull(), + NetworkInterfaces: types.ListNull(types.StringType), + KeypairName: types.StringNull(), + AffinityGroup: types.StringNull(), + UserData: types.StringNull(), + CreatedAt: types.StringNull(), + UpdatedAt: types.StringNull(), + LaunchedAt: types.StringNull(), + }, + true, + }, + { + "response_nil_fail", + DataSourceModel{}, + nil, + DataSourceModel{}, + false, + }, + { + "no_resource_id", + DataSourceModel{ + ProjectId: types.StringValue("pid"), + }, + &iaas.Server{}, + DataSourceModel{}, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := mapDataSourceFields(context.Background(), tt.input, &tt.state) + 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(tt.state, tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +}