Compare commits

..

3 commits

Author SHA1 Message Date
0188461ee9 chore: add protocol v6.0
Some checks failed
CI Workflow / Check GoReleaser config (pull_request) Successful in 10s
CI Workflow / CI (pull_request) Failing after 28m23s
CI Workflow / Code coverage report (pull_request) Has been skipped
CI Workflow / Test readiness for publishing provider (pull_request) Successful in 35m1s
chore: add action
2026-02-11 10:06:09 +01:00
79f25e8b33 chore: add protocol v6.0
chore: add action
2026-02-11 10:04:10 +01:00
399e8ccb0c
feat: update sql server flex configuration for user and database (#46)
## Description

<!-- **Please link some issue here describing what you are trying to achieve.**

In case there is no issue present for your PR, please consider creating one.
At least please give us some description what you are trying to achieve and why your change is needed. -->

relates to #1234

## Checklist

- [ ] Issue was linked above
- [ ] Code format was applied: `make fmt`
- [ ] Examples were added / adjusted (see `examples/` directory)
- [x] Docs are up-to-date: `make generate-docs` (will be checked by CI)
- [ ] Unit tests got implemented or updated
- [ ] Acceptance tests got implemented or updated (see e.g. [here](f5f99d1709/stackit/internal/services/dns/dns_acc_test.go))
- [x] Unit tests are passing: `make test` (will be checked by CI)
- [x] No linter issues: `make lint` (will be checked by CI)

Reviewed-on: #46
Reviewed-by: Marcel_Henselin <marcel.henselin@stackit.cloud>
Co-authored-by: Andre Harms <andre.harms@stackit.cloud>
Co-committed-by: Andre Harms <andre.harms@stackit.cloud>
2026-02-11 09:03:31 +00:00
22 changed files with 3267 additions and 1412 deletions

View file

@ -2,9 +2,12 @@ package sqlserverflexalpha
import (
"context"
"fmt"
"net/http"
"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/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexalpha"
@ -12,6 +15,7 @@ import (
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/core"
sqlserverflexalphaGen "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexalpha/database/datasources_gen"
sqlserverflexUtils "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexalpha/utils"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils"
)
// dataSourceModel maps the data source schema data.
@ -22,6 +26,7 @@ type dataSourceModel struct {
var _ datasource.DataSource = (*databaseDataSource)(nil)
// NewDatabaseDataSource creates a new database data source.
func NewDatabaseDataSource() datasource.DataSource {
return &databaseDataSource{}
}
@ -31,6 +36,7 @@ type databaseDataSource struct {
providerData core.ProviderData
}
// Metadata returns the data source type name.
func (d *databaseDataSource) Metadata(
_ context.Context,
req datasource.MetadataRequest,
@ -39,6 +45,7 @@ func (d *databaseDataSource) Metadata(
resp.TypeName = req.ProviderTypeName + "_sqlserverflexalpha_database"
}
// Schema defines the data source schema.
func (d *databaseDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
s := sqlserverflexalphaGen.DatabaseDataSourceSchema(ctx)
s.Attributes["id"] = schema.StringAttribute{
@ -67,24 +74,92 @@ func (d *databaseDataSource) Configure(
return
}
d.client = apiClient
tflog.Info(ctx, "SQL SERVER Flex database client configured")
tflog.Info(ctx, "SQL SERVER Flex alpha database client configured")
}
// Read retrieves the resource's state from the API.
func (d *databaseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data dataSourceModel
// Read Terraform configuration data into the model
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
var model dataSourceModel
diags := req.Config.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Todo: Read API call logic
ctx = core.InitProviderContext(ctx)
// Example data value setting
// data.Id = types.StringValue("example-id")
// Extract identifiers from the plan
projectId := model.ProjectId.ValueString()
instanceId := model.InstanceId.ValueString()
region := d.providerData.GetRegionWithOverride(model.Region)
databaseName := model.DatabaseName.ValueString()
// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "instance_id", instanceId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "database_name", databaseName)
// Fetch database from the API
databaseResp, err := d.client.GetDatabaseRequest(ctx, projectId, region, instanceId, databaseName).Execute()
if resp.Diagnostics.HasError() {
return
}
if err != nil {
handleReadError(ctx, &resp.Diagnostics, err, projectId, instanceId)
resp.State.RemoveResource(ctx)
return
}
ctx = core.LogResponse(ctx)
// Map response body to schema and populate Computed attribute values
err = mapFields(databaseResp, &model, region)
if err != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
"Error reading database",
fmt.Sprintf("Processing API payload: %v", err),
)
return
}
// Set refreshed state
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "SQL Server Flex alpha database read")
}
// handleReadError centralizes API error handling for the Read operation.
func handleReadError(ctx context.Context, diags *diag.Diagnostics, err error, projectId, instanceId string) {
utils.LogError(
ctx,
diags,
err,
"Reading database",
fmt.Sprintf(
"Could not retrieve database for instance %q in project %q.",
instanceId,
projectId,
),
map[int]string{
http.StatusBadRequest: fmt.Sprintf(
"Invalid request parameters for project %q and instance %q.",
projectId,
instanceId,
),
http.StatusNotFound: fmt.Sprintf(
"Database, instance %q, or project %q not found.",
instanceId,
projectId,
),
http.StatusForbidden: fmt.Sprintf("Forbidden access to project %q.", projectId),
},
)
}

View file

@ -0,0 +1,97 @@
package sqlserverflexalpha
import (
"fmt"
"strings"
"github.com/hashicorp/terraform-plugin-framework/types"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexalpha"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils"
)
// mapFields maps fields from a ListDatabase API response to a resourceModel for the data source.
func mapFields(source *sqlserverflexalpha.GetDatabaseResponse, model *dataSourceModel, region string) error {
if source == nil {
return fmt.Errorf("response is nil")
}
if source.Id == nil || *source.Id == 0 {
return fmt.Errorf("id not present")
}
if model == nil {
return fmt.Errorf("model given is nil")
}
var databaseId int64
if model.Id.ValueInt64() != 0 {
databaseId = model.Id.ValueInt64()
} else if source.Id != nil {
databaseId = *source.Id
} else {
return fmt.Errorf("database id not present")
}
model.Id = types.Int64Value(databaseId)
model.DatabaseName = types.StringValue(source.GetName())
model.Name = types.StringValue(source.GetName())
model.Owner = types.StringValue(strings.Trim(source.GetOwner(), "\""))
model.Region = types.StringValue(region)
model.ProjectId = types.StringValue(model.ProjectId.ValueString())
model.InstanceId = types.StringValue(model.InstanceId.ValueString())
model.CompatibilityLevel = types.Int64Value(source.GetCompatibilityLevel())
model.CollationName = types.StringValue(source.GetCollationName())
model.TerraformID = utils.BuildInternalTerraformId(
model.ProjectId.ValueString(),
region,
model.InstanceId.ValueString(),
model.DatabaseName.ValueString(),
)
return nil
}
// mapResourceFields maps fields from a ListDatabase API response to a resourceModel for the resource.
func mapResourceFields(source *sqlserverflexalpha.GetDatabaseResponse, model *resourceModel, region string) error {
if source == nil {
return fmt.Errorf("response is nil")
}
if source.Id == nil || *source.Id == 0 {
return fmt.Errorf("id not present")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
var databaseId int64
if model.Id.ValueInt64() != 0 {
databaseId = model.Id.ValueInt64()
} else if source.Id != nil {
databaseId = *source.Id
} else {
return fmt.Errorf("database id not present")
}
model.Id = types.Int64Value(databaseId)
model.DatabaseName = types.StringValue(source.GetName())
model.Name = types.StringValue(source.GetName())
model.Owner = types.StringValue(strings.Trim(source.GetOwner(), "\""))
model.Region = types.StringValue(region)
model.ProjectId = types.StringValue(model.ProjectId.ValueString())
model.InstanceId = types.StringValue(model.InstanceId.ValueString())
return nil
}
// toCreatePayload converts the resource model to an API create payload.
func toCreatePayload(model *resourceModel) (*sqlserverflexalpha.CreateDatabaseRequestPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
return &sqlserverflexalpha.CreateDatabaseRequestPayload{
Name: model.Name.ValueStringPointer(),
Owner: model.Owner.ValueStringPointer(),
Collation: model.Collation.ValueStringPointer(),
Compatibility: model.Compatibility.ValueInt64Pointer(),
}, nil
}

View file

@ -0,0 +1,227 @@
package sqlserverflexalpha
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexalpha"
datasource "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexalpha/database/datasources_gen"
)
func TestMapFields(t *testing.T) {
type given struct {
source *sqlserverflexalpha.GetDatabaseResponse
model *dataSourceModel
region string
}
type expected struct {
model *dataSourceModel
err bool
}
testcases := []struct {
name string
given given
expected expected
}{
{
name: "should map fields correctly",
given: given{
source: &sqlserverflexalpha.GetDatabaseResponse{
Id: utils.Ptr(int64(1)),
Name: utils.Ptr("my-db"),
CollationName: utils.Ptr("collation"),
CompatibilityLevel: utils.Ptr(int64(150)),
Owner: utils.Ptr("\"my-owner\""),
},
model: &dataSourceModel{
DatabaseModel: datasource.DatabaseModel{
ProjectId: types.StringValue("my-project"),
InstanceId: types.StringValue("my-instance"),
},
},
region: "eu01",
},
expected: expected{
model: &dataSourceModel{
DatabaseModel: datasource.DatabaseModel{
Id: types.Int64Value(1),
Name: types.StringValue("my-db"),
DatabaseName: types.StringValue("my-db"),
Owner: types.StringValue("my-owner"),
Region: types.StringValue("eu01"),
InstanceId: types.StringValue("my-instance"),
ProjectId: types.StringValue("my-project"),
CompatibilityLevel: types.Int64Value(150),
CollationName: types.StringValue("collation"),
},
TerraformID: types.StringValue("my-project,eu01,my-instance,my-db"),
},
},
},
{
name: "should fail on nil source",
given: given{
source: nil,
model: &dataSourceModel{},
},
expected: expected{err: true},
},
{
name: "should fail on nil source ID",
given: given{
source: &sqlserverflexalpha.GetDatabaseResponse{Id: nil},
model: &dataSourceModel{},
},
expected: expected{err: true},
},
{
name: "should fail on nil model",
given: given{
source: &sqlserverflexalpha.GetDatabaseResponse{Id: utils.Ptr(int64(1))},
model: nil,
},
expected: expected{err: true},
},
}
for _, tc := range testcases {
t.Run(
tc.name, func(t *testing.T) {
err := mapFields(tc.given.source, tc.given.model, tc.given.region)
if (err != nil) != tc.expected.err {
t.Fatalf("expected error: %v, got: %v", tc.expected.err, err)
}
if err == nil {
if diff := cmp.Diff(tc.expected.model, tc.given.model); diff != "" {
t.Errorf("model mismatch (-want +got):\n%s", diff)
}
}
},
)
}
}
func TestMapResourceFields(t *testing.T) {
type given struct {
source *sqlserverflexalpha.GetDatabaseResponse
model *resourceModel
region string
}
type expected struct {
model *resourceModel
err bool
}
testcases := []struct {
name string
given given
expected expected
}{
{
name: "should map fields correctly",
given: given{
source: &sqlserverflexalpha.GetDatabaseResponse{
Id: utils.Ptr(int64(1)),
Name: utils.Ptr("my-db"),
Owner: utils.Ptr("\"my-owner\""),
},
model: &resourceModel{
ProjectId: types.StringValue("my-project"),
InstanceId: types.StringValue("my-instance"),
},
region: "eu01",
},
expected: expected{
model: &resourceModel{
Id: types.Int64Value(1),
Name: types.StringValue("my-db"),
DatabaseName: types.StringValue("my-db"),
InstanceId: types.StringValue("my-instance"),
ProjectId: types.StringValue("my-project"),
Region: types.StringValue("eu01"),
Owner: types.StringValue("my-owner"),
},
},
},
{
name: "should fail on nil source",
given: given{
source: nil,
model: &resourceModel{},
},
expected: expected{err: true},
},
}
for _, tc := range testcases {
t.Run(
tc.name, func(t *testing.T) {
err := mapResourceFields(tc.given.source, tc.given.model, tc.given.region)
if (err != nil) != tc.expected.err {
t.Fatalf("expected error: %v, got: %v", tc.expected.err, err)
}
if err == nil {
if diff := cmp.Diff(tc.expected.model, tc.given.model); diff != "" {
t.Errorf("model mismatch (-want +got):\n%s", diff)
}
}
},
)
}
}
func TestToCreatePayload(t *testing.T) {
type given struct {
model *resourceModel
}
type expected struct {
payload *sqlserverflexalpha.CreateDatabaseRequestPayload
err bool
}
testcases := []struct {
name string
given given
expected expected
}{
{
name: "should convert model to payload",
given: given{
model: &resourceModel{
Name: types.StringValue("my-db"),
Owner: types.StringValue("my-owner"),
},
},
expected: expected{
payload: &sqlserverflexalpha.CreateDatabaseRequestPayload{
Name: utils.Ptr("my-db"),
Owner: utils.Ptr("my-owner"),
},
},
},
{
name: "should fail on nil model",
given: given{model: nil},
expected: expected{err: true},
},
}
for _, tc := range testcases {
t.Run(
tc.name, func(t *testing.T) {
actual, err := toCreatePayload(tc.given.model)
if (err != nil) != tc.expected.err {
t.Fatalf("expected error: %v, got: %v", tc.expected.err, err)
}
if err == nil {
if diff := cmp.Diff(tc.expected.payload, actual); diff != "" {
t.Errorf("payload mismatch (-want +got):\n%s", diff)
}
}
},
)
}
}

View file

@ -3,7 +3,9 @@ package sqlserverflexalpha
import (
"context"
_ "embed"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
@ -13,6 +15,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/stackit-sdk-go/core/config"
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexalpha"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion"
@ -28,6 +31,13 @@ var (
_ resource.ResourceWithImportState = &databaseResource{}
_ resource.ResourceWithModifyPlan = &databaseResource{}
_ resource.ResourceWithIdentity = &databaseResource{}
// Define errors
errDatabaseNotFound = errors.New("database not found")
// Error message constants
extractErrorSummary = "extracting failed"
extractErrorMessage = "Extracting identity data: %v"
)
func NewDatabaseResource() resource.Resource {
@ -137,73 +147,230 @@ func (r *databaseResource) Configure(
}
func (r *databaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data sqlserverflexalphaGen.DatabaseModel
// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
var model resourceModel
diags := req.Plan.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// TODO: Create API call logic
// Read identity data
var identityData DatabaseResourceIdentityModel
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
if resp.Diagnostics.HasError() {
return
}
// Example data value setting
// data.DatabaseId = types.StringValue("id-from-response")
ctx = core.InitProviderContext(ctx)
// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
projectId := identityData.ProjectID.ValueString()
region := identityData.ProjectID.ValueString()
instanceId := identityData.InstanceID.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "instance_id", instanceId)
ctx = tflog.SetField(ctx, "region", region)
// Generate API request body from model
payload, err := toCreatePayload(&model)
if err != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
"Error creating database",
fmt.Sprintf("Creating API payload: %v", err),
)
return
}
// Create new database
databaseResp, err := r.client.CreateDatabaseRequest(
ctx,
projectId,
region,
instanceId,
).CreateDatabaseRequestPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating database", fmt.Sprintf("Calling API: %v", err))
return
}
ctx = core.LogResponse(ctx)
if databaseResp == nil || databaseResp.Id == nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
"Error creating database",
"API didn't return database Id. A database might have been created",
)
return
}
databaseId := *databaseResp.Id
databaseName := model.DatabaseName.String()
ctx = tflog.SetField(ctx, "database_id", databaseId)
ctx = tflog.SetField(ctx, "database_name", databaseName)
ctx = core.LogResponse(ctx)
database, err := r.client.GetDatabaseRequest(ctx, projectId, region, instanceId, databaseName).Execute()
if err != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
"Error creating database",
fmt.Sprintf("Getting database details after creation: %v", err),
)
return
}
// Map response body to schema
err = mapResourceFields(database, &model, region)
if err != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
"Error creating database",
fmt.Sprintf("Processing API payload: %v", err),
)
return
}
// Set data returned by API in identity
identity := DatabaseResourceIdentityModel{
ProjectID: types.StringValue(projectId),
Region: types.StringValue(region),
InstanceID: types.StringValue(instanceId),
DatabaseName: types.StringValue(databaseName),
}
resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...)
if resp.Diagnostics.HasError() {
return
}
// Set state to fully populated data
resp.Diagnostics.Append(resp.State.Set(ctx, model)...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "sqlserverflexalpha.Database created")
}
func (r *databaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data resourceModel
// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
var model resourceModel
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Todo: Read API call logic
// Read identity data
var identityData DatabaseResourceIdentityModel
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
if resp.Diagnostics.HasError() {
return
}
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
ctx = core.InitProviderContext(ctx)
projectId, instanceId, region, databaseName, errExt := r.extractIdentityData(model, identityData)
if errExt != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
extractErrorSummary,
fmt.Sprintf(extractErrorMessage, errExt),
)
}
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "instance_id", instanceId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "database_name", databaseName)
databaseResp, err := r.client.GetDatabaseRequest(ctx, projectId, region, instanceId, databaseName).Execute()
if err != nil {
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
if (ok && oapiErr.StatusCode == http.StatusNotFound) || errors.Is(err, errDatabaseNotFound) {
resp.State.RemoveResource(ctx)
return
}
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading database", fmt.Sprintf("Calling API: %v", err))
return
}
ctx = core.LogResponse(ctx)
// Map response body to schema
err = mapResourceFields(databaseResp, &model, region)
if err != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
"Error reading database",
fmt.Sprintf("Processing API payload: %v", err),
)
return
}
// Set refreshed state
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "sqlserverflexalpha.Database read")
}
func (r *databaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data resourceModel
// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Todo: Update API call logic
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
tflog.Info(ctx, "sqlserverflexalpha.Database updated")
func (r *databaseResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) {
// TODO: Check update api endpoint - not available at the moment, so return an error for now
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating database", "Database can't be updated")
}
func (r *databaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data sqlserverflexalphaGen.DatabaseModel
// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
// nolint:gocritic // function signature required by Terraform
var model resourceModel
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Todo: Delete API call logic
// Read identity data
var identityData DatabaseResourceIdentityModel
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
if resp.Diagnostics.HasError() {
return
}
ctx = core.InitProviderContext(ctx)
projectId, instanceId, region, databaseName, errExt := r.extractIdentityData(model, identityData)
if errExt != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
extractErrorSummary,
fmt.Sprintf(extractErrorMessage, errExt),
)
}
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "instance_id", instanceId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "database_name", databaseName)
// Delete existing record set
err := r.client.DeleteDatabaseRequestExecute(ctx, projectId, region, instanceId, databaseName)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting database", fmt.Sprintf("Calling API: %v", err))
}
ctx = core.LogResponse(ctx)
tflog.Info(ctx, "sqlserverflexalpha.Database deleted")
}
@ -312,3 +479,46 @@ func (r *databaseResource) ImportState(
tflog.Info(ctx, "Sqlserverflexalpha database state imported")
}
// extractIdentityData extracts essential identifiers from the resource model, falling back to the identity model.
func (r *databaseResource) extractIdentityData(
model resourceModel,
identity DatabaseResourceIdentityModel,
) (projectId, region, instanceId, databaseName string, err error) {
if !model.DatabaseName.IsNull() && !model.DatabaseName.IsUnknown() {
databaseName = model.DatabaseName.ValueString()
} else {
if identity.DatabaseName.IsNull() || identity.DatabaseName.IsUnknown() {
return "", "", "", "", fmt.Errorf("database_name not found in config")
}
databaseName = identity.DatabaseName.ValueString()
}
if !model.ProjectId.IsNull() && !model.ProjectId.IsUnknown() {
projectId = model.ProjectId.ValueString()
} else {
if identity.ProjectID.IsNull() || identity.ProjectID.IsUnknown() {
return "", "", "", "", fmt.Errorf("project_id not found in config")
}
projectId = identity.ProjectID.ValueString()
}
if !model.Region.IsNull() && !model.Region.IsUnknown() {
region = r.providerData.GetRegionWithOverride(model.Region)
} else {
if identity.Region.IsNull() || identity.Region.IsUnknown() {
return "", "", "", "", fmt.Errorf("region not found in config")
}
region = r.providerData.GetRegionWithOverride(identity.Region)
}
if !model.InstanceId.IsNull() && !model.InstanceId.IsUnknown() {
instanceId = model.InstanceId.ValueString()
} else {
if identity.InstanceID.IsNull() || identity.InstanceID.IsUnknown() {
return "", "", "", "", fmt.Errorf("instance_id not found in config")
}
instanceId = identity.InstanceID.ValueString()
}
return projectId, region, instanceId, databaseName, nil
}

