feat: initial copy of v0.1.0
All checks were successful
Publish / Check GoReleaser config (push) Successful in 5s
Publish / Publish provider (push) Successful in 16m14s

This commit is contained in:
Marcel S. Henselin 2026-03-13 09:03:49 +01:00
parent 4cc801a7f3
commit 7d4cbb6b08
538 changed files with 63361 additions and 55213 deletions

View file

@ -0,0 +1,356 @@
package sqlserverFlexBetaFlavor
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"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"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/core"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils"
"github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v3beta1api"
sqlserverflexbetaGen "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexbeta/flavor/datasources_gen"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ datasource.DataSource = &flavorDataSource{}
_ datasource.DataSourceWithConfigure = &flavorDataSource{}
)
type FlavorModel struct {
ProjectId types.String `tfsdk:"project_id"`
Region types.String `tfsdk:"region"`
StorageClass types.String `tfsdk:"storage_class"`
Cpu types.Int64 `tfsdk:"cpu"`
Description types.String `tfsdk:"description"`
Id types.String `tfsdk:"id"`
FlavorId types.String `tfsdk:"flavor_id"`
MaxGb types.Int64 `tfsdk:"max_gb"`
Memory types.Int64 `tfsdk:"ram"`
MinGb types.Int64 `tfsdk:"min_gb"`
NodeType types.String `tfsdk:"node_type"`
StorageClasses types.List `tfsdk:"storage_classes"`
}
// NewFlavorDataSource is a helper function to simplify the provider implementation.
func NewFlavorDataSource() datasource.DataSource {
return &flavorDataSource{}
}
// flavorDataSource is the data source implementation.
type flavorDataSource struct {
client *v3beta1api.APIClient
providerData core.ProviderData
}
// Metadata returns the data source type name.
func (r *flavorDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_sqlserverflexbeta_flavor"
}
// Configure adds the provider configured client to the data source.
func (r *flavorDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
var ok bool
r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
apiClientConfigOptions := []config.ConfigurationOption{
config.WithCustomAuth(r.providerData.RoundTripper),
utils.UserAgentConfigOption(r.providerData.Version),
}
if r.providerData.SQLServerFlexCustomEndpoint != "" {
apiClientConfigOptions = append(
apiClientConfigOptions,
config.WithEndpoint(r.providerData.SQLServerFlexCustomEndpoint),
)
} else {
apiClientConfigOptions = append(
apiClientConfigOptions,
config.WithRegion(r.providerData.GetRegion()),
)
}
apiClient, err := v3beta1api.NewAPIClient(apiClientConfigOptions...)
if err != nil {
resp.Diagnostics.AddError(
"Error configuring API client",
fmt.Sprintf(
"Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration",
err,
),
)
return
}
r.client = apiClient
tflog.Info(ctx, "SQL Server Flex instance client configured")
}
func (r *flavorDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"project_id": schema.StringAttribute{
Required: true,
Description: "The project ID of the flavor.",
MarkdownDescription: "The project ID of the flavor.",
},
"region": schema.StringAttribute{
Required: true,
Description: "The region of the flavor.",
MarkdownDescription: "The region of the flavor.",
},
"cpu": schema.Int64Attribute{
Required: true,
Description: "The cpu count of the instance.",
MarkdownDescription: "The cpu count of the instance.",
},
"ram": schema.Int64Attribute{
Required: true,
Description: "The memory of the instance in Gibibyte.",
MarkdownDescription: "The memory of the instance in Gibibyte.",
},
"storage_class": schema.StringAttribute{
Required: true,
Description: "The memory of the instance in Gibibyte.",
MarkdownDescription: "The memory of the instance in Gibibyte.",
},
"node_type": schema.StringAttribute{
Required: true,
Description: "defines the nodeType it can be either single or HA",
MarkdownDescription: "defines the nodeType it can be either single or HA",
},
"flavor_id": schema.StringAttribute{
Computed: true,
Description: "The id of the instance flavor.",
MarkdownDescription: "The id of the instance flavor.",
},
"description": schema.StringAttribute{
Computed: true,
Description: "The flavor description.",
MarkdownDescription: "The flavor description.",
},
"id": schema.StringAttribute{
Computed: true,
Description: "The id of the instance flavor.",
MarkdownDescription: "The id of the instance flavor.",
},
"max_gb": schema.Int64Attribute{
Computed: true,
Description: "maximum storage which can be ordered for the flavor in Gigabyte.",
MarkdownDescription: "maximum storage which can be ordered for the flavor in Gigabyte.",
},
"min_gb": schema.Int64Attribute{
Computed: true,
Description: "minimum storage which is required to order in Gigabyte.",
MarkdownDescription: "minimum storage which is required to order in Gigabyte.",
},
"storage_classes": schema.ListNestedAttribute{
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"class": schema.StringAttribute{
Computed: true,
},
"max_io_per_sec": schema.Int64Attribute{
Computed: true,
},
"max_through_in_mb": schema.Int64Attribute{
Computed: true,
},
},
CustomType: sqlserverflexbetaGen.StorageClassesType{
ObjectType: types.ObjectType{
AttrTypes: sqlserverflexbetaGen.StorageClassesValue{}.AttributeTypes(ctx),
},
},
},
Computed: true,
Description: "maximum storage which can be ordered for the flavor in Gigabyte.",
MarkdownDescription: "maximum storage which can be ordered for the flavor in Gigabyte.",
},
},
//Attributes: map[string]schema.Attribute{
// "project_id": schema.StringAttribute{
// Required: true,
// Description: "The cpu count of the instance.",
// MarkdownDescription: "The cpu count of the instance.",
// },
// "region": schema.StringAttribute{
// Required: true,
// Description: "The flavor description.",
// MarkdownDescription: "The flavor description.",
// },
// "cpu": schema.Int64Attribute{
// Required: true,
// Description: "The cpu count of the instance.",
// MarkdownDescription: "The cpu count of the instance.",
// },
// "ram": schema.Int64Attribute{
// Required: true,
// Description: "The memory of the instance in Gibibyte.",
// MarkdownDescription: "The memory of the instance in Gibibyte.",
// },
// "storage_class": schema.StringAttribute{
// Required: true,
// Description: "The memory of the instance in Gibibyte.",
// MarkdownDescription: "The memory of the instance in Gibibyte.",
// },
// "description": schema.StringAttribute{
// Computed: true,
// Description: "The flavor description.",
// MarkdownDescription: "The flavor description.",
// },
// "id": schema.StringAttribute{
// Computed: true,
// Description: "The terraform id of the instance flavor.",
// MarkdownDescription: "The terraform id of the instance flavor.",
// },
// "flavor_id": schema.StringAttribute{
// Computed: true,
// Description: "The flavor id of the instance flavor.",
// MarkdownDescription: "The flavor id of the instance flavor.",
// },
// "max_gb": schema.Int64Attribute{
// Computed: true,
// Description: "maximum storage which can be ordered for the flavor in Gigabyte.",
// MarkdownDescription: "maximum storage which can be ordered for the flavor in Gigabyte.",
// },
// "min_gb": schema.Int64Attribute{
// Computed: true,
// Description: "minimum storage which is required to order in Gigabyte.",
// MarkdownDescription: "minimum storage which is required to order in Gigabyte.",
// },
// "node_type": schema.StringAttribute{
// Required: true,
// Description: "defines the nodeType it can be either single or replica",
// MarkdownDescription: "defines the nodeType it can be either single or replica",
// },
// "storage_classes": schema.ListNestedAttribute{
// Computed: true,
// NestedObject: schema.NestedAttributeObject{
// Attributes: map[string]schema.Attribute{
// "class": schema.StringAttribute{
// Computed: true,
// },
// "max_io_per_sec": schema.Int64Attribute{
// Computed: true,
// },
// "max_through_in_mb": schema.Int64Attribute{
// Computed: true,
// },
// },
// CustomType: sqlserverflexalphaGen.StorageClassesType{
// ObjectType: types.ObjectType{
// AttrTypes: sqlserverflexalphaGen.StorageClassesValue{}.AttributeTypes(ctx),
// },
// },
// },
// },
// },
}
}
func (r *flavorDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var model FlavorModel
diags := req.Config.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
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
}
var foundFlavors []v3beta1api.ListFlavors
for _, flavor := range flavors {
if model.Cpu.ValueInt64() != flavor.Cpu {
continue
}
if model.Memory.ValueInt64() != flavor.Memory {
continue
}
if model.NodeType.ValueString() != flavor.NodeType {
continue
}
for _, sc := range flavor.StorageClasses {
if model.StorageClass.ValueString() != sc.Class {
continue
}
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]
model.Description = types.StringValue(f.Description)
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, f.Id)
model.FlavorId = types.StringValue(f.Id)
model.MaxGb = types.Int64Value(int64(f.MaxGB))
model.MinGb = types.Int64Value(int64(f.MinGB))
if f.StorageClasses == nil {
model.StorageClasses = types.ListNull(sqlserverflexbetaGen.StorageClassesType{
ObjectType: basetypes.ObjectType{
AttrTypes: sqlserverflexbetaGen.StorageClassesValue{}.AttributeTypes(ctx),
},
})
} else {
var scList []attr.Value
for _, sc := range f.StorageClasses {
scList = append(
scList,
sqlserverflexbetaGen.NewStorageClassesValueMust(
sqlserverflexbetaGen.StorageClassesValue{}.AttributeTypes(ctx),
map[string]attr.Value{
"class": types.StringValue(sc.Class),
"max_io_per_sec": types.Int64Value(int64(sc.MaxIoPerSec)),
"max_through_in_mb": types.Int64Value(int64(sc.MaxThroughInMb)),
},
),
)
}
storageClassesList := types.ListValueMust(
sqlserverflexbetaGen.StorageClassesType{
ObjectType: basetypes.ObjectType{
AttrTypes: sqlserverflexbetaGen.StorageClassesValue{}.AttributeTypes(ctx),
},
},
scList,
)
model.StorageClasses = storageClassesList
}
// Set refreshed state
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "SQL Server Flex flavors read")
}

