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 }}"