View file

@ -4,23 +4,19 @@ import (
"context"
"fmt"
"net/http"
"strconv"
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
"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/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexalpha"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion"
sqlserverflexUtils "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexalpha/utils"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-log/tflog"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/core"
sqlserverflexUtils "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexalpha/utils"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/validate"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
// Ensure the implementation satisfies the expected interfaces.
@ -34,6 +30,7 @@ func NewUserDataSource() datasource.DataSource {
}
type dataSourceModel struct {
//TODO: check generated data source for the correct types and pointers
Id types.String `tfsdk:"id"` // needed by TF
UserId types.Int64 `tfsdk:"user_id"`
InstanceId types.String `tfsdk:"instance_id"`
@ -79,7 +76,7 @@ func (r *userDataSource) Configure(
return
}
r.client = apiClient
tflog.Info(ctx, "SQLServer Flex user client configured")
tflog.Info(ctx, "SQLServer Flex beta user client configured")
}
// Schema defines the schema for the data source.
@ -223,50 +220,5 @@ func (r *userDataSource) Read(
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "SQLServer Flex instance read")
}
func mapDataSourceFields(userResp *sqlserverflexalpha.GetUserResponse, model *dataSourceModel, region string) error {
if userResp == nil {
return fmt.Errorf("response is nil")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
user := userResp
var userId int64
if model.UserId.ValueInt64() != 0 {
userId = model.UserId.ValueInt64()
} else if user.Id != nil {
userId = *user.Id
} else {
return fmt.Errorf("user id not present")
}
model.Id = utils.BuildInternalTerraformId(
model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), strconv.FormatInt(userId, 10),
)
model.UserId = types.Int64Value(userId)
model.Username = types.StringPointerValue(user.Username)
if user.Roles == nil {
model.Roles = types.SetNull(types.StringType)
} else {
var roles []attr.Value
for _, role := range *user.Roles {
roles = append(roles, types.StringValue(string(role)))
}
rolesSet, diags := types.SetValue(types.StringType, roles)
if diags.HasError() {
return fmt.Errorf("failed to map roles: %w", core.DiagsToError(diags))
}
model.Roles = rolesSet
}
model.Host = types.StringPointerValue(user.Host)
model.Port = types.Int64PointerValue(user.Port)
model.Region = types.StringValue(region)
model.Status = types.StringPointerValue(user.Status)
model.DefaultDatabase = types.StringPointerValue(user.DefaultDatabase)
return nil
tflog.Info(ctx, "SQLServer Flex alpha instance read")
}

View file