View file

@ -0,0 +1,65 @@
package sqlserverFlexBetaFlavor
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
}

View file

@ -0,0 +1,135 @@
package sqlserverFlexBetaFlavor
// import (
// "context"
// "testing"
//
// "github.com/stackitcloud/stackit-sdk-go/core/utils"
//
// "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v3beta1api"
//)
//
// type mockRequest struct {
// executeFunc func() (*v3beta1api.GetFlavorsResponse, error)
//}
//
// func (m *mockRequest) Page(_ int64) v3beta1api.ApiGetFlavorsRequestRequest { return m }
// func (m *mockRequest) Size(_ int64) v3beta1api.ApiGetFlavorsRequestRequest { return m }
// func (m *mockRequest) Sort(_ v3beta1api.FlavorSort) v3beta1api.ApiGetFlavorsRequestRequest {
// return m
//}
// func (m *mockRequest) Execute() (*v3beta1api.GetFlavorsResponse, error) {
// return m.executeFunc()
//}
//
// type mockFlavorsClient struct {
// executeRequest func() v3beta1api.ApiGetFlavorsRequestRequest
//}
//
// func (m *mockFlavorsClient) GetFlavorsRequest(_ context.Context, _, _ string) v3beta1api.ApiGetFlavorsRequestRequest {
// return m.executeRequest()
//}
//
// var mockResp = func(page int64) (*v3beta1api.GetFlavorsResponse, error) {
// if page == 1 {
// return &v3beta1api.GetFlavorsResponse{
// Flavors: &[]v3beta1api.ListFlavors{
// {Id: utils.Ptr("flavor-1"), Description: utils.Ptr("first")},
// {Id: utils.Ptr("flavor-2"), Description: utils.Ptr("second")},
// },
// }, nil
// }
// if page == 2 {
// return &v3beta1api.GetFlavorsResponse{
// Flavors: &[]v3beta1api.ListFlavors{
// {Id: utils.Ptr("flavor-3"), Description: utils.Ptr("three")},
// },
// }, nil
// }
//
// return &v3beta1api.GetFlavorsResponse{
// Flavors: &[]v3beta1api.ListFlavors{},
// }, nil
//}
//
// func TestGetFlavorsByFilter(t *testing.T) {
// tests := []struct {
// description string
// projectId string
// region string
// mockErr error
// filter func(v3beta1api.ListFlavors) bool
// wantCount int
// wantErr bool
// }{
// {
// description: "Success - Get all flavors (2 pages)",
// projectId: "pid", region: "reg",
// filter: func(_ v3beta1api.ListFlavors) bool { return true },
// wantCount: 3,
// wantErr: false,
// },
// {
// description: "Success - Filter flavors by description",
// projectId: "pid", region: "reg",
// filter: func(f v3beta1api.ListFlavors) bool { return *f.Description == "first" },
// wantCount: 1,
// wantErr: false,
// },
// {
// description: "Error - Missing parameters",
// projectId: "", region: "reg",
// wantErr: true,
// },
// }
//
// for _, tt := range tests {
// t.Run(
// tt.description, func(t *testing.T) {
// var currentPage int64
// client := &mockFlavorsClient{
// executeRequest: func() v3beta1api.ApiGetFlavorsRequestRequest {
// return &mockRequest{
// executeFunc: func() (*v3beta1api.GetFlavorsResponse, error) {
// currentPage++
// return mockResp(currentPage)
// },
// }
// },
// }
// actual, err := getFlavorsByFilter(context.Background(), client, tt.projectId, tt.region, tt.filter)
//
// if (err != nil) != tt.wantErr {
// t.Errorf("getFlavorsByFilter() error = %v, wantErr %v", err, tt.wantErr)
// return
// }
//
// if !tt.wantErr && len(actual) != tt.wantCount {
// t.Errorf("getFlavorsByFilter() got %d flavors, want %d", len(actual), tt.wantCount)
// }
// },
// )
// }
//}
//
// func TestGetAllFlavors(t *testing.T) {
// var currentPage int64
// client := &mockFlavorsClient{
// executeRequest: func() v3beta1api.ApiGetFlavorsRequestRequest {
// return &mockRequest{
// executeFunc: func() (*v3beta1api.GetFlavorsResponse, error) {
// currentPage++
// return mockResp(currentPage)
// },
// }
// },
// }
//
// res, err := getAllFlavors(context.Background(), client, "pid", "reg")
// if err != nil {
// t.Errorf("getAllFlavors() unexpected error: %v", err)
// }
// if len(res) != 3 {
// t.Errorf("getAllFlavors() expected 3 flavor, got %d", len(res))
// }
//}