feat: enable flavor v2 handling in addition
This commit is contained in:
parent
0d408764e5
commit
0d9cde57e4
5 changed files with 276 additions and 21 deletions
|
|
@ -60,14 +60,6 @@ type flavorModel struct {
|
|||
RAM types.Int64 `tfsdk:"ram"`
|
||||
}
|
||||
|
||||
//// Types corresponding to flavorModel
|
||||
//var flavorTypes = map[string]attr.Type{
|
||||
// "id": basetypes.StringType{},
|
||||
// "description": basetypes.StringType{},
|
||||
// "cpu": basetypes.Int64Type{},
|
||||
// "ram": basetypes.Int64Type{},
|
||||
//}
|
||||
|
||||
func (r *instanceResource) ValidateConfig(
|
||||
ctx context.Context,
|
||||
req resource.ValidateConfigRequest,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
package sqlserverflexbeta
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v3beta1api"
|
||||
)
|
||||
|
||||
type flavorsClientReader interface {
|
||||
GetFlavorsRequest(
|
||||
ctx context.Context,
|
||||
projectId, region string,
|
||||
) v3beta1api.ApiGetFlavorsRequestRequest
|
||||
}
|
||||
|
||||
func getAllFlavors(ctx context.Context, client flavorsClientReader, projectId, region string) (
|
||||
[]v3beta1api.ListFlavors,
|
||||
error,
|
||||
) {
|
||||
getAllFilter := func(_ v3beta1api.ListFlavors) bool { return true }
|
||||
flavorList, err := getFlavorsByFilter(ctx, client, projectId, region, getAllFilter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return flavorList, nil
|
||||
}
|
||||
|
||||
// getFlavorsByFilter is a helper function to retrieve flavors using a filtern function.
|
||||
// Hint: The API does not have a GetFlavors endpoint, only ListFlavors
|
||||
func getFlavorsByFilter(
|
||||
ctx context.Context,
|
||||
client flavorsClientReader,
|
||||
projectId, region string,
|
||||
filter func(db v3beta1api.ListFlavors) bool,
|
||||
) ([]v3beta1api.ListFlavors, error) {
|
||||
if projectId == "" || region == "" {
|
||||
return nil, fmt.Errorf("listing v3beta1api flavors: projectId and region are required")
|
||||
}
|
||||
|
||||
const pageSize = 25
|
||||
|
||||
var result = make([]v3beta1api.ListFlavors, 0)
|
||||
|
||||
for page := int64(1); ; page++ {
|
||||
res, err := client.GetFlavorsRequest(ctx, projectId, region).
|
||||
Page(page).Size(pageSize).Sort(v3beta1api.FLAVORSORT_INDEX_ASC).Execute()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("requesting flavors list (page %d): %w", page, err)
|
||||
}
|
||||
|
||||
// If the API returns no flavors, we have reached the end of the list.
|
||||
if len(res.Flavors) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
for _, flavor := range res.Flavors {
|
||||
if filter(flavor) {
|
||||
result = append(result, flavor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ import (
|
|||
func mapResponseToModel(
|
||||
ctx context.Context,
|
||||
resp *v3beta1api.GetInstanceResponse,
|
||||
m *sqlserverflexbetaResGen.InstanceModel,
|
||||
m *LocalInstanceModel,
|
||||
tfDiags diag.Diagnostics,
|
||||
) error {
|
||||
m.BackupSchedule = types.StringValue(resp.GetBackupSchedule())
|
||||
|
|
@ -133,7 +133,7 @@ func mapDataResponseToModel(
|
|||
|
||||
func handleEncryption(
|
||||
ctx context.Context,
|
||||
m *sqlserverflexbetaResGen.InstanceModel,
|
||||
m *LocalInstanceModel,
|
||||
resp *v3beta1api.GetInstanceResponse,
|
||||
) sqlserverflexbetaResGen.EncryptionValue {
|
||||
if !resp.HasEncryption() ||
|
||||
|
|
@ -191,7 +191,7 @@ func handleDSEncryption(
|
|||
|
||||
func toCreatePayload(
|
||||
ctx context.Context,
|
||||
model *sqlserverflexbetaResGen.InstanceModel,
|
||||
model *LocalInstanceModel,
|
||||
) (*v3beta1api.CreateInstanceRequestPayload, error) {
|
||||
if model == nil {
|
||||
return nil, fmt.Errorf("nil model")
|
||||
|
|
@ -241,7 +241,7 @@ func toCreatePayload(
|
|||
|
||||
func toUpdatePayload(
|
||||
ctx context.Context,
|
||||
m *sqlserverflexbetaResGen.InstanceModel,
|
||||
m *LocalInstanceModel,
|
||||
resp *resource.UpdateResponse,
|
||||
) (*v3beta1api.UpdateInstanceRequestPayload, error) {
|
||||
if m == nil {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@ import (
|
|||
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"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/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"
|
||||
|
|
@ -42,8 +45,19 @@ type instanceResource struct {
|
|||
providerData core.ProviderData
|
||||
}
|
||||
|
||||
// resourceModel describes the resource data model.
|
||||
type resourceModel = sqlserverflexbetaResGen.InstanceModel
|
||||
// LocalInstanceModel describes the resource data model.
|
||||
type LocalInstanceModel struct {
|
||||
sqlserverflexbetaResGen.InstanceModel
|
||||
Flavor types.Object `tfsdk:"flavor"`
|
||||
}
|
||||
|
||||
// LocalFlavorModel Struct corresponding to Model.Flavor
|
||||
type LocalFlavorModel struct {
|
||||
Id types.String `tfsdk:"id"`
|
||||
Description types.String `tfsdk:"description"`
|
||||
CPU types.Int64 `tfsdk:"cpu"`
|
||||
RAM types.Int64 `tfsdk:"ram"`
|
||||
}
|
||||
|
||||
func (r *instanceResource) Metadata(
|
||||
_ context.Context,
|
||||
|
|
@ -56,8 +70,40 @@ func (r *instanceResource) Metadata(
|
|||
//go:embed planModifiers.yaml
|
||||
var modifiersFileByte []byte
|
||||
|
||||
func (r *instanceResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
func (r *instanceResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
s := sqlserverflexbetaResGen.InstanceResourceSchema(ctx)
|
||||
s.Attributes["flavor"] = schema.SingleNestedAttribute{
|
||||
Optional: true,
|
||||
DeprecationMessage: "Please use flavor_id instead.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
UseStateForUnknownIfFlavorUnchanged(req),
|
||||
},
|
||||
},
|
||||
"description": schema.StringAttribute{
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
UseStateForUnknownIfFlavorUnchanged(req),
|
||||
},
|
||||
},
|
||||
"cpu": schema.Int64Attribute{
|
||||
DeprecationMessage: "Please use flavor_id instead.",
|
||||
Optional: true,
|
||||
},
|
||||
"ram": schema.Int64Attribute{
|
||||
DeprecationMessage: "Please use flavor_id instead.",
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
s.Attributes["flavor_id"] = schema.StringAttribute{
|
||||
Optional: true,
|
||||
Description: "The id of the instance flavor.",
|
||||
MarkdownDescription: "The id of the instance flavor.",
|
||||
}
|
||||
|
||||
fields, err := utils.ReadModifiersConfig(modifiersFileByte)
|
||||
if err != nil {
|
||||
|
|
@ -123,7 +169,7 @@ func (r *instanceResource) ModifyPlan(
|
|||
if req.Config.Raw.IsNull() {
|
||||
return
|
||||
}
|
||||
var configModel resourceModel
|
||||
var configModel LocalInstanceModel
|
||||
resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
|
|
@ -132,7 +178,7 @@ func (r *instanceResource) ModifyPlan(
|
|||
if req.Plan.Raw.IsNull() {
|
||||
return
|
||||
}
|
||||
var planModel resourceModel
|
||||
var planModel LocalInstanceModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
|
|
@ -150,7 +196,7 @@ func (r *instanceResource) ModifyPlan(
|
|||
}
|
||||
|
||||
func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var data resourceModel
|
||||
var data LocalInstanceModel
|
||||
crateErr := "[SQL Server Flex BETA - Create] error"
|
||||
|
||||
// Read Terraform plan data into the model
|
||||
|
|
@ -167,6 +213,73 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques
|
|||
ctx = tflog.SetField(ctx, "project_id", projectID)
|
||||
ctx = tflog.SetField(ctx, "region", region)
|
||||
|
||||
// determine flavor ID
|
||||
var flModel = &LocalFlavorModel{}
|
||||
if !(data.Flavor.IsNull() || data.Flavor.IsUnknown()) {
|
||||
diags := data.Flavor.As(ctx, flModel, basetypes.ObjectAsOptions{})
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
flavors, err := getAllFlavors(ctx, r.client.DefaultAPI, projectID, region)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading flavors", fmt.Sprintf("getAllFlavors: %v", err))
|
||||
return
|
||||
}
|
||||
tflog.Debug(ctx, fmt.Sprintf("loaded flavors: %d", len(flavors)))
|
||||
|
||||
var foundFlavors []v3beta1api.ListFlavors
|
||||
for _, flavor := range flavors {
|
||||
if flModel.CPU.ValueInt64() != int64(flavor.Cpu) {
|
||||
// tflog.Debug(ctx, fmt.Sprintf("flavor - cpu did not match (%d - %d)", flModel.CPU.ValueInt64(), flavor.Cpu))
|
||||
continue
|
||||
}
|
||||
if flModel.RAM.ValueInt64() != int64(flavor.Memory) {
|
||||
// tflog.Debug(ctx, fmt.Sprintf("flavor - ram did not match (%d - %d)", flModel.RAM.ValueInt64(), flavor.Memory))
|
||||
continue
|
||||
}
|
||||
tmpNodeType := "Single"
|
||||
if data.Replicas.ValueInt64() > 1 {
|
||||
tmpNodeType = "Replica"
|
||||
}
|
||||
if strings.ToLower(tmpNodeType) != strings.ToLower(flavor.NodeType) {
|
||||
//tflog.Debug(
|
||||
// ctx,
|
||||
// fmt.Sprintf(
|
||||
// "flavor - nodeType did not match ('%s' - '%s')",
|
||||
// strings.ToLower(tmpNodeType),
|
||||
// strings.ToLower(flavor.NodeType),
|
||||
// ),
|
||||
//)
|
||||
continue
|
||||
}
|
||||
tflog.Debug(ctx, fmt.Sprintf("found flavor %s, checking storage classes", flavor.Id))
|
||||
for _, sc := range flavor.StorageClasses {
|
||||
if data.Storage.Class.ValueString() != sc.Class {
|
||||
continue
|
||||
}
|
||||
tflog.Debug(ctx, fmt.Sprintf("found storage class '%s' for flavor '%s', checking storage classes", sc.Class, flavor.Id))
|
||||
foundFlavors = append(foundFlavors, flavor)
|
||||
}
|
||||
}
|
||||
if len(foundFlavors) == 0 {
|
||||
resp.Diagnostics.AddError("get flavor", "could not find requested flavor")
|
||||
return
|
||||
}
|
||||
if len(foundFlavors) > 1 {
|
||||
resp.Diagnostics.AddError("get flavor", "found too many matching flavors")
|
||||
return
|
||||
}
|
||||
|
||||
f := foundFlavors[0]
|
||||
flModel.Description = types.StringValue(f.Description)
|
||||
flModel.Id = utils.BuildInternalTerraformId(data.ProjectId.ValueString(), region, f.Id)
|
||||
data.FlavorId = types.StringValue(f.Id)
|
||||
//flModel. .MaxGb = types.Int32Value(f.MaxGB)
|
||||
//flModel.MinGb = types.Int32Value(f.MinGB)
|
||||
}
|
||||
|
||||
// Generate API request body from model
|
||||
payload, err := toCreatePayload(ctx, &data)
|
||||
if err != nil {
|
||||
|
|
@ -256,7 +369,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques
|
|||
}
|
||||
|
||||
func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var data resourceModel
|
||||
var data LocalInstanceModel
|
||||
|
||||
// Read Terraform prior state data into the model
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||
|
|
@ -309,7 +422,7 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r
|
|||
}
|
||||
|
||||
func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var data resourceModel
|
||||
var data LocalInstanceModel
|
||||
updateInstanceError := "Error updating instance"
|
||||
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||
|
|
@ -389,7 +502,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques
|
|||
}
|
||||
|
||||
func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var data resourceModel
|
||||
var data LocalInstanceModel
|
||||
|
||||
// Read Terraform prior state data into the model
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
package sqlserverflexbeta
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
|
||||
)
|
||||
|
||||
type useStateForUnknownIfFlavorUnchangedModifier struct {
|
||||
Req resource.SchemaRequest
|
||||
}
|
||||
|
||||
// UseStateForUnknownIfFlavorUnchanged returns a plan modifier similar to UseStateForUnknown
|
||||
// if the RAM and CPU values are not changed in the plan. Otherwise, the plan modifier does nothing.
|
||||
func UseStateForUnknownIfFlavorUnchanged(req resource.SchemaRequest) planmodifier.String {
|
||||
return useStateForUnknownIfFlavorUnchangedModifier{
|
||||
Req: req,
|
||||
}
|
||||
}
|
||||
|
||||
func (m useStateForUnknownIfFlavorUnchangedModifier) Description(context.Context) string {
|
||||
return "UseStateForUnknownIfFlavorUnchanged returns a plan modifier similar to UseStateForUnknown if the RAM and CPU values are not changed in the plan. Otherwise, the plan modifier does nothing."
|
||||
}
|
||||
|
||||
func (m useStateForUnknownIfFlavorUnchangedModifier) MarkdownDescription(ctx context.Context) string {
|
||||
return m.Description(ctx)
|
||||
}
|
||||
|
||||
func (m useStateForUnknownIfFlavorUnchangedModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { // nolint:gocritic // function signature required by Terraform
|
||||
// Do nothing if there is no state value.
|
||||
if req.StateValue.IsNull() {
|
||||
return
|
||||
}
|
||||
|
||||
// Do nothing if there is a known planned value.
|
||||
if !req.PlanValue.IsUnknown() {
|
||||
return
|
||||
}
|
||||
|
||||
// Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up.
|
||||
if req.ConfigValue.IsUnknown() {
|
||||
return
|
||||
}
|
||||
|
||||
// The above checks are taken from the UseStateForUnknown plan modifier implementation
|
||||
// (https://github.com/hashicorp/terraform-plugin-framework/blob/main/resource/schema/stringplanmodifier/use_state_for_unknown.go#L38)
|
||||
|
||||
var stateModel LocalInstanceModel
|
||||
diags := req.State.Get(ctx, &stateModel)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
var stateFlavor = &LocalFlavorModel{}
|
||||
if !(stateModel.Flavor.IsNull() || stateModel.Flavor.IsUnknown()) {
|
||||
diags = stateModel.Flavor.As(ctx, stateFlavor, basetypes.ObjectAsOptions{})
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var planModel LocalInstanceModel
|
||||
diags = req.Plan.Get(ctx, &planModel)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
var planFlavor = &LocalFlavorModel{}
|
||||
if !(planModel.Flavor.IsNull() || planModel.Flavor.IsUnknown()) {
|
||||
diags = planModel.Flavor.As(ctx, planFlavor, basetypes.ObjectAsOptions{})
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if planFlavor.CPU == stateFlavor.CPU && planFlavor.RAM == stateFlavor.RAM {
|
||||
resp.PlanValue = req.StateValue
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue