fix: separated models for resource and data-source (#640)

This commit is contained in:
Rüdiger Schmitz 2025-01-22 14:48:52 +01:00 committed by GitHub
parent 4d6f860b26
commit a2ed2b6068
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 271 additions and 6 deletions

View file

@ -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
}

View file

@ -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)
}
}
})
}
}