feat: more_tests (#85)
Some checks failed
Publish / Check GoReleaser config (push) Successful in 7s
Publish / Publish provider (push) Successful in 7m41s
CI Workflow / Check GoReleaser config (pull_request) Successful in 6s
CI Workflow / Prepare GO cache (pull_request) Successful in 10m18s
CI Workflow / Test readiness for publishing provider (pull_request) Has been cancelled
CI Workflow / Code coverage report (pull_request) Has been cancelled
CI Workflow / CI run build and linting (pull_request) Has been cancelled
CI Workflow / CI run tests (pull_request) Has been cancelled
TF Acceptance Tests Workflow / Acceptance Tests (pull_request) Failing after 26m13s

## 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)

Co-authored-by: Marcel S. Henselin <marcel.henselin@stackit.cloud>
Reviewed-on: #85
This commit is contained in:
Marcel_Henselin 2026-03-17 15:02:08 +00:00
parent 3790894563
commit dd77da71dd
Signed by: tf-provider.git.onstackit.cloud
GPG key ID: 6D7E8A1ED8955A9C
37 changed files with 2473 additions and 1742 deletions

View file

@ -45,7 +45,7 @@ func DatabaseDataSourceSchema(ctx context.Context) schema.Schema {
MarkdownDescription: "The STACKIT project ID.",
},
"region": schema.StringAttribute{
Required: true,
Optional: true,
Description: "The region which should be addressed",
MarkdownDescription: "The region which should be addressed",
Validators: []validator.String{

View file

@ -64,17 +64,21 @@ func mapResourceFields(source *v3alpha1api.GetDatabaseResponse, model *resourceM
return fmt.Errorf("model input is nil")
}
var databaseId int64
if model.Id.ValueInt64() != 0 {
databaseId = model.Id.ValueInt64()
var databaseID int64
if model.DatabaseId.ValueInt64() != 0 {
if source.Id != 0 {
if model.DatabaseId.ValueInt64() != int64(source.Id) {
return fmt.Errorf("retrieved ID does not match known ID")
}
}
databaseID = model.DatabaseId.ValueInt64()
} else if source.Id != 0 {
databaseId = int64(source.Id)
databaseID = int64(source.Id)
} else {
return fmt.Errorf("database id not present")
}
model.Id = types.Int64Value(databaseId)
model.DatabaseId = types.Int64Value(databaseId)
model.DatabaseId = types.Int64Value(databaseID)
model.Name = types.StringValue(source.GetName())
model.Owner = types.StringValue(cleanString(source.Owner))
return nil

View file

@ -160,7 +160,7 @@ func TestMapResourceFields(t *testing.T) {
},
expected: expected{
model: &resourceModel{
Id: types.Int64Value(1),
Id: types.StringNull(),
Name: types.StringValue("my-db"),
Owner: types.StringValue("my-owner"),
DatabaseId: types.Int64Value(1),

View file

@ -11,9 +11,9 @@ import (
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"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/oapierror"
"github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3alpha1api"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion"
@ -30,11 +30,6 @@ var (
_ resource.ResourceWithConfigure = &databaseResource{}
_ resource.ResourceWithImportState = &databaseResource{}
_ resource.ResourceWithModifyPlan = &databaseResource{}
_ resource.ResourceWithIdentity = &databaseResource{}
// Error message constants
extractErrorSummary = "extracting failed"
extractErrorMessage = "Extracting identity data: %v"
)
// NewDatabaseResource is a helper function to simplify the provider implementation.
@ -45,14 +40,6 @@ func NewDatabaseResource() resource.Resource {
// resourceModel describes the resource data model.
type resourceModel = postgresflexalphaResGen.DatabaseModel
// DatabaseResourceIdentityModel describes the resource's identity attributes.
type DatabaseResourceIdentityModel struct {
ProjectID types.String `tfsdk:"project_id"`
Region types.String `tfsdk:"region"`
InstanceID types.String `tfsdk:"instance_id"`
DatabaseID types.Int64 `tfsdk:"database_id"`
}
// databaseResource is the resource implementation.
type databaseResource struct {
client *v3alpha1api.APIClient
@ -138,30 +125,6 @@ func (r *databaseResource) Schema(ctx context.Context, _ resource.SchemaRequest,
resp.Schema = s
}
// IdentitySchema defines the schema for the resource's identity attributes.
func (r *databaseResource) IdentitySchema(
_ context.Context,
_ resource.IdentitySchemaRequest,
response *resource.IdentitySchemaResponse,
) {
response.IdentitySchema = identityschema.Schema{
Attributes: map[string]identityschema.Attribute{
"project_id": identityschema.StringAttribute{
RequiredForImport: true,
},
"region": identityschema.StringAttribute{
RequiredForImport: true,
},
"instance_id": identityschema.StringAttribute{
RequiredForImport: true,
},
"database_id": identityschema.Int64Attribute{
RequiredForImport: true,
},
},
}
}
// Create creates the resource and sets the initial Terraform state.
func (r *databaseResource) Create(
ctx context.Context,
@ -178,12 +141,12 @@ func (r *databaseResource) Create(
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
projectID := model.ProjectId.ValueString()
region := model.Region.ValueString()
instanceId := model.InstanceId.ValueString()
instanceID := model.InstanceId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "instance_id", instanceId)
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
@ -200,9 +163,9 @@ func (r *databaseResource) Create(
// Create new database
databaseResp, err := r.client.DefaultAPI.CreateDatabaseRequest(
ctx,
projectId,
projectID,
region,
instanceId,
instanceID,
).CreateDatabaseRequestPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, funcErrorSummary, fmt.Sprintf("Calling API: %v", err))
@ -219,23 +182,33 @@ func (r *databaseResource) Create(
)
return
}
databaseId := int64(*dbID)
ctx = tflog.SetField(ctx, "database_id", databaseId)
databaseID := int64(*dbID)
databaseIDString := strconv.Itoa(int(*dbID))
ctx = tflog.SetField(ctx, "database_id", databaseID)
ctx = core.LogResponse(ctx)
// Save identity into Terraform state
identity := DatabaseResourceIdentityModel{
ProjectID: types.StringValue(projectId),
Region: types.StringValue(region),
InstanceID: types.StringValue(instanceId),
DatabaseID: types.Int64Value(databaseId),
}
resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...)
if resp.Diagnostics.HasError() {
return
}
model.DatabaseId = types.Int64Value(databaseID)
model.Id = utils.BuildInternalTerraformId(projectID, region, instanceID, databaseIDString)
database, err := postgresflexalphaWait.GetDatabaseByIdWaitHandler(ctx, r.client.DefaultAPI, projectId, instanceId, region, databaseId).
// Set data returned by API in id
resp.Diagnostics.Append(
resp.State.SetAttribute(
ctx,
path.Root("database_id"),
databaseID,
)...,
)
// Set data returned by API in id
resp.Diagnostics.Append(
resp.State.SetAttribute(
ctx,
path.Root("id"),
model.Id,
)...,
)
database, err := postgresflexalphaWait.GetDatabaseByIdWaitHandler(ctx, r.client.DefaultAPI, projectID, instanceID, region, databaseID).
SetTimeout(15 * time.Minute).
SetSleepBeforeWait(15 * time.Second).
WaitWithContext(ctx)
@ -284,17 +257,28 @@ func (r *databaseResource) Read(
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
instanceId := model.InstanceId.ValueString()
projectID := model.ProjectId.ValueString()
instanceID := model.InstanceId.ValueString()
region := model.Region.ValueString()
databaseId := model.DatabaseId.ValueInt64()
databaseID := model.DatabaseId.ValueInt64()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "instance_id", instanceId)
databaseIDString := strconv.Itoa(int(databaseID))
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_id", databaseId)
ctx = tflog.SetField(ctx, "database_id", databaseID)
databaseResp, err := postgresflexalphaWait.GetDatabaseByIdWaitHandler(ctx, r.client.DefaultAPI, projectId, instanceId, region, databaseId).
// Set data returned by API in id
resp.Diagnostics.Append(
resp.State.SetAttribute(
ctx,
path.Root("id"),
utils.BuildInternalTerraformId(projectID, region, instanceID, databaseIDString),
)...,
)
databaseResp, err := postgresflexalphaWait.GetDatabaseByIdWaitHandler(ctx, r.client.DefaultAPI, projectID, instanceID, region, databaseID).
SetTimeout(15 * time.Minute).
SetSleepBeforeWait(15 * time.Second).
WaitWithContext(ctx)
@ -322,18 +306,6 @@ func (r *databaseResource) Read(
return
}
// Save identity into Terraform state
identity := DatabaseResourceIdentityModel{
ProjectID: types.StringValue(projectId),
Region: types.StringValue(region),
InstanceID: types.StringValue(instanceId),
DatabaseID: types.Int64Value(int64(databaseResp.GetId())),
}
resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...)
if resp.Diagnostics.HasError() {
return
}
// Set refreshed state
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
@ -436,18 +408,6 @@ func (r *databaseResource) Update(
return
}
// Save identity into Terraform state
identity := DatabaseResourceIdentityModel{
ProjectID: types.StringValue(projectId),
Region: types.StringValue(region),
InstanceID: types.StringValue(instanceId),
DatabaseID: types.Int64Value(databaseId),
}
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() {
@ -469,38 +429,33 @@ func (r *databaseResource) Delete(
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, region, instanceId, databaseId64, errExt := r.extractIdentityData(model, identityData)
if errExt != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
extractErrorSummary,
fmt.Sprintf(extractErrorMessage, errExt),
)
}
projectID := model.ProjectId.ValueString()
instanceID := model.InstanceId.ValueString()
region := model.Region.ValueString()
databaseID64 := model.DatabaseId.ValueInt64()
if databaseId64 > math.MaxInt32 {
if databaseID64 > math.MaxInt32 {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error in type conversion", "int value too large (databaseId)")
return
}
databaseId := int32(databaseId64) // nolint:gosec // check is performed above
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "instance_id", instanceId)
databaseID := int32(databaseID64) // nolint:gosec // check is performed above
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_id", databaseId)
ctx = tflog.SetField(ctx, "database_id", databaseID)
// Delete existing record set
err := r.client.DefaultAPI.DeleteDatabaseRequest(ctx, projectId, region, instanceId, databaseId).Execute()
err := r.client.DefaultAPI.DeleteDatabaseRequest(ctx, projectID, region, instanceID, databaseID).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 {
if oapiErr.StatusCode == 404 {
resp.State.RemoveResource(ctx)
return
}
}
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting database", fmt.Sprintf("Calling API: %v", err))
}
@ -517,109 +472,44 @@ func (r *databaseResource) ImportState(
resp *resource.ImportStateResponse,
) {
ctx = core.InitProviderContext(ctx)
idParts := strings.Split(req.ID, core.Separator)
if req.ID != "" {
idParts := strings.Split(req.ID, core.Separator)
if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" {
core.LogAndAddError(
ctx, &resp.Diagnostics,
"Error importing database",
fmt.Sprintf(
"Expected import identifier with format [project_id],[region],[instance_id],[database_id], got %q",
req.ID,
),
)
return
}
if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" {
core.LogAndAddError(
ctx, &resp.Diagnostics,
"Error importing database",
fmt.Sprintf(
"Expected import identifier with format [project_id],[region],[instance_id],[database_id], got %q",
req.ID,
),
)
return
}
databaseId, err := strconv.ParseInt(idParts[3], 10, 64)
if err != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
"Error importing database",
fmt.Sprintf("Invalid database_id format: %q. It must be a valid integer.", idParts[3]),
)
return
}
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[2])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("database_id"), databaseId)...)
core.LogAndAddWarning(
databaseID, err := strconv.ParseInt(idParts[3], 10, 64)
if err != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
"Postgresflex 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.",
"Error importing database",
fmt.Sprintf("Invalid database_id format: %q. It must be a valid integer.", idParts[3]),
)
tflog.Info(ctx, "Postgres Flex database state imported")
return
}
// If no ID is provided, attempt to read identity attributes from the import configuration
var identityData DatabaseResourceIdentityModel
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
if resp.Diagnostics.HasError() {
return
}
tfIDString := utils.BuildInternalTerraformId(idParts...).ValueString()
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), tfIDString)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[2])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("database_id"), databaseID)...)
projectId := identityData.ProjectID.ValueString()
region := identityData.Region.ValueString()
instanceId := identityData.InstanceID.ValueString()
databaseId := identityData.DatabaseID.ValueInt64()
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), region)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), instanceId)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("database_id"), databaseId)...)
core.LogAndAddWarning(
ctx,
&resp.Diagnostics,
"Postgresflex 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.",
)
tflog.Info(ctx, "Postgres Flex 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 string, databaseId int64, err error) {
if !model.DatabaseId.IsNull() && !model.DatabaseId.IsUnknown() {
databaseId = model.DatabaseId.ValueInt64()
} else {
if identity.DatabaseID.IsNull() || identity.DatabaseID.IsUnknown() {
return "", "", "", 0, fmt.Errorf("database_id not found in config")
}
databaseId = identity.DatabaseID.ValueInt64()
}
if !model.ProjectId.IsNull() && !model.ProjectId.IsUnknown() {
projectId = model.ProjectId.ValueString()
} else {
if identity.ProjectID.IsNull() || identity.ProjectID.IsUnknown() {
return "", "", "", 0, 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 "", "", "", 0, 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 "", "", "", 0, fmt.Errorf("instance_id not found in config")
}
instanceId = identity.InstanceID.ValueString()
}
return projectId, region, instanceId, databaseId, nil
}