@ -1,147 +0,0 @@
package sqlserverflexalpha
import (
"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"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexalpha"
)
func TestMapDataSourceFields(t *testing.T) {
const testRegion = "region"
tests := []struct {
description string
input *sqlserverflexalpha.GetUserResponse
region string
expected dataSourceModel
isValid bool
}{
{
"default_values",
&sqlserverflexalpha.GetUserResponse{},
testRegion,
dataSourceModel{
Id: types.StringValue("pid,region,iid,1"),
UserId: types.Int64Value(1),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
Username: types.StringNull(),
Roles: types.SetNull(types.StringType),
Host: types.StringNull(),
Port: types.Int64Null(),
Region: types.StringValue(testRegion),
Status: types.StringNull(),
DefaultDatabase: types.StringNull(),
},
true,
},
{
"simple_values",
&sqlserverflexalpha.GetUserResponse{
Roles: &[]sqlserverflexalpha.UserRole{
"role_1",
"role_2",
"",
},
Username: utils.Ptr("username"),
Host: utils.Ptr("host"),
Port: utils.Ptr(int64(1234)),
Status: utils.Ptr("active"),
DefaultDatabase: utils.Ptr("default_db"),
},
testRegion,
dataSourceModel{
Id: types.StringValue("pid,region,iid,1"),
UserId: types.Int64Value(1),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
Username: types.StringValue("username"),
Roles: types.SetValueMust(
types.StringType, []attr.Value{
types.StringValue("role_1"),
types.StringValue("role_2"),
types.StringValue(""),
},
),
Host: types.StringValue("host"),
Port: types.Int64Value(1234),
Region: types.StringValue(testRegion),
Status: types.StringValue("active"),
DefaultDatabase: types.StringValue("default_db"),
},
true,
},
{
"null_fields_and_int_conversions",
&sqlserverflexalpha.GetUserResponse{
Id: utils.Ptr(int64(1)),
Roles: &[]sqlserverflexalpha.UserRole{},
Username: nil,
Host: nil,
Port: utils.Ptr(int64(2123456789)),
},
testRegion,
dataSourceModel{
Id: types.StringValue("pid,region,iid,1"),
UserId: types.Int64Value(1),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
Username: types.StringNull(),
Roles: types.SetValueMust(types.StringType, []attr.Value{}),
Host: types.StringNull(),
Port: types.Int64Value(2123456789),
Region: types.StringValue(testRegion),
},
true,
},
{
"nil_response",
nil,
testRegion,
dataSourceModel{},
false,
},
{
"nil_response_2",
&sqlserverflexalpha.GetUserResponse{},
testRegion,
dataSourceModel{},
false,
},
{
"no_resource_id",
&sqlserverflexalpha.GetUserResponse{},
testRegion,
dataSourceModel{},
false,
},
}
for _, tt := range tests {
t.Run(
tt.description, func(t *testing.T) {
state := &dataSourceModel{
ProjectId: tt.expected.ProjectId,
InstanceId: tt.expected.InstanceId,
UserId: tt.expected.UserId,
}
err := mapDataSourceFields(tt.input, state, tt.region)
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(state, &tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
},
)
}
}

View file

@ -0,0 +1,179 @@
package sqlserverflexalpha
import (
"fmt"
"strconv"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexalpha"
"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"
)
// mapDataSourceFields maps the API response to a dataSourceModel.
func mapDataSourceFields(userResp *sqlserverflexalpha.GetUserResponse, model *dataSourceModel, region string) error {
if userResp == nil {
return fmt.Errorf("response is nil")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
user := userResp
// Handle user ID
var userId int64
if model.UserId.ValueInt64() != 0 {
userId = model.UserId.ValueInt64()
} else if user.Id != nil {
userId = *user.Id
} else {
return fmt.Errorf("user id not present")
}
// Set main attributes
model.Id = utils.BuildInternalTerraformId(
model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), strconv.FormatInt(userId, 10),
)
model.UserId = types.Int64Value(userId)
model.Username = types.StringPointerValue(user.Username)
// Map roles
if user.Roles == nil {
model.Roles = types.SetNull(types.StringType)
} else {
var roles []attr.Value
for _, role := range *user.Roles {
roles = append(roles, types.StringValue(string(role)))
}
rolesSet, diags := types.SetValue(types.StringType, roles)
if diags.HasError() {
return fmt.Errorf("failed to map roles: %w", core.DiagsToError(diags))
}
model.Roles = rolesSet
}
// Set remaining attributes
model.Host = types.StringPointerValue(user.Host)
model.Port = types.Int64PointerValue(user.Port)
model.Region = types.StringValue(region)
model.Status = types.StringPointerValue(user.Status)
model.DefaultDatabase = types.StringPointerValue(user.DefaultDatabase)
return nil
}
// mapFields maps the API response to a resourceModel.
func mapFields(userResp *sqlserverflexalpha.GetUserResponse, model *resourceModel, region string) error {
if userResp == nil {
return fmt.Errorf("response is nil")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
user := userResp
// Handle user ID
var userId int64
if model.UserId.ValueInt64() != 0 {
userId = model.UserId.ValueInt64()
} else if user.Id != nil {
userId = *user.Id
} else {
return fmt.Errorf("user id not present")
}
// Set main attributes
model.Id = types.Int64Value(userId)
model.UserId = types.Int64Value(userId)
model.Username = types.StringPointerValue(user.Username)
// Map roles
if user.Roles != nil {
var roles []attr.Value
for _, role := range *user.Roles {
roles = append(roles, types.StringValue(string(role)))
}
rolesSet, diags := types.SetValue(types.StringType, roles)
if diags.HasError() {
return fmt.Errorf("failed to map roles: %w", core.DiagsToError(diags))
}
model.Roles = types.List(rolesSet)
}
// Ensure roles is not null
if model.Roles.IsNull() || model.Roles.IsUnknown() {
model.Roles = types.List(types.SetNull(types.StringType))
}
// Set connection details
model.Host = types.StringPointerValue(user.Host)
model.Port = types.Int64PointerValue(user.Port)
model.Region = types.StringValue(region)
return nil
}
// mapFieldsCreate maps the API response from creating a user to a resourceModel.
func mapFieldsCreate(userResp *sqlserverflexalpha.CreateUserResponse, model *resourceModel, region string) error {
if userResp == nil {
return fmt.Errorf("response is nil")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
user := userResp
if user.Id == nil {
return fmt.Errorf("user id not present")
}
userId := *user.Id
model.Id = types.Int64Value(userId)
model.UserId = types.Int64Value(userId)
model.Username = types.StringPointerValue(user.Username)
if user.Password == nil {
return fmt.Errorf("user password not present")
}
model.Password = types.StringValue(*user.Password)
if user.Roles != nil {
var roles []attr.Value
for _, role := range *user.Roles {
roles = append(roles, types.StringValue(string(role)))
}
rolesSet, diags := types.SetValue(types.StringType, roles)
if diags.HasError() {
return fmt.Errorf("failed to map roles: %w", core.DiagsToError(diags))
}
model.Roles = types.List(rolesSet)
}
if model.Roles.IsNull() || model.Roles.IsUnknown() {
model.Roles = types.List(types.SetNull(types.StringType))
}
model.Host = types.StringPointerValue(user.Host)
model.Port = types.Int64PointerValue(user.Port)
model.Region = types.StringValue(region)
model.Status = types.StringPointerValue(user.Status)
model.DefaultDatabase = types.StringPointerValue(user.DefaultDatabase)
return nil
}
// toCreatePayload converts a resourceModel to an API CreateUserRequestPayload.
func toCreatePayload(
model *resourceModel,
roles []sqlserverflexalpha.UserRole,
) (*sqlserverflexalpha.CreateUserRequestPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
return &sqlserverflexalpha.CreateUserRequestPayload{
Username: conversion.StringValueToPointer(model.Username),
DefaultDatabase: conversion.StringValueToPointer(model.DefaultDatabase),
Roles: &roles,
}, nil
}

View file

@ -0,0 +1,525 @@
package sqlserverflexalpha
import (
"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"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexalpha"
)
func TestMapDataSourceFields(t *testing.T) {
const testRegion = "region"
tests := []struct {
description string
input *sqlserverflexalpha.GetUserResponse
region string
expected dataSourceModel
isValid bool
}{
{
"default_values",
&sqlserverflexalpha.GetUserResponse{},
testRegion,
dataSourceModel{
Id: types.StringValue("pid,region,iid,1"),
UserId: types.Int64Value(1),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
Username: types.StringNull(),
Roles: types.SetNull(types.StringType),
Host: types.StringNull(),
Port: types.Int64Null(),
Region: types.StringValue(testRegion),
Status: types.StringNull(),
DefaultDatabase: types.StringNull(),
},
true,
},
{
"simple_values",
&sqlserverflexalpha.GetUserResponse{
Roles: &[]sqlserverflexalpha.UserRole{
"role_1",
"role_2",
"",
},
Username: utils.Ptr("username"),
Host: utils.Ptr("host"),
Port: utils.Ptr(int64(1234)),
Status: utils.Ptr("active"),
DefaultDatabase: utils.Ptr("default_db"),
},
testRegion,
dataSourceModel{
Id: types.StringValue("pid,region,iid,1"),
UserId: types.Int64Value(1),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
Username: types.StringValue("username"),
Roles: types.SetValueMust(
types.StringType, []attr.Value{
types.StringValue("role_1"),
types.StringValue("role_2"),
types.StringValue(""),
},
),
Host: types.StringValue("host"),
Port: types.Int64Value(1234),
Region: types.StringValue(testRegion),
Status: types.StringValue("active"),
DefaultDatabase: types.StringValue("default_db"),
},
true,
},
{
"null_fields_and_int_conversions",
&sqlserverflexalpha.GetUserResponse{
Id: utils.Ptr(int64(1)),
Roles: &[]sqlserverflexalpha.UserRole{},
Username: nil,
Host: nil,
Port: utils.Ptr(int64(2123456789)),
},
testRegion,
dataSourceModel{
Id: types.StringValue("pid,region,iid,1"),
UserId: types.Int64Value(1),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
Username: types.StringNull(),
Roles: types.SetValueMust(types.StringType, []attr.Value{}),
Host: types.StringNull(),
Port: types.Int64Value(2123456789),
Region: types.StringValue(testRegion),
},
true,
},
{
"nil_response",
nil,
testRegion,
dataSourceModel{},
false,
},
{
"nil_response_2",
&sqlserverflexalpha.GetUserResponse{},
testRegion,
dataSourceModel{},
false,
},
{
"no_resource_id",
&sqlserverflexalpha.GetUserResponse{},
testRegion,
dataSourceModel{},
false,
},
}
for _, tt := range tests {
t.Run(
tt.description, func(t *testing.T) {
state := &dataSourceModel{
ProjectId: tt.expected.ProjectId,
InstanceId: tt.expected.InstanceId,
UserId: tt.expected.UserId,
}
err := mapDataSourceFields(tt.input, state, tt.region)
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(state, &tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
},
)
}
}
func TestMapFieldsCreate(t *testing.T) {
const testRegion = "region"
tests := []struct {
description string
input *sqlserverflexalpha.CreateUserResponse
region string
expected resourceModel
isValid bool
}{
{
"default_values",
&sqlserverflexalpha.CreateUserResponse{
Id: utils.Ptr(int64(1)),
Password: utils.Ptr(""),
},
testRegion,
resourceModel{
Id: types.Int64Value(1),
UserId: types.Int64Value(1),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
Username: types.StringNull(),
Roles: types.List(types.SetNull(types.StringType)),
Password: types.StringValue(""),
Host: types.StringNull(),
Port: types.Int64Null(),
Region: types.StringValue(testRegion),
},
true,
},
{
"simple_values",
&sqlserverflexalpha.CreateUserResponse{
Id: utils.Ptr(int64(2)),
Roles: &[]sqlserverflexalpha.UserRole{
"role_1",
"role_2",
"",
},
Username: utils.Ptr("username"),
Password: utils.Ptr("password"),
Host: utils.Ptr("host"),
Port: utils.Ptr(int64(1234)),
Status: utils.Ptr("status"),
DefaultDatabase: utils.Ptr("default_db"),
},
testRegion,
resourceModel{
Id: types.Int64Value(2),
UserId: types.Int64Value(2),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
Username: types.StringValue("username"),
Roles: types.List(
types.SetValueMust(
types.StringType, []attr.Value{
types.StringValue("role_1"),
types.StringValue("role_2"),
types.StringValue(""),
},
),
),
Password: types.StringValue("password"),
Host: types.StringValue("host"),
Port: types.Int64Value(1234),
Region: types.StringValue(testRegion),
Status: types.StringValue("status"),
DefaultDatabase: types.StringValue("default_db"),
},
true,
},
{
"null_fields_and_int_conversions",
&sqlserverflexalpha.CreateUserResponse{
Id: utils.Ptr(int64(3)),
Roles: &[]sqlserverflexalpha.UserRole{},
Username: nil,
Password: utils.Ptr(""),
Host: nil,
Port: utils.Ptr(int64(2123456789)),
},
testRegion,
resourceModel{
Id: types.Int64Value(3),
UserId: types.Int64Value(3),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
Username: types.StringNull(),
Roles: types.List(types.SetValueMust(types.StringType, []attr.Value{})),
Password: types.StringValue(""),
Host: types.StringNull(),
Port: types.Int64Value(2123456789),
Region: types.StringValue(testRegion),
DefaultDatabase: types.StringNull(),
Status: types.StringNull(),
},
true,
},
{
"nil_response",
nil,
testRegion,
resourceModel{},
false,
},
{
"nil_response_2",
&sqlserverflexalpha.CreateUserResponse{},
testRegion,
resourceModel{},
false,
},
{
"no_resource_id",
&sqlserverflexalpha.CreateUserResponse{},
testRegion,
resourceModel{},
false,
},
{
"no_password",
&sqlserverflexalpha.CreateUserResponse{
Id: utils.Ptr(int64(1)),
},
testRegion,
resourceModel{},
false,
},
}
for _, tt := range tests {
t.Run(
tt.description, func(t *testing.T) {
state := &resourceModel{
ProjectId: tt.expected.ProjectId,
InstanceId: tt.expected.InstanceId,
}
err := mapFieldsCreate(tt.input, state, tt.region)
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(state, &tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
},
)
}
}
func TestMapFields(t *testing.T) {
const testRegion = "region"
tests := []struct {
description string
input *sqlserverflexalpha.GetUserResponse
region string
expected resourceModel
isValid bool
}{
{
"default_values",
&sqlserverflexalpha.GetUserResponse{},
testRegion,
resourceModel{
Id: types.Int64Value(1),
UserId: types.Int64Value(1),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
Username: types.StringNull(),
Roles: types.List(types.SetNull(types.StringType)),
Host: types.StringNull(),
Port: types.Int64Null(),
Region: types.StringValue(testRegion),
},
true,
},
{
"simple_values",
&sqlserverflexalpha.GetUserResponse{
Roles: &[]sqlserverflexalpha.UserRole{
"role_1",
"role_2",
"",
},
Username: utils.Ptr("username"),
Host: utils.Ptr("host"),
Port: utils.Ptr(int64(1234)),
},
testRegion,
resourceModel{
Id: types.Int64Value(2),
UserId: types.Int64Value(2),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
Username: types.StringValue("username"),
Roles: types.List(
types.SetValueMust(
types.StringType, []attr.Value{
types.StringValue("role_1"),
types.StringValue("role_2"),
types.StringValue(""),
},
),
),
Host: types.StringValue("host"),
Port: types.Int64Value(1234),
Region: types.StringValue(testRegion),
},
true,
},
{
"null_fields_and_int_conversions",
&sqlserverflexalpha.GetUserResponse{
Id: utils.Ptr(int64(1)),
Roles: &[]sqlserverflexalpha.UserRole{},
Username: nil,
Host: nil,
Port: utils.Ptr(int64(2123456789)),
},
testRegion,
resourceModel{
Id: types.Int64Value(1),
UserId: types.Int64Value(1),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
Username: types.StringNull(),
Roles: types.List(types.SetValueMust(types.StringType, []attr.Value{})),
Host: types.StringNull(),
Port: types.Int64Value(2123456789),
Region: types.StringValue(testRegion),
},
true,
},
{
"nil_response",
nil,
testRegion,
resourceModel{},
false,
},
{
"nil_response_2",
&sqlserverflexalpha.GetUserResponse{},
testRegion,
resourceModel{},
false,
},
{
"no_resource_id",
&sqlserverflexalpha.GetUserResponse{},
testRegion,
resourceModel{},
false,
},
}
for _, tt := range tests {
t.Run(
tt.description, func(t *testing.T) {
state := &resourceModel{
ProjectId: tt.expected.ProjectId,
InstanceId: tt.expected.InstanceId,
UserId: tt.expected.UserId,
}
err := mapFields(tt.input, state, tt.region)
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(state, &tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
},
)
}
}
func TestToCreatePayload(t *testing.T) {
tests := []struct {
description string
input *resourceModel
inputRoles []sqlserverflexalpha.UserRole
expected *sqlserverflexalpha.CreateUserRequestPayload
isValid bool
}{
{
"default_values",
&resourceModel{},
[]sqlserverflexalpha.UserRole{},
&sqlserverflexalpha.CreateUserRequestPayload{
Roles: &[]sqlserverflexalpha.UserRole{},
Username: nil,
},
true,
},
{
"default_values",
&resourceModel{
Username: types.StringValue("username"),
},
[]sqlserverflexalpha.UserRole{
"role_1",
"role_2",
},
&sqlserverflexalpha.CreateUserRequestPayload{
Roles: &[]sqlserverflexalpha.UserRole{
"role_1",
"role_2",
},
Username: utils.Ptr("username"),
},
true,
},
{
"null_fields_and_int_conversions",
&resourceModel{
Username: types.StringNull(),
},
[]sqlserverflexalpha.UserRole{
"",
},
&sqlserverflexalpha.CreateUserRequestPayload{
Roles: &[]sqlserverflexalpha.UserRole{
"",
},
Username: nil,
},
true,
},
{
"nil_model",
nil,
[]sqlserverflexalpha.UserRole{},
nil,
false,
},
{
"nil_roles",
&resourceModel{
Username: types.StringValue("username"),
},
[]sqlserverflexalpha.UserRole{},
&sqlserverflexalpha.CreateUserRequestPayload{
Roles: &[]sqlserverflexalpha.UserRole{},
Username: utils.Ptr("username"),
},
true,
},
}
for _, tt := range tests {
t.Run(
tt.description, func(t *testing.T) {
output, err := toCreatePayload(tt.input, tt.inputRoles)
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(output, tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
},
)
}
}

View file

@ -9,12 +9,12 @@ import (
"strconv"
"strings"
"github.com/hashicorp/terraform-plugin-framework/resource/identityschema"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexalpha"
sqlserverflexalphagen "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexalpha/user/resources_gen"
sqlserverflexalphaUtils "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexalpha/utils"
sqlserverflexalphaWait "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/wait/sqlserverflexalpha"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
@ -31,6 +31,7 @@ var (
_ resource.ResourceWithConfigure = &userResource{}
_ resource.ResourceWithImportState = &userResource{}
_ resource.ResourceWithModifyPlan = &userResource{}
_ resource.ResourceWithIdentity = &userResource{}
)
// NewUserResource is a helper function to simplify the provider implementation.
@ -131,6 +132,30 @@ func (r *userResource) Schema(ctx context.Context, _ resource.SchemaRequest, res
resp.Schema = s
}
// IdentitySchema defines the schema for the resource's identity attributes.
func (r *userResource) IdentitySchema(
_ context.Context,
_ resource.IdentitySchemaRequest,
response *resource.IdentitySchemaResponse,
) {
response.IdentitySchema = identityschema.Schema{
Attributes: map[string]identityschema.Attribute{
"project_id": identityschema.StringAttribute{
RequiredForImport: true, // must be set during import by the practitioner
},
"region": identityschema.StringAttribute{
RequiredForImport: true, // can be defaulted by the provider configuration
},
"instance_id": identityschema.StringAttribute{
RequiredForImport: true, // can be defaulted by the provider configuration
},
"user_id": identityschema.Int64Attribute{
RequiredForImport: true, // can be defaulted by the provider configuration
},
},
}
}
// Create creates the resource and sets the initial Terraform state.
func (r *userResource) Create(
ctx context.Context,
@ -402,109 +427,3 @@ func (r *userResource) ImportState(
)
tflog.Info(ctx, "SQLServer Flex user state imported")
}
func mapFieldsCreate(userResp *sqlserverflexalpha.CreateUserResponse, model *resourceModel, region string) error {
if userResp == nil {
return fmt.Errorf("response is nil")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
user := userResp
if user.Id == nil {
return fmt.Errorf("user id not present")
}
userId := *user.Id
model.Id = types.Int64Value(userId)
model.UserId = types.Int64Value(userId)
model.Username = types.StringPointerValue(user.Username)
if user.Password == nil {
return fmt.Errorf("user password not present")
}
model.Password = types.StringValue(*user.Password)
if user.Roles != nil {
var roles []attr.Value
for _, role := range *user.Roles {
roles = append(roles, types.StringValue(string(role)))
}
rolesSet, diags := types.SetValue(types.StringType, roles)
if diags.HasError() {
return fmt.Errorf("failed to map roles: %w", core.DiagsToError(diags))
}
model.Roles = types.List(rolesSet)
}
if model.Roles.IsNull() || model.Roles.IsUnknown() {
model.Roles = types.List(types.SetNull(types.StringType))
}
model.Host = types.StringPointerValue(user.Host)
model.Port = types.Int64PointerValue(user.Port)
model.Region = types.StringValue(region)
model.Status = types.StringPointerValue(user.Status)
model.DefaultDatabase = types.StringPointerValue(user.DefaultDatabase)
return nil
}
func mapFields(userResp *sqlserverflexalpha.GetUserResponse, model *resourceModel, region string) error {
if userResp == nil {
return fmt.Errorf("response is nil")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
user := userResp
var userId int64
if model.UserId.ValueInt64() != 0 {
userId = model.UserId.ValueInt64()
} else if user.Id != nil {
userId = *user.Id
} else {
return fmt.Errorf("user id not present")
}
model.Id = types.Int64Value(userId)
model.UserId = types.Int64Value(userId)
model.Username = types.StringPointerValue(user.Username)
if user.Roles != nil {
var roles []attr.Value
for _, role := range *user.Roles {
roles = append(roles, types.StringValue(string(role)))
}
rolesSet, diags := types.SetValue(types.StringType, roles)
if diags.HasError() {
return fmt.Errorf("failed to map roles: %w", core.DiagsToError(diags))
}
model.Roles = types.List(rolesSet)
}
if model.Roles.IsNull() || model.Roles.IsUnknown() {
model.Roles = types.List(types.SetNull(types.StringType))
}
model.Host = types.StringPointerValue(user.Host)
model.Port = types.Int64PointerValue(user.Port)
model.Region = types.StringValue(region)
return nil
}
func toCreatePayload(
model *resourceModel,
roles []sqlserverflexalpha.UserRole,
) (*sqlserverflexalpha.CreateUserRequestPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
return &sqlserverflexalpha.CreateUserRequestPayload{
Username: conversion.StringValueToPointer(model.Username),
DefaultDatabase: conversion.StringValueToPointer(model.DefaultDatabase),
Roles: &roles,
}, nil
}

View file

@ -1,388 +0,0 @@
package sqlserverflexalpha
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexalpha"
)
func TestMapFieldsCreate(t *testing.T) {
const testRegion = "region"
tests := []struct {
description string
input *sqlserverflexalpha.CreateUserResponse
region string
expected resourceModel
isValid bool
}{
//{
// "default_values",
// &sqlserverflexalpha.CreateUserResponse{
// Id: utils.Ptr(int64(1)),
// Password: utils.Ptr(""),
// },
// testRegion,
// resourceModel{
// Id: types.Int64Value(1),
// UserId: types.Int64Value(1),
// InstanceId: types.StringValue("iid"),
// ProjectId: types.StringValue("pid"),
// Username: types.StringNull(),
// Roles: types.List(types.SetNull(types.StringType)),
// Password: types.StringValue(""),
// Host: types.StringNull(),
// Port: types.Int64Null(),
// Region: types.StringValue(testRegion),
// },
// true,
//},
//{
// "simple_values",
// &sqlserverflexalpha.CreateUserResponse{
// Id: utils.Ptr(int64(2)),
// Roles: &[]sqlserverflexalpha.UserRole{
// "role_1",
// "role_2",
// "",
// },
// Username: utils.Ptr("username"),
// Password: utils.Ptr("password"),
// Host: utils.Ptr("host"),
// Port: utils.Ptr(int64(1234)),
// Status: utils.Ptr("status"),
// DefaultDatabase: utils.Ptr("default_db"),
// },
// testRegion,
// resourceModel{
// Id: types.Int64Value(2),
// UserId: types.Int64Value(2),
// InstanceId: types.StringValue("iid"),
// ProjectId: types.StringValue("pid"),
// Username: types.StringValue("username"),
// Roles: types.List(
// types.SetValueMust(
// types.StringType, []attr.Value{
// types.StringValue("role_1"),
// types.StringValue("role_2"),
// types.StringValue(""),
// },
// ),
// ),
// Password: types.StringValue("password"),
// Host: types.StringValue("host"),
// Port: types.Int64Value(1234),
// Region: types.StringValue(testRegion),
// Status: types.StringValue("status"),
// DefaultDatabase: types.StringValue("default_db"),
// },
// true,
//},
//{
// "null_fields_and_int_conversions",
// &sqlserverflexalpha.CreateUserResponse{
// Id: utils.Ptr(int64(3)),
// Roles: &[]sqlserverflexalpha.UserRole{},
// Username: nil,
// Password: utils.Ptr(""),
// Host: nil,
// Port: utils.Ptr(int64(2123456789)),
// },
// testRegion,
// resourceModel{
// Id: types.Int64Value(3),
// UserId: types.Int64Value(3),
// InstanceId: types.StringValue("iid"),
// ProjectId: types.StringValue("pid"),
// Username: types.StringNull(),
// Roles: types.List(types.SetValueMust(types.StringType, []attr.Value{})),
// Password: types.StringValue(""),
// Host: types.StringNull(),
// Port: types.Int64Value(2123456789),
// Region: types.StringValue(testRegion),
// DefaultDatabase: types.StringNull(),
// Status: types.StringNull(),
// },
// true,
//},
{
"nil_response",
nil,
testRegion,
resourceModel{},
false,
},
{
"nil_response_2",
&sqlserverflexalpha.CreateUserResponse{},
testRegion,
resourceModel{},
false,
},
{
"no_resource_id",
&sqlserverflexalpha.CreateUserResponse{},
testRegion,
resourceModel{},
false,
},
{
"no_password",
&sqlserverflexalpha.CreateUserResponse{
Id: utils.Ptr(int64(1)),
},
testRegion,
resourceModel{},
false,
},
}
for _, tt := range tests {
t.Run(
tt.description, func(t *testing.T) {
state := &resourceModel{
ProjectId: tt.expected.ProjectId,
InstanceId: tt.expected.InstanceId,
}
err := mapFieldsCreate(tt.input, state, tt.region)
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(state, &tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
},
)
}
}
func TestMapFields(t *testing.T) {
const testRegion = "region"
tests := []struct {
description string
input *sqlserverflexalpha.GetUserResponse
region string
expected resourceModel
isValid bool
}{
//{
// "default_values",
// &sqlserverflexalpha.GetUserResponse{},
// testRegion,
// resourceModel{
// Id: types.Int64Value(1),
// UserId: types.Int64Value(1),
// InstanceId: types.StringValue("iid"),
// ProjectId: types.StringValue("pid"),
// Username: types.StringNull(),
// Roles: types.List(types.SetNull(types.StringType)),
// Host: types.StringNull(),
// Port: types.Int64Null(),
// Region: types.StringValue(testRegion),
// },
// true,
//},
//{
// "simple_values",
// &sqlserverflexalpha.GetUserResponse{
// Roles: &[]sqlserverflexalpha.UserRole{
// "role_1",
// "role_2",
// "",
// },
// Username: utils.Ptr("username"),
// Host: utils.Ptr("host"),
// Port: utils.Ptr(int64(1234)),
// },
// testRegion,
// resourceModel{
// Id: types.Int64Value(2),
// UserId: types.Int64Value(2),
// InstanceId: types.StringValue("iid"),
// ProjectId: types.StringValue("pid"),
// Username: types.StringValue("username"),
// Roles: types.List(
// types.SetValueMust(
// types.StringType, []attr.Value{
// types.StringValue("role_1"),
// types.StringValue("role_2"),
// types.StringValue(""),
// },
// ),
// ),
// Host: types.StringValue("host"),
// Port: types.Int64Value(1234),
// Region: types.StringValue(testRegion),
// },
// true,
//},
//{
// "null_fields_and_int_conversions",
// &sqlserverflexalpha.GetUserResponse{
// Id: utils.Ptr(int64(1)),
// Roles: &[]sqlserverflexalpha.UserRole{},
// Username: nil,
// Host: nil,
// Port: utils.Ptr(int64(2123456789)),
// },
// testRegion,
// resourceModel{
// Id: types.Int64Value(1),
// UserId: types.Int64Value(1),
// InstanceId: types.StringValue("iid"),
// ProjectId: types.StringValue("pid"),
// Username: types.StringNull(),
// Roles: types.List(types.SetValueMust(types.StringType, []attr.Value{})),
// Host: types.StringNull(),
// Port: types.Int64Value(2123456789),
// Region: types.StringValue(testRegion),
// },
// true,
//},
{
"nil_response",
nil,
testRegion,
resourceModel{},
false,
},
{
"nil_response_2",
&sqlserverflexalpha.GetUserResponse{},
testRegion,
resourceModel{},
false,
},
{
"no_resource_id",
&sqlserverflexalpha.GetUserResponse{},
testRegion,
resourceModel{},
false,
},
}
for _, tt := range tests {
t.Run(
tt.description, func(t *testing.T) {
state := &resourceModel{
ProjectId: tt.expected.ProjectId,
InstanceId: tt.expected.InstanceId,
UserId: tt.expected.UserId,
}
err := mapFields(tt.input, state, tt.region)
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(state, &tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
},
)
}
}
func TestToCreatePayload(t *testing.T) {
tests := []struct {
description string
input *resourceModel
inputRoles []sqlserverflexalpha.UserRole
expected *sqlserverflexalpha.CreateUserRequestPayload
isValid bool
}{
{
"default_values",
&resourceModel{},
[]sqlserverflexalpha.UserRole{},
&sqlserverflexalpha.CreateUserRequestPayload{
Roles: &[]sqlserverflexalpha.UserRole{},
Username: nil,
},
true,
},
{
"default_values",
&resourceModel{
Username: types.StringValue("username"),
},
[]sqlserverflexalpha.UserRole{
"role_1",
"role_2",
},
&sqlserverflexalpha.CreateUserRequestPayload{
Roles: &[]sqlserverflexalpha.UserRole{
"role_1",
"role_2",
},
Username: utils.Ptr("username"),
},
true,
},
{
"null_fields_and_int_conversions",
&resourceModel{
Username: types.StringNull(),
},
[]sqlserverflexalpha.UserRole{
"",
},
&sqlserverflexalpha.CreateUserRequestPayload{
Roles: &[]sqlserverflexalpha.UserRole{
"",
},
Username: nil,
},
true,
},
{
"nil_model",
nil,
[]sqlserverflexalpha.UserRole{},
nil,
false,
},
{
"nil_roles",
&resourceModel{
Username: types.StringValue("username"),
},
[]sqlserverflexalpha.UserRole{},
&sqlserverflexalpha.CreateUserRequestPayload{
Roles: &[]sqlserverflexalpha.UserRole{},
Username: utils.Ptr("username"),
},
true,
},
}
for _, tt := range tests {
t.Run(
tt.description, func(t *testing.T) {
output, err := toCreatePayload(tt.input, tt.inputRoles)
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(output, tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
},
)
}
}

View file

@ -7,6 +7,7 @@ import (
"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/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/stackit-sdk-go/core/config"
@ -28,7 +29,7 @@ func NewDatabaseDataSource() datasource.DataSource {
type dataSourceModel struct {
sqlserverflexbetaGen.DatabaseModel
TfId types.String `tfsdk:"id"`
TerraformId types.String `tfsdk:"id"`
}
type databaseDataSource struct {
@ -97,8 +98,6 @@ func (d *databaseDataSource) Configure(
func (d *databaseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data dataSourceModel
readErr := "Read DB error"
// Read Terraform configuration data into the model
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
@ -108,6 +107,7 @@ func (d *databaseDataSource) Read(ctx context.Context, req datasource.ReadReques
ctx = core.InitProviderContext(ctx)
// Extract identifiers from the plan
projectId := data.ProjectId.ValueString()
region := d.providerData.GetRegionWithOverride(data.Region)
instanceId := data.InstanceId.ValueString()
@ -120,44 +120,55 @@ func (d *databaseDataSource) Read(ctx context.Context, req datasource.ReadReques
databaseResp, err := d.client.GetDatabaseRequest(ctx, projectId, region, instanceId, databaseName).Execute()
if err != nil {
utils.LogError(
ctx,
&resp.Diagnostics,
err,
"Reading database",
fmt.Sprintf("database with %q does not exist in project %q.", databaseName, projectId),
map[int]string{
http.StatusForbidden: fmt.Sprintf("Project with %q not found or forbidden access", projectId),
},
)
handleReadError(ctx, &resp.Diagnostics, err, projectId, instanceId)
resp.State.RemoveResource(ctx)
return
}
ctx = core.LogResponse(ctx)
dbId, ok := databaseResp.GetIdOk()
if !ok {
// Map response body to schema and populate Computed attribute values
err = mapFields(databaseResp, &data, region)
if err != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
readErr,
"Database creation waiting: returned id is nil",
"Error reading database",
fmt.Sprintf("Processing API payload: %v", err),
)
return
}
data.Id = types.Int64Value(dbId)
data.TfId = utils.BuildInternalTerraformId(projectId, region, instanceId, databaseName)
data.Owner = types.StringValue(databaseResp.GetOwner())
data.CollationName = types.StringValue(databaseResp.GetCollationName())
data.CompatibilityLevel = types.Int64Value(databaseResp.GetCompatibilityLevel())
data.DatabaseName = types.StringValue(databaseResp.GetName())
// data.InstanceId = types.StringValue(databaseResp.GetInstanceId())
// data.Name = types.Sometype(apiResponse.GetName())
// data.ProjectId = types.Sometype(apiResponse.GetProjectId())
// data.Region = types.Sometype(apiResponse.GetRegion())
// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
tflog.Info(ctx, "SQL Server Flex beta database read")
}
// handleReadError centralizes API error handling for the Read operation.
func handleReadError(ctx context.Context, diags *diag.Diagnostics, err error, projectId, instanceId string) {
utils.LogError(
ctx,
diags,
err,
"Reading database",
fmt.Sprintf(
"Could not retrieve database for instance %q in project %q.",
instanceId,
projectId,
),
map[int]string{
http.StatusBadRequest: fmt.Sprintf(
"Invalid request parameters for project %q and instance %q.",
projectId,
instanceId,
),
http.StatusNotFound: fmt.Sprintf(
"Database, instance %q, or project %q not found.",
instanceId,
projectId,
),
http.StatusForbidden: fmt.Sprintf("Forbidden access to project %q.", projectId),
},
)
}

View file

@ -0,0 +1,97 @@
package sqlserverflexbeta
import (
"fmt"
"strings"
"github.com/hashicorp/terraform-plugin-framework/types"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexbeta"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils"
)
// mapFields maps fields from a ListDatabase API response to a resourceModel for the data source.
func mapFields(source *sqlserverflexbeta.GetDatabaseResponse, model *dataSourceModel, region string) error {
if source == nil {
return fmt.Errorf("response is nil")
}
if source.Id == nil || *source.Id == 0 {
return fmt.Errorf("id not present")
}
if model == nil {
return fmt.Errorf("model given is nil")
}
var databaseId int64
if model.Id.ValueInt64() != 0 {
databaseId = model.Id.ValueInt64()
} else if source.Id != nil {
databaseId = *source.Id
} else {
return fmt.Errorf("database id not present")
}
model.Id = types.Int64Value(databaseId)
model.DatabaseName = types.StringValue(source.GetName())
model.Name = types.StringValue(source.GetName())
model.Owner = types.StringValue(strings.Trim(source.GetOwner(), "\""))
model.Region = types.StringValue(region)
model.ProjectId = types.StringValue(model.ProjectId.ValueString())
model.InstanceId = types.StringValue(model.InstanceId.ValueString())
model.CompatibilityLevel = types.Int64Value(source.GetCompatibilityLevel())
model.CollationName = types.StringValue(source.GetCollationName())
model.TerraformId = utils.BuildInternalTerraformId(
model.ProjectId.ValueString(),
region,
model.InstanceId.ValueString(),
model.DatabaseName.ValueString(),
)
return nil
}
// mapResourceFields maps fields from a ListDatabase API response to a resourceModel for the resource.
func mapResourceFields(source *sqlserverflexbeta.GetDatabaseResponse, model *resourceModel, region string) error {
if source == nil {
return fmt.Errorf("response is nil")
}
if source.Id == nil || *source.Id == 0 {
return fmt.Errorf("id not present")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
var databaseId int64
if model.Id.ValueInt64() != 0 {
databaseId = model.Id.ValueInt64()
} else if source.Id != nil {
databaseId = *source.Id
} else {
return fmt.Errorf("database id not present")
}
model.Id = types.Int64Value(databaseId)
model.DatabaseName = types.StringValue(source.GetName())
model.Name = types.StringValue(source.GetName())
model.Owner = types.StringValue(strings.Trim(source.GetOwner(), "\""))
model.Region = types.StringValue(region)
model.ProjectId = types.StringValue(model.ProjectId.ValueString())
model.InstanceId = types.StringValue(model.InstanceId.ValueString())
return nil
}
// toCreatePayload converts the resource model to an API create payload.
func toCreatePayload(model *resourceModel) (*sqlserverflexbeta.CreateDatabaseRequestPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
return &sqlserverflexbeta.CreateDatabaseRequestPayload{
Name: model.Name.ValueStringPointer(),
Owner: model.Owner.ValueStringPointer(),
Collation: model.Collation.ValueStringPointer(),
Compatibility: model.Compatibility.ValueInt64Pointer(),
}, nil
}

View file

@ -0,0 +1,227 @@
package sqlserverflexbeta
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexbeta"
datasource "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexbeta/database/datasources_gen"
)
func TestMapFields(t *testing.T) {
type given struct {
source *sqlserverflexbeta.GetDatabaseResponse
model *dataSourceModel
region string
}
type expected struct {
model *dataSourceModel
err bool
}
testcases := []struct {
name string
given given
expected expected
}{
{
name: "should map fields correctly",
given: given{
source: &sqlserverflexbeta.GetDatabaseResponse{
Id: utils.Ptr(int64(1)),
Name: utils.Ptr("my-db"),
CollationName: utils.Ptr("collation"),
CompatibilityLevel: utils.Ptr(int64(150)),
Owner: utils.Ptr("\"my-owner\""),
},
model: &dataSourceModel{
DatabaseModel: datasource.DatabaseModel{
ProjectId: types.StringValue("my-project"),
InstanceId: types.StringValue("my-instance"),
},
},
region: "eu01",
},
expected: expected{
model: &dataSourceModel{
DatabaseModel: datasource.DatabaseModel{
Id: types.Int64Value(1),
Name: types.StringValue("my-db"),
DatabaseName: types.StringValue("my-db"),
Owner: types.StringValue("my-owner"),
Region: types.StringValue("eu01"),
InstanceId: types.StringValue("my-instance"),
ProjectId: types.StringValue("my-project"),
CompatibilityLevel: types.Int64Value(150),
CollationName: types.StringValue("collation"),
},
TerraformId: types.StringValue("my-project,eu01,my-instance,my-db"),
},
},
},
{
name: "should fail on nil source",
given: given{
source: nil,
model: &dataSourceModel{},
},
expected: expected{err: true},
},
{
name: "should fail on nil source ID",
given: given{
source: &sqlserverflexbeta.GetDatabaseResponse{Id: nil},
model: &dataSourceModel{},
},
expected: expected{err: true},
},
{
name: "should fail on nil model",
given: given{
source: &sqlserverflexbeta.GetDatabaseResponse{Id: utils.Ptr(int64(1))},
model: nil,
},
expected: expected{err: true},
},
}
for _, tc := range testcases {
t.Run(
tc.name, func(t *testing.T) {
err := mapFields(tc.given.source, tc.given.model, tc.given.region)
if (err != nil) != tc.expected.err {
t.Fatalf("expected error: %v, got: %v", tc.expected.err, err)
}
if err == nil {
if diff := cmp.Diff(tc.expected.model, tc.given.model); diff != "" {
t.Errorf("model mismatch (-want +got):\n%s", diff)
}
}
},
)
}
}
func TestMapResourceFields(t *testing.T) {
type given struct {
source *sqlserverflexbeta.GetDatabaseResponse
model *resourceModel
region string
}
type expected struct {
model *resourceModel
err bool
}
testcases := []struct {
name string
given given
expected expected
}{
{
name: "should map fields correctly",
given: given{
source: &sqlserverflexbeta.GetDatabaseResponse{
Id: utils.Ptr(int64(1)),
Name: utils.Ptr("my-db"),
Owner: utils.Ptr("\"my-owner\""),
},
model: &resourceModel{
ProjectId: types.StringValue("my-project"),
InstanceId: types.StringValue("my-instance"),
},
region: "eu01",
},
expected: expected{
model: &resourceModel{
Id: types.Int64Value(1),
Name: types.StringValue("my-db"),
DatabaseName: types.StringValue("my-db"),
InstanceId: types.StringValue("my-instance"),
ProjectId: types.StringValue("my-project"),
Region: types.StringValue("eu01"),
Owner: types.StringValue("my-owner"),
},
},
},
{
name: "should fail on nil source",
given: given{
source: nil,
model: &resourceModel{},
},
expected: expected{err: true},
},
}
for _, tc := range testcases {
t.Run(
tc.name, func(t *testing.T) {
err := mapResourceFields(tc.given.source, tc.given.model, tc.given.region)
if (err != nil) != tc.expected.err {
t.Fatalf("expected error: %v, got: %v", tc.expected.err, err)
}
if err == nil {
if diff := cmp.Diff(tc.expected.model, tc.given.model); diff != "" {
t.Errorf("model mismatch (-want +got):\n%s", diff)
}
}
},
)
}
}
func TestToCreatePayload(t *testing.T) {
type given struct {
model *resourceModel
}
type expected struct {
payload *sqlserverflexbeta.CreateDatabaseRequestPayload
err bool
}
testcases := []struct {
name string
given given
expected expected
}{
{
name: "should convert model to payload",
given: given{
model: &resourceModel{
Name: types.StringValue("my-db"),
Owner: types.StringValue("my-owner"),
},
},
expected: expected{
payload: &sqlserverflexbeta.CreateDatabaseRequestPayload{
Name: utils.Ptr("my-db"),
Owner: utils.Ptr("my-owner"),
},
},
},
{
name: "should fail on nil model",
given: given{model: nil},
expected: expected{err: true},
},
}
for _, tc := range testcases {
t.Run(
tc.name, func(t *testing.T) {
actual, err := toCreatePayload(tc.given.model)
if (err != nil) != tc.expected.err {
t.Fatalf("expected error: %v, got: %v", tc.expected.err, err)
}
if err == nil {
if diff := cmp.Diff(tc.expected.payload, actual); diff != "" {
t.Errorf("payload mismatch (-want +got):\n%s", diff)
}
}
},
)
}
}

View file

@ -3,9 +3,10 @@ package sqlserverflexbeta
import (
"context"
_ "embed"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
@ -13,10 +14,9 @@ import (
"github.com/hashicorp/terraform-plugin-framework/types"
"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"
wait "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/wait/sqlserverflexbeta"
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexbeta"
"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"
@ -30,6 +30,13 @@ var (
_ resource.ResourceWithImportState = &databaseResource{}
_ resource.ResourceWithModifyPlan = &databaseResource{}
_ resource.ResourceWithIdentity = &databaseResource{}
// Define errors
errDatabaseNotFound = errors.New("database not found")
// Error message constants
extractErrorSummary = "extracting failed"
extractErrorMessage = "Extracting identity data: %v"
)
func NewDatabaseResource() resource.Resource {
@ -192,18 +199,24 @@ func (r *databaseResource) Create(ctx context.Context, req resource.CreateReques
return
}
ctx = core.LogResponse(ctx)
createId, ok := createResp.GetIdOk()
if !ok {
if createResp == nil || createResp.Id == nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
createErr,
fmt.Sprintf("Calling API: %v", err),
"Error creating database",
"API didn't return database Id. A database might have been created",
)
return
}
waitResp, err := wait.CreateDatabaseWaitHandler(
databaseId := *createResp.Id
ctx = tflog.SetField(ctx, "database_id", databaseId)
ctx = core.LogResponse(ctx)
// TODO: is this neccessary to wait for the database-> API say 200 ?
/*waitResp, err := wait.CreateDatabaseWaitHandler(
ctx,
r.client,
projectId,
@ -263,23 +276,58 @@ func (r *databaseResource) Create(ctx context.Context, req resource.CreateReques
"Database creation waiting: returned name is different",
)
return
}*/
database, err := r.client.GetDatabaseRequest(ctx, projectId, region, instanceId, databaseName).Execute()
if err != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
"Error creating database",
fmt.Sprintf("Getting database details after creation: %v", err),
)
return
}
data.Id = types.Int64PointerValue(waitResp.Id)
data.Name = types.StringPointerValue(waitResp.Name)
// Map response body to schema
err = mapResourceFields(database, &data, region)
if err != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
"Error creating database",
fmt.Sprintf("Processing API payload: %v", err),
)
return
}
// Set data returned by API in identity
identity := DatabaseResourceIdentityModel{
ProjectID: types.StringValue(projectId),
Region: types.StringValue(region),
InstanceID: types.StringValue(instanceId),
DatabaseName: types.StringValue(databaseName),
}
resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...)
if resp.Diagnostics.HasError() {
return
}
// Set state to fully populated data
resp.Diagnostics.Append(resp.State.Set(ctx, data)...)
if resp.Diagnostics.HasError() {
return
}
// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
tflog.Info(ctx, "sqlserverflexbeta.Database created")
}
func (r *databaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data resourceModel
readErr := "[Database Read]"
// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
var model resourceModel
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
@ -293,78 +341,66 @@ func (r *databaseResource) Read(ctx context.Context, req resource.ReadRequest, r
ctx = core.InitProviderContext(ctx)
projectId := identityData.ProjectID.ValueString()
region := identityData.Region.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
instanceId := identityData.InstanceID.ValueString()
ctx = tflog.SetField(ctx, "instance_id", instanceId)
dbName := identityData.DatabaseName.ValueString()
ctx = tflog.SetField(ctx, "database_name", dbName)
getResp, err := r.client.GetDatabaseRequest(ctx, projectId, region, instanceId, dbName).Execute()
if err != nil {
projectId, instanceId, region, databaseName, errExt := r.extractIdentityData(model, identityData)
if errExt != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
readErr,
fmt.Sprintf("Calling API: %v", err),
extractErrorSummary,
fmt.Sprintf(extractErrorMessage, errExt),
)
}
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "instance_id", instanceId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "database_name", databaseName)
databaseResp, err := r.client.GetDatabaseRequest(ctx, projectId, region, instanceId, databaseName).Execute()
if err != nil {
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
if (ok && oapiErr.StatusCode == http.StatusNotFound) || errors.Is(err, errDatabaseNotFound) {
resp.State.RemoveResource(ctx)
return
}
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading database", fmt.Sprintf("Calling API: %v", err))
return
}
ctx = core.LogResponse(ctx)
data.Id = types.Int64Value(getResp.GetId())
data.Owner = types.StringValue(getResp.GetOwner())
data.Name = types.StringValue(getResp.GetName())
data.DatabaseName = types.StringValue(getResp.GetName())
data.CollationName = types.StringValue(getResp.GetCollationName())
data.CompatibilityLevel = types.Int64Value(getResp.GetCompatibilityLevel())
// Map response body to schema
err = mapResourceFields(databaseResp, &model, region)
if err != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
"Error reading database",
fmt.Sprintf("Processing API payload: %v", err),
)
return
}
// Set refreshed state
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
tflog.Info(ctx, "sqlserverflexbeta.Database read")
}
func (r *databaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data resourceModel
// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Read identity data
var identityData DatabaseResourceIdentityModel
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
if resp.Diagnostics.HasError() {
return
}
ctx = core.InitProviderContext(ctx)
projectId := identityData.ProjectID.ValueString()
region := identityData.Region.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
// Todo: Update API call logic
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
tflog.Info(ctx, "sqlserverflexbeta.Database updated")
// TODO: Check update api endpoint - not available at the moment, so return an error for now
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating database", "Database can't be updated")
}
func (r *databaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data resourceModel
// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
// nolint:gocritic // function signature required by Terraform
var model resourceModel
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
@ -378,12 +414,28 @@ func (r *databaseResource) Delete(ctx context.Context, req resource.DeleteReques
ctx = core.InitProviderContext(ctx)
projectId := identityData.ProjectID.ValueString()
region := identityData.Region.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
projectId, instanceId, region, databaseName, errExt := r.extractIdentityData(model, identityData)
if errExt != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
extractErrorSummary,
fmt.Sprintf(extractErrorMessage, errExt),
)
}
// Todo: Delete API call logic
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "instance_id", instanceId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "database_name", databaseName)
// Delete existing record set
err := r.client.DeleteDatabaseRequestExecute(ctx, projectId, region, instanceId, databaseName)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting database", fmt.Sprintf("Calling API: %v", err))
}
ctx = core.LogResponse(ctx)
tflog.Info(ctx, "sqlserverflexbeta.Database deleted")
}
@ -504,3 +556,46 @@ func (r *databaseResource) ImportState(
tflog.Info(ctx, "Sqlserverflexbeta database state imported")
}
// extractIdentityData extracts essential identifiers from the resource model, falling back to the identity model.
func (r *databaseResource) extractIdentityData(
model resourceModel,
identity DatabaseResourceIdentityModel,
) (projectId, region, instanceId, databaseName string, err error) {
if !model.DatabaseName.IsNull() && !model.DatabaseName.IsUnknown() {
databaseName = model.DatabaseName.ValueString()
} else {
if identity.DatabaseName.IsNull() || identity.DatabaseName.IsUnknown() {
return "", "", "", "", fmt.Errorf("database_name not found in config")
}
databaseName = identity.DatabaseName.ValueString()
}
if !model.ProjectId.IsNull() && !model.ProjectId.IsUnknown() {
projectId = model.ProjectId.ValueString()
} else {
if identity.ProjectID.IsNull() || identity.ProjectID.IsUnknown() {
return "", "", "", "", fmt.Errorf("project_id not found in config")
}
projectId = identity.ProjectID.ValueString()
}
if !model.Region.IsNull() && !model.Region.IsUnknown() {
region = r.providerData.GetRegionWithOverride(model.Region)
} else {
if identity.Region.IsNull() || identity.Region.IsUnknown() {
return "", "", "", "", fmt.Errorf("region not found in config")
}
region = r.providerData.GetRegionWithOverride(identity.Region)
}
if !model.InstanceId.IsNull() && !model.InstanceId.IsUnknown() {
instanceId = model.InstanceId.ValueString()
} else {
if identity.InstanceID.IsNull() || identity.InstanceID.IsUnknown() {
return "", "", "", "", fmt.Errorf("instance_id not found in config")
}
instanceId = identity.InstanceID.ValueString()
}
return projectId, region, instanceId, databaseName, nil
}

View file

@ -8,13 +8,12 @@ import (
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"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"
sqlserverflexUtils "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexbeta/utils"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils"
sqlserverflexbetaPkg "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexbeta"
// sqlserverflexbetaUtils "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexbeta/utils"
sqlserverflexbetaGen "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexbeta/user/datasources_gen"
)
@ -29,7 +28,7 @@ func NewUserDataSource() datasource.DataSource {
type dataSourceModel struct {
DefaultDatabase types.String `tfsdk:"default_database"`
Host types.String `tfsdk:"host"`
Id types.Int64 `tfsdk:"id"`
Id types.String `tfsdk:"id"`
InstanceId types.String `tfsdk:"instance_id"`
Port types.Int64 `tfsdk:"port"`
ProjectId types.String `tfsdk:"project_id"`
@ -63,50 +62,52 @@ func (d *userDataSource) Configure(
req datasource.ConfigureRequest,
resp *datasource.ConfigureResponse,
) {
//var ok bool
//d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
//if !ok {
// return
//}
//
//apiClient := sqlserverflexbetaUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics)
//if resp.Diagnostics.HasError() {
// return
//}
//d.client = apiClient
tflog.Info(ctx, fmt.Sprintf("%s client configured", errorPrefix))
var ok bool
d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
apiClient := sqlserverflexUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
d.client = apiClient
tflog.Info(ctx, "SQL SERVER Flex alpha database client configured")
}
func (d *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data dataSourceModel
// Read Terraform configuration data into the model
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
var model dataSourceModel
diags := req.Config.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
ctx = core.InitProviderContext(ctx)
projectId := data.ProjectId.ValueString()
region := d.providerData.GetRegionWithOverride(data.Region)
instanceId := data.InstanceId.ValueString()
userId := data.UserId.ValueInt64()
projectId := model.ProjectId.ValueString()
instanceId := model.InstanceId.ValueString()
userId := model.UserId.ValueInt64()
region := d.providerData.GetRegionWithOverride(model.Region)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "instance_id", instanceId)
ctx = tflog.SetField(ctx, "user_id", userId)
ctx = tflog.SetField(ctx, "region", region)
userResp, err := d.client.GetUserRequest(ctx, projectId, region, instanceId, userId).Execute()
recordSetResp, err := d.client.GetUserRequest(ctx, projectId, region, instanceId, userId).Execute()
if err != nil {
utils.LogError(
ctx,
&resp.Diagnostics,
err,
"Reading user",
fmt.Sprintf("user with ID %q does not exist in project %q.", userId, projectId),
fmt.Sprintf(
"User with ID %q or instance with ID %q does not exist in project %q.",
userId,
instanceId,
projectId,
),
map[int]string{
http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId),
},
@ -117,22 +118,23 @@ func (d *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, r
ctx = core.LogResponse(ctx)
// Todo: Read API call logic
// Example data value setting
// data.Id = types.StringValue("example-id")
err = mapResponseToModel(ctx, userResp, &data, resp.Diagnostics)
// Map response body to schema and populate Computed attribute values
err = mapDataSourceFields(recordSetResp, &model, region)
if err != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
fmt.Sprintf("%s Read", errorPrefix),
"Error reading user",
fmt.Sprintf("Processing API payload: %v", err),
)
return
}
// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
// Set refreshed state
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "SQLServer Flex beta instance read")
}

View file

@ -1,98 +0,0 @@
package sqlserverflexbeta
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexbeta"
)
func mapResponseToModel(
ctx context.Context,
resp *sqlserverflexbeta.GetUserResponse,
m *dataSourceModel,
tfDiags diag.Diagnostics,
) error {
if resp == nil {
return fmt.Errorf("response is nil")
}
m.Id = types.Int64Value(resp.GetId())
m.UserId = types.Int64Value(resp.GetId())
m.Username = types.StringValue(resp.GetUsername())
m.Port = types.Int64Value(resp.GetPort())
m.Host = types.StringValue(resp.GetHost())
m.DefaultDatabase = types.StringValue(resp.GetDefaultDatabase())
m.Status = types.StringValue(resp.GetStatus())
if resp.Roles != nil {
roles, diags := types.ListValueFrom(ctx, types.StringType, *resp.Roles)
tfDiags.Append(diags...)
if tfDiags.HasError() {
return fmt.Errorf("failed to map roles")
}
m.Roles = roles
} else {
m.Roles = types.ListNull(types.StringType)
}
if resp.Status != nil {
m.Status = types.StringValue(*resp.Status)
} else {
m.Status = types.StringNull()
}
// TODO: complete and refactor
/*
sampleList, diags := types.ListValueFrom(ctx, types.StringType, resp.GetList())
tfDiags.Append(diags...)
if diags.HasError() {
return fmt.Errorf(
"error converting list response value",
)
}
sample, diags := sqlserverflexbetaResGen.NewSampleValue(
sqlserverflexbetaResGen.SampleValue{}.AttributeTypes(ctx),
map[string]attr.Value{
"field": types.StringValue(string(resp.GetField())),
},
)
tfDiags.Append(diags...)
if diags.HasError() {
return fmt.Errorf(
"error converting sample response value",
"sample",
types.StringValue(string(resp.GetField())),
)
}
m.Sample = sample
*/
return nil
}
//func toCreatePayload(
// ctx context.Context,
// model *resourceModel,
//) (*sqlserverflexbeta.CreateUserRequestPayload, error) {
// if model == nil {
// return nil, fmt.Errorf("nil model")
// }
//
// var roles []sqlserverflexbeta.UserRole
// if !model.Roles.IsNull() && !model.Roles.IsUnknown() {
// diags := model.Roles.ElementsAs(ctx, &roles, false)
// if diags.HasError() {
// return nil, fmt.Errorf("failed to convert roles: %v", diags)
// }
// }
//
// return &sqlserverflexbeta.CreateUserRequestPayload{
// DefaultDatabase: model.DefaultDatabase.ValueStringPointer(),
// Username: model.Username.ValueStringPointer(),
// Roles: &roles,
// }, nil
//}

View file

@ -0,0 +1,179 @@
package sqlserverflexbeta
import (
"fmt"
"strconv"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexbeta"
"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"
)
// mapDataSourceFields maps the API response to a dataSourceModel.
func mapDataSourceFields(userResp *sqlserverflexbeta.GetUserResponse, model *dataSourceModel, region string) error {
if userResp == nil {
return fmt.Errorf("response is nil")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
user := userResp
// Handle user ID
var userId int64
if model.UserId.ValueInt64() != 0 {
userId = model.UserId.ValueInt64()
} else if user.Id != nil {
userId = *user.Id
} else {
return fmt.Errorf("user id not present")
}
// Set main attributes
model.Id = utils.BuildInternalTerraformId(
model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), strconv.FormatInt(userId, 10),
)
model.UserId = types.Int64Value(userId)
model.Username = types.StringPointerValue(user.Username)
// Map roles
if user.Roles == nil {
model.Roles = types.List(types.SetNull(types.StringType))
} else {
var roles []attr.Value
for _, role := range *user.Roles {
roles = append(roles, types.StringValue(string(role)))
}
rolesSet, diags := types.SetValue(types.StringType, roles)
if diags.HasError() {
return fmt.Errorf("failed to map roles: %w", core.DiagsToError(diags))
}
model.Roles = types.List(rolesSet)
}
// Set remaining attributes
model.Host = types.StringPointerValue(user.Host)
model.Port = types.Int64PointerValue(user.Port)
model.Region = types.StringValue(region)
model.Status = types.StringPointerValue(user.Status)
model.DefaultDatabase = types.StringPointerValue(user.DefaultDatabase)
return nil
}
// mapFields maps the API response to a resourceModel.
func mapFields(userResp *sqlserverflexbeta.GetUserResponse, model *resourceModel, region string) error {
if userResp == nil {
return fmt.Errorf("response is nil")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
user := userResp
// Handle user ID
var userId int64
if model.UserId.ValueInt64() != 0 {
userId = model.UserId.ValueInt64()
} else if user.Id != nil {
userId = *user.Id
} else {
return fmt.Errorf("user id not present")
}
// Set main attributes
model.Id = types.Int64Value(userId)
model.UserId = types.Int64Value(userId)
model.Username = types.StringPointerValue(user.Username)
// Map roles
if user.Roles != nil {
var roles []attr.Value
for _, role := range *user.Roles {
roles = append(roles, types.StringValue(string(role)))
}
rolesSet, diags := types.SetValue(types.StringType, roles)
if diags.HasError() {
return fmt.Errorf("failed to map roles: %w", core.DiagsToError(diags))
}
model.Roles = types.List(rolesSet)
}
// Ensure roles is not null
if model.Roles.IsNull() || model.Roles.IsUnknown() {
model.Roles = types.List(types.SetNull(types.StringType))
}
// Set connection details
model.Host = types.StringPointerValue(user.Host)
model.Port = types.Int64PointerValue(user.Port)
model.Region = types.StringValue(region)
return nil
}
// mapFieldsCreate maps the API response from creating a user to a resourceModel.
func mapFieldsCreate(userResp *sqlserverflexbeta.CreateUserResponse, model *resourceModel, region string) error {
if userResp == nil {
return fmt.Errorf("response is nil")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
user := userResp
if user.Id == nil {
return fmt.Errorf("user id not present")
}
userId := *user.Id
model.Id = types.Int64Value(userId)
model.UserId = types.Int64Value(userId)
model.Username = types.StringPointerValue(user.Username)
if user.Password == nil {
return fmt.Errorf("user password not present")
}
model.Password = types.StringValue(*user.Password)
if user.Roles != nil {
var roles []attr.Value
for _, role := range *user.Roles {
roles = append(roles, types.StringValue(string(role)))
}
rolesSet, diags := types.SetValue(types.StringType, roles)
if diags.HasError() {
return fmt.Errorf("failed to map roles: %w", core.DiagsToError(diags))
}
model.Roles = types.List(rolesSet)
}
if model.Roles.IsNull() || model.Roles.IsUnknown() {
model.Roles = types.List(types.SetNull(types.StringType))
}
model.Host = types.StringPointerValue(user.Host)
model.Port = types.Int64PointerValue(user.Port)
model.Region = types.StringValue(region)
model.Status = types.StringPointerValue(user.Status)
model.DefaultDatabase = types.StringPointerValue(user.DefaultDatabase)
return nil
}
// toCreatePayload converts a resourceModel to an API CreateUserRequestPayload.
func toCreatePayload(
model *resourceModel,
roles []sqlserverflexbeta.UserRole,
) (*sqlserverflexbeta.CreateUserRequestPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
return &sqlserverflexbeta.CreateUserRequestPayload{
Username: conversion.StringValueToPointer(model.Username),
DefaultDatabase: conversion.StringValueToPointer(model.DefaultDatabase),
Roles: &roles,
}, nil
}

View file

@ -0,0 +1,527 @@
package sqlserverflexbeta
import (
"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"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexbeta"
)
func TestMapDataSourceFields(t *testing.T) {
const testRegion = "region"
tests := []struct {
description string
input *sqlserverflexbeta.GetUserResponse
region string
expected dataSourceModel
isValid bool
}{
{
"default_values",
&sqlserverflexbeta.GetUserResponse{},
testRegion,
dataSourceModel{
Id: types.StringValue("pid,region,iid,1"),
UserId: types.Int64Value(1),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
Username: types.StringNull(),
Roles: types.List(types.SetNull(types.StringType)),
Host: types.StringNull(),
Port: types.Int64Null(),
Region: types.StringValue(testRegion),
Status: types.StringNull(),
DefaultDatabase: types.StringNull(),
},
true,
},
{
"simple_values",
&sqlserverflexbeta.GetUserResponse{
Roles: &[]sqlserverflexbeta.UserRole{
"role_1",
"role_2",
"",
},
Username: utils.Ptr("username"),
Host: utils.Ptr("host"),
Port: utils.Ptr(int64(1234)),
Status: utils.Ptr("active"),
DefaultDatabase: utils.Ptr("default_db"),
},
testRegion,
dataSourceModel{
Id: types.StringValue("pid,region,iid,1"),
UserId: types.Int64Value(1),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
Username: types.StringValue("username"),
Roles: types.List(
types.SetValueMust(
types.StringType, []attr.Value{
types.StringValue("role_1"),
types.StringValue("role_2"),
types.StringValue(""),
},
),
),
Host: types.StringValue("host"),
Port: types.Int64Value(1234),
Region: types.StringValue(testRegion),
Status: types.StringValue("active"),
DefaultDatabase: types.StringValue("default_db"),
},
true,
},
{
"null_fields_and_int_conversions",
&sqlserverflexbeta.GetUserResponse{
Id: utils.Ptr(int64(1)),
Roles: &[]sqlserverflexbeta.UserRole{},
Username: nil,
Host: nil,
Port: utils.Ptr(int64(2123456789)),
},
testRegion,
dataSourceModel{
Id: types.StringValue("pid,region,iid,1"),
UserId: types.Int64Value(1),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
Username: types.StringNull(),
Roles: types.List(types.SetValueMust(types.StringType, []attr.Value{})),
Host: types.StringNull(),
Port: types.Int64Value(2123456789),
Region: types.StringValue(testRegion),
},
true,
},
{
"nil_response",
nil,
testRegion,
dataSourceModel{},
false,
},
{
"nil_response_2",
&sqlserverflexbeta.GetUserResponse{},
testRegion,
dataSourceModel{},
false,
},
{
"no_resource_id",
&sqlserverflexbeta.GetUserResponse{},
testRegion,
dataSourceModel{},
false,
},
}
for _, tt := range tests {
t.Run(
tt.description, func(t *testing.T) {
state := &dataSourceModel{
ProjectId: tt.expected.ProjectId,
InstanceId: tt.expected.InstanceId,
UserId: tt.expected.UserId,
}
err := mapDataSourceFields(tt.input, state, tt.region)
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(state, &tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
},
)
}
}
func TestMapFieldsCreate(t *testing.T) {
const testRegion = "region"
tests := []struct {
description string
input *sqlserverflexbeta.CreateUserResponse
region string
expected resourceModel
isValid bool
}{
{
"default_values",
&sqlserverflexbeta.CreateUserResponse{
Id: utils.Ptr(int64(1)),
Password: utils.Ptr(""),
},
testRegion,
resourceModel{
Id: types.Int64Value(1),
UserId: types.Int64Value(1),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
Username: types.StringNull(),
Roles: types.List(types.SetNull(types.StringType)),
Password: types.StringValue(""),
Host: types.StringNull(),
Port: types.Int64Null(),
Region: types.StringValue(testRegion),
},
true,
},
{
"simple_values",
&sqlserverflexbeta.CreateUserResponse{
Id: utils.Ptr(int64(2)),
Roles: &[]sqlserverflexbeta.UserRole{
"role_1",
"role_2",
"",
},
Username: utils.Ptr("username"),
Password: utils.Ptr("password"),
Host: utils.Ptr("host"),
Port: utils.Ptr(int64(1234)),
Status: utils.Ptr("status"),
DefaultDatabase: utils.Ptr("default_db"),
},
testRegion,
resourceModel{
Id: types.Int64Value(2),
UserId: types.Int64Value(2),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
Username: types.StringValue("username"),
Roles: types.List(
types.SetValueMust(
types.StringType, []attr.Value{
types.StringValue("role_1"),
types.StringValue("role_2"),
types.StringValue(""),
},
),
),
Password: types.StringValue("password"),
Host: types.StringValue("host"),
Port: types.Int64Value(1234),
Region: types.StringValue(testRegion),
Status: types.StringValue("status"),
DefaultDatabase: types.StringValue("default_db"),
},
true,
},
{
"null_fields_and_int_conversions",
&sqlserverflexbeta.CreateUserResponse{
Id: utils.Ptr(int64(3)),
Roles: &[]sqlserverflexbeta.UserRole{},
Username: nil,
Password: utils.Ptr(""),
Host: nil,
Port: utils.Ptr(int64(2123456789)),
},
testRegion,
resourceModel{
Id: types.Int64Value(3),
UserId: types.Int64Value(3),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
Username: types.StringNull(),
Roles: types.List(types.SetValueMust(types.StringType, []attr.Value{})),
Password: types.StringValue(""),
Host: types.StringNull(),
Port: types.Int64Value(2123456789),
Region: types.StringValue(testRegion),
DefaultDatabase: types.StringNull(),
Status: types.StringNull(),
},
true,
},
{
"nil_response",
nil,
testRegion,
resourceModel{},
false,
},
{
"nil_response_2",
&sqlserverflexbeta.CreateUserResponse{},
testRegion,
resourceModel{},
false,
},
{
"no_resource_id",
&sqlserverflexbeta.CreateUserResponse{},
testRegion,
resourceModel{},
false,
},
{
"no_password",
&sqlserverflexbeta.CreateUserResponse{
Id: utils.Ptr(int64(1)),
},
testRegion,
resourceModel{},
false,
},
}
for _, tt := range tests {
t.Run(
tt.description, func(t *testing.T) {
state := &resourceModel{
ProjectId: tt.expected.ProjectId,
InstanceId: tt.expected.InstanceId,
}
err := mapFieldsCreate(tt.input, state, tt.region)
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(state, &tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
},
)
}
}
func TestMapFields(t *testing.T) {
const testRegion = "region"
tests := []struct {
description string
input *sqlserverflexbeta.GetUserResponse
region string
expected resourceModel
isValid bool
}{
{
"default_values",
&sqlserverflexbeta.GetUserResponse{},
testRegion,
resourceModel{
Id: types.Int64Value(1),
UserId: types.Int64Value(1),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
Username: types.StringNull(),
Roles: types.List(types.SetNull(types.StringType)),
Host: types.StringNull(),
Port: types.Int64Null(),
Region: types.StringValue(testRegion),
},
true,
},
{
"simple_values",
&sqlserverflexbeta.GetUserResponse{
Roles: &[]sqlserverflexbeta.UserRole{
"role_1",
"role_2",
"",
},
Username: utils.Ptr("username"),
Host: utils.Ptr("host"),
Port: utils.Ptr(int64(1234)),
},
testRegion,
resourceModel{
Id: types.Int64Value(2),
UserId: types.Int64Value(2),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
Username: types.StringValue("username"),
Roles: types.List(
types.SetValueMust(
types.StringType, []attr.Value{
types.StringValue("role_1"),
types.StringValue("role_2"),
types.StringValue(""),
},
),
),
Host: types.StringValue("host"),
Port: types.Int64Value(1234),
Region: types.StringValue(testRegion),
},
true,
},
{
"null_fields_and_int_conversions",
&sqlserverflexbeta.GetUserResponse{
Id: utils.Ptr(int64(1)),
Roles: &[]sqlserverflexbeta.UserRole{},
Username: nil,
Host: nil,
Port: utils.Ptr(int64(2123456789)),
},
testRegion,
resourceModel{
Id: types.Int64Value(1),
UserId: types.Int64Value(1),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
Username: types.StringNull(),
Roles: types.List(types.SetValueMust(types.StringType, []attr.Value{})),
Host: types.StringNull(),
Port: types.Int64Value(2123456789),
Region: types.StringValue(testRegion),
},
true,
},
{
"nil_response",
nil,
testRegion,
resourceModel{},
false,
},
{
"nil_response_2",
&sqlserverflexbeta.GetUserResponse{},
testRegion,
resourceModel{},
false,
},
{
"no_resource_id",
&sqlserverflexbeta.GetUserResponse{},
testRegion,
resourceModel{},
false,
},
}
for _, tt := range tests {
t.Run(
tt.description, func(t *testing.T) {
state := &resourceModel{
ProjectId: tt.expected.ProjectId,
InstanceId: tt.expected.InstanceId,
UserId: tt.expected.UserId,
}
err := mapFields(tt.input, state, tt.region)
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(state, &tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
},
)
}
}
func TestToCreatePayload(t *testing.T) {
tests := []struct {
description string
input *resourceModel
inputRoles []sqlserverflexbeta.UserRole
expected *sqlserverflexbeta.CreateUserRequestPayload
isValid bool
}{
{
"default_values",
&resourceModel{},
[]sqlserverflexbeta.UserRole{},
&sqlserverflexbeta.CreateUserRequestPayload{
Roles: &[]sqlserverflexbeta.UserRole{},
Username: nil,
},
true,
},
{
"default_values",
&resourceModel{
Username: types.StringValue("username"),
},
[]sqlserverflexbeta.UserRole{
"role_1",
"role_2",
},
&sqlserverflexbeta.CreateUserRequestPayload{
Roles: &[]sqlserverflexbeta.UserRole{
"role_1",
"role_2",
},
Username: utils.Ptr("username"),
},
true,
},
{
"null_fields_and_int_conversions",
&resourceModel{
Username: types.StringNull(),
},
[]sqlserverflexbeta.UserRole{
"",
},
&sqlserverflexbeta.CreateUserRequestPayload{
Roles: &[]sqlserverflexbeta.UserRole{
"",
},
Username: nil,
},
true,
},
{
"nil_model",
nil,
[]sqlserverflexbeta.UserRole{},
nil,
false,
},
{
"nil_roles",
&resourceModel{
Username: types.StringValue("username"),
},
[]sqlserverflexbeta.UserRole{},
&sqlserverflexbeta.CreateUserRequestPayload{
Roles: &[]sqlserverflexbeta.UserRole{},
Username: utils.Ptr("username"),
},
true,
},
}
for _, tt := range tests {
t.Run(
tt.description, func(t *testing.T) {
output, err := toCreatePayload(tt.input, tt.inputRoles)
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(output, tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
},
)
}
}

View file

@ -3,7 +3,9 @@ package sqlserverflexbeta
import (
"context"
_ "embed"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
@ -12,9 +14,12 @@ import (
"github.com/hashicorp/terraform-plugin-framework/resource/identityschema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/stackit-sdk-go/core/config"
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexbeta"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion"
sqlserverflexbetagen "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexbeta/user/resources_gen"
sqlserverflexbetaUtils "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexbeta/utils"
sqlserverflexbetaWait "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/wait/sqlserverflexbeta"
"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"
@ -54,319 +59,20 @@ func (r *userResource) Metadata(ctx context.Context, req resource.MetadataReques
resp.TypeName = req.ProviderTypeName + "_sqlserverflexbeta_user"
}
//go:embed planModifiers.yaml
var modifiersFileByte []byte
func (r *userResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
s := sqlserverflexbetaResGen.UserResourceSchema(ctx)
fields, err := utils.ReadModifiersConfig(modifiersFileByte)
if err != nil {
resp.Diagnostics.AddError("error during read modifiers config file", err.Error())
return
}
err = utils.AddPlanModifiersToResourceSchema(fields, &s)
if err != nil {
resp.Diagnostics.AddError("error adding plan modifiers", err.Error())
return
}
resp.Schema = s
}
func (r *userResource) IdentitySchema(
_ context.Context,
_ resource.IdentitySchemaRequest,
resp *resource.IdentitySchemaResponse,
) {
resp.IdentitySchema = identityschema.Schema{
Attributes: map[string]identityschema.Attribute{
"project_id": identityschema.StringAttribute{
RequiredForImport: true, // must be set during import by the practitioner
},
"region": identityschema.StringAttribute{
RequiredForImport: true, // can be defaulted by the provider configuration
},
"instance_id": identityschema.StringAttribute{
RequiredForImport: true, // can be defaulted by the provider configuration
},
},
}
}
// Configure adds the provider configured client to the resource.
func (r *userResource) Configure(
ctx context.Context,
req resource.ConfigureRequest,
resp *resource.ConfigureResponse,
) {
func (r *userResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.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 := sqlserverflexbeta.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,
),
)
apiClient := sqlserverflexbetaUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
r.client = apiClient
tflog.Info(ctx, "sqlserverflexbeta.User client configured")
}
func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data resourceModel
// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Read identity data
var identityData UserResourceIdentityModel
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
if resp.Diagnostics.HasError() {
return
}
ctx = core.InitProviderContext(ctx)
projectId := identityData.ProjectID.ValueString()
region := identityData.Region.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
//payload, err := toCreatePayload(ctx, &data)
//if err != nil {
// core.LogAndAddError(
// ctx,
// &resp.Diagnostics,
// "Error creating User",
// fmt.Sprintf("Creating API payload: %v", err),
// )
// return
//}
//payload = payload
// TODO: Create API call logic
/*
// Generate API request body from model
payload, err := toCreatePayload(ctx, &model)
if err != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
"Error creating User",
fmt.Sprintf("Creating API payload: %v", err),
)
return
}
// Create new User
createResp, err := r.client.CreateUserRequest(
ctx,
projectId,
region,
).CreateUserRequestPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating User", fmt.Sprintf("Calling API: %v", err))
return
}
ctx = core.LogResponse(ctx)
UserId := *createResp.Id
*/
// Example data value setting
//data.UserId = types.StringValue("id-from-response")
// TODO: Set data returned by API in identity
identity := UserResourceIdentityModel{
ProjectID: types.StringValue(projectId),
Region: types.StringValue(region),
// TODO: add missing values
// UserID: types.StringValue(UserId),
}
resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...)
if resp.Diagnostics.HasError() {
return
}
// TODO: implement wait handler if needed
/*
waitResp, err := wait.CreateUserWaitHandler(
ctx,
r.client,
projectId,
UserId,
region,
).SetSleepBeforeWait(
30 * time.Second,
).SetTimeout(
90 * time.Minute,
).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
"Error creating User",
fmt.Sprintf("User creation waiting: %v", err),
)
return
}
if waitResp.Id == nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
"Error creating User",
"User creation waiting: returned id is nil",
)
return
}
// Map response body to schema
err = mapResponseToModel(ctx, waitResp, &model, resp.Diagnostics)
if err != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
"Error creating User",
fmt.Sprintf("Processing API payload: %v", err),
)
return
}
*/
// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
tflog.Info(ctx, "sqlserverflexbeta.User created")
}
func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data resourceModel
// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Read identity data
var identityData UserResourceIdentityModel
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
if resp.Diagnostics.HasError() {
return
}
ctx = core.InitProviderContext(ctx)
projectId := identityData.ProjectID.ValueString()
region := identityData.Region.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
// Todo: Read API call logic
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
// TODO: Set data returned by API in identity
identity := UserResourceIdentityModel{
ProjectID: types.StringValue(projectId),
Region: types.StringValue(region),
// InstanceID: types.StringValue(instanceId),
}
resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "sqlserverflexbeta.User read")
}
func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data resourceModel
// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Read identity data
var identityData UserResourceIdentityModel
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
if resp.Diagnostics.HasError() {
return
}
ctx = core.InitProviderContext(ctx)
projectId := identityData.ProjectID.ValueString()
region := identityData.Region.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
// Todo: Update API call logic
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
tflog.Info(ctx, "sqlserverflexbeta.User updated")
}
func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data resourceModel
// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Read identity data
var identityData UserResourceIdentityModel
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
if resp.Diagnostics.HasError() {
return
}
ctx = core.InitProviderContext(ctx)
projectId := identityData.ProjectID.ValueString()
region := identityData.Region.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
// Todo: Delete API call logic
tflog.Info(ctx, "sqlserverflexbeta.User deleted")
tflog.Info(ctx, "SQLServer Beta Flex user client configured")
}
// ModifyPlan implements resource.ResourceWithModifyPlan.
@ -397,25 +103,256 @@ func (r *userResource) ModifyPlan(
return
}
var identityModel UserResourceIdentityModel
identityModel.ProjectID = planModel.ProjectId
identityModel.Region = planModel.Region
// TODO: complete
//if !planModel.InstanceId.IsNull() && !planModel.InstanceId.IsUnknown() {
// identityModel.InstanceID = planModel.InstanceId
//}
resp.Diagnostics.Append(resp.Identity.Set(ctx, identityModel)...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...)
if resp.Diagnostics.HasError() {
return
}
}
//go:embed planModifiers.yaml
var modifiersFileByte []byte
// Schema defines the schema for the resource.
func (r *userResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
s := sqlserverflexbetagen.UserResourceSchema(ctx)
fields, err := utils.ReadModifiersConfig(modifiersFileByte)
if err != nil {
resp.Diagnostics.AddError("error during read modifiers config file", err.Error())
return
}
err = utils.AddPlanModifiersToResourceSchema(fields, &s)
if err != nil {
resp.Diagnostics.AddError("error adding plan modifiers", err.Error())
return
}
resp.Schema = s
}
// IdentitySchema defines the schema for the resource's identity attributes.
func (r *userResource) IdentitySchema(
_ context.Context,
_ resource.IdentitySchemaRequest,
response *resource.IdentitySchemaResponse,
) {
response.IdentitySchema = identityschema.Schema{
Attributes: map[string]identityschema.Attribute{
"project_id": identityschema.StringAttribute{
RequiredForImport: true, // must be set during import by the practitioner
},
"region": identityschema.StringAttribute{
RequiredForImport: true, // can be defaulted by the provider configuration
},
"instance_id": identityschema.StringAttribute{
RequiredForImport: true, // can be defaulted by the provider configuration
},
"user_id": identityschema.Int64Attribute{
RequiredForImport: true, // can be defaulted by the provider configuration
},
},
}
}
// Create creates the resource and sets the initial Terraform state.
func (r *userResource) Create(
ctx context.Context,
req resource.CreateRequest,
resp *resource.CreateResponse,
) { // nolint:gocritic // function signature required by Terraform
var model resourceModel
diags := req.Plan.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
instanceId := model.InstanceId.ValueString()
region := model.Region.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "instance_id", instanceId)
ctx = tflog.SetField(ctx, "region", region)
var roles []sqlserverflexbeta.UserRole
if !model.Roles.IsNull() && !model.Roles.IsUnknown() {
diags = model.Roles.ElementsAs(ctx, &roles, false)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
// Generate API request body from model
payload, err := toCreatePayload(&model, roles)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Creating API payload: %v", err))
return
}
// Create new user
userResp, err := r.client.CreateUserRequest(
ctx,
projectId,
region,
instanceId,
).CreateUserRequestPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Calling API: %v", err))
return
}
ctx = core.LogResponse(ctx)
if userResp == nil || userResp.Id == nil || *userResp.Id == 0 {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
"Error creating user",
"API didn't return user Id. A user might have been created",
)
return
}
userId := *userResp.Id
ctx = tflog.SetField(ctx, "user_id", userId)
// Map response body to schema
err = mapFieldsCreate(userResp, &model, region)
if err != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
"Error creating user",
fmt.Sprintf("Processing API payload: %v", err),
)
return
}
// Set state to fully populated data
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "SQLServer Flex user created")
}
// Read refreshes the Terraform state with the latest data.
func (r *userResource) Read(
ctx context.Context,
req resource.ReadRequest,
resp *resource.ReadResponse,
) { // nolint:gocritic // function signature required by Terraform
var model resourceModel
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
instanceId := model.InstanceId.ValueString()
userId := model.UserId.ValueInt64()
region := r.providerData.GetRegionWithOverride(model.Region)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "instance_id", instanceId)
ctx = tflog.SetField(ctx, "user_id", userId)
ctx = tflog.SetField(ctx, "region", region)
recordSetResp, err := r.client.GetUserRequest(ctx, projectId, region, instanceId, userId).Execute()
if err != nil {
var oapiErr *oapierror.GenericOpenAPIError
ok := errors.As(
err,
&oapiErr,
)
//nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
if ok && oapiErr.StatusCode == http.StatusNotFound {
resp.State.RemoveResource(ctx)
return
}
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Calling API: %v", err))
return
}
ctx = core.LogResponse(ctx)
// Map response body to schema
err = mapFields(recordSetResp, &model, region)
if err != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
"Error reading user",
fmt.Sprintf("Processing API payload: %v", err),
)
return
}
// Set refreshed state
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "SQLServer Flex user read")
}
// Update updates the resource and sets the updated Terraform state on success.
func (r *userResource) Update(
ctx context.Context,
_ resource.UpdateRequest,
resp *resource.UpdateResponse,
) { // nolint:gocritic // function signature required by Terraform
// Update shouldn't be called
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", "User can't be updated")
}
// Delete deletes the resource and removes the Terraform state on success.
func (r *userResource) Delete(
ctx context.Context,
req resource.DeleteRequest,
resp *resource.DeleteResponse,
) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from plan
var model resourceModel
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
instanceId := model.InstanceId.ValueString()
userId := model.UserId.ValueInt64()
region := model.Region.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "instance_id", instanceId)
ctx = tflog.SetField(ctx, "user_id", userId)
ctx = tflog.SetField(ctx, "region", region)
// Delete existing record set
// err := r.client.DeleteUserRequest(ctx, projectId, region, instanceId, userId).Execute()
// Delete existing record set
_, err := sqlserverflexbetaWait.DeleteUserWaitHandler(ctx, r.client, projectId, region, instanceId, userId).
WaitWithContext(ctx)
//err := r.client.DeleteUserRequest(ctx, arg.projectId, arg.region, arg.instanceId, userId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "User Delete Error", fmt.Sprintf("Calling API: %v", err))
return
}
ctx = core.LogResponse(ctx)
tflog.Info(ctx, "SQLServer Flex user deleted")
}
// ImportState imports a resource into the Terraform state on success.
// The expected format of the resource import identifier is: project_id,zone_id,record_set_id
func (r *userResource) ImportState(
@ -423,6 +360,7 @@ func (r *userResource) ImportState(
req resource.ImportStateRequest,
resp *resource.ImportStateResponse,
) {
ctx = core.InitProviderContext(ctx)
if req.ID != "" {
@ -457,7 +395,7 @@ func (r *userResource) ImportState(
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[2])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("user_id"), userId)...)
tflog.Info(ctx, "Sqlserverflexbeta user state imported")
tflog.Info(ctx, "Postgres Flex user state imported")
return
}
@ -482,9 +420,8 @@ func (r *userResource) ImportState(
core.LogAndAddWarning(
ctx,
&resp.Diagnostics,
"Sqlserverflexbeta database imported with empty password",
"The database password is not imported as it is only available upon creation of a new database. The password field will be empty.",
"SQLServer Flex user imported with empty password",
"The user password is not imported as it is only available upon creation of a new user. The password field will be empty.",
)
tflog.Info(ctx, "Sqlserverflexbeta user state imported")
tflog.Info(ctx, "SQLServer Flex user state imported")
}

