Onboard MongoDB Flex instance (#86)

* Onboard instance resource

* Add options.type as required field

* Implement resource unit tests

* Implement data source

* Implement acc tests

* Adjust update acc test

* Fix typo

* Adjust update unit tests

* Adjustments after review

* Minor adjustment for uniformity

* Adjustments after review
This commit is contained in:
João Palet 2023-10-17 11:20:22 +02:00 committed by GitHub
parent 6372434e56
commit ade77eb544
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 2245 additions and 106 deletions

View file

@ -64,7 +64,7 @@ var flavorTypes = map[string]attr.Type{
"ram": basetypes.Int64Type{},
}
// Struct corresponding to DataSourceModel.Storage
// Struct corresponding to Model.Storage
type storageModel struct {
Class types.String `tfsdk:"class"`
Size types.Int64 `tfsdk:"size"`
@ -261,8 +261,9 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques
if resp.Diagnostics.HasError() {
return
}
r.loadFlavorId(ctx, &resp.Diagnostics, &model, flavor)
if resp.Diagnostics.HasError() {
err := loadFlavorId(ctx, r.client, &model, flavor)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Loading flavor ID: %v", err))
return
}
}
@ -395,8 +396,9 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques
if resp.Diagnostics.HasError() {
return
}
r.loadFlavorId(ctx, &resp.Diagnostics, &model, flavor)
if resp.Diagnostics.HasError() {
err := loadFlavorId(ctx, r.client, &model, flavor)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Loading flavor ID: %v", err))
return
}
}
@ -435,7 +437,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques
// Map response body to schema
err = mapFields(got, &model, flavor, storage)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields in update", err.Error())
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err))
return
}
diags = resp.State.Set(ctx, model)
@ -524,15 +526,15 @@ func mapFields(resp *postgresflex.InstanceResponse, model *Model, flavor *flavor
}
aclList, diags = types.ListValue(types.StringType, acl)
if diags.HasError() {
return fmt.Errorf("failed to map ACL: %w", core.DiagsToError(diags))
return fmt.Errorf("mapping ACL: %w", core.DiagsToError(diags))
}
}
var flavorValues map[string]attr.Value
if instance.Flavor == nil {
flavorValues = map[string]attr.Value{
"id": types.StringNull(),
"description": types.StringNull(),
"id": flavor.Id,
"description": flavor.Description,
"cpu": flavor.CPU,
"ram": flavor.RAM,
}
@ -540,13 +542,13 @@ func mapFields(resp *postgresflex.InstanceResponse, model *Model, flavor *flavor
flavorValues = map[string]attr.Value{
"id": types.StringValue(*instance.Flavor.Id),
"description": types.StringValue(*instance.Flavor.Description),
"cpu": types.Int64Value(int64(*instance.Flavor.Cpu)),
"ram": types.Int64Value(int64(*instance.Flavor.Memory)),
"cpu": conversion.ToTypeInt64(instance.Flavor.Cpu),
"ram": conversion.ToTypeInt64(instance.Flavor.Memory),
}
}
flavorObject, diags := types.ObjectValue(flavorTypes, flavorValues)
if diags.HasError() {
return fmt.Errorf("failed to create flavor: %w", core.DiagsToError(diags))
return fmt.Errorf("creating flavor: %w", core.DiagsToError(diags))
}
var storageValues map[string]attr.Value
@ -558,12 +560,12 @@ func mapFields(resp *postgresflex.InstanceResponse, model *Model, flavor *flavor
} else {
storageValues = map[string]attr.Value{
"class": types.StringValue(*instance.Storage.Class),
"size": types.Int64Value(int64(*instance.Storage.Size)),
"size": conversion.ToTypeInt64(instance.Storage.Size),
}
}
storageObject, diags := types.ObjectValue(storageTypes, storageValues)
if diags.HasError() {
return fmt.Errorf("failed to create storage: %w", core.DiagsToError(diags))
return fmt.Errorf("creating storage: %w", core.DiagsToError(diags))
}
idParts := []string{
@ -574,29 +576,13 @@ func mapFields(resp *postgresflex.InstanceResponse, model *Model, flavor *flavor
strings.Join(idParts, core.Separator),
)
model.InstanceId = types.StringValue(instanceId)
if instance.Name == nil {
model.Name = types.StringNull()
} else {
model.Name = types.StringValue(*instance.Name)
}
model.Name = types.StringPointerValue(instance.Name)
model.ACL = aclList
if instance.BackupSchedule == nil {
model.BackupSchedule = types.StringNull()
} else {
model.BackupSchedule = types.StringValue(*instance.BackupSchedule)
}
model.BackupSchedule = types.StringPointerValue(instance.BackupSchedule)
model.Flavor = flavorObject
if instance.Replicas == nil {
model.Replicas = types.Int64Null()
} else {
model.Replicas = types.Int64Value(int64(*instance.Replicas))
}
model.Replicas = conversion.ToTypeInt64(instance.Replicas)
model.Storage = storageObject
if instance.Version == nil {
model.Version = types.StringNull()
} else {
model.Version = types.StringValue(*instance.Version)
}
model.Version = types.StringPointerValue(instance.Version)
return nil
}
@ -660,37 +646,35 @@ func toUpdatePayload(model *Model, acl []string, flavor *flavorModel, storage *s
}, nil
}
func (r *instanceResource) loadFlavorId(ctx context.Context, diags *diag.Diagnostics, model *Model, flavor *flavorModel) {
type postgresFlexClient interface {
GetFlavorsExecute(ctx context.Context, projectId string) (*postgresflex.FlavorsResponse, error)
}
func loadFlavorId(ctx context.Context, client postgresFlexClient, model *Model, flavor *flavorModel) error {
if model == nil {
diags.AddError("invalid model", "nil model")
return
return fmt.Errorf("nil model")
}
if flavor == nil {
diags.AddError("invalid flavor", "nil flavor")
return
return fmt.Errorf("nil flavor")
}
cpu := conversion.ToPtrInt32(flavor.CPU)
if cpu == nil {
diags.AddError("invalid flavor", "nil CPU")
return
return fmt.Errorf("nil CPU")
}
ram := conversion.ToPtrInt32(flavor.RAM)
if ram == nil {
diags.AddError("invalid flavor", "nil RAM")
return
return fmt.Errorf("nil RAM")
}
projectId := model.ProjectId.ValueString()
res, err := r.client.GetFlavors(ctx, projectId).Execute()
res, err := client.GetFlavorsExecute(ctx, projectId)
if err != nil {
diags.AddError("failed to list postgresflex flavors", err.Error())
return
return fmt.Errorf("listing postgresflex flavors: %w", err)
}
avl := ""
if res.Flavors == nil {
diags.AddError("no flavors", fmt.Sprintf("couldn't find flavors for id %s", flavor.Id.ValueString()))
return
return fmt.Errorf("finding flavors for project %s", projectId)
}
for _, f := range *res.Flavors {
if f.Id == nil || f.Cpu == nil || f.Memory == nil {
@ -698,12 +682,14 @@ func (r *instanceResource) loadFlavorId(ctx context.Context, diags *diag.Diagnos
}
if *f.Cpu == *cpu && *f.Memory == *ram {
flavor.Id = types.StringValue(*f.Id)
flavor.Description = types.StringValue(*f.Description)
break
}
avl = fmt.Sprintf("%s\n- %d CPU, %d GB RAM", avl, *f.Cpu, *f.Cpu)
}
if flavor.Id.ValueString() == "" {
diags.AddError("invalid flavor", fmt.Sprintf("couldn't find flavor.\navailable specs are:%s", avl))
return
return fmt.Errorf("couldn't find flavor, available specs are:%s", avl)
}
return nil
}

View file

@ -1,6 +1,8 @@
package postgresflex
import (
"context"
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
@ -10,6 +12,19 @@ import (
"github.com/stackitcloud/stackit-sdk-go/services/postgresflex"
)
type postgresFlexClientMocked struct {
returnError bool
getFlavorsResp *postgresflex.FlavorsResponse
}
func (c *postgresFlexClientMocked) GetFlavorsExecute(_ context.Context, _ string) (*postgresflex.FlavorsResponse, error) {
if c.returnError {
return nil, fmt.Errorf("get flavors failed")
}
return c.getFlavorsResp, nil
}
func TestMapFields(t *testing.T) {
tests := []struct {
description string
@ -507,3 +522,156 @@ func TestToUpdatePayload(t *testing.T) {
})
}
}
func TestLoadFlavorId(t *testing.T) {
tests := []struct {
description string
inputFlavor *flavorModel
mockedResp *postgresflex.FlavorsResponse
expected *flavorModel
getFlavorsFails bool
isValid bool
}{
{
"ok_flavor",
&flavorModel{
CPU: types.Int64Value(2),
RAM: types.Int64Value(8),
},
&postgresflex.FlavorsResponse{
Flavors: &[]postgresflex.InstanceFlavor{
{
Id: utils.Ptr("fid-1"),
Cpu: utils.Ptr(int32(2)),
Description: utils.Ptr("description"),
Memory: utils.Ptr(int32(8)),
},
},
},
&flavorModel{
Id: types.StringValue("fid-1"),
Description: types.StringValue("description"),
CPU: types.Int64Value(2),
RAM: types.Int64Value(8),
},
false,
true,
},
{
"ok_flavor_2",
&flavorModel{
CPU: types.Int64Value(2),
RAM: types.Int64Value(8),
},
&postgresflex.FlavorsResponse{
Flavors: &[]postgresflex.InstanceFlavor{
{
Id: utils.Ptr("fid-1"),
Cpu: utils.Ptr(int32(2)),
Description: utils.Ptr("description"),
Memory: utils.Ptr(int32(8)),
},
{
Id: utils.Ptr("fid-2"),
Cpu: utils.Ptr(int32(1)),
Description: utils.Ptr("description"),
Memory: utils.Ptr(int32(4)),
},
},
},
&flavorModel{
Id: types.StringValue("fid-1"),
Description: types.StringValue("description"),
CPU: types.Int64Value(2),
RAM: types.Int64Value(8),
},
false,
true,
},
{
"no_matching_flavor",
&flavorModel{
CPU: types.Int64Value(2),
RAM: types.Int64Value(8),
},
&postgresflex.FlavorsResponse{
Flavors: &[]postgresflex.InstanceFlavor{
{
Id: utils.Ptr("fid-1"),
Cpu: utils.Ptr(int32(1)),
Description: utils.Ptr("description"),
Memory: utils.Ptr(int32(8)),
},
{
Id: utils.Ptr("fid-2"),
Cpu: utils.Ptr(int32(1)),
Description: utils.Ptr("description"),
Memory: utils.Ptr(int32(4)),
},
},
},
&flavorModel{
CPU: types.Int64Value(2),
RAM: types.Int64Value(8),
},
false,
false,
},
{
"nil_response",
&flavorModel{
CPU: types.Int64Value(2),
RAM: types.Int64Value(8),
},
&postgresflex.FlavorsResponse{},
&flavorModel{
CPU: types.Int64Value(2),
RAM: types.Int64Value(8),
},
false,
false,
},
{
"error_response",
&flavorModel{
CPU: types.Int64Value(2),
RAM: types.Int64Value(8),
},
&postgresflex.FlavorsResponse{},
&flavorModel{
CPU: types.Int64Value(2),
RAM: types.Int64Value(8),
},
true,
false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
client := &postgresFlexClientMocked{
returnError: tt.getFlavorsFails,
getFlavorsResp: tt.mockedResp,
}
model := &Model{
ProjectId: types.StringValue("pid"),
}
flavorModel := &flavorModel{
CPU: tt.inputFlavor.CPU,
RAM: tt.inputFlavor.RAM,
}
err := loadFlavorId(context.Background(), client, model, flavorModel)
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(flavorModel, tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
})
}
}