View file

@ -20,7 +20,7 @@ func DatabaseResourceSchema(ctx context.Context) schema.Schema {
Description: "The ID of the database.",
MarkdownDescription: "The ID of the database.",
},
"id": schema.Int64Attribute{
"id": schema.StringAttribute{
Computed: true,
Description: "The id of the database.",
MarkdownDescription: "The id of the database.",
@ -65,7 +65,7 @@ func DatabaseResourceSchema(ctx context.Context) schema.Schema {
type DatabaseModel struct {
DatabaseId types.Int64 `tfsdk:"database_id"`
Id types.Int64 `tfsdk:"id"`
Id types.String `tfsdk:"id"`
InstanceId types.String `tfsdk:"instance_id"`
Name types.String `tfsdk:"name"`
Owner types.String `tfsdk:"owner"`

View file

@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3alpha1api"
@ -72,7 +73,13 @@ func (r *instanceDataSource) Configure(
// Schema defines the schema for the data source.
func (r *instanceDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = postgresflexalpha2.InstanceDataSourceSchema(ctx)
sch := postgresflexalpha2.InstanceDataSourceSchema(ctx)
sch.Attributes["id"] = schema.StringAttribute{
Computed: true,
Description: "internal ID",
MarkdownDescription: "internal ID",
}
resp.Schema = sch
}
// Read refreshes the Terraform state with the latest data.
@ -90,22 +97,22 @@ func (r *instanceDataSource) Read(
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
instanceId := model.InstanceId.ValueString()
projectID := model.ProjectId.ValueString()
instanceID := model.InstanceId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "instance_id", instanceId)
ctx = tflog.SetField(ctx, "project_id", projectID)
ctx = tflog.SetField(ctx, "instance_id", instanceID)
ctx = tflog.SetField(ctx, "region", region)
instanceResp, err := r.client.DefaultAPI.GetInstanceRequest(ctx, projectId, region, instanceId).Execute()
instanceResp, err := r.client.DefaultAPI.GetInstanceRequest(ctx, projectID, region, instanceID).Execute()
if err != nil {
utils.LogError(
ctx,
&resp.Diagnostics,
err,
"Reading instance",
fmt.Sprintf("Instance with ID %q does not exist in project %q.", instanceId, projectId),
fmt.Sprintf("Instance with ID %q does not exist in project %q.", instanceID, projectID),
map[int]string{
http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId),
http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectID),
},
)
resp.State.RemoveResource(ctx)

View file

@ -155,7 +155,7 @@ func InstanceDataSourceSchema(ctx context.Context) schema.Schema {
MarkdownDescription: "The STACKIT project ID.",
},
"region": schema.StringAttribute{
Required: true,
Optional: true,
Description: "The region which should be addressed",
MarkdownDescription: "The region which should be addressed",
Validators: []validator.String{

View file

@ -55,23 +55,21 @@ func mapGetInstanceResponseToModel(
}
m.FlavorId = types.StringValue(resp.GetFlavorId())
if m.Id.IsNull() || m.Id.IsUnknown() {
m.Id = utils.BuildInternalTerraformId(
m.ProjectId.ValueString(),
m.Region.ValueString(),
m.InstanceId.ValueString(),
)
}
m.Id = utils.BuildInternalTerraformId(
m.ProjectId.ValueString(),
m.Region.ValueString(),
resp.Id,
)
m.InstanceId = types.StringValue(resp.Id)
m.IsDeletable = types.BoolValue(resp.GetIsDeletable())
netAcl, diags := types.ListValueFrom(ctx, types.StringType, resp.Network.GetAcl())
netACL, diags := types.ListValueFrom(ctx, types.StringType, resp.Network.GetAcl())
if diags.HasError() {
return fmt.Errorf("failed converting network acl from response")
}
m.Acl = netAcl
m.Acl = netACL
netInstAdd := types.StringValue("")
if instAdd, ok := resp.Network.GetInstanceAddressOk(); ok {
@ -87,7 +85,7 @@ func mapGetInstanceResponseToModel(
postgresflexalpharesource.NetworkValue{}.AttributeTypes(ctx),
map[string]attr.Value{
"access_scope": basetypes.NewStringValue(string(resp.Network.GetAccessScope())),
"acl": netAcl,
"acl": netACL,
"instance_address": netInstAdd,
"router_address": netRtrAdd,
},
@ -130,7 +128,8 @@ func mapGetDataInstanceResponseToModel(
handleConnectionInfo(ctx, m, resp)
m.FlavorId = types.StringValue(resp.GetFlavorId())
m.Id = utils.BuildInternalTerraformId(m.ProjectId.ValueString(), m.Region.ValueString(), m.InstanceId.ValueString())
m.Id = types.StringValue(resp.Id)
m.TerraformID = utils.BuildInternalTerraformId(m.ProjectId.ValueString(), m.Region.ValueString(), m.InstanceId.ValueString())
m.InstanceId = types.StringValue(resp.Id)
m.IsDeletable = types.BoolValue(resp.GetIsDeletable())
m.Name = types.StringValue(resp.GetName())
@ -212,14 +211,14 @@ func handleNetwork(ctx context.Context, m *dataSourceModel, resp *postgresflex.G
}
func handleEncryption(m *dataSourceModel, resp *postgresflex.GetInstanceResponse) {
keyId := ""
if keyIdVal, ok := resp.Encryption.GetKekKeyIdOk(); ok {
keyId = *keyIdVal
keyID := ""
if keyIDVal, ok := resp.Encryption.GetKekKeyIdOk(); ok {
keyID = *keyIDVal
}
keyRingId := ""
if keyRingIdVal, ok := resp.Encryption.GetKekKeyRingIdOk(); ok {
keyRingId = *keyRingIdVal
keyRingID := ""
if keyRingIDVal, ok := resp.Encryption.GetKekKeyRingIdOk(); ok {
keyRingID = *keyRingIDVal
}
keyVersion := ""
@ -233,8 +232,8 @@ func handleEncryption(m *dataSourceModel, resp *postgresflex.GetInstanceResponse
}
m.Encryption = postgresflexalphadatasource.EncryptionValue{
KekKeyId: types.StringValue(keyId),
KekKeyRingId: types.StringValue(keyRingId),
KekKeyId: types.StringValue(keyID),
KekKeyRingId: types.StringValue(keyRingID),
KekKeyVersion: types.StringValue(keyVersion),
ServiceAccount: types.StringValue(svcAcc),
}

View file

@ -7,11 +7,10 @@ import (
"math"
"net/http"
"strings"
"time"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"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/oapierror"
coreUtils "github.com/stackitcloud/stackit-sdk-go/core/utils"
@ -32,7 +31,6 @@ var (
_ resource.ResourceWithImportState = &instanceResource{}
_ resource.ResourceWithModifyPlan = &instanceResource{}
_ resource.ResourceWithValidateConfig = &instanceResource{}
_ resource.ResourceWithIdentity = &instanceResource{}
)
// NewInstanceResource is a helper function to simplify the provider implementation.
@ -40,15 +38,6 @@ func NewInstanceResource() resource.Resource {
return &instanceResource{}
}
// resourceModel describes the resource data model.
type resourceModel = postgresflexalpha.InstanceModel
type InstanceResourceIdentityModel struct {
ProjectID types.String `tfsdk:"project_id"`
Region types.String `tfsdk:"region"`
InstanceID types.String `tfsdk:"instance_id"`
}
// instanceResource is the resource implementation.
type instanceResource struct {
client *v3alpha1api.APIClient
@ -60,7 +49,7 @@ func (r *instanceResource) ValidateConfig(
req resource.ValidateConfigRequest,
resp *resource.ValidateConfigResponse,
) {
var data resourceModel
var data postgresflexalpha.InstanceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
@ -84,7 +73,7 @@ func (r *instanceResource) ModifyPlan(
req resource.ModifyPlanRequest,
resp *resource.ModifyPlanResponse,
) { // nolint:gocritic // function signature required by Terraform
var configModel resourceModel
var configModel postgresflexalpha.InstanceModel
// skip initial empty configuration to avoid follow-up errors
if req.Config.Raw.IsNull() {
return
@ -94,7 +83,7 @@ func (r *instanceResource) ModifyPlan(
return
}
var planModel resourceModel
var planModel postgresflexalpha.InstanceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...)
if resp.Diagnostics.HasError() {
return
@ -160,33 +149,13 @@ func (r *instanceResource) Schema(ctx context.Context, _ resource.SchemaRequest,
resp.Schema = schema
}
func (r *instanceResource) 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
},
},
}
}
// Create creates the resource and sets the initial Terraform state.
func (r *instanceResource) Create(
ctx context.Context,
req resource.CreateRequest,
resp *resource.CreateResponse,
) { // nolint:gocritic // function signature required by Terraform
var model resourceModel
var model postgresflexalpha.InstanceModel
diags := req.Plan.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
@ -201,15 +170,15 @@ func (r *instanceResource) Create(
ctx = tflog.SetField(ctx, "project_id", projectID)
ctx = tflog.SetField(ctx, "region", region)
var netAcl []string
diag := model.Network.Acl.ElementsAs(ctx, &netAcl, false)
var netACL []string
diag := model.Network.Acl.ElementsAs(ctx, &netACL, false)
resp.Diagnostics.Append(diags...)
if diag.HasError() {
return
}
replVal := model.Replicas.ValueInt64() // nolint:gosec // check is performed above
payload := modelToCreateInstancePayload(netAcl, model, replVal)
payload := modelToCreateInstancePayload(netACL, model, replVal)
// Create new instance
createResp, err := r.client.DefaultAPI.CreateInstanceRequest(
@ -229,18 +198,18 @@ func (r *instanceResource) Create(
return
}
// Set data returned by API in identity
identity := InstanceResourceIdentityModel{
ProjectID: types.StringValue(projectID),
Region: types.StringValue(region),
InstanceID: types.StringPointerValue(instanceID),
}
resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...)
if resp.Diagnostics.HasError() {
return
}
// Set data returned by API in id
resp.Diagnostics.Append(
resp.State.SetAttribute(
ctx,
path.Root("id"),
utils.BuildInternalTerraformId(projectID, region, *instanceID),
)...,
)
waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client.DefaultAPI, projectID, region, *instanceID).
SetTimeout(30 * time.Minute).
SetSleepBeforeWait(10 * time.Second).
WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(
@ -314,7 +283,7 @@ func (r *instanceResource) Read(
) { // nolint:gocritic // function signature required by Terraform
functionErrorSummary := "read instance failed"
var model resourceModel
var model postgresflexalpha.InstanceModel
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
@ -323,9 +292,9 @@ func (r *instanceResource) Read(
ctx = core.InitProviderContext(ctx)
var projectId string
var projectID string
if !model.ProjectId.IsNull() && !model.ProjectId.IsUnknown() {
projectId = model.ProjectId.ValueString()
projectID = model.ProjectId.ValueString()
}
var region string
@ -333,16 +302,16 @@ func (r *instanceResource) Read(
region = r.providerData.GetRegionWithOverride(model.Region)
}
var instanceId string
var instanceID string
if !model.InstanceId.IsNull() && !model.InstanceId.IsUnknown() {
instanceId = model.InstanceId.ValueString()
instanceID = model.InstanceId.ValueString()
}
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "instance_id", instanceId)
ctx = tflog.SetField(ctx, "project_id", projectID)
ctx = tflog.SetField(ctx, "instance_id", instanceID)
ctx = tflog.SetField(ctx, "region", region)
instanceResp, err := r.client.DefaultAPI.GetInstanceRequest(ctx, projectId, region, instanceId).Execute()
instanceResp, err := r.client.DefaultAPI.GetInstanceRequest(ctx, projectID, region, instanceID).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 {
@ -361,7 +330,7 @@ func (r *instanceResource) Read(
return
}
if !model.InstanceId.IsUnknown() && !model.InstanceId.IsNull() {
if *respInstanceID != instanceId {
if *respInstanceID != instanceID {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
@ -372,6 +341,10 @@ func (r *instanceResource) Read(
}
}
if model.Id.IsUnknown() || model.Id.IsNull() {
model.Id = utils.BuildInternalTerraformId(projectID, region, instanceID)
}
err = mapGetInstanceResponseToModel(ctx, &model, instanceResp)
if err != nil {
core.LogAndAddError(
@ -389,17 +362,6 @@ func (r *instanceResource) Read(
return
}
// Set data returned by API in identity
identity := InstanceResourceIdentityModel{
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, "Postgres Flex instance read")
}
@ -409,7 +371,7 @@ func (r *instanceResource) Update(
req resource.UpdateRequest,
resp *resource.UpdateResponse,
) { // nolint:gocritic // function signature required by Terraform
var model resourceModel
var model postgresflexalpha.InstanceModel
diags := req.Plan.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
@ -419,15 +381,8 @@ func (r *instanceResource) Update(
ctx = core.InitProviderContext(ctx)
// Read identity data
var identityData InstanceResourceIdentityModel
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
if resp.Diagnostics.HasError() {
return
}
projectID := identityData.ProjectID.ValueString()
instanceID := identityData.InstanceID.ValueString()
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)
@ -490,7 +445,10 @@ func (r *instanceResource) Update(
projectID,
region,
instanceID,
).WaitWithContext(ctx)
).
SetTimeout(30 * time.Minute).
SetSleepBeforeWait(10 * time.Second).
WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(
ctx,
@ -526,7 +484,7 @@ func (r *instanceResource) Delete(
req resource.DeleteRequest,
resp *resource.DeleteResponse,
) { // nolint:gocritic // function signature required by Terraform
var model resourceModel
var model postgresflexalpha.InstanceModel
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
@ -536,15 +494,15 @@ func (r *instanceResource) Delete(
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
instanceId := model.InstanceId.ValueString()
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, "project_id", projectID)
ctx = tflog.SetField(ctx, "instance_id", instanceID)
ctx = tflog.SetField(ctx, "region", region)
// Delete existing instance
err := r.client.DefaultAPI.DeleteInstanceRequest(ctx, projectId, region, instanceId).Execute()
err := r.client.DefaultAPI.DeleteInstanceRequest(ctx, projectID, region, instanceID).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err))
return
@ -552,7 +510,7 @@ func (r *instanceResource) Delete(
ctx = core.LogResponse(ctx)
_, err = r.client.DefaultAPI.GetInstanceRequest(ctx, projectId, region, instanceId).Execute()
_, err = r.client.DefaultAPI.GetInstanceRequest(ctx, projectID, region, instanceID).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 {
@ -574,41 +532,30 @@ func (r *instanceResource) ImportState(
) {
ctx = core.InitProviderContext(ctx)
if req.ID != "" {
idParts := strings.Split(req.ID, core.Separator)
idParts := strings.Split(req.ID, core.Separator)
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
core.LogAndAddError(
ctx, &resp.Diagnostics,
"Error importing instance",
fmt.Sprintf(
"Expected import identifier with format [project_id],[region],[instance_id] Got: %q",
req.ID,
),
)
return
}
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[2])...)
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
core.LogAndAddError(
ctx, &resp.Diagnostics,
"Error importing instance",
fmt.Sprintf(
"Expected import identifier with format [project_id],[region],[instance_id] Got: %q",
req.ID,
),
)
return
}
// If no ID is provided, attempt to read identity attributes from the import configuration
var identityData InstanceResourceIdentityModel
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
if resp.Diagnostics.HasError() {
return
}
projectId := identityData.ProjectID.ValueString()
region := identityData.Region.ValueString()
instanceId := identityData.InstanceID.ValueString()
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), region)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), instanceId)...)
resp.Diagnostics.Append(
resp.State.SetAttribute(
ctx,
path.Root("id"),
utils.BuildInternalTerraformId(idParts...),
)...,
)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[2])...)
tflog.Info(ctx, "Postgres Flex instance state imported")
}

View file

@ -23,16 +23,21 @@ resource "stackitprivatepreview_postgresflexalpha_instance" "{{ .TfName }}" {
}
{{ end }}
network = {
acl = ["{{ .ACLString }}"]
acl = [{{ range $i, $v := .ACLStrings }}{{if $i}},{{end}}"{{$v}}"{{end}}]
access_scope = "{{ .AccessScope }}"
}
version = {{ .Version }}
{{ if .Version }}
version = "{{ .Version }}"
{{ end }}
}
{{ if .Users }}
{{ $tfName := .TfName }}
{{ range $user := .Users }}
resource "stackitprivatepreview_postgresflexalpha_user" "{{ $user.Name }}" {
depends_on = [
stackitprivatepreview_postgresflexalpha_instance.{{ $tfName }}
]
project_id = "{{ $user.ProjectID }}"
instance_id = stackitprivatepreview_postgresflexalpha_instance.{{ $tfName }}.instance_id
name = "{{ $user.Name }}"
@ -45,6 +50,10 @@ resource "stackitprivatepreview_postgresflexalpha_user" "{{ $user.Name }}" {
{{ $tfName := .TfName }}
{{ range $db := .Databases }}
resource "stackitprivatepreview_postgresflexalpha_database" "{{ $db.Name }}" {
depends_on = [
stackitprivatepreview_postgresflexalpha_instance.{{ $tfName }},
stackitprivatepreview_postgresflexalpha_user.{{ $db.Owner }}
]
project_id = "{{ $db.ProjectID }}"
instance_id = stackitprivatepreview_postgresflexalpha_instance.{{ $tfName }}.instance_id
name = "{{ $db.Name }}"
@ -52,3 +61,32 @@ resource "stackitprivatepreview_postgresflexalpha_database" "{{ $db.Name }}" {
}
{{ end }}
{{ end }}
{{ if .DataSourceTest }}
data "stackitprivatepreview_postgresflexalpha_instance" "{{ .TfName }}" {
project_id = stackitprivatepreview_postgresflexalpha_instance.{{ .TfName }}.project_id
instance_id = stackitprivatepreview_postgresflexalpha_instance.{{ .TfName }}.instance_id
}
{{ if .Users }}
{{ $tfName := .TfName }}
{{ range $user := .Users }}
data "stackitprivatepreview_postgresflexalpha_user" "{{ $user.Name }}" {
project_id = stackitprivatepreview_postgresflexalpha_instance.{{ $tfName }}.project_id
instance_id = stackitprivatepreview_postgresflexalpha_instance.{{ $tfName }}.instance_id
user_id = stackitprivatepreview_postgresflexalpha_user.{{ $user.Name }}.user_id
}
{{ end }}
{{ end }}
{{ if .Databases }}
{{ $tfName := .TfName }}
{{ range $db := .Databases }}
data "stackitprivatepreview_postgresflexalpha_database" "{{ $db.Name }}" {
project_id = stackitprivatepreview_postgresflexalpha_instance.{{ $tfName }}.project_id
instance_id = stackitprivatepreview_postgresflexalpha_instance.{{ $tfName }}.instance_id
database_id = stackitprivatepreview_postgresflexalpha_database.{{ $db.Name }}.database_id
}
{{ end }}
{{ end }}
{{ end }}

View file

@ -35,7 +35,7 @@ func UserDataSourceSchema(ctx context.Context) schema.Schema {
MarkdownDescription: "The STACKIT project ID.",
},
"region": schema.StringAttribute{
Required: true,
Optional: true,
Description: "The region which should be addressed",
MarkdownDescription: "The region which should be addressed",
Validators: []validator.String{

View file

@ -116,7 +116,12 @@ func mapResourceFields(userResp *v3alpha1api.GetUserResponse, model *resourceMod
return fmt.Errorf("user id not present")
}
model.Id = types.Int64Value(userID)
model.Id = utils.BuildInternalTerraformId(
model.ProjectId.ValueString(),
model.Region.ValueString(),
model.InstanceId.ValueString(),
strconv.FormatInt(userID, 10),
)
model.UserId = types.Int64Value(userID)
model.Name = types.StringValue(user.Name)

View file

@ -1,6 +1,7 @@
package postgresflexalpha
import (
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
@ -165,7 +166,7 @@ func TestMapFieldsCreate(t *testing.T) {
},
testRegion,
resourceModel{
Id: types.Int64Value(1),
Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%d", "pid", testRegion, "iid", 1)),
UserId: types.Int64Value(1),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
@ -187,7 +188,7 @@ func TestMapFieldsCreate(t *testing.T) {
},
testRegion,
resourceModel{
Id: types.Int64Value(1),
Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%d", "pid", testRegion, "iid", 1)),
UserId: types.Int64Value(1),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
@ -209,7 +210,7 @@ func TestMapFieldsCreate(t *testing.T) {
},
testRegion,
resourceModel{
Id: types.Int64Value(1),
Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%d", "pid", testRegion, "iid", 1)),
UserId: types.Int64Value(1),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
@ -249,6 +250,7 @@ func TestMapFieldsCreate(t *testing.T) {
tt.description, func(t *testing.T) {
state := &resourceModel{
ProjectId: tt.expected.ProjectId,
Region: types.StringValue(testRegion),
InstanceId: tt.expected.InstanceId,
}
@ -286,7 +288,7 @@ func TestMapFields(t *testing.T) {
},
testRegion,
resourceModel{
Id: types.Int64Value(1),
Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%d", "pid", testRegion, "iid", 1)),
UserId: types.Int64Value(int64(1)),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
@ -311,7 +313,7 @@ func TestMapFields(t *testing.T) {
},
testRegion,
resourceModel{
Id: types.Int64Value(1),
Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%d", "pid", testRegion, "iid", 1)),
UserId: types.Int64Value(1),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
@ -339,7 +341,7 @@ func TestMapFields(t *testing.T) {
},
testRegion,
resourceModel{
Id: types.Int64Value(1),
Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%d", "pid", testRegion, "iid", 1)),
UserId: types.Int64Value(1),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
@ -379,6 +381,7 @@ func TestMapFields(t *testing.T) {
state := &resourceModel{
ProjectId: tt.expected.ProjectId,
InstanceId: tt.expected.InstanceId,
Region: types.StringValue(tt.region),
}
err := mapResourceFields(tt.input, state, tt.region)
if !tt.isValid && err == nil {
@ -388,7 +391,7 @@ func TestMapFields(t *testing.T) {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(state, &tt.expected)
diff := cmp.Diff(&tt.expected, state)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
@ -476,7 +479,7 @@ func TestToCreatePayload(t *testing.T) {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(output, tt.expected)
diff := cmp.Diff(tt.expected, output)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}

View file

@ -11,7 +11,7 @@ import (
"time"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource/identityschema"
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
"github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3alpha1api"
postgresflexalpha "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/user/resources_gen"
@ -34,12 +34,7 @@ var (
_ resource.ResourceWithConfigure = &userResource{}
_ resource.ResourceWithImportState = &userResource{}
_ resource.ResourceWithModifyPlan = &userResource{}
_ resource.ResourceWithIdentity = &userResource{}
_ resource.ResourceWithValidateConfig = &userResource{}
// Error message constants
extractErrorSummary = "extracting failed"
extractErrorMessage = "Extracting identity data: %v"
)
// NewUserResource is a helper function to simplify the provider implementation.
@ -50,14 +45,6 @@ func NewUserResource() resource.Resource {
// resourceModel represents the Terraform resource state for a PostgreSQL Flex user.
type resourceModel = postgresflexalpha.UserModel
// UserResourceIdentityModel describes the resource's identity attributes.
type UserResourceIdentityModel struct {
ProjectID types.String `tfsdk:"project_id"`
Region types.String `tfsdk:"region"`
InstanceID types.String `tfsdk:"instance_id"`
UserID types.Int64 `tfsdk:"user_id"`
}
// userResource implements the resource handling for a PostgreSQL Flex user.
type userResource struct {
client *v3alpha1api.APIClient
@ -232,23 +219,14 @@ func (r *userResource) Create(
}
arg.userID = int64(*id)
model.Id = utils.BuildInternalTerraformId(arg.projectID, arg.region, arg.instanceID, strconv.FormatInt(arg.userID, 10))
ctx = tflog.SetField(ctx, "id", model.Id.ValueString())
ctx = tflog.SetField(ctx, "user_id", id)
ctx = core.LogResponse(ctx)
// Set data returned by API in identity
identity := UserResourceIdentityModel{
ProjectID: types.StringValue(arg.projectID),
Region: types.StringValue(arg.region),
InstanceID: types.StringValue(arg.instanceID),
UserID: types.Int64Value(int64(*id)),
}
resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...)
if resp.Diagnostics.HasError() {
return
}
model.Id = types.Int64Value(int64(*id))
model.Id = utils.BuildInternalTerraformId(arg.projectID, arg.region, arg.instanceID, strconv.FormatInt(arg.userID, 10))
model.UserId = types.Int64Value(int64(*id))
model.Password = types.StringValue(userResp.GetPassword())
model.Status = types.StringValue(userResp.GetStatus())
@ -370,15 +348,14 @@ func (r *userResource) Read(
ctx = core.LogResponse(ctx)
// Set data returned by API in identity
identity := UserResourceIdentityModel{
ProjectID: types.StringValue(arg.projectID),
Region: types.StringValue(arg.region),
InstanceID: types.StringValue(arg.instanceID),
UserID: types.Int64Value(arg.userID),
}
resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...)
if resp.Diagnostics.HasError() {
err = mapResourceFields(waitResp, &model, model.Region.ValueString())
if err != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
"read user",
fmt.Sprintf("Wait response mapping: %v", err),
)
return
}
@ -457,18 +434,6 @@ func (r *userResource) Update(
ctx = core.LogResponse(ctx)
// Set data returned by API in identity
identity := UserResourceIdentityModel{
ProjectID: types.StringValue(arg.projectID),
Region: types.StringValue(arg.region),
InstanceID: types.StringValue(arg.instanceID),
UserID: types.Int64Value(userID64),
}
resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...)
if resp.Diagnostics.HasError() {
return
}
// Verify update
waitResp, err := postgresflexalphaWait.GetUserByIdWaitHandler(
ctx,
@ -525,26 +490,17 @@ func (r *userResource) Delete(
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)
arg, errExt := r.extractIdentityData(model, identityData)
if errExt != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
extractErrorSummary,
fmt.Sprintf(extractErrorMessage, errExt),
)
arg := clientArg{
projectID: model.ProjectId.ValueString(),
instanceID: model.InstanceId.ValueString(),
region: model.Region.ValueString(),
userID: model.UserId.ValueInt64(),
}
ctx = r.setTFLogFields(ctx, arg)
ctx = r.setTFLogFields(ctx, &arg)
ctx = core.InitProviderContext(ctx)
userID64 := arg.userID
@ -557,7 +513,14 @@ func (r *userResource) Delete(
// Delete existing record set
err := r.client.DefaultAPI.DeleteUserRequest(ctx, arg.projectID, arg.region, arg.instanceID, userID).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting user", fmt.Sprintf("Calling API: %v", err))
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 {
if oapiErr.StatusCode == 404 {
resp.State.RemoveResource(ctx)
return
}
}
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting user", fmt.Sprintf("error from API: %v", err))
}
ctx = core.LogResponse(ctx)
@ -581,30 +544,6 @@ func (r *userResource) Delete(
tflog.Info(ctx, "Postgres Flex user deleted")
}
// IdentitySchema defines the fields that are required to uniquely identify a resource.
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,
},
"region": identityschema.StringAttribute{
RequiredForImport: true,
},
"instance_id": identityschema.StringAttribute{
RequiredForImport: true,
},
"user_id": identityschema.Int64Attribute{
RequiredForImport: true,
},
},
}
}
// clientArg holds the arguments for API calls.
type clientArg struct {
projectID string
@ -622,112 +561,41 @@ func (r *userResource) ImportState(
) {
ctx = core.InitProviderContext(ctx)
if req.ID != "" {
idParts := strings.Split(req.ID, core.Separator)
if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" {
core.LogAndAddError(
ctx, &resp.Diagnostics,
"Error importing user",
fmt.Sprintf(
"Expected import identifier with format [project_id],[region],[instance_id],[user_id], got %q",
req.ID,
),
)
return
}
userID, err := strconv.ParseInt(idParts[3], 10, 64)
if err != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
"Error importing user",
fmt.Sprintf("Invalid user_id format: %q. It must be a valid integer.", idParts[3]),
)
return
}
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...)
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, "Postgres Flex user state imported")
idParts := strings.Split(req.ID, core.Separator)
if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" {
core.LogAndAddError(
ctx, &resp.Diagnostics,
"Error importing user",
fmt.Sprintf(
"Expected import identifier with format [project_id],[region],[instance_id],[user_id], got %q",
req.ID,
),
)
return
}
// If no ID is provided, attempt to read identity attributes from the import configuration
var identityData UserResourceIdentityModel
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
if resp.Diagnostics.HasError() {
userID, err := strconv.ParseInt(idParts[3], 10, 64)
if err != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
"Error importing user",
fmt.Sprintf("Invalid user_id format: %q. It must be a valid integer.", idParts[3]),
)
return
}
projectID := identityData.ProjectID.ValueString()
region := identityData.Region.ValueString()
instanceID := identityData.InstanceID.ValueString()
userID := identityData.UserID.ValueInt64()
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectID)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), region)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), instanceID)...)
idString := utils.BuildInternalTerraformId(idParts...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), idString)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...)
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, "Postgres Flex user state imported")
}
// extractIdentityData extracts essential identifiers from the resource model, falling back to the identity model.
func (r *userResource) extractIdentityData(
model resourceModel,
identity UserResourceIdentityModel,
) (*clientArg, error) {
var projectID, region, instanceID string
var userID int64
if !model.UserId.IsNull() && !model.UserId.IsUnknown() {
userID = model.UserId.ValueInt64()
} else {
if identity.UserID.IsNull() || identity.UserID.IsUnknown() {
return nil, fmt.Errorf("user_id not found in config")
}
userID = identity.UserID.ValueInt64()
}
if !model.ProjectId.IsNull() && !model.ProjectId.IsUnknown() {
projectID = model.ProjectId.ValueString()
} else {
if identity.ProjectID.IsNull() || identity.ProjectID.IsUnknown() {
return nil, 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 nil, 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 nil, fmt.Errorf("instance_id not found in config")
}
instanceID = identity.InstanceID.ValueString()
}
return &clientArg{
projectID: projectID,
instanceID: instanceID,
region: region,
userID: userID,
}, nil
}
// setTFLogFields adds relevant fields to the context for terraform logging purposes.
func (r *userResource) setTFLogFields(ctx context.Context, arg *clientArg) context.Context {
ctx = tflog.SetField(ctx, "project_id", arg.projectID)

View file

@ -14,7 +14,7 @@ import (
func UserResourceSchema(ctx context.Context) schema.Schema {
return schema.Schema{
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
"id": schema.StringAttribute{
Computed: true,
Description: "The ID of the user.",
MarkdownDescription: "The ID of the user.",
@ -75,7 +75,7 @@ func UserResourceSchema(ctx context.Context) schema.Schema {
}
type UserModel struct {
Id types.Int64 `tfsdk:"id"`
Id types.String `tfsdk:"id"`
InstanceId types.String `tfsdk:"instance_id"`
Name types.String `tfsdk:"name"`
Password types.String `tfsdk:"password"`

View file

@ -198,10 +198,10 @@ func TestAccInstanceNoEncryption(t *testing.T) {
Roles: []string{
"##STACKIT_DatabaseManager##",
"##STACKIT_LoginManager##",
//"##STACKIT_ProcessManager##",
//"##STACKIT_SQLAgentManager##",
//"##STACKIT_SQLAgentUser##",
//"##STACKIT_ServerManager##",
// "##STACKIT_ProcessManager##",
// "##STACKIT_SQLAgentManager##",
// "##STACKIT_SQLAgentUser##",
// "##STACKIT_ServerManager##",
},
},
}

View file

@ -11,6 +11,8 @@ import (
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils"
"github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v3beta1api"
sqlserverflexbetaDataGen "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexbeta/instance/datasources_gen"
@ -27,7 +29,7 @@ func mapResponseToModel(
m.Edition = types.StringValue(string(resp.GetEdition()))
m.Encryption = handleEncryption(ctx, m, resp)
m.FlavorId = types.StringValue(resp.GetFlavorId())
m.Id = types.StringValue(resp.GetId())
m.Id = utils.BuildInternalTerraformId(m.ProjectId.ValueString(), m.Region.ValueString(), resp.GetId())
m.InstanceId = types.StringValue(resp.GetId())
m.IsDeletable = types.BoolValue(resp.GetIsDeletable())
m.Name = types.StringValue(resp.GetName())

View file

@ -10,7 +10,6 @@ import (
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"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"
@ -32,7 +31,6 @@ var (
_ resource.ResourceWithConfigure = &instanceResource{}
_ resource.ResourceWithImportState = &instanceResource{}
_ resource.ResourceWithModifyPlan = &instanceResource{}
_ resource.ResourceWithIdentity = &instanceResource{}
)
func NewInstanceResource() resource.Resource {
@ -47,12 +45,6 @@ type instanceResource struct {
// resourceModel describes the resource data model.
type resourceModel = sqlserverflexbetaResGen.InstanceModel
type InstanceResourceIdentityModel struct {
ProjectID types.String `tfsdk:"project_id"`
Region types.String `tfsdk:"region"`
InstanceID types.String `tfsdk:"instance_id"`
}
func (r *instanceResource) Metadata(
_ context.Context,
req resource.MetadataRequest,
@ -81,26 +73,6 @@ func (r *instanceResource) Schema(ctx context.Context, _ resource.SchemaRequest,
resp.Schema = s
}
func (r *instanceResource) 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 *instanceResource) Configure(
ctx context.Context,
@ -190,9 +162,9 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques
ctx = core.InitProviderContext(ctx)
projectId := data.ProjectId.ValueString()
projectID := data.ProjectId.ValueString()
region := data.Region.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "project_id", projectID)
ctx = tflog.SetField(ctx, "region", region)
// Generate API request body from model
@ -210,7 +182,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques
// Create new Instance
createResp, err := r.client.DefaultAPI.CreateInstanceRequest(
ctx,
projectId,
projectID,
region,
).CreateInstanceRequestPayload(*payload).Execute()
if err != nil {
@ -220,24 +192,25 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques
ctx = core.LogResponse(ctx)
instanceId := createResp.Id
data.InstanceId = types.StringValue(instanceId)
instanceID := createResp.Id
data.InstanceId = types.StringValue(instanceID)
data.Id = utils.BuildInternalTerraformId(projectID, region, instanceID)
identity := InstanceResourceIdentityModel{
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
}
// Set data returned by API in id
resp.Diagnostics.Append(
resp.State.SetAttribute(
ctx,
path.Root("id"),
utils.BuildInternalTerraformId(projectID, region, instanceID),
)...,
)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), instanceID)...)
waitResp, err := wait.CreateInstanceWaitHandler(
ctx,
r.client.DefaultAPI,
projectId,
instanceId,
projectID,
instanceID,
region,
).SetSleepBeforeWait(
10 * time.Second,
@ -293,15 +266,15 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r
ctx = core.InitProviderContext(ctx)
projectId := data.ProjectId.ValueString()
projectID := data.ProjectId.ValueString()
region := data.Region.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "project_id", projectID)
ctx = tflog.SetField(ctx, "region", region)
instanceId := data.InstanceId.ValueString()
ctx = tflog.SetField(ctx, "instance_id", instanceId)
instanceID := data.InstanceId.ValueString()
ctx = tflog.SetField(ctx, "instance_id", instanceID)
instanceResp, err := r.client.DefaultAPI.GetInstanceRequest(ctx, projectId, region, instanceId).Execute()
instanceResp, err := r.client.DefaultAPI.GetInstanceRequest(ctx, projectID, region, instanceID).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 {
@ -326,17 +299,6 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r
return
}
// Save identity into Terraform state
identity := InstanceResourceIdentityModel{
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
}
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
if resp.Diagnostics.HasError() {
@ -357,13 +319,13 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques
ctx = core.InitProviderContext(ctx)
projectId := data.ProjectId.ValueString()
projectID := data.ProjectId.ValueString()
region := data.Region.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "project_id", projectID)
ctx = tflog.SetField(ctx, "region", region)
instanceId := data.InstanceId.ValueString()
ctx = tflog.SetField(ctx, "instance_id", instanceId)
instanceID := data.InstanceId.ValueString()
ctx = tflog.SetField(ctx, "instance_id", instanceID)
// Generate API request body from model
payload, err := toUpdatePayload(ctx, &data, resp)
@ -379,9 +341,9 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques
// Update existing instance
err = r.client.DefaultAPI.UpdateInstanceRequest(
ctx,
projectId,
projectID,
region,
instanceId,
instanceID,
).UpdateInstanceRequestPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, updateInstanceError, err.Error())
@ -391,7 +353,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques
ctx = core.LogResponse(ctx)
waitResp, err := wait.
UpdateInstanceWaitHandler(ctx, r.client.DefaultAPI, projectId, instanceId, region).
UpdateInstanceWaitHandler(ctx, r.client.DefaultAPI, projectID, instanceID, region).
SetSleepBeforeWait(15 * time.Second).
SetTimeout(45 * time.Minute).
WaitWithContext(ctx)
@ -417,16 +379,6 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques
return
}
identity := InstanceResourceIdentityModel{
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
}
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
if resp.Diagnostics.HasError() {
@ -445,25 +397,18 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques
return
}
// Read identity data
var identityData InstanceResourceIdentityModel
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)
projectID := data.ProjectId.ValueString()
region := data.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)
instanceID := data.InstanceId.ValueString()
ctx = tflog.SetField(ctx, "instance_id", instanceID)
// Delete existing instance
err := r.client.DefaultAPI.DeleteInstanceRequest(ctx, projectId, region, instanceId).Execute()
err := r.client.DefaultAPI.DeleteInstanceRequest(ctx, projectID, region, instanceID).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err))
return
@ -471,7 +416,7 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques
ctx = core.LogResponse(ctx)
delResp, err := wait.DeleteInstanceWaitHandler(ctx, r.client.DefaultAPI, projectId, instanceId, region).WaitWithContext(ctx)
delResp, err := wait.DeleteInstanceWaitHandler(ctx, r.client.DefaultAPI, projectID, instanceID, region).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(
ctx,
@ -506,41 +451,24 @@ func (r *instanceResource) ImportState(
) {
ctx = core.InitProviderContext(ctx)
if req.ID != "" {
idParts := strings.Split(req.ID, core.Separator)
idParts := strings.Split(req.ID, core.Separator)
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
core.LogAndAddError(
ctx, &resp.Diagnostics,
"Error importing instance",
fmt.Sprintf(
"Expected import identifier with format [project_id],[region],[instance_id] Got: %q",
req.ID,
),
)
return
}
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[2])...)
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
core.LogAndAddError(
ctx, &resp.Diagnostics,
"Error importing instance",
fmt.Sprintf(
"Expected import identifier with format [project_id],[region],[instance_id] Got: %q",
req.ID,
),
)
return
}
// If no ID is provided, attempt to read identity attributes from the import configuration
var identityData InstanceResourceIdentityModel
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
if resp.Diagnostics.HasError() {
return
}
projectId := identityData.ProjectID.ValueString()
region := identityData.Region.ValueString()
instanceId := identityData.InstanceID.ValueString()
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), region)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), instanceId)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), utils.BuildInternalTerraformId(idParts...).ValueString())...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[2])...)
tflog.Info(ctx, "Sqlserverflexbeta instance state imported")
}

View file

@ -3,13 +3,32 @@ package sqlserverflexbeta_test
import (
"context"
_ "embed"
"errors"
"fmt"
"log"
"net/http"
"os"
"strconv"
"strings"
"testing"
"time"
"github.com/hashicorp/terraform-plugin-testing/compare"
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
"github.com/hashicorp/terraform-plugin-testing/plancheck"
"github.com/hashicorp/terraform-plugin-testing/statecheck"
"github.com/hashicorp/terraform-plugin-testing/terraform"
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
"github.com/stackitcloud/stackit-sdk-go/core/config"
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/core"
wait "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/wait/sqlserverflexbeta"
"github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v3beta1api"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/internal/testutils"
sqlserverflexbeta "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexbeta/instance"
@ -20,7 +39,7 @@ import (
fwresource "github.com/hashicorp/terraform-plugin-framework/resource"
)
const providerPrefix = "stackitprivatepreview_sqlserverflexbeta"
const pfx = "stackitprivatepreview_sqlserverflexbeta"
func TestInstanceResourceSchema(t *testing.T) {
t.Parallel()
@ -59,20 +78,20 @@ func testAccPreCheck(t *testing.T) {
type resData struct {
ServiceAccountFilePath string
ProjectId string
ProjectID string
Region string
Name string
TfName string
FlavorId string
FlavorID string
BackupSchedule string
UseEncryption bool
KekKeyId string
KekKeyRingId string
KekKeyID string
KekKeyRingID string
KekKeyVersion uint8
KekServiceAccount string
PerformanceClass string
Size uint32
AclString string
ACLStrings []string
AccessScope string
RetentionDays uint32
Version string
@ -82,37 +101,33 @@ type resData struct {
type User struct {
Name string
ProjectId string
ProjectID string
Roles []string
}
type Database struct {
Name string
ProjectId string
ProjectID string
Owner string
Collation string
Compatibility string
}
func resName(res, name string) string {
return fmt.Sprintf("%s_%s.%s", providerPrefix, res, name)
}
func getExample() resData {
name := acctest.RandomWithPrefix("tf-acc")
return resData{
Region: os.Getenv("TF_ACC_REGION"),
ServiceAccountFilePath: os.Getenv("TF_ACC_SERVICE_ACCOUNT_FILE"),
ProjectId: os.Getenv("TF_ACC_PROJECT_ID"),
ProjectID: os.Getenv("TF_ACC_PROJECT_ID"),
Name: name,
TfName: name,
FlavorId: "4.16-Single",
FlavorID: "4.16-Single",
BackupSchedule: "0 0 * * *",
UseEncryption: false,
RetentionDays: 33,
PerformanceClass: "premium-perf2-stackit",
Size: 10,
AclString: "0.0.0.0/0",
ACLStrings: []string{"0.0.0.0/0"},
AccessScope: "PUBLIC",
Version: "2022",
}
@ -127,118 +142,160 @@ func TestAccInstance(t *testing.T) {
updSizeData := exData
updSizeData.Size = 25
testInstanceID := testutils.ResStr(pfx, "instance", exData.TfName)
compareValuesSame := statecheck.CompareValue(compare.ValuesSame())
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
t.Logf(" ... working on instance %s", exData.TfName)
t.Logf(" ... %s - %s", t.Name(), exData.TfName)
},
CheckDestroy: testAccCheckSQLServerFlexDestroy,
ProtoV6ProviderFactories: testutils.TestAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create and verify
{
PreConfig: func() {
t.Logf("testing: %s - %s", t.Name(), "create and verify")
},
ExpectNonEmptyPlan: true,
Config: testutils.StringFromTemplateMust(
"testdata/instance_template.gompl",
exData,
),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(resName("instance", exData.TfName), "name", exData.Name),
resource.TestCheckResourceAttrSet(resName("instance", exData.TfName), "id"),
// TODO: check all fields
),
ConfigStateChecks: []statecheck.StateCheck{
compareValuesSame.AddStateValue(
testInstanceID,
tfjsonpath.New("id"),
),
statecheck.ExpectKnownValue(
testInstanceID,
tfjsonpath.New("is_deletable"),
knownvalue.Bool(true),
),
},
Check: defaultNoEncInstanceTestChecks(testInstanceID, exData),
},
// Update name and verify
{
PreConfig: func() {
t.Logf("testing: %s - %s", t.Name(), "update name and verify")
},
ExpectNonEmptyPlan: true,
Config: testutils.StringFromTemplateMust(
"testdata/instance_template.gompl",
updNameData,
),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectNonEmptyPlan(),
},
},
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resName("instance", exData.TfName), "name", updNameData.Name),
defaultNoEncInstanceTestChecks(testInstanceID, updNameData),
),
},
// Update size and verify
{
PreConfig: func() {
t.Logf("testing: %s - %s", t.Name(), "update storage.size and verify")
},
ExpectNonEmptyPlan: true,
Config: testutils.StringFromTemplateMust(
"testdata/instance_template.gompl",
updSizeData,
),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(
testutils.ResStr(providerPrefix, "instance", exData.TfName),
"storage.size",
strconv.Itoa(int(updSizeData.Size)),
),
defaultNoEncInstanceTestChecks(testInstanceID, updSizeData),
),
},
// Import test
// test instance imports
{
RefreshState: true,
PreConfig: func() {
t.Logf("testing: %s - %s", t.Name(), "import instance")
},
ResourceName: testInstanceID,
// ImportStateIdPrefix: "",
// ImportStateVerifyIdentifierAttribute: "id",
ImportStateIdFunc: getInstanceTestID(exData.TfName),
ImportStateKind: resource.ImportCommandWithID,
ImportState: true,
ImportStateVerify: true,
},
//// Import test
//{
// ResourceName: resName("instance", exData.TfName),
// ImportState: true,
// ImportStateVerify: true,
// },
},
})
}
func TestAccInstanceReApply(t *testing.T) {
exData := getExample()
testInstanceID := testutils.ResStr(pfx, "instance", exData.TfName)
compareValuesSame := statecheck.CompareValue(compare.ValuesSame())
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
t.Logf(" ... working on instance %s", exData.TfName)
t.Logf(" ... %s - %s", t.Name(), exData.TfName)
},
CheckDestroy: testAccCheckSQLServerFlexDestroy,
ProtoV6ProviderFactories: testutils.TestAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create and verify
{
PreConfig: func() {
t.Logf("testing: %s - %s", t.Name(), "create and verify")
},
Config: testutils.StringFromTemplateMust(
"testdata/instance_template.gompl",
exData,
),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(resName("instance", exData.TfName), "name", exData.Name),
resource.TestCheckResourceAttrSet(resName("instance", exData.TfName), "id"),
// TODO: check all fields
),
ConfigStateChecks: []statecheck.StateCheck{
compareValuesSame.AddStateValue(
testInstanceID,
tfjsonpath.New("id"),
),
statecheck.ExpectKnownValue(
testInstanceID,
tfjsonpath.New("is_deletable"),
knownvalue.Bool(true),
),
},
Check: defaultNoEncInstanceTestChecks(testInstanceID, exData),
},
// Create and verify
// Second apply should not have changes
{
PreConfig: func() {
t.Logf("testing: %s - %s", t.Name(), "second apply")
},
ExpectNonEmptyPlan: false,
ResourceName: testInstanceID,
Config: testutils.StringFromTemplateMust(
"testdata/instance_template.gompl",
exData,
),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(resName("instance", exData.TfName), "name", exData.Name),
resource.TestCheckResourceAttrSet(resName("instance", exData.TfName), "id"),
// TODO: check all fields
),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectEmptyPlan(),
},
},
ConfigStateChecks: []statecheck.StateCheck{
compareValuesSame.AddStateValue(
testInstanceID,
tfjsonpath.New("id"),
),
statecheck.ExpectKnownValue(
testInstanceID,
tfjsonpath.New("is_deletable"),
knownvalue.Bool(true),
),
},
},
// Refresh state test
{
PreConfig: func() {
t.Logf("testing: %s - %s", t.Name(), "refresh state")
},
RefreshState: true,
},
// Create and verify
{
Config: testutils.StringFromTemplateMust(
"testdata/instance_template.gompl",
exData,
),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(resName("instance", exData.TfName), "name", exData.Name),
resource.TestCheckResourceAttrSet(resName("instance", exData.TfName), "id"),
// TODO: check all fields
),
},
// Import test
{
ResourceName: resName("instance", exData.TfName),
ImportStateKind: resource.ImportBlockWithResourceIdentity,
ImportState: true,
// ImportStateVerify is not supported with plannable import blocks
// ImportStateVerify: true,
},
},
})
}
@ -251,7 +308,7 @@ func TestAccInstanceNoEncryption(t *testing.T) {
data.Users = []User{
{
Name: userName,
ProjectId: os.Getenv("TF_ACC_PROJECT_ID"),
ProjectID: os.Getenv("TF_ACC_PROJECT_ID"),
Roles: []string{
"##STACKIT_DatabaseManager##",
"##STACKIT_LoginManager##",
@ -265,16 +322,19 @@ func TestAccInstanceNoEncryption(t *testing.T) {
data.Databases = []Database{
{
Name: dbName,
ProjectId: os.Getenv("TF_ACC_PROJECT_ID"),
ProjectID: os.Getenv("TF_ACC_PROJECT_ID"),
Owner: userName,
},
}
testInstanceID := testutils.ResStr(pfx, "instance", data.TfName)
testDatabaseID := testutils.ResStr(pfx, "database", dbName)
testUserID := testutils.ResStr(pfx, "user", userName)
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
t.Logf(" ... working on instance %s", data.TfName)
t.Logf(" ... %s - %s", t.Name(), data.TfName)
},
CheckDestroy: testAccCheckSQLServerFlexDestroy,
ProtoV6ProviderFactories: testutils.TestAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create and verify
@ -284,62 +344,22 @@ func TestAccInstanceNoEncryption(t *testing.T) {
data,
),
Check: resource.ComposeAggregateTestCheckFunc(
// check instance values are set
resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "id"),
resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "backup_schedule"),
resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "edition"),
resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "flavor_id"),
resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "instance_id"),
resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "is_deletable"),
resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "name"),
resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "replicas"),
resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "retention_days"),
resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "status"),
resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "version"),
resource.TestCheckNoResourceAttr(resName("instance", data.TfName), "encryption"),
// resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "encryption"),
// resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "encryption.kek_key_id"),
// resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "encryption.kek_key_version"),
// resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "encryption.kek_key_ring_id"),
// resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "encryption.service_account"),
// resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "network.access_scope"),
// resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "network.acl"),
// resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "network.instance_address"),
// resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "network.router_address"),
// resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "storage.class"),
// resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "storage.size"),
// check instance values are correct
resource.TestCheckResourceAttr(resName("instance", data.TfName), "name", data.Name),
// check user values are set
resource.TestCheckResourceAttrSet(resName("user", userName), "id"),
resource.TestCheckResourceAttrSet(resName("user", userName), "username"),
// resource.TestCheckResourceAttrSet(resName("user", userName), "roles"),
// func(s *terraform.State) error {
// return nil
// },
defaultNoEncInstanceTestChecks(testInstanceID, data),
// check user values are correct
resource.TestCheckResourceAttr(resName("user", userName), "username", userName),
resource.TestCheckResourceAttr(resName("user", userName), "roles.#", strconv.Itoa(len(data.Users[0].Roles))),
resource.TestCheckResourceAttr(testUserID, "username", userName),
resource.TestCheckResourceAttr(testUserID, "roles.#", strconv.Itoa(len(data.Users[0].Roles))),
// check database values are set
resource.TestCheckResourceAttrSet(resName("database", dbName), "id"),
resource.TestCheckResourceAttrSet(resName("database", dbName), "name"),
resource.TestCheckResourceAttrSet(resName("database", dbName), "owner"),
resource.TestCheckResourceAttrSet(resName("database", dbName), "compatibility"),
resource.TestCheckResourceAttrSet(resName("database", dbName), "collation"),
resource.TestCheckResourceAttrSet(testDatabaseID, "id"),
resource.TestCheckResourceAttrSet(testDatabaseID, "name"),
resource.TestCheckResourceAttrSet(testDatabaseID, "owner"),
resource.TestCheckResourceAttrSet(testDatabaseID, "compatibility"),
resource.TestCheckResourceAttrSet(testDatabaseID, "collation"),
// check database values are correct
resource.TestCheckResourceAttr(resName("database", dbName), "name", dbName),
resource.TestCheckResourceAttr(resName("database", dbName), "owner", userName),
resource.TestCheckResourceAttr(testDatabaseID, "name", dbName),
resource.TestCheckResourceAttr(testDatabaseID, "owner", userName),
),
},
},
@ -354,29 +374,34 @@ func TestAccInstanceEncryption(t *testing.T) {
data.Users = []User{
{
Name: userName,
ProjectId: os.Getenv("TF_ACC_PROJECT_ID"),
ProjectID: os.Getenv("TF_ACC_PROJECT_ID"),
Roles: []string{"##STACKIT_DatabaseManager##", "##STACKIT_LoginManager##"},
},
}
data.Databases = []Database{
{
Name: dbName,
ProjectId: os.Getenv("TF_ACC_PROJECT_ID"),
ProjectID: os.Getenv("TF_ACC_PROJECT_ID"),
Owner: userName,
},
}
data.UseEncryption = true
data.KekKeyId = "fe039bcf-8d7b-431a-801d-9e81371a6b7b"
data.KekKeyRingId = "6a2d95ab-3c4c-4963-a2bb-08d17a320e27"
data.KekKeyID = "fe039bcf-8d7b-431a-801d-9e81371a6b7b"
data.KekKeyRingID = "6a2d95ab-3c4c-4963-a2bb-08d17a320e27"
data.KekKeyVersion = 1
data.KekServiceAccount = "henselinm-u2v3ex1@sa.stackit.cloud"
testInstanceID := testutils.ResStr(pfx, "instance", data.TfName)
testDatabaseID := testutils.ResStr(pfx, "database", dbName)
testUserID := testutils.ResStr(pfx, "user", userName)
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
t.Logf(" ... working on instance %s", data.TfName)
t.Logf(" ... %s - %s", t.Name(), data.TfName)
},
CheckDestroy: testAccCheckSQLServerFlexDestroy,
ProtoV6ProviderFactories: testutils.TestAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create and verify
@ -386,61 +411,296 @@ func TestAccInstanceEncryption(t *testing.T) {
data,
),
Check: resource.ComposeAggregateTestCheckFunc(
// check instance values are set
resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "id"),
resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "backup_schedule"),
resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "edition"),
resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "flavor_id"),
resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "instance_id"),
resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "is_deletable"),
resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "name"),
resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "replicas"),
resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "retention_days"),
resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "status"),
resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "version"),
// resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "encryption"),
// resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "encryption.kek_key_id"),
// resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "encryption.kek_key_version"),
// resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "encryption.kek_key_ring_id"),
// resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "encryption.service_account"),
// resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "network.access_scope"),
// resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "network.acl"),
// resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "network.instance_address"),
// resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "network.router_address"),
// resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "storage.class"),
// resource.TestCheckResourceAttrSet(resName("instance", data.TfName), "storage.size"),
// check instance values are correct
resource.TestCheckResourceAttr(resName("instance", data.TfName), "name", data.Name),
defaultEncInstanceTestChecks(testInstanceID, data),
// check user values are set
resource.TestCheckResourceAttrSet(resName("user", userName), "id"),
resource.TestCheckResourceAttrSet(resName("user", userName), "username"),
resource.TestCheckResourceAttrSet(testUserID, "id"),
resource.TestCheckResourceAttrSet(testUserID, "username"),
// func(s *terraform.State) error {
// return nil
// },
// check user values are correct
resource.TestCheckResourceAttr(resName("user", userName), "username", userName),
resource.TestCheckResourceAttr(resName("user", userName), "roles.#", "2"),
resource.TestCheckResourceAttr(testUserID, "username", userName),
resource.TestCheckResourceAttr(testUserID, "roles.#", "2"),
// check database values are set
resource.TestCheckResourceAttrSet(resName("database", dbName), "id"),
resource.TestCheckResourceAttrSet(resName("database", dbName), "name"),
resource.TestCheckResourceAttrSet(resName("database", dbName), "owner"),
resource.TestCheckResourceAttrSet(resName("database", dbName), "compatibility"),
resource.TestCheckResourceAttrSet(resName("database", dbName), "collation"),
resource.TestCheckResourceAttrSet(testDatabaseID, "id"),
resource.TestCheckResourceAttrSet(testDatabaseID, "name"),
resource.TestCheckResourceAttrSet(testDatabaseID, "owner"),
resource.TestCheckResourceAttrSet(testDatabaseID, "compatibility"),
resource.TestCheckResourceAttrSet(testDatabaseID, "collation"),
// check database values are correct
resource.TestCheckResourceAttr(resName("database", dbName), "name", dbName),
resource.TestCheckResourceAttr(resName("database", dbName), "owner", userName),
resource.TestCheckResourceAttr(testDatabaseID, "name", dbName),
resource.TestCheckResourceAttr(testDatabaseID, "owner", userName),
),
},
},
})
}
func defaultNoEncInstanceTestChecks(testItemID string, data resData) resource.TestCheckFunc {
return resource.ComposeAggregateTestCheckFunc(
defaultInstanceTestChecks(testItemID, data),
// check absent attr
resource.TestCheckNoResourceAttr(testItemID, "encryption"),
resource.TestCheckNoResourceAttr(testItemID, "encryption.kek_key_id"),
resource.TestCheckNoResourceAttr(testItemID, "encryption.kek_key_ring_id"),
resource.TestCheckNoResourceAttr(testItemID, "encryption.kek_key_version"),
resource.TestCheckNoResourceAttr(testItemID, "encryption.service_account"),
)
}
func defaultEncInstanceTestChecks(testItemID string, data resData) resource.TestCheckFunc {
return resource.ComposeAggregateTestCheckFunc(
defaultInstanceTestChecks(testItemID, data),
// check absent attr
resource.TestCheckResourceAttr(testItemID, "encryption.%", "4"),
resource.TestCheckResourceAttrSet(testItemID, "encryption.kek_key_id"),
resource.TestCheckResourceAttr(testItemID, "encryption.kek_key_id", data.KekKeyID),
resource.TestCheckResourceAttrSet(testItemID, "encryption.kek_key_ring_id"),
resource.TestCheckResourceAttr(testItemID, "encryption.kek_key_ring_id", data.KekKeyRingID),
resource.TestCheckResourceAttrSet(testItemID, "encryption.kek_key_version"),
resource.TestCheckResourceAttr(testItemID, "encryption.kek_key_version", strconv.Itoa(int(data.KekKeyVersion))),
resource.TestCheckResourceAttrSet(testItemID, "encryption.service_account"),
resource.TestCheckResourceAttr(testItemID, "encryption.service_account", data.KekServiceAccount),
)
}
func defaultInstanceTestChecks(testItemID string, data resData) resource.TestCheckFunc {
// if AccessScope == SNA these are set
if data.AccessScope == "SNA" {
return resource.ComposeAggregateTestCheckFunc(
basicInstanceTestChecks(testItemID, data),
resource.TestCheckResourceAttrSet(testItemID, "network.instance_address"),
resource.TestCheckResourceAttrSet(testItemID, "network.router_address"),
)
}
// if AccessScope == PUBLIC these are empty - but they are set
return resource.ComposeAggregateTestCheckFunc(
basicInstanceTestChecks(testItemID, data),
resource.TestCheckResourceAttr(testItemID, "network.instance_address", ""),
resource.TestCheckResourceAttr(testItemID, "network.router_address", ""),
)
}
func basicInstanceTestChecks(testItemID string, data resData) resource.TestCheckFunc {
return resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrSet(testItemID, "backup_schedule"),
resource.TestCheckResourceAttr(testItemID, "backup_schedule", data.BackupSchedule),
resource.TestCheckResourceAttrSet(testItemID, "flavor_id"),
resource.TestCheckResourceAttr(testItemID, "flavor_id", data.FlavorID),
resource.TestCheckResourceAttrSet(testItemID, "id"),
resource.TestCheckResourceAttrSet(testItemID, "instance_id"),
resource.TestCheckResourceAttrSet(testItemID, "edition"),
resource.TestCheckResourceAttrSet(testItemID, "is_deletable"),
resource.TestCheckResourceAttr(testItemID, "is_deletable", "true"),
resource.TestCheckResourceAttrSet(testItemID, "name"),
resource.TestCheckResourceAttr(testItemID, "name", data.Name),
// network params check
resource.TestCheckResourceAttr(testItemID, "network.%", "4"),
resource.TestCheckResourceAttrSet(testItemID, "network.access_scope"),
resource.TestCheckResourceAttr(testItemID, "network.access_scope", data.AccessScope),
// resource.TestCheckResourceAttrSet(testItemID, "network.acl"),
resource.TestCheckResourceAttr(testItemID, "network.acl.#", strconv.Itoa(len(data.ACLStrings))),
// instance_address and router_address are only checked in enc
resource.TestCheckResourceAttrSet(testItemID, "project_id"),
resource.TestCheckResourceAttr(testItemID, "project_id", data.ProjectID),
resource.TestCheckResourceAttrSet(testItemID, "region"),
resource.TestCheckResourceAttr(testItemID, "region", data.Region),
resource.TestCheckResourceAttrSet(testItemID, "retention_days"),
resource.TestCheckResourceAttr(testItemID, "retention_days", strconv.Itoa(int(data.RetentionDays))),
resource.TestCheckResourceAttrSet(testItemID, "status"),
resource.TestCheckResourceAttr(testItemID, "status", "READY"),
// storage params check
resource.TestCheckResourceAttr(testItemID, "storage.%", "2"),
resource.TestCheckResourceAttrSet(testItemID, "storage.class"),
resource.TestCheckResourceAttr(testItemID, "storage.class", data.PerformanceClass),
resource.TestCheckResourceAttrSet(testItemID, "storage.size"),
resource.TestCheckResourceAttr(testItemID, "storage.size", strconv.Itoa(int(data.Size))),
resource.TestCheckResourceAttrSet(testItemID, "version"),
resource.TestCheckResourceAttr(testItemID, "version", data.Version),
)
}
func getInstanceTestID(name string) func(s *terraform.State) (string, error) {
return func(s *terraform.State) (string, error) {
r, ok := s.RootModule().Resources[testutils.ResStr(pfx, "instance", name)]
if !ok {
return "", fmt.Errorf("couldn't find resource stackitprivatepreview_postgresflexalpha_instance.%s", name)
}
projectID, ok := r.Primary.Attributes["project_id"]
if !ok {
return "", fmt.Errorf("couldn't find attribute project_id")
}
region, ok := r.Primary.Attributes["region"]
if !ok {
return "", fmt.Errorf("couldn't find attribute region")
}
instanceID, ok := r.Primary.Attributes["instance_id"]
if !ok {
return "", fmt.Errorf("couldn't find attribute instance_id")
}
return fmt.Sprintf("%s,%s,%s", projectID, region, instanceID), nil
}
}
/*
func getDatabaseTestID(name string) func(s *terraform.State) (string, error) {
return func(s *terraform.State) (string, error) {
r, ok := s.RootModule().Resources[testutils.ResStr(pfx, "database", name)]
if !ok {
return "", fmt.Errorf("couldn't find resource stackitprivatepreview_postgresflexalpha_instance.%s", name)
}
projectID, ok := r.Primary.Attributes["project_id"]
if !ok {
return "", fmt.Errorf("couldn't find attribute project_id")
}
region, ok := r.Primary.Attributes["region"]
if !ok {
return "", fmt.Errorf("couldn't find attribute region")
}
instanceID, ok := r.Primary.Attributes["instance_id"]
if !ok {
return "", fmt.Errorf("couldn't find attribute instance_id")
}
databaseID, ok := r.Primary.Attributes["database_id"]
if !ok {
return "", fmt.Errorf("couldn't find attribute database_id")
}
return fmt.Sprintf("%s,%s,%s,%s", projectID, region, instanceID, databaseID), nil
}
}
func getUserTestID(name string) func(s *terraform.State) (string, error) {
return func(s *terraform.State) (string, error) {
r, ok := s.RootModule().Resources[testutils.ResStr(pfx, "user", name)]
if !ok {
return "", fmt.Errorf("couldn't find resource stackitprivatepreview_postgresflexalpha_instance.%s", name)
}
projectID, ok := r.Primary.Attributes["project_id"]
if !ok {
return "", fmt.Errorf("couldn't find attribute project_id")
}
region, ok := r.Primary.Attributes["region"]
if !ok {
return "", fmt.Errorf("couldn't find attribute region")
}
instanceID, ok := r.Primary.Attributes["instance_id"]
if !ok {
return "", fmt.Errorf("couldn't find attribute instance_id")
}
userID, ok := r.Primary.Attributes["user_id"]
if !ok {
return "", fmt.Errorf("couldn't find attribute user_id")
}
return fmt.Sprintf("%s,%s,%s,%s", projectID, region, instanceID, userID), nil
}
}
*/
func testAccCheckSQLServerFlexDestroy(s *terraform.State) error {
testutils.Setup()
pID, ok := os.LookupEnv("TF_ACC_PROJECT_ID")
if !ok {
log.Fatalln("unable to read TF_ACC_PROJECT_ID")
}
ctx := context.Background()
var client *v3beta1api.APIClient
var err error
var region, projectID string
region = testutils.Region
if region == "" {
region = "eu01"
}
projectID = pID
if projectID == "" {
return fmt.Errorf("projectID could not be determined in destroy function")
}
apiClientConfigOptions := []config.ConfigurationOption{
config.WithServiceAccountKeyPath(os.Getenv("TF_ACC_SERVICE_ACCOUNT_FILE")),
config.WithRegion(region),
}
if testutils.PostgresFlexCustomEndpoint != "" {
apiClientConfigOptions = append(
apiClientConfigOptions,
config.WithEndpoint(testutils.PostgresFlexCustomEndpoint),
)
}
client, err = v3beta1api.NewAPIClient(apiClientConfigOptions...)
if err != nil {
log.Fatalln(err)
}
instancesToDestroy := []string{}
for _, rs := range s.RootModule().Resources {
if rs.Type != "stackitprivatepreview_postgresflexalpha_instance" &&
rs.Type != "stackitprivatepreview_postgresflexbeta_instance" {
continue
}
// instance terraform ID: = "[project_id],[region],[instance_id]"
instanceID := strings.Split(rs.Primary.ID, core.Separator)[2]
instancesToDestroy = append(instancesToDestroy, instanceID)
}
instancesResp, err := client.DefaultAPI.ListInstancesRequest(ctx, projectID, region).
Size(100).
Execute()
if err != nil {
return fmt.Errorf("getting instancesResp: %w", err)
}
items := instancesResp.GetInstances()
for i := range items {
if items[i].Id == "" {
continue
}
if utils.Contains(instancesToDestroy, items[i].Id) {
err := client.DefaultAPI.DeleteInstanceRequest(ctx, projectID, region, items[i].Id).Execute()
if err != nil {
return fmt.Errorf("deleting instance %s during CheckDestroy: %w", items[i].Id, err)
}
w := wait.DeleteInstanceWaitHandler(
ctx,
client.DefaultAPI,
testutils.ProjectId,
testutils.Region,
items[i].Id,
)
_, waitErr := w.SetTimeout(90 * time.Second).WaitWithContext(context.Background())
if waitErr != nil {
var oapiErr *oapierror.GenericOpenAPIError
isOapiErr := errors.As(waitErr, &oapiErr)
if !isOapiErr {
return fmt.Errorf("could not convert error to oapierror.GenericOpenAPIError")
}
if oapiErr.StatusCode != http.StatusNotFound {
return waitErr
}
}
}
}
return nil
}