View file

@ -0,0 +1,47 @@
package utils
import (
"context"
"fmt"
sqlserverflex "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexbeta"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/stackitcloud/stackit-sdk-go/core/config"
"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"
)
func ConfigureClient(
ctx context.Context,
providerData *core.ProviderData,
diags *diag.Diagnostics,
) *sqlserverflex.APIClient {
apiClientConfigOptions := []config.ConfigurationOption{
config.WithCustomAuth(providerData.RoundTripper),
utils.UserAgentConfigOption(providerData.Version),
}
if providerData.SQLServerFlexCustomEndpoint != "" {
apiClientConfigOptions = append(
apiClientConfigOptions,
config.WithEndpoint(providerData.SQLServerFlexCustomEndpoint),
)
} else {
apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion()))
}
apiClient, err := sqlserverflex.NewAPIClient(apiClientConfigOptions...)
if err != nil {
core.LogAndAddError(
ctx,
diags,
"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 nil
}
return apiClient
}

View file

@ -0,0 +1,97 @@
package utils
import (
"context"
"os"
"reflect"
"testing"
"github.com/hashicorp/terraform-plugin-framework/diag"
sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients"
"github.com/stackitcloud/stackit-sdk-go/core/config"
sqlserverflex "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexbeta"
"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"
)
const (
testVersion = "1.2.3"
testCustomEndpoint = "https://sqlserverflex-custom-endpoint.api.stackit.cloud"
)
func TestConfigureClient(t *testing.T) {
/* mock authentication by setting service account token env variable */
os.Clearenv()
err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val")
if err != nil {
t.Errorf("error setting env variable: %v", err)
}
type args struct {
providerData *core.ProviderData
}
tests := []struct {
name string
args args
wantErr bool
expected *sqlserverflex.APIClient
}{
{
name: "default endpoint",
args: args{
providerData: &core.ProviderData{
Version: testVersion,
},
},
expected: func() *sqlserverflex.APIClient {
apiClient, err := sqlserverflex.NewAPIClient(
config.WithRegion("eu01"),
utils.UserAgentConfigOption(testVersion),
)
if err != nil {
t.Errorf("error configuring client: %v", err)
}
return apiClient
}(),
wantErr: false,
},
{
name: "custom endpoint",
args: args{
providerData: &core.ProviderData{
Version: testVersion,
SQLServerFlexCustomEndpoint: testCustomEndpoint,
},
},
expected: func() *sqlserverflex.APIClient {
apiClient, err := sqlserverflex.NewAPIClient(
utils.UserAgentConfigOption(testVersion),
config.WithEndpoint(testCustomEndpoint),
)
if err != nil {
t.Errorf("error configuring client: %v", err)
}
return apiClient
}(),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
ctx := context.Background()
diags := diag.Diagnostics{}
actual := ConfigureClient(ctx, tt.args.providerData, &diags)
if diags.HasError() != tt.wantErr {
t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr)
}
if !reflect.DeepEqual(actual, tt.expected) {
t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected)
}
},
)
}
}