View file

@ -4,35 +4,37 @@ provider "stackitprivatepreview" {
}
resource "stackitprivatepreview_sqlserverflexbeta_instance" "{{ .TfName }}" {
project_id = "{{ .ProjectId }}"
project_id = "{{ .ProjectID }}"
name = "{{ .Name }}"
backup_schedule = "{{ .BackupSchedule }}"
retention_days = {{ .RetentionDays }}
flavor_id = "{{ .FlavorId }}"
flavor_id = "{{ .FlavorID }}"
storage = {
class = "{{ .PerformanceClass }}"
size = {{ .Size }}
}
{{ if .UseEncryption }}
encryption = {
kek_key_id = "{{ .KekKeyId }}"
kek_key_ring_id = "{{ .KekKeyRingId }}"
kek_key_id = "{{ .KekKeyID }}"
kek_key_ring_id = "{{ .KekKeyRingID }}"
kek_key_version = {{ .KekKeyVersion }}
service_account = "{{ .KekServiceAccount }}"
}
{{ end }}
network = {
acl = ["{{ .AclString }}"]
acl = [{{ range $i, $v := .ACLStrings }}{{if $i}},{{end}}"{{$v}}"{{end}}]
access_scope = "{{ .AccessScope }}"
}
{{ if .Version }}
version = "{{ .Version }}"
{{ end }}
}
{{ if .Users }}
{{ $tfName := .TfName }}
{{ range $user := .Users }}
resource "stackitprivatepreview_sqlserverflexbeta_user" "{{ $user.Name }}" {
project_id = "{{ $user.ProjectId }}"
project_id = "{{ $user.ProjectID }}"
instance_id = stackitprivatepreview_sqlserverflexbeta_instance.{{ $tfName }}.instance_id
username = "{{ $user.Name }}"
roles = [{{ range $i, $v := $user.Roles }}{{if $i}},{{end}}"{{$v}}"{{end}}]
@ -45,7 +47,7 @@ resource "stackitprivatepreview_sqlserverflexbeta_user" "{{ $user.Name }}" {
{{ range $db := .Databases }}
resource "stackitprivatepreview_sqlserverflexbeta_database" "{{ $db.Name }}" {
depends_on = [stackitprivatepreview_sqlserverflexbeta_user.{{ $db.Owner }}]
project_id = "{{ $db.ProjectId }}"
project_id = "{{ $db.ProjectID }}"
instance_id = stackitprivatepreview_sqlserverflexbeta_instance.{{ $tfName }}.instance_id
name = "{{ $db.Name }}"
owner = "{{ $db.Owner }}"

View file

@ -2,9 +2,11 @@ package postgresflexalpha
import (
"context"
"crypto/rand"
"errors"
"fmt"
"math"
"math/big"
"net/http"
"time"
@ -29,45 +31,47 @@ const (
// APIClientInstanceInterface Interface needed for tests
type APIClientInstanceInterface interface {
GetInstanceRequest(ctx context.Context, projectId, region, instanceId string) v3alpha1api.ApiGetInstanceRequestRequest
GetInstanceRequest(ctx context.Context, projectID, region, instanceID string) v3alpha1api.ApiGetInstanceRequestRequest
ListUsersRequest(
ctx context.Context,
projectId string,
projectID string,
region string,
instanceId string,
instanceID string,
) v3alpha1api.ApiListUsersRequestRequest
}
// APIClientUserInterface Interface needed for tests
type APIClientUserInterface interface {
GetUserRequest(ctx context.Context, projectId, region, instanceId string, userId int32) v3alpha1api.ApiGetUserRequestRequest
GetUserRequest(ctx context.Context, projectID, region, instanceID string, userID int32) v3alpha1api.ApiGetUserRequestRequest
}
// APIClientDatabaseInterface Interface needed for tests
type APIClientDatabaseInterface interface {
GetDatabaseRequest(ctx context.Context, projectId string, region string, instanceId string, databaseId int32) v3alpha1api.ApiGetDatabaseRequestRequest
GetDatabaseRequest(ctx context.Context, projectID string, region string, instanceID string, databaseID int32) v3alpha1api.ApiGetDatabaseRequestRequest
}
// CreateInstanceWaitHandler will wait for instance creation
func CreateInstanceWaitHandler(
ctx context.Context, a APIClientInstanceInterface, projectId, region,
instanceId string,
ctx context.Context, a APIClientInstanceInterface, projectID, region,
instanceID string,
) *wait.AsyncActionHandler[v3alpha1api.GetInstanceResponse] {
instanceCreated := false
var instanceGetResponse *v3alpha1api.GetInstanceResponse
maxWait := time.Minute * 45
startTime := time.Now()
extendedTimeout := 0
maxFailedCount := 3
failedCount := 0
handler := wait.New(
func() (waitFinished bool, response *v3alpha1api.GetInstanceResponse, err error) {
if !instanceCreated {
s, err := a.GetInstanceRequest(ctx, projectId, region, instanceId).Execute()
if err != nil {
return false, nil, err
s, getErr := a.GetInstanceRequest(ctx, projectID, region, instanceID).Execute()
if getErr != nil {
return false, nil, getErr
}
if s == nil || s.Id != instanceId {
if s == nil || s.Id != instanceID {
return false, nil, nil
}
tflog.Debug(
@ -77,7 +81,7 @@ func CreateInstanceWaitHandler(
)
switch s.Status {
default:
return true, s, fmt.Errorf("instance with id %s has unexpected status %s", instanceId, s.Status)
return true, s, fmt.Errorf("instance with id %s has unexpected status %s", instanceID, s.Status)
case InstanceStateEmpty:
return false, nil, nil
case InstanceStatePending:
@ -94,30 +98,15 @@ func CreateInstanceWaitHandler(
"Wait handler still got status %s after %v for instance: %s",
InstanceStateProgressing,
maxWait,
instanceId,
instanceID,
),
)
if extendedTimeout < 3 {
maxWait += time.Minute * 5
extendedTimeout++
if *s.Network.AccessScope == "SNA" {
ready := true
if s.Network.InstanceAddress == nil {
tflog.Warn(ctx, "Waiting for instance_address")
ready = false
}
if s.Network.RouterAddress == nil {
tflog.Warn(ctx, "Waiting for router_address")
ready = false
}
if !ready {
return false, nil, nil
}
}
return false, nil, nil
}
instanceCreated = true
instanceGetResponse = s
return false, nil, fmt.Errorf("instance after max timeout still in state %s", InstanceStateProgressing)
case InstanceStateSuccess:
if s.Network.AccessScope != nil && *s.Network.AccessScope == "SNA" {
if s.Network.InstanceAddress == nil {
@ -132,8 +121,27 @@ func CreateInstanceWaitHandler(
instanceCreated = true
instanceGetResponse = s
case InstanceStateFailed:
tflog.Warn(ctx, fmt.Sprintf("Wait handler got status FAILURE for instance: %s", instanceId))
return false, nil, nil
if failedCount < maxFailedCount {
failedCount++
tflog.Warn(
ctx, "got failed status from API retry", map[string]interface{}{
"failedCount": failedCount,
},
)
var waitCounter int64 = 1
maxWaitInt := big.NewInt(7)
n, randErr := rand.Int(rand.Reader, maxWaitInt)
if randErr == nil {
waitCounter = n.Int64() + 1
}
time.Sleep(time.Duration(waitCounter*30) * time.Second) //nolint:gosec // not that important and temporary
return false, nil, nil
}
return true, s, fmt.Errorf(
"update got status FAILURE for instance with id %s after %d retries",
instanceID,
failedCount,
)
// API responds with FAILURE for some seconds and then the instance goes to READY
// return true, s, fmt.Errorf("create failed for instance with id %s", instanceId)
}
@ -142,7 +150,7 @@ func CreateInstanceWaitHandler(
tflog.Info(ctx, "Waiting for instance (calling list users")
// // User operations aren't available right after an instance is deemed successful
// // To check if they are, perform a users request
_, err = a.ListUsersRequest(ctx, projectId, region, instanceId).Execute()
_, err = a.ListUsersRequest(ctx, projectID, region, instanceID).Execute()
if err == nil {
return true, instanceGetResponse, nil
}
@ -150,7 +158,7 @@ func CreateInstanceWaitHandler(
if !ok {
return false, nil, err
}
// TODO: refactor and cooperate with api guys to mitigate
// TODO: refactor and cooperate with api guys to mitigate // nolint: // reason upfront
if oapiErr.StatusCode < 500 {
return true, instanceGetResponse, fmt.Errorf(
"users request after instance creation returned %d status code",
@ -160,8 +168,6 @@ func CreateInstanceWaitHandler(
return false, nil, nil
},
)
// Sleep before wait is set because sometimes API returns 404 right after creation request
handler.SetTimeout(90 * time.Minute).SetSleepBeforeWait(30 * time.Second)
return handler
}
@ -170,6 +176,8 @@ func PartialUpdateInstanceWaitHandler(
ctx context.Context, a APIClientInstanceInterface, projectID, region,
instanceID string,
) *wait.AsyncActionHandler[v3alpha1api.GetInstanceResponse] {
maxFailedCount := 3
failedCount := 0
handler := wait.New(
func() (waitFinished bool, response *v3alpha1api.GetInstanceResponse, err error) {
s, err := a.GetInstanceRequest(ctx, projectID, region, instanceID).Execute()
@ -195,11 +203,30 @@ func PartialUpdateInstanceWaitHandler(
case InstanceStateUnknown:
return false, nil, nil
case InstanceStateFailed:
return true, s, fmt.Errorf("update got status FAILURE for instance with id %s", instanceID)
if failedCount < maxFailedCount {
failedCount++
tflog.Warn(
ctx, "got failed status from API retry", map[string]interface{}{
"failedCount": failedCount,
},
)
var waitCounter int64 = 1
maxWait := big.NewInt(7)
n, err := rand.Int(rand.Reader, maxWait)
if err == nil {
waitCounter = n.Int64() + 1
}
time.Sleep(time.Duration(waitCounter*30) * time.Second) //nolint:gosec // not that important and temporary
return false, nil, nil
}
return true, s, fmt.Errorf(
"update got status FAILURE for instance with id %s after %d retries",
instanceID,
failedCount,
)
}
},
)
handler.SetTimeout(45 * time.Minute).SetSleepBeforeWait(30 * time.Second)
return handler
}
@ -295,6 +322,8 @@ func DeleteInstanceWaitHandler(
instanceID string,
timeout, sleepBeforeWait time.Duration,
) error {
maxFailedCount := 3
failedCount := 0
handler := wait.New(
func() (waitFinished bool, response *v3alpha1api.GetInstanceResponse, err error) {
s, err := a.GetInstanceRequest(ctx, projectID, region, instanceID).Execute()
@ -314,6 +343,22 @@ func DeleteInstanceWaitHandler(
case InstanceStateEmpty, InstanceStatePending, InstanceStateUnknown, InstanceStateProgressing, InstanceStateSuccess:
return false, nil, nil
case InstanceStateFailed:
if failedCount < maxFailedCount {
failedCount++
tflog.Warn(
ctx, "got failed status from API retry", map[string]interface{}{
"failedCount": failedCount,
},
)
var waitCounter int64 = 1
maxWait := big.NewInt(7)
n, err := rand.Int(rand.Reader, maxWait)
if err == nil {
waitCounter = n.Int64() + 1
}
time.Sleep(time.Duration(waitCounter*30) * time.Second) //nolint:gosec // not that important and temporary
return false, nil, nil
}
return true, nil, fmt.Errorf("wait handler got status FAILURE for instance: %s", instanceID)
default:
return true, s, fmt.Errorf("instance with id %s has unexpected status %s", instanceID, s.Status)

View file

@ -4,6 +4,7 @@ package postgresflexalpha
import (
"context"
"os"
"testing"
"time"
@ -22,6 +23,8 @@ func TestCreateInstanceWaitHandler(t *testing.T) {
usersGetErrorStatus int
wantErr bool
wantRes *v3alpha1api.GetInstanceResponse
timeout time.Duration
onlyOnLong bool
}{
{
desc: "create_succeeded",
@ -46,6 +49,7 @@ func TestCreateInstanceWaitHandler(t *testing.T) {
},
},
{
onlyOnLong: true,
desc: "create_failed",
instanceGetFails: false,
instanceState: InstanceStateFailed,
@ -56,7 +60,18 @@ func TestCreateInstanceWaitHandler(t *testing.T) {
RouterAddress: utils.Ptr("10.0.0.1"),
},
wantErr: true,
wantRes: nil,
wantRes: &v3alpha1api.GetInstanceResponse{
Id: "foo-bar",
Status: InstanceStateFailed,
Network: v3alpha1api.InstanceNetwork{
AccessScope: nil,
Acl: nil,
InstanceAddress: utils.Ptr("10.0.0.1"),
RouterAddress: utils.Ptr("10.0.0.1"),
},
},
// waiter uses random timeouts up to 8 times 30 secs
timeout: 300 * time.Second,
},
{
desc: "create_failed_2",
@ -142,6 +157,13 @@ func TestCreateInstanceWaitHandler(t *testing.T) {
},
}
for _, tt := range tests {
if tt.onlyOnLong {
_, ok := os.LookupEnv("TF_RUN_LONG_TESTS")
if !ok {
t.Logf("skipping test '%s' because TF_RUN_LONG_TESTS env var is missing", tt.desc)
continue
}
}
t.Run(
tt.desc, func(t *testing.T) {
instanceID := "foo-bar"
@ -181,9 +203,13 @@ func TestCreateInstanceWaitHandler(t *testing.T) {
ListUsersRequestExecuteMock: &listUsersMock,
}
handler := CreateInstanceWaitHandler(context.Background(), apiClientMock, "", "", instanceID)
handler := CreateInstanceWaitHandler(context.Background(), apiClientMock, "", "", instanceID).
SetTimeout(10 * time.Millisecond)
if tt.timeout != 0 {
handler.SetTimeout(tt.timeout)
}
gotRes, err := handler.SetTimeout(10 * time.Millisecond).SetSleepBeforeWait(1 * time.Millisecond).WaitWithContext(context.Background())
gotRes, err := handler.SetSleepBeforeWait(1 * time.Millisecond).WaitWithContext(context.Background())
if (err != nil) != tt.wantErr {
t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr)
}
@ -204,6 +230,8 @@ func TestUpdateInstanceWaitHandler(t *testing.T) {
instanceNetwork v3alpha1api.InstanceNetwork
wantErr bool
wantRes *v3alpha1api.GetInstanceResponse
timeout time.Duration
onlyOnLong bool
}{
{
desc: "update_succeeded",
@ -228,6 +256,7 @@ func TestUpdateInstanceWaitHandler(t *testing.T) {
},
},
{
onlyOnLong: true,
desc: "update_failed",
instanceGetFails: false,
instanceState: InstanceStateFailed,
@ -248,6 +277,7 @@ func TestUpdateInstanceWaitHandler(t *testing.T) {
RouterAddress: utils.Ptr("10.0.0.1"),
},
},
timeout: 300 * time.Second,
},
{
desc: "update_failed_2",
@ -283,6 +313,14 @@ func TestUpdateInstanceWaitHandler(t *testing.T) {
},
}
for _, tt := range tests {
if tt.onlyOnLong {
_, ok := os.LookupEnv("TF_RUN_LONG_TESTS")
if !ok {
t.Logf("skipping test '%s' because TF_RUN_LONG_TESTS env var is missing", tt.desc)
continue
}
}
t.Run(
tt.desc, func(t *testing.T) {
instanceID := "foo-bar"
@ -316,9 +354,13 @@ func TestUpdateInstanceWaitHandler(t *testing.T) {
ListUsersRequestExecuteMock: &listUsersMock,
}
handler := PartialUpdateInstanceWaitHandler(context.Background(), apiClientMock, "", "", instanceID)
handler := PartialUpdateInstanceWaitHandler(context.Background(), apiClientMock, "", "", instanceID).
SetTimeout(10 * time.Millisecond)
if tt.timeout > 0 {
handler.SetTimeout(tt.timeout)
}
gotRes, err := handler.SetTimeout(10 * time.Millisecond).WaitWithContext(context.Background())
gotRes, err := handler.WaitWithContext(context.Background())
if (err != nil) != tt.wantErr {
t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr)
}

View file

@ -30,35 +30,35 @@ const (
type APIClientInterface interface {
GetInstanceRequest(
ctx context.Context,
projectId, region, instanceId string,
projectID, region, instanceID string,
) sqlserverflex.ApiGetInstanceRequestRequest
GetDatabaseRequest(
ctx context.Context,
projectId string,
projectID string,
region string,
instanceId string,
instanceID string,
databaseName string,
) sqlserverflex.ApiGetDatabaseRequestRequest
GetUserRequest(
ctx context.Context,
projectId string,
projectID string,
region string,
instanceId string,
userId int64,
instanceID string,
userID int64,
) sqlserverflex.ApiGetUserRequestRequest
ListRolesRequest(
ctx context.Context,
projectId string,
projectID string,
region string,
instanceId string,
instanceID string,
) sqlserverflex.ApiListRolesRequestRequest
ListUsersRequest(
ctx context.Context,
projectId string,
projectID string,
region string,
instanceId string,
instanceID string,
) sqlserverflex.ApiListUsersRequestRequest
}
@ -66,10 +66,10 @@ type APIClientInterface interface {
type APIClientUserInterface interface {
DeleteUserRequestExecute(
ctx context.Context,
projectId string,
projectID string,
region string,
instanceId string,
userId int64,
instanceID string,
userID int64,
) error
}
@ -77,11 +77,11 @@ type APIClientUserInterface interface {
func CreateInstanceWaitHandler(
ctx context.Context,
a APIClientInterface,
projectId, instanceId, region string,
projectID, instanceID, region string,
) *wait.AsyncActionHandler[sqlserverflex.GetInstanceResponse] {
handler := wait.New(
func() (waitFinished bool, response *sqlserverflex.GetInstanceResponse, err error) {
s, err := a.GetInstanceRequest(ctx, projectId, region, instanceId).Execute()
s, err := a.GetInstanceRequest(ctx, projectID, region, instanceID).Execute()
if err != nil {
var oapiErr *oapierror.GenericOpenAPIError
ok := errors.As(err, &oapiErr)
@ -95,7 +95,7 @@ func CreateInstanceWaitHandler(
return false, nil, fmt.Errorf("api error: %w", err)
}
}
if s == nil || s.Id != instanceId {
if s == nil || s.Id != instanceID {
return false, nil, nil
}
switch strings.ToLower(string(s.Status)) {
@ -113,7 +113,7 @@ func CreateInstanceWaitHandler(
tflog.Info(ctx, "trying to get roles")
time.Sleep(10 * time.Second)
_, rolesErr := a.ListRolesRequest(ctx, projectId, region, instanceId).Execute()
_, rolesErr := a.ListRolesRequest(ctx, projectID, region, instanceID).Execute()
if rolesErr != nil {
var oapiErr *oapierror.GenericOpenAPIError
ok := errors.As(rolesErr, &oapiErr)
@ -137,7 +137,7 @@ func CreateInstanceWaitHandler(
tflog.Info(ctx, "trying to get users")
time.Sleep(10 * time.Second)
_, usersErr := a.ListUsersRequest(ctx, projectId, region, instanceId).Execute()
_, usersErr := a.ListUsersRequest(ctx, projectID, region, instanceID).Execute()
if usersErr != nil {
var oapiErr *oapierror.GenericOpenAPIError
ok := errors.As(usersErr, &oapiErr)
@ -162,13 +162,13 @@ func CreateInstanceWaitHandler(
case strings.ToLower(InstanceStateUnknown):
return true, nil, fmt.Errorf(
"create failed for instance %s with status %s",
instanceId,
instanceID,
InstanceStateUnknown,
)
case strings.ToLower(InstanceStateFailed):
return true, nil, fmt.Errorf(
"create failed for instance %s with status %s",
instanceId,
instanceID,
InstanceStateFailed,
)
case strings.ToLower(InstanceStatePending), strings.ToLower(InstanceStateProcessing):
@ -182,7 +182,7 @@ func CreateInstanceWaitHandler(
default:
tflog.Info(
ctx, "Wait (create) received unknown status", map[string]interface{}{
"instanceId": instanceId,
"instanceId": instanceID,
"status": s.Status,
},
)
@ -197,22 +197,22 @@ func CreateInstanceWaitHandler(
func UpdateInstanceWaitHandler(
ctx context.Context,
a APIClientInterface,
projectId, instanceId, region string,
projectID, instanceID, region string,
) *wait.AsyncActionHandler[sqlserverflex.GetInstanceResponse] {
handler := wait.New(
func() (waitFinished bool, response *sqlserverflex.GetInstanceResponse, err error) {
s, err := a.GetInstanceRequest(ctx, projectId, region, instanceId).Execute()
s, err := a.GetInstanceRequest(ctx, projectID, region, instanceID).Execute()
if err != nil {
return false, nil, err
}
if s == nil || s.Id != instanceId {
if s == nil || s.Id != instanceID {
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)
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{}{
@ -223,7 +223,7 @@ func UpdateInstanceWaitHandler(
default:
tflog.Info(
ctx, "Wait (update) received unknown status", map[string]interface{}{
"instanceId": instanceId,
"instanceId": instanceID,
"status": s.Status,
},
)
@ -238,11 +238,11 @@ func UpdateInstanceWaitHandler(
func DeleteInstanceWaitHandler(
ctx context.Context,
a APIClientInterface,
projectId, instanceId, region string,
projectID, instanceID, region string,
) *wait.AsyncActionHandler[sqlserverflex.GetInstanceResponse] {
handler := wait.New(
func() (waitFinished bool, response *sqlserverflex.GetInstanceResponse, err error) {
s, err := a.GetInstanceRequest(ctx, projectId, region, instanceId).Execute()
s, err := a.GetInstanceRequest(ctx, projectID, region, instanceID).Execute()
if err == nil {
return false, s, nil
}
@ -265,11 +265,11 @@ func DeleteInstanceWaitHandler(
func CreateDatabaseWaitHandler(
ctx context.Context,
a APIClientInterface,
projectId, instanceId, region, databaseName string,
projectID, instanceID, region, databaseName string,
) *wait.AsyncActionHandler[sqlserverflex.GetDatabaseResponse] {
handler := wait.New(
func() (waitFinished bool, response *sqlserverflex.GetDatabaseResponse, err error) {
s, err := a.GetDatabaseRequest(ctx, projectId, region, instanceId, databaseName).Execute()
s, err := a.GetDatabaseRequest(ctx, projectID, region, instanceID, databaseName).Execute()
if err != nil {
var oapiErr *oapierror.GenericOpenAPIError
ok := errors.As(err, &oapiErr)
@ -297,12 +297,12 @@ func CreateDatabaseWaitHandler(
func CreateUserWaitHandler(
ctx context.Context,
a APIClientInterface,
projectId, instanceId, region string,
userId int64,
projectID, instanceID, region string,
userID int64,
) *wait.AsyncActionHandler[sqlserverflex.GetUserResponse] {
handler := wait.New(
func() (waitFinished bool, response *sqlserverflex.GetUserResponse, err error) {
s, err := a.GetUserRequest(ctx, projectId, region, instanceId, userId).Execute()
s, err := a.GetUserRequest(ctx, projectID, region, instanceID, userID).Execute()
if err != nil {
var oapiErr *oapierror.GenericOpenAPIError
ok := errors.As(err, &oapiErr)
@ -324,7 +324,7 @@ func CreateUserWaitHandler(
func WaitForUserWaitHandler(
ctx context.Context,
a APIClientInterface,
projectId, instanceId, region, userName string,
projectID, instanceID, region, userName string,
) *wait.AsyncActionHandler[sqlserverflex.ListUserResponse] {
startTime := time.Now()
timeOut := 2 * time.Minute
@ -334,7 +334,7 @@ func WaitForUserWaitHandler(
if time.Since(startTime) > timeOut {
return false, nil, errors.New("ran into timeout")
}
s, err := a.ListUsersRequest(ctx, projectId, region, instanceId).Size(100).Execute()
s, err := a.ListUsersRequest(ctx, projectID, region, instanceID).Size(100).Execute()
if err != nil {
var oapiErr *oapierror.GenericOpenAPIError
ok := errors.As(err, &oapiErr)
@ -376,12 +376,12 @@ func WaitForUserWaitHandler(
func DeleteUserWaitHandler(
ctx context.Context,
a APIClientInterface,
projectId, region, instanceId string,
userId int64,
projectID, region, instanceID string,
userID int64,
) *wait.AsyncActionHandler[struct{}] {
handler := wait.New(
func() (waitFinished bool, response *struct{}, err error) {
_, err = a.GetUserRequest(ctx, projectId, region, instanceId, userId).Execute()
_, err = a.GetUserRequest(ctx, projectID, region, instanceID, userID).Execute()
if err == nil {
return false, nil, nil
}