View file

@ -27,9 +27,35 @@ const (
// APIClientInterface Interface needed for tests
type APIClientInterface interface {
GetInstanceRequestExecute(ctx context.Context, projectId, region, instanceId string) (*sqlserverflex.GetInstanceResponse, error)
GetDatabaseRequestExecute(ctx context.Context, projectId string, region string, instanceId string, databaseName string) (*sqlserverflex.GetDatabaseResponse, error)
GetUserRequestExecute(ctx context.Context, projectId string, region string, instanceId string, userId int64) (*sqlserverflex.GetUserResponse, error)
GetInstanceRequestExecute(
ctx context.Context,
projectId, region, instanceId string,
) (*sqlserverflex.GetInstanceResponse, error)
GetDatabaseRequestExecute(
ctx context.Context,
projectId string,
region string,
instanceId string,
databaseName string,
) (*sqlserverflex.GetDatabaseResponse, error)
GetUserRequestExecute(
ctx context.Context,
projectId string,
region string,
instanceId string,
userId int64,
) (*sqlserverflex.GetUserResponse, error)
}
// APIClientUserInterface Interface needed for tests
type APIClientUserInterface interface {
DeleteUserRequestExecute(
ctx context.Context,
projectId string,
region string,
instanceId string,
userId int64,
) error
}
// CreateInstanceWaitHandler will wait for instance creation
@ -38,93 +64,115 @@ func CreateInstanceWaitHandler(
a APIClientInterface,
projectId, instanceId, region string,
) *wait.AsyncActionHandler[sqlserverflex.GetInstanceResponse] {
handler := wait.New(func() (waitFinished bool, response *sqlserverflex.GetInstanceResponse, err error) {
s, err := a.GetInstanceRequestExecute(ctx, projectId, region, instanceId)
if err != nil {
return false, nil, err
}
if s == nil || s.Id == nil || *s.Id != instanceId || s.Status == nil {
return false, nil, nil
}
switch strings.ToLower(string(*s.Status)) {
case strings.ToLower(InstanceStateSuccess):
if *s.Network.AccessScope == "SNA" {
if s.Network.InstanceAddress == nil {
tflog.Info(ctx, "Waiting for instance_address")
return false, nil, nil
}
if s.Network.RouterAddress == nil {
tflog.Info(ctx, "Waiting for router_address")
return false, nil, nil
}
handler := wait.New(
func() (waitFinished bool, response *sqlserverflex.GetInstanceResponse, err error) {
s, err := a.GetInstanceRequestExecute(ctx, projectId, region, instanceId)
if err != nil {
return false, nil, err
}
return true, s, nil
case strings.ToLower(InstanceStateUnknown), strings.ToLower(InstanceStateFailed):
return true, s, fmt.Errorf("create failed for instance with id %s", instanceId)
case strings.ToLower(InstanceStatePending), strings.ToLower(InstanceStateProcessing):
tflog.Info(ctx, "request is being handled", map[string]interface{}{
"status": *s.Status,
})
return false, nil, nil
default:
tflog.Info(ctx, "Wait (create) received unknown status", map[string]interface{}{
"instanceId": instanceId,
"status": s.Status,
})
return false, s, nil
}
})
if s == nil || s.Id == nil || *s.Id != instanceId || s.Status == nil {
return false, nil, nil
}
switch strings.ToLower(string(*s.Status)) {
case strings.ToLower(InstanceStateSuccess):
if *s.Network.AccessScope == "SNA" {
if s.Network.InstanceAddress == nil {
tflog.Info(ctx, "Waiting for instance_address")
return false, nil, nil
}
if s.Network.RouterAddress == nil {
tflog.Info(ctx, "Waiting for router_address")
return false, nil, nil
}
}
return true, s, nil
case strings.ToLower(InstanceStateUnknown), strings.ToLower(InstanceStateFailed):
return true, s, fmt.Errorf("create failed for instance with id %s", instanceId)
case strings.ToLower(InstanceStatePending), strings.ToLower(InstanceStateProcessing):
tflog.Info(
ctx, "request is being handled", map[string]interface{}{
"status": *s.Status,
},
)
return false, nil, nil
default:
tflog.Info(
ctx, "Wait (create) received unknown status", map[string]interface{}{
"instanceId": instanceId,
"status": s.Status,
},
)
return false, s, nil
}
},
)
return handler
}
// UpdateInstanceWaitHandler will wait for instance update
func UpdateInstanceWaitHandler(ctx context.Context, a APIClientInterface, projectId, instanceId, region string) *wait.AsyncActionHandler[sqlserverflex.GetInstanceResponse] {
handler := wait.New(func() (waitFinished bool, response *sqlserverflex.GetInstanceResponse, err error) {
s, err := a.GetInstanceRequestExecute(ctx, projectId, region, instanceId)
if err != nil {
return false, nil, err
}
if s == nil || s.Id == nil || *s.Id != instanceId || s.Status == nil {
return false, nil, nil
}
switch strings.ToLower(string(*s.Status)) {
case strings.ToLower(InstanceStateSuccess):
return true, s, nil
case strings.ToLower(InstanceStateUnknown), strings.ToLower(InstanceStateFailed):
return true, s, fmt.Errorf("update failed for instance with id %s", instanceId)
case strings.ToLower(InstanceStatePending), strings.ToLower(InstanceStateProcessing):
tflog.Info(ctx, "request is being handled", map[string]interface{}{
"status": *s.Status,
})
return false, s, nil
default:
tflog.Info(ctx, "Wait (update) received unknown status", map[string]interface{}{
"instanceId": instanceId,
"status": s.Status,
})
return false, s, nil
}
})
func UpdateInstanceWaitHandler(
ctx context.Context,
a APIClientInterface,
projectId, instanceId, region string,
) *wait.AsyncActionHandler[sqlserverflex.GetInstanceResponse] {
handler := wait.New(
func() (waitFinished bool, response *sqlserverflex.GetInstanceResponse, err error) {
s, err := a.GetInstanceRequestExecute(ctx, projectId, region, instanceId)
if err != nil {
return false, nil, err
}
if s == nil || s.Id == nil || *s.Id != instanceId || s.Status == nil {
return false, nil, nil
}
switch strings.ToLower(string(*s.Status)) {
case strings.ToLower(InstanceStateSuccess):
return true, s, nil
case strings.ToLower(InstanceStateUnknown), strings.ToLower(InstanceStateFailed):
return true, s, fmt.Errorf("update failed for instance with id %s", instanceId)
case strings.ToLower(InstanceStatePending), strings.ToLower(InstanceStateProcessing):
tflog.Info(
ctx, "request is being handled", map[string]interface{}{
"status": *s.Status,
},
)
return false, s, nil
default:
tflog.Info(
ctx, "Wait (update) received unknown status", map[string]interface{}{
"instanceId": instanceId,
"status": s.Status,
},
)
return false, s, nil
}
},
)
return handler
}
// DeleteInstanceWaitHandler will wait for instance deletion
func DeleteInstanceWaitHandler(ctx context.Context, a APIClientInterface, projectId, instanceId, region string) *wait.AsyncActionHandler[sqlserverflex.GetInstanceResponse] {
handler := wait.New(func() (waitFinished bool, response *sqlserverflex.GetInstanceResponse, err error) {
s, err := a.GetInstanceRequestExecute(ctx, projectId, region, instanceId)
if err == nil {
return false, s, nil
}
var oapiErr *oapierror.GenericOpenAPIError
ok := errors.As(err, &oapiErr)
if !ok {
return false, nil, fmt.Errorf("could not convert error to oapierror.GenericOpenAPIError")
}
if oapiErr.StatusCode != http.StatusNotFound {
return false, nil, err
}
return true, nil, nil
})
func DeleteInstanceWaitHandler(
ctx context.Context,
a APIClientInterface,
projectId, instanceId, region string,
) *wait.AsyncActionHandler[sqlserverflex.GetInstanceResponse] {
handler := wait.New(
func() (waitFinished bool, response *sqlserverflex.GetInstanceResponse, err error) {
s, err := a.GetInstanceRequestExecute(ctx, projectId, region, instanceId)
if err == nil {
return false, s, nil
}
var oapiErr *oapierror.GenericOpenAPIError
ok := errors.As(err, &oapiErr)
if !ok {
return false, nil, fmt.Errorf("could not convert error to oapierror.GenericOpenAPIError")
}
if oapiErr.StatusCode != http.StatusNotFound {
return false, nil, err
}
return true, nil, nil
},
)
handler.SetTimeout(30 * time.Minute)
return handler
}
@ -135,23 +183,60 @@ func CreateDatabaseWaitHandler(
a APIClientInterface,
projectId, instanceId, region, databaseName string,
) *wait.AsyncActionHandler[sqlserverflex.GetDatabaseResponse] {
handler := wait.New(func() (waitFinished bool, response *sqlserverflex.GetDatabaseResponse, err error) {
s, err := a.GetDatabaseRequestExecute(ctx, projectId, region, instanceId, databaseName)
if err != nil {
return false, nil, err
}
if s == nil || s.Name == nil || *s.Name != databaseName {
return false, nil, nil
}
var oapiErr *oapierror.GenericOpenAPIError
ok := errors.As(err, &oapiErr)
if !ok {
return false, nil, fmt.Errorf("could not convert error to oapierror.GenericOpenAPIError")
}
if oapiErr.StatusCode != http.StatusNotFound {
return false, nil, err
}
return true, nil, nil
})
handler := wait.New(
func() (waitFinished bool, response *sqlserverflex.GetDatabaseResponse, err error) {
s, err := a.GetDatabaseRequestExecute(ctx, projectId, region, instanceId, databaseName)
if err != nil {
return false, nil, err
}
if s == nil || s.Name == nil || *s.Name != databaseName {
return false, nil, nil
}
var oapiErr *oapierror.GenericOpenAPIError
ok := errors.As(err, &oapiErr)
if !ok {
return false, nil, fmt.Errorf("could not convert error to oapierror.GenericOpenAPIError")
}
if oapiErr.StatusCode != http.StatusNotFound {
return false, nil, err
}
return true, nil, nil
},
)
return handler
}
// DeleteUserWaitHandler will wait for instance deletion
func DeleteUserWaitHandler(
ctx context.Context,
a APIClientUserInterface,
projectId, instanceId, region string,
userId int64,
) *wait.AsyncActionHandler[struct{}] {
handler := wait.New(
func() (waitFinished bool, response *struct{}, err error) {
err = a.DeleteUserRequestExecute(ctx, projectId, region, instanceId, userId)
if err == nil {
return false, nil, nil
}
var oapiErr *oapierror.GenericOpenAPIError
ok := errors.As(err, &oapiErr)
if !ok {
return false, nil, fmt.Errorf("could not convert error to oapierror.GenericOpenAPIError")
}
switch oapiErr.StatusCode {
case http.StatusNotFound:
return true, nil, nil
case http.StatusInternalServerError:
tflog.Warn(ctx, "Wait handler got error 500")
return false, nil, nil
default:
return false, nil, err
}
},
)
handler.SetTimeout(15 * time.Minute)
handler.SetSleepBeforeWait(15 * time.Second)
return handler
}